From 3a2b6490d912eef60402740530cff8dd1e3b565e Mon Sep 17 00:00:00 2001 From: Amy Jeanes Date: Wed, 9 Sep 2026 05:50:04 +0100 Subject: [PATCH 01/15] build: native Windows development builds with MSYS2 clang64 Development builds only: the daemons, modeld, pandad, v4l and socketcan stay Linux. SCons runs build commands through MSYS2 bash so the SConscripts' POSIX shell syntax keeps working. Portable forms replace dirent, readlink, socketpair, aligned_alloc and the rm -rf shell-outs on every platform; what the CRT still spells differently is shimmed in common/util.h. replace_file retries the rename a Windows reader blocks. teleoprtc has no Windows wheels yet, so it is marked off there. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_016AgqYZYWLpwE2T5vS3nVEt --- .gitignore | 3 + SConstruct | 55 ++++++++++-- openpilot/common/esim/lpa.py | 8 +- openpilot/common/gpio.py | 3 +- openpilot/common/hardware/hw.h | 23 ++++- openpilot/common/hardware/hw.py | 13 ++- openpilot/common/i2c.py | 3 +- openpilot/common/params.cc | 35 ++++---- openpilot/common/params.py | 2 +- openpilot/common/prefix.h | 25 +++--- openpilot/common/prefix.py | 7 +- openpilot/common/serial.py | 9 +- openpilot/common/timing.h | 5 +- openpilot/common/util.cc | 60 +++++++++---- openpilot/common/util.h | 39 ++++++++- openpilot/common/win32.h | 18 ++++ .../lib/longitudinal_mpc_lib/SConscript | 18 ++-- openpilot/selfdrive/pandad/SConscript | 2 +- openpilot/system/loggerd/SConscript | 7 +- openpilot/tools/cabana/SConscript | 10 ++- openpilot/tools/cabana/panda.cc | 2 + openpilot/tools/cabana/panda.h | 5 +- openpilot/tools/cabana/routes.cc | 1 + openpilot/tools/cabana/settings.cc | 31 ++++--- .../tools/cabana/streams/devicestream.cc | 31 +++++++ openpilot/tools/cabana/streams/devicestream.h | 2 +- openpilot/tools/cabana/streams/pandastream.cc | 2 +- openpilot/tools/cabana/tests/test_cabana.cc | 1 + .../tools/cabana/ui/dialogs/streamselector.cc | 1 + openpilot/tools/cabana/ui/theme.cc | 2 +- openpilot/tools/cabana/utils/strings.cc | 1 + openpilot/tools/cabana/utils/util.cc | 29 +++++-- openpilot/tools/jotpluggler/SConscript | 12 +-- openpilot/tools/jotpluggler/app.cc | 2 +- openpilot/tools/jotpluggler/common.cc | 4 +- .../jotpluggler/generate_event_extractors.py | 4 +- openpilot/tools/jotpluggler/icons.cc | 2 +- openpilot/tools/jotpluggler/map.cc | 5 +- openpilot/tools/jotpluggler/util.cc | 2 + openpilot/tools/replay/SConscript | 2 +- openpilot/tools/replay/framereader.cc | 4 +- openpilot/tools/replay/framereader.h | 4 +- openpilot/tools/replay/main.cc | 1 + openpilot/tools/replay/py_downloader.cc | 87 ++++++++++++++++++- openpilot/tools/replay/util.cc | 25 ++---- pyproject.toml | 2 +- uv.lock | 10 ++- 47 files changed, 467 insertions(+), 152 deletions(-) create mode 100644 openpilot/common/win32.h diff --git a/.gitignore b/.gitignore index 54f9f176b73b94..f15a1a3367e854 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,9 @@ bin/ *.os-* *.so *.a +*.dll +*.pyd +*.exe st[0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z] *.unchunked *.clb diff --git a/SConstruct b/SConstruct index c6d758b318c08f..e61a4166c8dac0 100644 --- a/SConstruct +++ b/SConstruct @@ -4,6 +4,8 @@ import sys import sysconfig import platform import shlex +import shutil +import tempfile import importlib import numpy as np @@ -46,6 +48,8 @@ if external_pythonpath := os.environ.get("PYTHONPATH"): arch = subprocess.check_output(["uname", "-m"], encoding='utf8').rstrip() if platform.system() == "Darwin": arch = "Darwin" +elif platform.system() == "Windows": + arch = "Windows" elif arch == "aarch64" and COMMA_HARDWARE: arch = "comma_arm64" assert arch in [ @@ -53,7 +57,9 @@ assert arch in [ "aarch64", # linux pc arm64 "x86_64", # linux pc x64 "Darwin", # macOS arm64 (x86 not supported) + "Windows", # windows pc x64, development only (MSYS2 clang64 toolchain) ] +WINDOWS = arch == "Windows" pkg_names = ['acados', 'capnproto', 'ffmpeg', 'json11', 'ncurses', 'zeromq', 'zstd'] pkgs = [importlib.import_module(name) for name in pkg_names] @@ -65,13 +71,13 @@ ffmpeg = pkgs[pkg_names.index('ffmpeg')] # TODO: drop the static fallback once device venvs have comma-deps-ffmpeg>=7.1.0.post94 _ffmpeg_lib_names = os.listdir(ffmpeg.LIB_DIR) if os.path.isdir(ffmpeg.LIB_DIR) else [] ffmpeg_shared = any( - n.startswith('libavcodec.so') or (n.startswith('libavcodec') and n.endswith('.dylib')) + n.startswith('libavcodec.so') or (n.startswith('libavcodec') and n.endswith(('.dylib', '.dll.a'))) for n in _ffmpeg_lib_names ) ffmpeg_libs = ['avformat', 'avcodec', 'swresample', 'avutil'] if not ffmpeg_shared: ffmpeg_libs += ['x264', 'z'] - if arch != "Darwin": + if arch not in ("Darwin", "Windows"): ffmpeg_libs += ['va', 'va-drm', 'drm'] acados_include_dirs = [ acados.INCLUDE_DIR, @@ -88,12 +94,15 @@ acados_include_dirs = [ allowed_system_libs = { "EGL", "GLESv2", "GL", "dl", "drm", "gbm", "m", "pthread", + "opengl32", "gdi32", "winmm", "shell32", "user32", "setupapi", # Windows SDK import libraries } +# static libzmq/capnp need these on every Windows link; import libs only pull in what is referenced +windows_link_libs = ["pthread", "ws2_32", "iphlpapi", "rpcrt4", "bcrypt", "advapi32"] if WINDOWS else [] def _resolve_lib(env, name): for d in env.Flatten(env.get('LIBPATH', [])): p = Dir(str(d)).abspath - for ext in ('.a', '.so', '.dylib'): + for ext in ('.a', '.so', '.dylib', '.dll.a'): f = File(os.path.join(p, f'lib{name}{ext}')) if f.exists() or f.has_builder(): return name @@ -114,11 +123,28 @@ def _libflags(target, source, env, for_signature): libs.append(_resolve_lib(env, lib)) else: libs.append(lib) + libs += windows_link_libs return _stripixes(env['LIBLINKPREFIX'], libs, env['LIBLINKSUFFIX'], env['LIBPREFIXES'], env['LIBSUFFIXES'], env, env['LIBLITERALPREFIX']) +if WINDOWS: + # build commands run through MSYS2 bash: the SConscripts use POSIX shell syntax, the submodules' Environments too + import SCons.Platform.posix + import SCons.Platform.win32 + _bash = shutil.which("bash") + if not _bash or "system32" in _bash.lower(): # System32's bash.exe is the WSL launcher + raise SCons.Errors.UserError("run scons from an MSYS2 CLANG64 shell") + + def _bash_spawn(sh, escape, cmd, args, env): + args = [a if '"' in a else a.replace("\\", "/") for a in args] # bash reads backslashes as escapes; quoted defines stay + return subprocess.call([_bash, "-c", " ".join(args)], env=env) + SCons.Platform.win32.spawn = _bash_spawn + SCons.Platform.win32.escape = SCons.Platform.posix.escape + env = Environment( ENV={ + # Windows processes need the system root (DLLs), a temp dir and ccache's cache dir + **({k: os.environ[k] for k in ("SYSTEMROOT", "TEMP", "TMP", "LOCALAPPDATA") if k in os.environ} if WINDOWS else {}), "PATH": os.environ['PATH'], "PYTHONPATH": os.pathsep.join(submodule_python_paths), "ACADOS_SOURCE_DIR": acados.DIR, @@ -132,7 +158,7 @@ env = Environment( "-O2", "-Wunused", "-Werror", - "-Wshadow" if arch in ("Darwin", "comma_arm64") else "-Wshadow=local", + "-Wshadow" if arch in ("Darwin", "comma_arm64", "Windows") else "-Wshadow=local", "-Wno-unknown-warning-option", "-Wno-inconsistent-missing-override", "-Wno-c99-designator", @@ -163,7 +189,7 @@ env = Environment( CYTHONCFILESUFFIX=".cpp", COMPILATIONDB_USE_ABSPATH=True, REDNOSE_ROOT="#rednose_repo", - tools=["default", "cython", "compilation_db", "rednose_filter"], + tools=["mingw" if WINDOWS else "default", "cython", "compilation_db", "rednose_filter"], toolpath=["#msgq_repo/site_scons/site_tools", "#rednose_repo/site_scons/site_tools"], ) # SCons' Darwin linker tool doesn't define the variables used to expand RPATH. @@ -173,6 +199,14 @@ if arch == "Darwin": env["_RPATH"] = "${_concat(RPATHPREFIX, RPATH, RPATHSUFFIX, __env__)}" if arch != "comma_arm64": env['_LIBFLAGS'] = _libflags +if WINDOWS: + # clang and lld through the mingw tool, whose defaults are gcc; shared libraries keep the lib prefix the SConscripts expect + env["CC"], env["CXX"] = "clang", "clang++" + env["SHLIBPREFIX"] = "lib" + # PE has no rpath; DLLs are found next to the executable or via PATH + env["_RPATH"] = "" + # static runtime: Python does not search PATH for the DLLs an extension module needs + env.Append(LINKFLAGS=["-static"]) # Arch-specific flags and paths if arch == "comma_arm64": @@ -190,6 +224,9 @@ elif arch == "Darwin": ]) env.Append(CCFLAGS=["-DGL_SILENCE_DEPRECATION"]) env.Append(CXXFLAGS=["-DGL_SILENCE_DEPRECATION"]) +elif arch == "Windows": + # strict -std=c++1z hides vasprintf and M_PI in mingw's headers; the vendored libzmq is a static archive + env.Append(CCFLAGS=["-D_GNU_SOURCE", "-D_USE_MATH_DEFINES", "-DZMQ_STATIC"]) _extra_cc = shlex.split(GetOption('ccflags') or '') if _extra_cc: @@ -224,6 +261,9 @@ envCython["CCFLAGS"].remove("-Werror") envCython["LIBS"] = [] if arch == "Darwin": envCython["LINKFLAGS"] = env["LINKFLAGS"] + ["-bundle", "-undefined", "dynamic_lookup"] +elif arch == "Windows": + envCython["LINKFLAGS"] = ["-shared", "-static"] + envCython["LIBS"] += [File(f"{sys.base_prefix}/libs/python{sys.version_info.major}{sys.version_info.minor}.lib")] else: envCython["LINKFLAGS"] = ["-pthread", "-shared"] @@ -233,7 +273,7 @@ Export('envCython', 'np_version') Export('env', 'arch', 'acados', 'ffmpeg_libs') # Setup cache dir -cache_dir = '/data/scons_cache' if arch == "comma_arm64" else '/tmp/scons_cache' +cache_dir = '/data/scons_cache' if arch == "comma_arm64" else os.path.join(tempfile.gettempdir(), 'scons_cache') cache_size_limit = 4e9 if "CI" in os.environ else 2e9 CacheDir(cache_dir) Clean(["."], cache_dir) @@ -287,9 +327,10 @@ SConscript([ 'openpilot/selfdrive/pandad/SConscript', 'openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript', 'openpilot/selfdrive/locationd/SConscript', - 'openpilot/selfdrive/modeld/SConscript', 'openpilot/selfdrive/ui/SConscript', ]) +if arch != "Windows": # modeld needs tinygrad's compiled model, Linux/macOS only + SConscript(['openpilot/selfdrive/modeld/SConscript']) # Build desktop-only tools if GetOption('extras') and arch != "comma_arm64": diff --git a/openpilot/common/esim/lpa.py b/openpilot/common/esim/lpa.py index 600869fcb765e7..e09e05fffde46d 100644 --- a/openpilot/common/esim/lpa.py +++ b/openpilot/common/esim/lpa.py @@ -2,15 +2,19 @@ import atexit import base64 -import fcntl import hashlib import os import requests import subprocess import sys -import termios import time +if sys.platform == "win32": + fcntl = termios = None # POSIX only; the device modem is unused on a Windows dev build +else: + import fcntl + import termios + from collections.abc import Callable, Generator from contextlib import contextmanager from typing import Any diff --git a/openpilot/common/gpio.py b/openpilot/common/gpio.py index 8f025a2daf726e..902ad70b5948f2 100644 --- a/openpilot/common/gpio.py +++ b/openpilot/common/gpio.py @@ -1,5 +1,4 @@ import os -import fcntl import ctypes from functools import cache @@ -83,6 +82,8 @@ def gpiochip_get_ro_value_fd(label: str, gpiochip_id: int, pin: int) -> int: rq.eventflags = GPIOEVENT_REQUEST_BOTH_EDGES rq.label = label.encode('utf-8')[:31] + b'\0' + import fcntl # POSIX only, keep the module importable on Windows + fd = os.open(f"/dev/gpiochip{gpiochip_id}", os.O_RDONLY) fcntl.ioctl(fd, GPIO_GET_LINEEVENT_IOCTL, rq) os.close(fd) diff --git a/openpilot/common/hardware/hw.h b/openpilot/common/hardware/hw.h index 83dc452da85c66..72f381cca2bea3 100644 --- a/openpilot/common/hardware/hw.h +++ b/openpilot/common/hardware/hw.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include "common/hardware/base.h" @@ -18,8 +19,12 @@ namespace Path { return util::getenv("OPENPILOT_PREFIX", ""); } + inline std::string home() { + return util::getenv("USERPROFILE", util::getenv("HOME")); // Python's Path.home() ignores HOME on Windows + } + inline std::string comma_home() { - return util::getenv("HOME") + "/.comma" + Path::openpilot_prefix(); + return home() + "/.comma" + Path::openpilot_prefix(); } inline std::string log_root() { @@ -38,7 +43,21 @@ namespace Path { } inline std::string swaglog_ipc() { +#ifdef _WIN32 + // libzmq has no ipc:// transport on MinGW: derive a loopback port from the prefix (FNV-1a, mirrored in hw.py) + uint64_t h = 14695981039346656037ULL; + for (unsigned char c : Path::openpilot_prefix()) { + h ^= c; + h *= 1099511628211ULL; + } + return "tcp://127.0.0.1:" + std::to_string(26000 + h % 1000); +#else return "ipc:///tmp/logmessage" + Path::openpilot_prefix(); +#endif + } + + inline std::string tmp_dir() { + return util::getenv("TEMP", "/tmp"); // hw.TMP_DIR } inline std::string download_cache_root() { @@ -51,6 +70,8 @@ namespace Path { inline std::string shm_path() { #ifdef __APPLE__ return"/tmp"; + #elif defined(_WIN32) + return tmp_dir(); #else return "/dev/shm"; #endif diff --git a/openpilot/common/hardware/hw.py b/openpilot/common/hardware/hw.py index 1041a17c1c7fc4..4064c3e64346df 100644 --- a/openpilot/common/hardware/hw.py +++ b/openpilot/common/hardware/hw.py @@ -1,9 +1,11 @@ import os import platform +import sys from pathlib import Path from openpilot.common.hardware import PC +TMP_DIR = os.environ.get("TEMP", "/tmp") # Path::tmp_dir() DEFAULT_DOWNLOAD_CACHE_ROOT = "/tmp/comma_download_cache" class Paths: @@ -29,7 +31,14 @@ def swaglog_root() -> str: @staticmethod def swaglog_ipc() -> str: - return "ipc:///tmp/logmessage" + os.environ.get("OPENPILOT_PREFIX", "") + prefix = os.environ.get("OPENPILOT_PREFIX", "") + if sys.platform == "win32": + # libzmq has no ipc:// transport on MinGW: derive a loopback port from the prefix (FNV-1a, mirrored in hw.h) + h = 14695981039346656037 + for c in prefix.encode(): + h = ((h ^ c) * 1099511628211) & 0xFFFFFFFFFFFFFFFF + return f"tcp://127.0.0.1:{26000 + h % 1000}" + return "ipc:///tmp/logmessage" + prefix @staticmethod def download_cache_root() -> str: @@ -55,4 +64,6 @@ def config_root() -> str: def shm_path() -> str: if PC and platform.system() == "Darwin": return "/tmp" # This is not really shared memory on macOS, but it's the closest we can get + if sys.platform == "win32": + return TMP_DIR # msgq reads %TEMP% for the same directory return "/dev/shm" diff --git a/openpilot/common/i2c.py b/openpilot/common/i2c.py index 1dfaa659ad302e..33519938058d48 100644 --- a/openpilot/common/i2c.py +++ b/openpilot/common/i2c.py @@ -1,5 +1,4 @@ import os -import fcntl import ctypes # I2C constants from /usr/include/linux/i2c-dev.h @@ -49,10 +48,12 @@ def close(self) -> None: self._fd = -1 def _set_address(self, addr: int, force: bool = False) -> None: + import fcntl # POSIX only, keep the module importable on Windows ioctl_arg = I2C_SLAVE_FORCE if force else I2C_SLAVE fcntl.ioctl(self._fd, ioctl_arg, addr) def _smbus_access(self, read_write: int, command: int, size: int, data: _I2cSmbusData) -> None: + import fcntl ioctl_data = _I2cSmbusIoctlData(read_write, command, size, ctypes.pointer(data)) fcntl.ioctl(self._fd, I2C_SMBUS, ioctl_data) diff --git a/openpilot/common/params.cc b/openpilot/common/params.cc index 495feb0a99111a..da00cee824f931 100644 --- a/openpilot/common/params.cc +++ b/openpilot/common/params.cc @@ -1,11 +1,9 @@ #include "common/params.h" -#include -#include - #include #include #include +#include #include #include "common/params_keys.h" @@ -22,6 +20,9 @@ void params_sig_handler(int signal) { } int fsync_dir(const std::string &path) { +#ifdef _WIN32 + return 0; // directories cannot be opened through the CRT; NTFS journals the rename +#endif int result = -1; int fd = HANDLE_EINTR(open(path.c_str(), O_RDONLY, 0755)); if (fd >= 0) { @@ -39,6 +40,9 @@ bool create_params_path(const std::string ¶m_path, const std::string &key_pa // See if the symlink exists, otherwise create it if (!util::file_exists(key_path)) { +#ifdef _WIN32 + return mkdir(key_path.c_str(), 0775) == 0 || errno == EEXIST; // symlinks need privileges; a plain directory does +#else // 1) Create temp folder // 2) Symlink it to temp link // 3) Move symlink to /d @@ -59,6 +63,7 @@ bool create_params_path(const std::string ¶m_path, const std::string &key_pa if (rename(link_path.c_str(), key_path.c_str()) != 0 && errno != EEXIST) { return false; } +#endif } return true; @@ -78,7 +83,7 @@ class FileLock { public: FileLock(const std::string &fn) { fd_ = HANDLE_EINTR(open(fn.c_str(), O_CREAT, 0775)); - if (fd_ < 0 || HANDLE_EINTR(flock(fd_, LOCK_EX)) < 0) { + if (fd_ < 0 || util::lock_file_exclusive(fd_) < 0) { LOGE("Failed to lock file %s, errno=%d", fn.c_str(), errno); } } @@ -149,17 +154,19 @@ int Params::put(const char* key, const char* value, size_t value_size) { // fsync to force persist the changes. if ((result = HANDLE_EINTR(fsync(tmp_fd))) < 0) break; + close(tmp_fd); + tmp_fd = -1; // closed before the move: Windows cannot rename an open file FileLock file_lock(params_path + "/.lock"); // Move temp into place. - if ((result = rename(tmp_path.c_str(), getParamPath(key).c_str())) < 0) break; + if ((result = util::replace_file(tmp_path.c_str(), getParamPath(key).c_str())) < 0) break; // fsync parent directory result = fsync_dir(getParamPath()); } while (false); - close(tmp_fd); + if (tmp_fd >= 0) close(tmp_fd); if (result != 0) { ::unlink(tmp_path.c_str()); } @@ -208,17 +215,13 @@ void Params::clearAll(ParamKeyFlag key_flag) { // 1) delete params of key_flag // 2) delete files that are not defined in the keys. - if (DIR *d = opendir(getParamPath().c_str())) { - struct dirent *de = NULL; - while ((de = readdir(d))) { - if (de->d_type != DT_DIR) { - auto it = keys.find(de->d_name); - if (it == keys.end() || (it->second.flags & key_flag)) { - unlink(getParamPath(de->d_name).c_str()); - } - } + std::error_code ec; + for (const auto &entry : std::filesystem::directory_iterator(getParamPath(), ec)) { + if (entry.is_directory()) continue; + auto it = keys.find(entry.path().filename().string()); + if (it == keys.end() || (it->second.flags & key_flag)) { + unlink(entry.path().string().c_str()); } - closedir(d); } fsync_dir(getParamPath()); diff --git a/openpilot/common/params.py b/openpilot/common/params.py index 9357a0a5d34966..74b08860909b8d 100644 --- a/openpilot/common/params.py +++ b/openpilot/common/params.py @@ -30,7 +30,7 @@ class ParamKeyType(IntEnum): BYTES = 6 -_suffix = ".dylib" if sys.platform == "darwin" else ".so" +_suffix = {"darwin": ".dylib", "win32": ".dll"}.get(sys.platform, ".so") lib = ctypes.CDLL(Path(__file__).with_name(f"libparams_c{_suffix}")) ParamsHandle = ctypes.c_void_p diff --git a/openpilot/common/prefix.h b/openpilot/common/prefix.h index 0f2c592527913b..b2397fac7f4c84 100644 --- a/openpilot/common/prefix.h +++ b/openpilot/common/prefix.h @@ -1,9 +1,9 @@ #pragma once #include +#include #include -#include "common/params.h" #include "common/util.h" #include "common/hardware/hw.h" @@ -13,28 +13,23 @@ class OpenpilotPrefix { if (prefix.empty()) { prefix = util::random_string(15); } -#ifdef __APPLE__ - msgq_path = "/tmp/msgq_" + prefix; -#else - msgq_path = "/dev/shm/msgq_" + prefix; -#endif + msgq_path = Path::shm_path() + "/msgq_" + prefix; bool ret = util::create_directories(msgq_path, 0777); assert(ret); setenv("OPENPILOT_PREFIX", prefix.c_str(), 1); } ~OpenpilotPrefix() { - auto param_path = Params().getParamPath(); - if (util::file_exists(param_path)) { - std::string real_path = util::readlink(param_path); - util::check_system(util::string_format("rm -rf %s", real_path.c_str())); - unlink(param_path.c_str()); - } + std::error_code ec; + // Params::getParamPath() without params.h: its BOOL/INT/FLOAT enumerators clash with cabana's Win32 typedefs + auto param_path = Path::params() + "/" + util::getenv("OPENPILOT_PREFIX"); + std::filesystem::remove_all(util::readlink(param_path), ec); // the temp folder behind the symlink, see params.cc + std::filesystem::remove_all(param_path, ec); if (getenv("COMMA_CACHE") == nullptr) { - util::check_system(util::string_format("rm -rf %s", Path::download_cache_root().c_str())); + std::filesystem::remove_all(Path::download_cache_root(), ec); } - util::check_system(util::string_format("rm -rf %s", Path::comma_home().c_str())); - util::check_system(util::string_format("rm -rf %s", msgq_path.c_str())); + std::filesystem::remove_all(Path::comma_home(), ec); + std::filesystem::remove_all(msgq_path, ec); unsetenv("OPENPILOT_PREFIX"); } diff --git a/openpilot/common/prefix.py b/openpilot/common/prefix.py index d0be8997ae1ac6..a710fe462a5e55 100644 --- a/openpilot/common/prefix.py +++ b/openpilot/common/prefix.py @@ -1,5 +1,4 @@ import os -import platform import shutil import uuid @@ -12,8 +11,7 @@ class OpenpilotPrefix: def __init__(self, prefix: str | None = None, create_dirs_on_enter: bool = True, clean_dirs_on_exit: bool = True, shared_download_cache: bool = False): self.prefix = prefix if prefix else str(uuid.uuid4().hex[0:15]) - shm_path = "/tmp" if platform.system() == "Darwin" else "/dev/shm" - self.msgq_path = os.path.join(shm_path, "msgq_" + self.prefix) + self.msgq_path = os.path.join(Paths.shm_path(), "msgq_" + self.prefix) self.create_dirs_on_enter = create_dirs_on_enter self.clean_dirs_on_exit = clean_dirs_on_exit self.shared_download_cache = shared_download_cache @@ -52,7 +50,8 @@ def clean_dirs(self): symlink_path = Params().get_param_path() if os.path.exists(symlink_path): shutil.rmtree(os.path.realpath(symlink_path), ignore_errors=True) - os.remove(symlink_path) + if os.path.islink(symlink_path): # a plain directory on Windows, see params.cc + os.remove(symlink_path) shutil.rmtree(self.msgq_path, ignore_errors=True) if PC: shutil.rmtree(Paths.log_root(), ignore_errors=True) diff --git a/openpilot/common/serial.py b/openpilot/common/serial.py index 68083f40a6e455..409581d2216067 100644 --- a/openpilot/common/serial.py +++ b/openpilot/common/serial.py @@ -1,11 +1,16 @@ import errno -import fcntl import os import select import struct -import termios +import sys import time +if sys.platform == "win32": + fcntl = termios = None # POSIX only; the device modem is unused on a Windows dev build +else: + import fcntl + import termios + # Modem control lines (linux/termios.h); fall back to common x86_64 values. TIOCMBIS = getattr(termios, "TIOCMBIS", 0x5416) diff --git a/openpilot/common/timing.h b/openpilot/common/timing.h index 83f55e0c4009f5..c3a353a7a395d8 100644 --- a/openpilot/common/timing.h +++ b/openpilot/common/timing.h @@ -3,9 +3,12 @@ #include #include -#ifdef __APPLE__ +#if defined(__APPLE__) || defined(_WIN32) #define CLOCK_BOOTTIME CLOCK_MONOTONIC #endif +#ifdef _WIN32 +#define CLOCK_MONOTONIC_RAW CLOCK_MONOTONIC +#endif static inline uint64_t nanos_since_boot() { struct timespec t; diff --git a/openpilot/common/util.cc b/openpilot/common/util.cc index 84b47e187ee05e..1d08e36656df28 100644 --- a/openpilot/common/util.cc +++ b/openpilot/common/util.cc @@ -1,14 +1,21 @@ +#ifdef _WIN32 +#include "common/win32.h" +#endif + #include "common/util.h" #include "common/swaglog.h" -#include #include +#ifndef _WIN32 +#include +#include #include +#endif #include #include #include -#include +#include #include #include #include @@ -63,6 +70,9 @@ int set_core_affinity(std::vector cores) { } int set_file_descriptor_limit(uint64_t limit_val) { +#ifdef _WIN32 + return 0; // the CRT allows 8192 open files +#else struct rlimit limit; int status; @@ -74,6 +84,7 @@ int set_file_descriptor_limit(uint64_t limit_val) { return status; return 0; +#endif } std::string read_file(const std::string& fn) { @@ -102,17 +113,12 @@ std::string read_file(const std::string& fn) { std::map read_files_in_dir(const std::string &path) { std::map ret; - DIR *d = opendir(path.c_str()); - if (!d) return ret; - - struct dirent *de = NULL; - while ((de = readdir(d))) { - if (de->d_type != DT_DIR) { - ret[de->d_name] = util::read_file(path + "/" + de->d_name); + std::error_code ec; + for (const auto &entry : std::filesystem::directory_iterator(path, ec)) { + if (!entry.is_directory()) { + ret[entry.path().filename().string()] = util::read_file(entry.path().string()); } } - - closedir(d); return ret; } @@ -152,6 +158,7 @@ int safe_fflush(FILE *stream) { return ret; } +#ifndef _WIN32 int safe_ioctl(int fd, unsigned long request, void *argp, const char* exception_msg) { int ret; do { @@ -164,15 +171,11 @@ int safe_ioctl(int fd, unsigned long request, void *argp, const char* exception_ } return ret; } +#endif std::string readlink(const std::string &path) { - char buff[4096]; - ssize_t len = ::readlink(path.c_str(), buff, sizeof(buff)-1); - if (len != -1) { - buff[len] = '\0'; - return std::string(buff); - } - return ""; + std::error_code ec; + return std::filesystem::read_symlink(path, ec).string(); } bool file_exists(const std::string& fn) { @@ -285,6 +288,27 @@ std::string strip(const std::string &str) { return str.substr(start, end - start + 1); } +int lock_file_exclusive(int fd) { +#ifdef _WIN32 + OVERLAPPED ov = {}; + return LockFileEx((HANDLE)_get_osfhandle(fd), LOCKFILE_EXCLUSIVE_LOCK, 0, 1, 0, &ov) ? 0 : -1; +#else + return HANDLE_EINTR(flock(fd, LOCK_EX)); +#endif +} + +int replace_file(const char *from, const char *to) { + std::error_code ec; + // Windows refuses to replace a file another process has open for reading; those reads take microseconds + for (int i = 0; i < 200; ++i) { + std::filesystem::rename(from, to, ec); + if (ec != std::errc::permission_denied) break; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + if (ec) errno = ec.default_error_condition().value(); // callers report strerror(errno) + return ec ? -1 : 0; +} + std::string check_output(const std::string& command) { char buffer[128]; std::string result; diff --git a/openpilot/common/util.h b/openpilot/common/util.h index e4483ee7a57c4e..bb8c3298c85197 100644 --- a/openpilot/common/util.h +++ b/openpilot/common/util.h @@ -4,6 +4,38 @@ #include #include +#ifdef _WIN32 +#include +#include +#include +#include +#include +#include +#include +// POSIX calls this codebase uses that the Windows CRT spells differently +inline int mkdir(const char *path, mode_t) { return _mkdir(path); } +inline int fsync(int fd) { return _commit(fd); } +inline int setenv(const char *name, const char *value, int) { return _putenv_s(name, value); } +inline int unsetenv(const char *name) { return _putenv_s(name, ""); } +inline struct tm *localtime_r(const time_t *t, struct tm *out) { + struct tm *r = localtime(t); // thread-local storage in the Windows CRT + if (r) *out = *r; + return r ? out : nullptr; +} +inline time_t timegm(struct tm *tm) { return _mkgmtime(tm); } +inline char *strptime(const char *s, const char *format, struct tm *tm) { + std::istringstream in(s); + in >> std::get_time(tm, format); + if (in.fail()) return nullptr; + return const_cast(s) + (in.eof() ? strlen(s) : static_cast(in.tellg())); +} +// popen/pclose return the exit code directly, there is no wait status to decode +#define WIFEXITED(status) 1 +#define WEXITSTATUS(status) (status) +#define WIFSIGNALED(status) 0 +#define WTERMSIG(status) 0 +#endif + #include #include #include @@ -81,6 +113,9 @@ int random_int(int min, int max); std::string random_string(std::string::size_type length); // **** file helpers ***** +// an exclusive lock held until fd closes, and a rename that replaces an existing target (waiting out Windows readers) +int lock_file_exclusive(int fd); +int replace_file(const char *from, const char *to); std::string read_file(const std::string& fn); std::map read_files_in_dir(const std::string& path); int write_file(const char* path, const void* data, size_t size, int flags = O_WRONLY, mode_t mode = 0664); @@ -119,7 +154,7 @@ class ExitHandler { std::signal(SIGINT, (sighandler_t)set_do_exit); std::signal(SIGTERM, (sighandler_t)set_do_exit); -#ifndef __APPLE__ +#if !defined(__APPLE__) && !defined(_WIN32) std::signal(SIGPWR, (sighandler_t)set_do_exit); #endif } @@ -133,7 +168,7 @@ class ExitHandler { } private: static void set_do_exit(int sig) { -#ifndef __APPLE__ +#if !defined(__APPLE__) && !defined(_WIN32) power_failure = (sig == SIGPWR); #endif signal = sig; diff --git a/openpilot/common/win32.h b/openpilot/common/win32.h new file mode 100644 index 00000000000000..a73ff9f747ea07 --- /dev/null +++ b/openpilot/common/win32.h @@ -0,0 +1,18 @@ +#pragma once + +// Use instead of : lean headers, no min/max/MessageBox/NO_ERROR macros (cabana's MessageBox, capnp's NO_ERROR). +// Only from .cc files that never see common/params.h: its BOOL/INT/FLOAT enumerators clash with the Win32 typedefs. +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef NOGDI +#define NOGDI +#endif +#include +#undef NO_ERROR +#undef MessageBox +#endif diff --git a/openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript b/openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript index fa249765bc3cbf..7a3a12b85558b8 100644 --- a/openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript +++ b/openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript @@ -63,11 +63,11 @@ source_list = ['long_mpc.py', lenv = env.Clone() copied_acados_libs = [] -if arch != "Darwin": +if arch not in ("Darwin", "Windows"): # the Windows acados wheel ships static archives, they link into the solver library for lib in ["libacados.so", "libblasfeo.so", "libhpipm.so", "libqpOASES_e.so.3.1"]: copied_acados_libs += lenv.Command(f"{gen}/{lib}", Dir(acados.LIB_DIR).File(lib), [Mkdir(Dir(gen)), Copy("$TARGET", "$SOURCE")]) lenv["RPATH"] += [lenv.Literal('\\$$ORIGIN')] -else: +elif arch == "Darwin": acados_rel_path = Dir(gen).rel_path(Dir(acados.LIB_DIR)) lenv["RPATH"] += [lenv.Literal(f'\\$$ORIGIN/{acados_rel_path}')] lenv.Clean(generated_files, Dir(gen)) @@ -79,9 +79,9 @@ lenv.Depends(generated_long, [msgq_python, common_python]) lenv["CFLAGS"].append("-DACADOS_WITH_QPOASES") lenv["CXXFLAGS"].append("-DACADOS_WITH_QPOASES") lenv["CCFLAGS"].append("-Wno-unused") -if arch != "Darwin": +if arch not in ("Darwin", "Windows"): lenv["LINKFLAGS"].append("-Wl,--disable-new-dtags") -else: +elif arch == "Darwin": lenv["LINKFLAGS"].append("-Wl,-install_name,@loader_path/libacados_ocp_solver_long.dylib") lenv["LINKFLAGS"].append(f"-Wl,-rpath,@loader_path/{acados_rel_path}") lib_solver = lenv.SharedLibrary(f"{gen}/acados_ocp_solver_long", @@ -100,11 +100,11 @@ lenv2["RPATH"] += [lenv2.Literal('\\$$ORIGIN')] lenv2.Command(libacados_ocp_solver_c, [acados_ocp_solver_pyx, acados_ocp_solver_common, libacados_ocp_solver_pxd], f'cython' + \ - f' -o {libacados_ocp_solver_c.get_labspath()}' + \ - f' -I {libacados_ocp_solver_pxd.get_dir().get_labspath()}' + \ - f' -I {acados_ocp_solver_common.get_dir().get_labspath()}' + \ - f' {acados_ocp_solver_pyx.get_labspath()}') -lib_cython = lenv2.Program(f'{gen}/acados_ocp_solver_pyx.so', [libacados_ocp_solver_c], LIBS=['acados_ocp_solver_long']) + f' -o {libacados_ocp_solver_c.abspath}' + \ + f' -I {libacados_ocp_solver_pxd.get_dir().abspath}' + \ + f' -I {acados_ocp_solver_common.get_dir().abspath}' + \ + f' {acados_ocp_solver_pyx.abspath}') +lib_cython = lenv2.Program(f'{gen}/acados_ocp_solver_pyx.so', [libacados_ocp_solver_c], LIBS=lenv2["LIBS"] + ['acados_ocp_solver_long']) lenv2.Depends(lib_cython, lib_solver) lenv2.Depends(lib_cython, copied_acados_libs) lenv2.Depends(libacados_ocp_solver_c, np_version) diff --git a/openpilot/selfdrive/pandad/SConscript b/openpilot/selfdrive/pandad/SConscript index fd59db98537941..afb931b4b51ee0 100644 --- a/openpilot/selfdrive/pandad/SConscript +++ b/openpilot/selfdrive/pandad/SConscript @@ -1,6 +1,6 @@ Import('env', 'arch', 'common', 'messaging') -if arch != "Darwin": +if arch not in ("Darwin", "Windows"): libs = [common, messaging, 'pthread'] panda = env.Library('panda', ['panda.cc', 'spi.cc']) diff --git a/openpilot/system/loggerd/SConscript b/openpilot/system/loggerd/SConscript index 6890c296550a91..aff590c17f4083 100644 --- a/openpilot/system/loggerd/SConscript +++ b/openpilot/system/loggerd/SConscript @@ -14,6 +14,7 @@ else: logger_lib = env.Library('logger', src) libs.insert(0, logger_lib) -env.Program('loggerd', ['loggerd.cc'], LIBS=libs, FRAMEWORKS=frameworks) -env.Program('encoderd', ['encoderd.cc'], LIBS=libs, FRAMEWORKS=frameworks) -env.Program('bootlog.cc', LIBS=libs, FRAMEWORKS=frameworks) +if arch != "Windows": + env.Program('loggerd', ['loggerd.cc'], LIBS=libs, FRAMEWORKS=frameworks) + env.Program('encoderd', ['encoderd.cc'], LIBS=libs, FRAMEWORKS=frameworks) + env.Program('bootlog.cc', LIBS=libs, FRAMEWORKS=frameworks) diff --git a/openpilot/tools/cabana/SConscript b/openpilot/tools/cabana/SConscript index 0249a019bbaddd..5117f1d6a7d466 100644 --- a/openpilot/tools/cabana/SConscript +++ b/openpilot/tools/cabana/SConscript @@ -7,7 +7,7 @@ from openpilot.common.basedir import BASEDIR Import('env', 'arch', 'common', 'messaging', 'visionipc', 'cereal', 'replay_lib', 'ffmpeg_libs') -opendbc_path = '-DOPENDBC_FILE_PATH=\'"%s"\'' % (env.Dir("../../../opendbc_repo/opendbc/dbc").abspath) +opendbc_path = '-DOPENDBC_FILE_PATH=\'"%s"\'' % (env.Dir("../../../opendbc_repo/opendbc/dbc").abspath.replace(os.sep, "/")) # embed the bootstrap icons SVG into the binary def build_bootstrap_icons_src(target, source, env): @@ -29,7 +29,7 @@ bootstrap_icons_src = env.Command('assets/bootstrap_icons.cc', str(bootstrap_ico core_srcs = ['streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', 'utils/export.cc', 'utils/util.cc', 'utils/strings.cc', 'commands.cc', 'settings.cc', 'routes.cc', 'panda.cc'] -if arch != "Darwin": +if arch not in ("Darwin", "Windows"): core_srcs += ['streams/socketcanstream.cc'] # imgui frontend (tools/cabana/ui), no Qt @@ -39,8 +39,8 @@ ui_env['LIBPATH'] += [imgui.MESA_DIR, libusb.LIB_DIR] ui_env['CXXFLAGS'] += [ opendbc_path, "-DGLFW_INCLUDE_NONE", - '-DCABANA_FONTS_DIR=\'"%s"\'' % os.path.join(os.path.realpath(BASEDIR), "openpilot", "selfdrive", "assets", "fonts"), - '-DBOOTSTRAP_ICONS_TTF=\'"%s"\'' % bootstrap_icons.TTF_PATH, + '-DCABANA_FONTS_DIR=\'"%s"\'' % os.path.join(os.path.realpath(BASEDIR), "openpilot", "selfdrive", "assets", "fonts").replace(os.sep, "/"), + '-DBOOTSTRAP_ICONS_TTF=\'"%s"\'' % bootstrap_icons.TTF_PATH.as_posix(), ] ui_objs = [ui_env.Object('ui/obj/' + src.replace('/', '_')[:-3], src) for src in core_srcs] ui_objs += [ui_env.Object('ui/obj/bootstrap_icons', bootstrap_icons_src)] @@ -49,6 +49,8 @@ ui_libs = [replay_lib, common, messaging, visionipc, cereal, File(f"{imgui.LIB_D ffmpeg_libs + ['zstd', 'm', 'pthread', 'usb-1.0'] if arch == "Darwin": ui_env['FRAMEWORKS'] = ['OpenGL', 'Cocoa', 'IOKit', 'CoreFoundation', 'CoreVideo', 'CoreMedia', 'Security', 'VideoToolbox'] +elif arch == "Windows": + ui_libs += ['opengl32', 'gdi32', 'winmm', 'shell32', 'user32', 'setupapi', 'dl'] else: ui_libs += ['GL', 'dl'] cabana_ui = ui_env.Program('cabana', ui_objs, LIBS=ui_libs) diff --git a/openpilot/tools/cabana/panda.cc b/openpilot/tools/cabana/panda.cc index 9bb5320eaca485..e6406c6f144d20 100644 --- a/openpilot/tools/cabana/panda.cc +++ b/openpilot/tools/cabana/panda.cc @@ -1,5 +1,7 @@ #include "tools/cabana/panda.h" +#include + #include #include #include diff --git a/openpilot/tools/cabana/panda.h b/openpilot/tools/cabana/panda.h index c99be1f5cfd953..e2621335ba00a6 100644 --- a/openpilot/tools/cabana/panda.h +++ b/openpilot/tools/cabana/panda.h @@ -11,7 +11,10 @@ #include #include -#include + +// libusb.h pulls in on Windows, which clashes with the capnp and params enums, so only panda.cc includes it +struct libusb_context; +struct libusb_device_handle; #include "openpilot/cereal/gen/cpp/car.capnp.h" #include "openpilot/cereal/gen/cpp/log.capnp.h" diff --git a/openpilot/tools/cabana/routes.cc b/openpilot/tools/cabana/routes.cc index 50a1eacf8d8e79..146e13570e89b4 100644 --- a/openpilot/tools/cabana/routes.cc +++ b/openpilot/tools/cabana/routes.cc @@ -5,6 +5,7 @@ #include #include +#include "common/util.h" #include "json11/json11.hpp" #include "tools/replay/py_downloader.h" diff --git a/openpilot/tools/cabana/settings.cc b/openpilot/tools/cabana/settings.cc index 7f8e6473c2b9a1..ca8d7a0afd5492 100644 --- a/openpilot/tools/cabana/settings.cc +++ b/openpilot/tools/cabana/settings.cc @@ -17,8 +17,12 @@ #include #include -#include #include +#ifdef _WIN32 +#include +#define fsync _commit +#define O_CLOEXEC 0 +#endif #ifdef __APPLE__ #include @@ -29,6 +33,9 @@ #include "json11/json11.hpp" #include "tools/cabana/utils/util.h" +// util.h cannot be included here: its Rect collides with MacTypes' under CoreFoundation +namespace util { int lock_file_exclusive(int fd); int replace_file(const char *from, const char *to); } + Settings settings; namespace { @@ -46,9 +53,9 @@ struct LoadedSettings { class FileLock { public: explicit FileLock(const std::filesystem::path &path) { - fd = open(path.c_str(), O_CREAT | O_CLOEXEC, 0600); - if (fd < 0 || flock(fd, LOCK_EX) < 0) { - fprintf(stderr, "failed to lock Cabana settings %s: %s\n", path.c_str(), strerror(errno)); + fd = open(path.string().c_str(), O_CREAT | O_CLOEXEC, 0600); + if (fd < 0 || util::lock_file_exclusive(fd) < 0) { + fprintf(stderr, "failed to lock Cabana settings %s: %s\n", path.string().c_str(), strerror(errno)); if (fd >= 0) close(fd); fd = -1; } @@ -70,7 +77,7 @@ LoadedSettings loadSettings() { std::string error; auto settings_json = json11::Json::parse(contents, error); if (!error.empty() || !settings_json.is_object()) { - fprintf(stderr, "failed to read Cabana settings %s%s%s\n", settingsFile().c_str(), error.empty() ? "" : ": ", error.c_str()); + fprintf(stderr, "failed to read Cabana settings %s%s%s\n", settingsFile().string().c_str(), error.empty() ? "" : ": ", error.c_str()); return {.exists = true, .valid = false}; } return {.values = settings_json.object_items(), .exists = true}; @@ -81,7 +88,7 @@ bool ensureSettingsDirectory() { std::error_code error; std::filesystem::create_directories(path.parent_path(), error); if (error) { - fprintf(stderr, "failed to create Cabana settings directory %s: %s\n", path.parent_path().c_str(), error.message().c_str()); + fprintf(stderr, "failed to create Cabana settings directory %s: %s\n", path.parent_path().string().c_str(), error.message().c_str()); return false; } return true; @@ -110,18 +117,20 @@ bool saveSettings(const json11::Json::object &settings_json) { bool success = writeAll(fd, contents) && fsync(fd) == 0; if (close(fd) < 0) success = false; - if (success && rename(temporary_path.c_str(), path.c_str()) < 0) success = false; + if (success && util::replace_file(temporary_path.c_str(), path.string().c_str()) < 0) success = false; +#ifndef _WIN32 // directories cannot be fsynced through the Windows CRT if (success) { int dir_fd = open(path.parent_path().c_str(), O_RDONLY | O_CLOEXEC); success = dir_fd >= 0 && fsync(dir_fd) == 0; if (dir_fd >= 0 && close(dir_fd) < 0) success = false; } +#endif if (!success) { const int saved_errno = errno; unlink(temporary_path.c_str()); - fprintf(stderr, "failed to save Cabana settings to %s: %s\n", path.c_str(), strerror(saved_errno)); + fprintf(stderr, "failed to save Cabana settings to %s: %s\n", path.string().c_str(), strerror(saved_errno)); } return success; } @@ -134,11 +143,11 @@ bool preserveCorruptSettings() { backup = path; backup += ".corrupt." + std::to_string(i); } - if (rename(path.c_str(), backup.c_str()) < 0) { - fprintf(stderr, "failed to preserve corrupt Cabana settings %s: %s\n", path.c_str(), strerror(errno)); + if (util::replace_file(path.string().c_str(), backup.string().c_str()) < 0) { + fprintf(stderr, "failed to preserve corrupt Cabana settings %s: %s\n", path.string().c_str(), strerror(errno)); return false; } - fprintf(stderr, "preserved corrupt Cabana settings at %s\n", backup.c_str()); + fprintf(stderr, "preserved corrupt Cabana settings at %s\n", backup.string().c_str()); return true; } diff --git a/openpilot/tools/cabana/streams/devicestream.cc b/openpilot/tools/cabana/streams/devicestream.cc index 986ca13558c688..84c4efdeb891ad 100644 --- a/openpilot/tools/cabana/streams/devicestream.cc +++ b/openpilot/tools/cabana/streams/devicestream.cc @@ -12,8 +12,14 @@ #include #include #include +#ifdef _WIN32 +#include +#include "common/win32.h" +#else #include +#endif +#include "common/util.h" #include "openpilot/cereal/services.h" #include "tools/cabana/utils/util.h" @@ -27,6 +33,30 @@ DeviceStream::~DeviceStream() { stopBridge(); } +#ifdef _WIN32 +void DeviceStream::stopBridge() { + if (bridge_pid == -1) return; + TerminateProcess((HANDLE)bridge_pid, 0); + WaitForSingleObject((HANDLE)bridge_pid, 3000); + CloseHandle((HANDLE)bridge_pid); + bridge_pid = -1; +} + +void DeviceStream::start() { + if (!zmq_address.empty()) { + stopBridge(); + const std::string path = (executableDir() / "../../cereal/messaging/bridge").lexically_normal().string(); + // the CRT joins argv unquoted into a command line, so the filter's quotes are escaped for the child's parser + const char *argv[] = {path.c_str(), zmq_address.c_str(), "\"/\\\"can/\\\"\"", nullptr}; + if ((bridge_pid = _spawnv(_P_NOWAIT, path.c_str(), argv)) == -1) { + error(std::string("Failed to start bridge: ") + strerror(errno)); + return; + } + } + + LiveStream::start(); +} +#else void DeviceStream::stopBridge() { if (bridge_pid <= 0) return; @@ -92,6 +122,7 @@ void DeviceStream::start() { LiveStream::start(); } +#endif void DeviceStream::streamThread() { zmq_address.empty() ? unsetenv("ZMQ") : setenv("ZMQ", "1", 1); diff --git a/openpilot/tools/cabana/streams/devicestream.h b/openpilot/tools/cabana/streams/devicestream.h index 3770d952aa5c9c..a839b410ed97cf 100644 --- a/openpilot/tools/cabana/streams/devicestream.h +++ b/openpilot/tools/cabana/streams/devicestream.h @@ -17,6 +17,6 @@ class DeviceStream : public LiveStream { void start() override; void streamThread() override; void stopBridge(); - pid_t bridge_pid = -1; + intptr_t bridge_pid = -1; // a process handle on Windows const std::string zmq_address; }; diff --git a/openpilot/tools/cabana/streams/pandastream.cc b/openpilot/tools/cabana/streams/pandastream.cc index 0e72443c7afc2b..6f33e2b19c74f4 100644 --- a/openpilot/tools/cabana/streams/pandastream.cc +++ b/openpilot/tools/cabana/streams/pandastream.cc @@ -60,7 +60,7 @@ void PandaStream::streamThread() { MessageBuilder msg; auto evt = msg.initEvent(); auto canData = evt.initCan(raw_can_data.size()); - for (uint i = 0; i #include "common/tests/native_test.h" +#include "common/util.h" #include "tools/cabana/dbc/dbcfile.h" #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/routes.h" diff --git a/openpilot/tools/cabana/ui/dialogs/streamselector.cc b/openpilot/tools/cabana/ui/dialogs/streamselector.cc index f1d2b1da136558..d3f5af5961c92a 100644 --- a/openpilot/tools/cabana/ui/dialogs/streamselector.cc +++ b/openpilot/tools/cabana/ui/dialogs/streamselector.cc @@ -4,6 +4,7 @@ #include #include +#include "common/util.h" #include "imgui.h" #include "imgui_internal.h" #include "tools/cabana/settings.h" diff --git a/openpilot/tools/cabana/ui/theme.cc b/openpilot/tools/cabana/ui/theme.cc index ff02ca24c7c706..1a1b4e59748cf1 100644 --- a/openpilot/tools/cabana/ui/theme.cc +++ b/openpilot/tools/cabana/ui/theme.cc @@ -59,7 +59,7 @@ ImFont *addFont(const fs::path &path, float size) { ImFontConfig cfg; cfg.OversampleH = 2; cfg.OversampleV = 2; - ImFont *font = ImGui::GetIO().Fonts->AddFontFromFileTTF(path.c_str(), size, &cfg); + ImFont *font = ImGui::GetIO().Fonts->AddFontFromFileTTF(path.string().c_str(), size, &cfg); if (font != nullptr) addIconFont(size, font); return font; } diff --git a/openpilot/tools/cabana/utils/strings.cc b/openpilot/tools/cabana/utils/strings.cc index 3a1191609e9f0f..d22d3e0dbda0a2 100644 --- a/openpilot/tools/cabana/utils/strings.cc +++ b/openpilot/tools/cabana/utils/strings.cc @@ -5,6 +5,7 @@ #include #include +#include "common/util.h" #include "tools/cabana/dbc/dbc.h" namespace utils { diff --git a/openpilot/tools/cabana/utils/util.cc b/openpilot/tools/cabana/utils/util.cc index 1d1c8c75e4c164..561f6775dfcc83 100644 --- a/openpilot/tools/cabana/utils/util.cc +++ b/openpilot/tools/cabana/utils/util.cc @@ -14,13 +14,18 @@ #include #include #include -#include +#ifdef _WIN32 +#include +#define pipe(fds) _pipe(fds, 64, _O_BINARY) +#else #include +#endif #include #ifdef __APPLE__ #include #endif +#include "common/hardware/hw.h" #include "common/util.h" static const std::thread::id main_thread_id = std::this_thread::get_id(); @@ -82,14 +87,14 @@ std::pair SegmentTree::get_minmax(int n, int left, int right, in // UnixSignalHandler UnixSignalHandler::UnixSignalHandler(std::function on_signal) { - if (::socketpair(AF_UNIX, SOCK_STREAM, 0, sig_fd)) { - fprintf(stderr, "Couldn't create TERM socketpair\n"); + if (::pipe(sig_fd)) { + fprintf(stderr, "Couldn't create TERM pipe\n"); abort(); } waiter = std::thread([this, on_signal = std::move(on_signal)]() { int tmp = 0; - while (::read(sig_fd[1], &tmp, sizeof(tmp)) < 0) { + while (::read(sig_fd[0], &tmp, sizeof(tmp)) < 0) { if (errno != EINTR) return; } if (shutting_down.load()) return; @@ -104,14 +109,14 @@ UnixSignalHandler::UnixSignalHandler(std::function on_signal) { UnixSignalHandler::~UnixSignalHandler() { shutting_down.store(true); int dummy = 0; - (void)!::write(sig_fd[0], &dummy, sizeof(dummy)); + (void)!::write(sig_fd[1], &dummy, sizeof(dummy)); if (waiter.joinable()) waiter.join(); ::close(sig_fd[0]); ::close(sig_fd[1]); } void UnixSignalHandler::signalHandler(int s) { - (void)!::write(sig_fd[0], &s, sizeof(s)); + (void)!::write(sig_fd[1], &s, sizeof(s)); } // validators @@ -243,8 +248,7 @@ static std::unordered_map load_bootstrap_icons() { namespace utils { std::string homePath() { - const char *home = ::getenv("HOME"); - return home ? home : ""; + return Path::home(); } std::filesystem::path configPath() { @@ -259,6 +263,9 @@ std::filesystem::path configPath() { #ifdef __APPLE__ static const char *clipboard_read_cmds[] = {"pbpaste"}; static const char *clipboard_write_cmds[] = {"pbcopy"}; +#elif defined(_WIN32) +static const char *clipboard_read_cmds[] = {"powershell -NoProfile -Command Get-Clipboard -Raw"}; +static const char *clipboard_write_cmds[] = {"clip"}; #else static const char *clipboard_read_cmds[] = {"wl-paste --no-newline 2>/dev/null", "xclip -selection clipboard -o 2>/dev/null", "xsel -ob 2>/dev/null"}; static const char *clipboard_write_cmds[] = {"wl-copy 2>/dev/null", "xclip -selection clipboard 2>/dev/null", "xsel -ib 2>/dev/null"}; @@ -284,7 +291,9 @@ bool getClipboardText(std::string *text) { } bool setClipboardText(const std::string &text) { +#ifdef SIGPIPE std::signal(SIGPIPE, SIG_IGN); +#endif for (const char *cmd : clipboard_write_cmds) { FILE *f = ::popen(cmd, "w"); if (!f) continue; @@ -317,6 +326,10 @@ std::filesystem::path executableDir() { std::error_code ec; auto path = std::filesystem::canonical(buf, ec); return (ec ? std::filesystem::path(buf) : path).parent_path(); +#elif defined(_WIN32) + char *exe = nullptr; + _get_pgmptr(&exe); + return std::filesystem::path(exe).parent_path(); #else return std::filesystem::path(util::readlink("/proc/self/exe")).parent_path(); #endif diff --git a/openpilot/tools/jotpluggler/SConscript b/openpilot/tools/jotpluggler/SConscript index bfe6e907427a6f..c1e62e6e4b713b 100644 --- a/openpilot/tools/jotpluggler/SConscript +++ b/openpilot/tools/jotpluggler/SConscript @@ -16,8 +16,8 @@ jot_env["LIBPATH"] += [imgui.MESA_DIR, libusb.LIB_DIR] jot_env["CPPPATH"] += [imgui.INCLUDE_DIR, libusb.INCLUDE_DIR] jot_env["CXXFLAGS"] += [ "-DGLFW_INCLUDE_NONE", - '-DJOTP_REPO_ROOT=\'"%s"\'' % os.path.realpath(BASEDIR), - '-DBOOTSTRAP_ICONS_TTF=\'"%s"\'' % bootstrap_icons.TTF_PATH, + '-DJOTP_REPO_ROOT=\'"%s"\'' % os.path.realpath(BASEDIR).replace(os.sep, "/"), + '-DBOOTSTRAP_ICONS_TTF=\'"%s"\'' % bootstrap_icons.TTF_PATH.as_posix(), ] def materialize_generated_dbcs(target, source, env): @@ -29,10 +29,10 @@ def materialize_generated_dbcs(target, source, env): os.unlink(os.path.join(out_dir, name)) for name, content in sorted(get_generated_dbcs().items()): - with open(os.path.join(out_dir, f"{name}.dbc"), "w") as f: + with open(os.path.join(out_dir, f"{name}.dbc"), "w", encoding="utf-8") as f: f.write(content) - with open(str(target[0]), "w") as f: + with open(str(target[0]), "w", encoding="utf-8") as f: f.write("ok\n") return None @@ -75,7 +75,7 @@ def write_car_fingerprint_to_dbc_header(target, source, env): "", ]) - with open(str(target[0]), "w") as f: + with open(str(target[0]), "w", encoding="utf-8") as f: f.write("\n".join(lines)) return None @@ -105,6 +105,8 @@ libs = [replay_lib, common, messaging, visionipc, cereal, File(f"{imgui.LIB_DIR} ffmpeg_libs + ["zstd", "m", "pthread", "usb-1.0"] if arch == "Darwin": jot_env["FRAMEWORKS"] = ["OpenGL", "Cocoa", "IOKit", "CoreFoundation", "CoreVideo", "CoreMedia", "VideoToolbox"] +elif arch == "Windows": + libs += ["opengl32", "gdi32", "winmm", "shell32", "user32", "setupapi", "dl"] else: libs += ["GL", "dl"] diff --git a/openpilot/tools/jotpluggler/app.cc b/openpilot/tools/jotpluggler/app.cc index e6ba696bae8c95..15d2cbf6890502 100644 --- a/openpilot/tools/jotpluggler/app.cc +++ b/openpilot/tools/jotpluggler/app.cc @@ -257,7 +257,7 @@ void configure_style() { font_cfg.RasterizerDensity = 1.0f; icon_add_font(16.0f); const auto add_font_with_icons = [&](const fs::path &path, float size) -> ImFont * { - ImFont *font = io.Fonts->AddFontFromFileTTF(path.c_str(), size, &font_cfg); + ImFont *font = io.Fonts->AddFontFromFileTTF(path.string().c_str(), size, &font_cfg); if (font != nullptr) { icon_add_font(size, true, font); } diff --git a/openpilot/tools/jotpluggler/common.cc b/openpilot/tools/jotpluggler/common.cc index 50f5fc0b95810f..98dc880cd8409c 100644 --- a/openpilot/tools/jotpluggler/common.cc +++ b/openpilot/tools/jotpluggler/common.cc @@ -148,7 +148,9 @@ bool app_begin_popup_modal(const char *name, bool *p_open, ImGuiWindowFlags flag } void open_external_url(std::string_view url) { -#ifdef __APPLE__ +#if defined(_WIN32) + const std::string command = "start \"\" \"" + std::string(url) + "\""; +#elif defined(__APPLE__) const std::string command = "open " + shell_quote(url) + " &"; #else const std::string command = "xdg-open " + shell_quote(url) + " >/dev/null 2>&1 &"; diff --git a/openpilot/tools/jotpluggler/generate_event_extractors.py b/openpilot/tools/jotpluggler/generate_event_extractors.py index a424ebc237b694..9f9c0d111381d2 100644 --- a/openpilot/tools/jotpluggler/generate_event_extractors.py +++ b/openpilot/tools/jotpluggler/generate_event_extractors.py @@ -183,7 +183,7 @@ def emit_list(self, indent, type_proto, schema, list_expr, path, path_expr, dyna if elem_scalar is not None: self.emit(indent, f"if ({list_expr}.size() <= 16) {{") index_var = self.tmp("i") - self.emit(indent + 2, f"for (uint {index_var} = 0; {index_var} < {list_expr}.size(); ++{index_var}) {{") + self.emit(indent + 2, f"for (unsigned int {index_var} = 0; {index_var} < {list_expr}.size(); ++{index_var}) {{") item_series = self.tmp("item_series") self.emit(indent + 4, f"RouteSeries *{item_series} = ensure_list_scalar_series({base_path_var}, {index_var}, series);") if elem_scalar == "Enum": @@ -195,7 +195,7 @@ def emit_list(self, indent, type_proto, schema, list_expr, path, path_expr, dyna if elem_kind in {"struct", "list"}: index_var = self.tmp("i") - self.emit(indent, f"for (uint {index_var} = 0; {index_var} < {list_expr}.size(); ++{index_var}) {{") + self.emit(indent, f"for (unsigned int {index_var} = 0; {index_var} < {list_expr}.size(); ++{index_var}) {{") item_path = self.tmp("item_path") self.emit(indent + 2, f"const std::string {item_path} = {base_path_var} + \"/\" + std::to_string({index_var});") item = self.tmp("item") diff --git a/openpilot/tools/jotpluggler/icons.cc b/openpilot/tools/jotpluggler/icons.cc index 29edabad4ee3df..55b3a83b9ee793 100644 --- a/openpilot/tools/jotpluggler/icons.cc +++ b/openpilot/tools/jotpluggler/icons.cc @@ -15,7 +15,7 @@ void icon_add_font(float size, bool merge, const ImFont *base_font) { config.GlyphOffset.y = std::round(size * 0.5f - base_center); } static const ImWchar ranges[] = {0xF000, 0xF8FF, 0}; - io.Fonts->AddFontFromFileTTF(ttf.c_str(), size, &config, ranges); + io.Fonts->AddFontFromFileTTF(ttf.string().c_str(), size, &config, ranges); } bool icon_menu_item(const char *glyph, const char *label, const char *shortcut, bool selected, bool enabled) { diff --git a/openpilot/tools/jotpluggler/map.cc b/openpilot/tools/jotpluggler/map.cc index fb4e03c9122a34..0ace8db6d6d8f0 100644 --- a/openpilot/tools/jotpluggler/map.cc +++ b/openpilot/tools/jotpluggler/map.cc @@ -24,6 +24,7 @@ extern "C" { #include #include +#include "common/hardware/hw.h" #include "common/util.h" #include "json11/json11.hpp" @@ -425,8 +426,8 @@ uint64_t fnv1a64(std::string_view text) { } fs::path basemap_cache_root() { - const char *home = std::getenv("HOME"); - fs::path root = home != nullptr ? fs::path(home) / ".comma" : fs::temp_directory_path(); + const std::string home = Path::home(); + fs::path root = !home.empty() ? fs::path(home) / ".comma" : fs::temp_directory_path(); root /= "jotpluggler_vector_map"; fs::create_directories(root); return root; diff --git a/openpilot/tools/jotpluggler/util.cc b/openpilot/tools/jotpluggler/util.cc index 5c20e795f6ba06..0f4f84dec379ae 100644 --- a/openpilot/tools/jotpluggler/util.cc +++ b/openpilot/tools/jotpluggler/util.cc @@ -3,7 +3,9 @@ #include #include #include +#ifndef _WIN32 #include +#endif std::string read_file_or_throw(const std::filesystem::path &path) { const std::string contents = util::read_file(path.string()); diff --git a/openpilot/tools/replay/SConscript b/openpilot/tools/replay/SConscript index 9e060bc82946fb..dcd85188f94511 100644 --- a/openpilot/tools/replay/SConscript +++ b/openpilot/tools/replay/SConscript @@ -8,7 +8,7 @@ base_libs = [common, messaging, cereal, visionipc, 'm', 'pthread'] replay_lib_src = ["replay.cc", "consoleui.cc", "camera.cc", "filereader.cc", "logreader.cc", "framereader.cc", "route.cc", "util.cc", "seg_mgr.cc", "timeline.cc", "py_downloader.cc"] -if arch != "Darwin": +if arch not in ("Darwin", "Windows"): replay_lib_src.append("#openpilot/system/loggerd/encoder/v4l_decoder.cc") replay_lib = replay_env.Library("replay", replay_lib_src, LIBS=base_libs, FRAMEWORKS=base_frameworks) Export('replay_lib') diff --git a/openpilot/tools/replay/framereader.cc b/openpilot/tools/replay/framereader.cc index 19e5fa0d0f0800..86940bec409891 100644 --- a/openpilot/tools/replay/framereader.cc +++ b/openpilot/tools/replay/framereader.cc @@ -40,7 +40,7 @@ struct DecoderManager { } std::unique_ptr decoder; - #ifndef __APPLE__ + #ifdef __linux__ if (!Hardware::PC() && hw_decoder) { decoder = std::make_unique(); } else @@ -268,7 +268,7 @@ bool FFmpegVideoDecoder::copyBuffer(AVFrame *f, VisionBuf *buf) { return true; } -#ifndef __APPLE__ +#ifdef __linux__ bool V4LVideoDecoder::open(AVCodecParameters *codecpar, bool hw_decoder) { if (codecpar->codec_id != AV_CODEC_ID_HEVC) { rError("Hardware decoder only supports HEVC codec"); diff --git a/openpilot/tools/replay/framereader.h b/openpilot/tools/replay/framereader.h index 65feb5b3b72bc3..8407a8cb82d57b 100644 --- a/openpilot/tools/replay/framereader.h +++ b/openpilot/tools/replay/framereader.h @@ -6,7 +6,7 @@ #include "msgq/visionipc/visionbuf.h" #include "tools/replay/util.h" -#ifndef __APPLE__ +#ifdef __linux__ #include "system/loggerd/encoder/v4l_decoder.h" #endif @@ -66,7 +66,7 @@ class FFmpegVideoDecoder : public VideoDecoder { AVBufferRef *hw_device_ctx = nullptr; }; -#ifndef __APPLE__ +#ifdef __linux__ class V4LVideoDecoder : public VideoDecoder { public: V4LVideoDecoder() {}; diff --git a/openpilot/tools/replay/main.cc b/openpilot/tools/replay/main.cc index 50233189a1238d..14ad9e204e4b5a 100644 --- a/openpilot/tools/replay/main.cc +++ b/openpilot/tools/replay/main.cc @@ -9,6 +9,7 @@ #include "common/prefix.h" #include "common/timing.h" +#include "common/util.h" #include "tools/replay/consoleui.h" #include "tools/replay/replay.h" #include "tools/replay/util.h" diff --git a/openpilot/tools/replay/py_downloader.cc b/openpilot/tools/replay/py_downloader.cc index a7ab5baa917a38..cf4dc9afc6c323 100644 --- a/openpilot/tools/replay/py_downloader.cc +++ b/openpilot/tools/replay/py_downloader.cc @@ -5,14 +5,18 @@ #include #include #include +#include +#include +#ifdef _WIN32 +#include "common/win32.h" +#else #include #ifdef __APPLE__ #include #endif #include -#include #include -#include +#endif #include "tools/replay/util.h" @@ -30,6 +34,84 @@ void reportProgress(const char *line) { // Run a Python command and capture stdout. Stderr is scanned for PROGRESS lines and otherwise passed // through to the parent's stderr. Returns stdout content. If abort is signaled, kills the child process. +#ifdef _WIN32 +static void readLines(HANDLE pipe, const std::function &on_line) { + std::string buf; + char chunk[4096]; + for (DWORD n = 0; ReadFile(pipe, chunk, sizeof(chunk), &n, nullptr) && n > 0;) { + buf.append(chunk, n); + for (size_t nl; (nl = buf.find('\n')) != std::string::npos; buf.erase(0, nl + 1)) on_line(buf.substr(0, nl + 1)); + } + if (!buf.empty()) on_line(buf); + CloseHandle(pipe); +} + +std::string runPython(const std::vector &args, std::atomic *abort = nullptr) { + std::string cmdline = "python3 -m openpilot.tools.lib.file_downloader"; + for (const auto &a : args) cmdline += " \"" + a + "\""; + // the child must not inherit OPENPILOT_PREFIX, the parent's IPC namespace + std::string env; + LPCH block = GetEnvironmentStringsA(); + for (const char *e = block; *e; e += strlen(e) + 1) { + if (strncmp(e, "OPENPILOT_PREFIX=", 17) != 0) env.append(e, strlen(e) + 1); + } + FreeEnvironmentStringsA(block); + env.push_back('\0'); + + SECURITY_ATTRIBUTES inheritable = {sizeof(inheritable), nullptr, TRUE}; + HANDLE out[2], err[2]; // read end, write end + if (!CreatePipe(&out[0], &out[1], &inheritable, 0) || !CreatePipe(&err[0], &err[1], &inheritable, 0)) { + rWarning("py_downloader: CreatePipe() failed"); + return {}; + } + SetHandleInformation(out[0], HANDLE_FLAG_INHERIT, 0); + SetHandleInformation(err[0], HANDLE_FLAG_INHERIT, 0); + STARTUPINFOA si = {sizeof(si)}; + si.dwFlags = STARTF_USESTDHANDLES; + si.hStdInput = INVALID_HANDLE_VALUE; + si.hStdOutput = out[1]; + si.hStdError = err[1]; + PROCESS_INFORMATION pi; + bool started = CreateProcessA(nullptr, cmdline.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW, env.data(), nullptr, &si, &pi); + CloseHandle(out[1]); + CloseHandle(err[1]); + if (!started) { + rWarning("py_downloader: CreateProcess() failed: %lu", GetLastError()); + CloseHandle(out[0]); + CloseHandle(err[0]); + return {}; + } + CloseHandle(pi.hThread); + + std::string stdout_data; + std::thread stdout_thread(readLines, out[0], [&](const std::string &line) { stdout_data += line; }); + std::thread stderr_thread(readLines, err[0], [](const std::string &line) { + if (strncmp(line.c_str(), "PROGRESS:", 9) == 0) { + reportProgress(line.c_str()); + } else { + fputs(line.c_str(), stderr); + } + }); + while (WaitForSingleObject(pi.hProcess, 100) == WAIT_TIMEOUT) { + if (abort && *abort) TerminateProcess(pi.hProcess, 1); + } + stdout_thread.join(); + stderr_thread.join(); + DWORD code = 1; + GetExitCodeProcess(pi.hProcess, &code); + CloseHandle(pi.hProcess); + + const bool aborted = abort && *abort; + if (aborted || code != 0) { + if (!aborted) rWarning("py_downloader: process exited with code %lu", code); + std::lock_guard lk(handler_mutex); + if (progress_handler) progress_handler(0, 0, false); + return {}; + } + while (!stdout_data.empty() && (stdout_data.back() == '\n' || stdout_data.back() == '\r')) stdout_data.pop_back(); + return stdout_data; +} +#else std::string runPython(const std::vector &args, std::atomic *abort = nullptr) { // Build argv for the downloader module std::vector argv; @@ -200,6 +282,7 @@ std::string runPython(const std::vector &args, std::atomic *a return stdout_data; } +#endif } // namespace diff --git a/openpilot/tools/replay/util.cc b/openpilot/tools/replay/util.cc index 44e144443abbd7..ceacdcc9334202 100644 --- a/openpilot/tools/replay/util.cc +++ b/openpilot/tools/replay/util.cc @@ -55,21 +55,12 @@ std::string getUrlWithoutQuery(const std::string &url) { } void precise_nano_sleep(int64_t nanoseconds, std::atomic &interrupt_requested) { - struct timespec req, rem; - req.tv_sec = nanoseconds / 1000000000; - req.tv_nsec = nanoseconds % 1000000000; + // sleep in slices so an interrupt is noticed within a few ms on every platform + const auto deadline = std::chrono::steady_clock::now() + std::chrono::nanoseconds(nanoseconds); while (!interrupt_requested) { -#ifdef __APPLE__ - int ret = nanosleep(&req, &rem); - if (ret == 0 || errno != EINTR) - break; -#else - int ret = clock_nanosleep(CLOCK_MONOTONIC, 0, &req, &rem); - if (ret == 0 || ret != EINTR) - break; -#endif - // Retry sleep if interrupted by a signal - req = rem; + const auto left = deadline - std::chrono::steady_clock::now(); + if (left <= left.zero()) break; + std::this_thread::sleep_for(std::min(left, std::chrono::milliseconds(5))); } } @@ -99,10 +90,10 @@ void *MonotonicBuffer::allocate(size_t bytes, size_t alignment) { assert(bytes > 0); void *p = std::align(alignment, bytes, current_buf, available); if (p == nullptr) { - available = next_buffer_size = std::max(next_buffer_size, bytes); - current_buf = buffers.emplace_back(std::aligned_alloc(alignment, next_buffer_size)); + available = next_buffer_size = std::max(next_buffer_size, bytes + alignment); + current_buf = buffers.emplace_back(malloc(next_buffer_size)); next_buffer_size *= growth_factor; - p = current_buf; + p = std::align(alignment, bytes, current_buf, available); } current_buf = (char *)current_buf + bytes; diff --git a/pyproject.toml b/pyproject.toml index 2c97e6444ea37b..de9574d8dda5b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ submodules = [ "opendbc", "pandacan", "rednose", - "teleoprtc", + "teleoprtc; sys_platform != 'win32'", # TODO: drop when libdatachannel-py ships win_amd64 wheels "tinygrad", ] diff --git a/uv.lock b/uv.lock index fe85a470bca963..3d2a6be4f48f71 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,10 @@ version = 1 revision = 3 requires-python = ">=3.12.3, <3.13" +resolution-markers = [ + "sys_platform == 'win32'", + "sys_platform != 'win32'", +] [manifest] overrides = [{ name = "opendbc", editable = "opendbc_repo" }] @@ -622,7 +626,7 @@ submodules = [ { name = "opendbc" }, { name = "pandacan" }, { name = "rednose" }, - { name = "teleoprtc" }, + { name = "teleoprtc", marker = "sys_platform != 'win32'" }, { name = "tinygrad" }, ] testing = [ @@ -677,7 +681,7 @@ requires-dist = [ { name = "scons" }, { name = "setproctitle" }, { name = "sounddevice" }, - { name = "teleoprtc", marker = "extra == 'submodules'", editable = "teleoprtc_repo" }, + { name = "teleoprtc", marker = "sys_platform != 'win32' and extra == 'submodules'", editable = "teleoprtc_repo" }, { name = "tinygrad", marker = "extra == 'submodules'", editable = "tinygrad_repo" }, { name = "tqdm" }, { name = "ty", marker = "extra == 'testing'" }, @@ -1003,7 +1007,7 @@ name = "teleoprtc" version = "1.0.1" source = { editable = "teleoprtc_repo" } dependencies = [ - { name = "libdatachannel-py" }, + { name = "libdatachannel-py", marker = "sys_platform != 'win32'" }, ] [package.metadata] From c354eb400e78409e4c49420a6fd47cbb1a8053af Mon Sep 17 00:00:00 2001 From: Amy Jeanes Date: Wed, 9 Sep 2026 05:50:05 +0100 Subject: [PATCH 02/15] ui, url_file: run on Windows url_file: os.register_at_fork does not exist on Windows. ui: the Windows CRT has no vasprintf for the log callback; a missing AF_UNIX family degrades WifiManager like a missing system D-Bus, and the init worker is skipped then instead of failing on the None router. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_016AgqYZYWLpwE2T5vS3nVEt --- openpilot/system/ui/lib/application.py | 2 ++ openpilot/system/ui/lib/wifi_manager.py | 5 +++-- openpilot/tools/lib/url_file.py | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/openpilot/system/ui/lib/application.py b/openpilot/system/ui/lib/application.py index 230ddc635e2d9e..c92f48ef9b7824 100644 --- a/openpilot/system/ui/lib/application.py +++ b/openpilot/system/ui/lib/application.py @@ -769,6 +769,8 @@ def _begin_scissor_mode_scaled(x, y, width, height): rl.begin_scissor_mode = _begin_scissor_mode_scaled def _set_log_callback(self): + if sys.platform == "win32": + return # no vasprintf in the Windows CRT; raylib keeps its default stdout logging ffi_libc = cffi.FFI() ffi_libc.cdef(""" int vasprintf(char **strp, const char *fmt, void *ap); diff --git a/openpilot/system/ui/lib/wifi_manager.py b/openpilot/system/ui/lib/wifi_manager.py index 26474be942c1d0..221022fb8ba03e 100644 --- a/openpilot/system/ui/lib/wifi_manager.py +++ b/openpilot/system/ui/lib/wifi_manager.py @@ -166,7 +166,7 @@ def __init__(self): _wrap_router(self._router_main) self._conn_monitor = open_dbus_connection_blocking(bus="SYSTEM") # used by state monitor thread self._nm = DBusAddress(NM_PATH, bus_name=NM, interface=NM_IFACE) - except FileNotFoundError: + except (FileNotFoundError, AttributeError): # AttributeError: no AF_UNIX sockets on Windows cloudlog.exception("Failed to connect to system D-Bus") self._router_main = None self._conn_monitor = None @@ -203,7 +203,8 @@ def __init__(self): self._scan_lock = threading.Lock() self._scan_thread = threading.Thread(target=self._network_scanner, daemon=True) self._state_thread = threading.Thread(target=self._monitor_state, daemon=True) - self._initialize() + if not self._exit: # no D-Bus, nothing to scan with + self._initialize() atexit.register(self.stop) def _initialize(self): diff --git a/openpilot/tools/lib/url_file.py b/openpilot/tools/lib/url_file.py index ec0a3d58153c7b..6b80f294651489 100644 --- a/openpilot/tools/lib/url_file.py +++ b/openpilot/tools/lib/url_file.py @@ -217,4 +217,5 @@ def name(self) -> str: return self._url -os.register_at_fork(after_in_child=URLFile.reset) +if hasattr(os, "register_at_fork"): # no fork on Windows + os.register_at_fork(after_in_child=URLFile.reset) From 28ebe97f9beb520fca20c72588f272b3314e8fa9 Mon Sep 17 00:00:00 2001 From: Amy Jeanes Date: Wed, 9 Sep 2026 05:50:06 +0100 Subject: [PATCH 03/15] replay: Windows startup crash and unthrottled playback currentSeconds() subtracted two uint64 timestamps that start 1 ns apart in the wrong order; the wrapped result made ctime() return NULL on the UCRT. winpthreads' clock_nanosleep rejects CLOCK_MONOTONIC, which the sleep loop took as "done sleeping": precise_nano_sleep now sleeps in slices and polls the interrupt flag on every platform, which also replaces the SIGUSR1 wake-up. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_016AgqYZYWLpwE2T5vS3nVEt --- openpilot/tools/replay/replay.cc | 9 --------- openpilot/tools/replay/replay.h | 4 ++-- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/openpilot/tools/replay/replay.cc b/openpilot/tools/replay/replay.cc index 26e10e7c7bbbdc..88cd38ecc49dbf 100644 --- a/openpilot/tools/replay/replay.cc +++ b/openpilot/tools/replay/replay.cc @@ -1,15 +1,12 @@ #include "tools/replay/replay.h" #include -#include #include #include #include "openpilot/cereal/services.h" #include "common/params.h" #include "tools/replay/util.h" -static void interrupt_sleep_handler(int signal) {} - // Helper function to notify events with safety checks template void notifyEvent(Callback &callback, Args &&...args) { @@ -19,8 +16,6 @@ void notifyEvent(Callback &callback, Args &&...args) { Replay::Replay(const std::string &route, std::vector allow, std::vector block, SubMaster *sm, uint32_t flags, const std::string &data_dir, bool auto_source) : sm_(sm), flags_(flags), seg_mgr_(std::make_unique(route, flags, data_dir, auto_source)) { - std::signal(SIGUSR1, interrupt_sleep_handler); - if (flags_ & REPLAY_FLAG_BENCHMARK) { benchmark_stats_.process_start_ts = nanos_since_boot(); seg_mgr_->setBenchmarkCallback([this](int seg_num, const std::string& event) { @@ -104,9 +99,6 @@ bool Replay::load() { } void Replay::interruptStream(const std::function &update_fn) { - if (stream_thread_.joinable() && stream_thread_id) { - pthread_kill(stream_thread_id, SIGUSR1); // Interrupt sleep in stream thread - } { interrupt_requested_ = true; std::unique_lock lock(stream_lock_); @@ -273,7 +265,6 @@ void Replay::publishFrame(const Event *e) { } void Replay::streamThread() { - stream_thread_id = pthread_self(); std::unique_lock lk(stream_lock_); int last_processed_segment = -1; diff --git a/openpilot/tools/replay/replay.h b/openpilot/tools/replay/replay.h index 59e1d67d48a043..7e8477c36412ae 100644 --- a/openpilot/tools/replay/replay.h +++ b/openpilot/tools/replay/replay.h @@ -51,7 +51,8 @@ class Replay { void setLoop(bool loop) { loop ? flags_ &= ~REPLAY_FLAG_NO_LOOP : flags_ |= REPLAY_FLAG_NO_LOOP; } bool loop() const { return !(flags_ & REPLAY_FLAG_NO_LOOP); } const Route &route() const { return seg_mgr_->route_; } - inline double currentSeconds() const { return double(cur_mono_time_ - route_start_ts_) / 1e9; } + // signed: cur_mono_time_ starts 1 ns before route_start_ts_ so the first event is not skipped + inline double currentSeconds() const { return double(int64_t(cur_mono_time_ - route_start_ts_)) / 1e9; } inline std::time_t routeDateTime() const { return route_date_time_; } inline uint64_t routeStartNanos() const { return route_start_ts_; } inline double toSeconds(uint64_t mono_time) const { return (mono_time - route_start_ts_) / 1e9; } @@ -91,7 +92,6 @@ class Replay { std::unique_ptr seg_mgr_; Timeline timeline_; - pthread_t stream_thread_id = 0; std::thread stream_thread_; std::mutex stream_lock_; bool user_paused_ = false; From f23e8c481adcc9be5524691bef70c41fdc786e49 Mon Sep 17 00:00:00 2001 From: Amy Jeanes Date: Wed, 9 Sep 2026 05:50:06 +0100 Subject: [PATCH 04/15] replay: console UI on PDCurses is_termresized() stays true until resize_term() acknowledges it, so every frame rebuilt the screen; chgat() takes a pair number and never changes the character, so the markers were pair 255 blanks; 16-colour terminals reject the 256-colour indices; wbkgd() paints an attribute-only background just on the cells it clears itself. The bookmark and alert markers now draw '_' like the legend, on ncurses too. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_016AgqYZYWLpwE2T5vS3nVEt --- openpilot/tools/replay/consoleui.cc | 43 +++++++++++++++++------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/openpilot/tools/replay/consoleui.cc b/openpilot/tools/replay/consoleui.cc index aa58cc959d6b04..846e26a67ce86d 100644 --- a/openpilot/tools/replay/consoleui.cc +++ b/openpilot/tools/replay/consoleui.cc @@ -72,14 +72,15 @@ ConsoleUI::ConsoleUI(Replay *replay) : replay(replay), sm({"carState", "vehicleP // Initialize all the colors. https://www.ditig.com/256-colors-cheat-sheet start_color(); - init_pair(Color::Debug, 246, COLOR_BLACK); // #949494 - init_pair(Color::Yellow, 184, COLOR_BLACK); + auto color = [](int xterm256, int basic) { return COLORS >= 256 ? xterm256 : basic; }; // 8/16-color terminals + init_pair(Color::Debug, color(246, COLOR_WHITE), COLOR_BLACK); // #949494 + init_pair(Color::Yellow, color(184, COLOR_YELLOW), COLOR_BLACK); init_pair(Color::Red, COLOR_RED, COLOR_BLACK); init_pair(Color::Cyan, COLOR_CYAN, COLOR_BLACK); - init_pair(Color::BrightWhite, 15, COLOR_BLACK); + init_pair(Color::BrightWhite, color(15, COLOR_WHITE), COLOR_BLACK); init_pair(Color::Disengaged, COLOR_BLUE, COLOR_BLUE); - init_pair(Color::Engaged, 28, 28); - init_pair(Color::Green, 34, COLOR_BLACK); + init_pair(Color::Engaged, color(28, COLOR_GREEN), color(28, COLOR_GREEN)); + init_pair(Color::Green, color(34, COLOR_GREEN), COLOR_BLACK); initWindows(); @@ -125,6 +126,7 @@ void ConsoleUI::initWindows() { // set the title bar wbkgd(w[Win::Title], A_REVERSE); + werase(w[Win::Title]); // PDCurses applies a colorless background only to cells it clears itself mvwprintw(w[Win::Title], 0, 3, "openpilot replay %s", COMMA_VERSION); // show windows on the real screen @@ -139,16 +141,20 @@ void ConsoleUI::initWindows() { } void ConsoleUI::updateSize() { - if (is_term_resized(max_height, max_width)) { - for (auto win : w) { - if (win) delwin(win); - } - endwin(); - clear(); - refresh(); - initWindows(); - rWarning("resize term %dx%d", max_height, max_width); +#ifdef PDCURSES + if (!is_termresized()) return; + resize_term(0, 0); // PDCurses reports the resize until it is acknowledged +#else + if (!is_term_resized(max_height, max_width)) return; +#endif + for (auto win : w) { + if (win) delwin(win); } + endwin(); + clear(); + refresh(); + initWindows(); + rWarning("resize term %dx%d", max_height, max_width); } void ConsoleUI::updateStatus() { @@ -260,17 +266,18 @@ void ConsoleUI::updateTimeline() { for (const auto &entry : *replay->getTimeline()) { int start_pos = ((entry.start_time - replay->minSeconds()) / total_sec) * width; int end_pos = ((entry.end_time - replay->minSeconds()) / total_sec) * width; + // chgat takes a color pair, not attribute bits; the markers draw the legend's '_' if (entry.type == TimelineType::Engaged) { - mvwchgat(win, 1, start_pos, end_pos - start_pos + 1, A_COLOR, Color::Engaged, NULL); - mvwchgat(win, 2, start_pos, end_pos - start_pos + 1, A_COLOR, Color::Engaged, NULL); + mvwchgat(win, 1, start_pos, end_pos - start_pos + 1, A_NORMAL, Color::Engaged, NULL); + mvwchgat(win, 2, start_pos, end_pos - start_pos + 1, A_NORMAL, Color::Engaged, NULL); } else if (entry.type == TimelineType::UserBookmark) { - mvwchgat(win, 3, start_pos, end_pos - start_pos + 1, ACS_S3, Color::Cyan, NULL); + mvwhline(win, 3, start_pos, '_' | COLOR_PAIR(Color::Cyan), end_pos - start_pos + 1); } else { auto color_id = Color::Green; if (entry.type != TimelineType::AlertInfo) { color_id = entry.type == TimelineType::AlertWarning ? Color::Yellow : Color::Red; } - mvwchgat(win, 3, start_pos, end_pos - start_pos + 1, ACS_S3, color_id, NULL); + mvwhline(win, 3, start_pos, '_' | COLOR_PAIR(color_id), end_pos - start_pos + 1); } } From a3baffa4a0a7a42086e270b5678444434395c6ba Mon Sep 17 00:00:00 2001 From: Amy Jeanes Date: Wed, 9 Sep 2026 05:50:07 +0100 Subject: [PATCH 05/15] paths: download cache under the platform temp directory /tmp/comma_download_cache resolved to :\tmp on Windows. The cache now lives under Path::tmp_dir() / hw.TMP_DIR: $TEMP, /tmp when unset. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_016AgqYZYWLpwE2T5vS3nVEt --- openpilot/common/hardware/hw.h | 2 +- openpilot/common/hardware/hw.py | 2 +- openpilot/tools/cabana/streams/replaystream.cc | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openpilot/common/hardware/hw.h b/openpilot/common/hardware/hw.h index 72f381cca2bea3..d753d8060025b6 100644 --- a/openpilot/common/hardware/hw.h +++ b/openpilot/common/hardware/hw.h @@ -64,7 +64,7 @@ namespace Path { if (const char *env = getenv("COMMA_CACHE")) { return env; } - return "/tmp/comma_download_cache" + Path::openpilot_prefix() + "/"; + return tmp_dir() + "/comma_download_cache" + Path::openpilot_prefix() + "/"; } inline std::string shm_path() { diff --git a/openpilot/common/hardware/hw.py b/openpilot/common/hardware/hw.py index 4064c3e64346df..2cdf3d30d3bc52 100644 --- a/openpilot/common/hardware/hw.py +++ b/openpilot/common/hardware/hw.py @@ -6,7 +6,7 @@ from openpilot.common.hardware import PC TMP_DIR = os.environ.get("TEMP", "/tmp") # Path::tmp_dir() -DEFAULT_DOWNLOAD_CACHE_ROOT = "/tmp/comma_download_cache" +DEFAULT_DOWNLOAD_CACHE_ROOT = os.path.join(TMP_DIR, "comma_download_cache") class Paths: @staticmethod diff --git a/openpilot/tools/cabana/streams/replaystream.cc b/openpilot/tools/cabana/streams/replaystream.cc index c0ab46abc93707..b0951757e16d2f 100644 --- a/openpilot/tools/cabana/streams/replaystream.cc +++ b/openpilot/tools/cabana/streams/replaystream.cc @@ -2,13 +2,14 @@ #include +#include "common/hardware/hw.h" #include "common/timing.h" #include "common/util.h" #include "tools/cabana/settings.h" ReplayStream::ReplayStream() { unsetenv("ZMQ"); - setenv("COMMA_CACHE", "/tmp/comma_download_cache", 1); + setenv("COMMA_CACHE", (Path::tmp_dir() + "/comma_download_cache").c_str(), 1); op_prefix = std::make_unique(); From 60e0b95914ddbbd3b9366b5aa5d69c32f08f762b Mon Sep 17 00:00:00 2001 From: Amy Jeanes Date: Wed, 9 Sep 2026 05:50:08 +0100 Subject: [PATCH 06/15] messaging: perf_counter for logMonoTime and SubMaster timing time.monotonic() on Windows Python 3.12 ticks every 15.6 ms, so SubMaster measured dt=0 and its frequency checks divided by zero. perf_counter is the same CLOCK_MONOTONIC on Linux and macOS, and on Windows the QueryPerformanceCounter clock 3.13 moved monotonic to. The tests that time SubMaster and logMonoTime read the same clock. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_016AgqYZYWLpwE2T5vS3nVEt --- openpilot/cereal/messaging/__init__.py | 4 ++-- openpilot/cereal/messaging/tests/test_messaging.py | 2 +- openpilot/cereal/messaging/tests/test_pub_sub_master.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openpilot/cereal/messaging/__init__.py b/openpilot/cereal/messaging/__init__.py index dcc54baeb0ff96..a467ed263fbb05 100644 --- a/openpilot/cereal/messaging/__init__.py +++ b/openpilot/cereal/messaging/__init__.py @@ -73,7 +73,7 @@ def log_from_bytes(dat: bytes, struct: capnp.lib.capnp._StructModule = log.Event def new_message(service: str | None, size: int | None = None, **kwargs) -> capnp.lib.capnp._DynamicStructBuilder: args = { 'valid': False, - 'logMonoTime': int(time.monotonic() * 1e9), + 'logMonoTime': int(time.perf_counter() * 1e9), **kwargs } dat = log.Event.new_message(**args) @@ -240,7 +240,7 @@ def update(self, timeout: int = 100) -> None: # non-blocking receive for non-polled sockets for s in self.non_polled_services: msgs.append(recv_one_or_none(self.sock[s])) - self.update_msgs(time.monotonic(), msgs) + self.update_msgs(time.perf_counter(), msgs) def update_msgs(self, cur_time: float, msgs: list[capnp.lib.capnp._DynamicStructReader]) -> None: self.frame += 1 diff --git a/openpilot/cereal/messaging/tests/test_messaging.py b/openpilot/cereal/messaging/tests/test_messaging.py index 462f9dc2a7832a..1856ee8dcffc37 100644 --- a/openpilot/cereal/messaging/tests/test_messaging.py +++ b/openpilot/cereal/messaging/tests/test_messaging.py @@ -54,7 +54,7 @@ def test_new_message(self, evt): msg = messaging.new_message(evt) except capnp.lib.capnp.KjException: msg = messaging.new_message(evt, random.randrange(200)) - assert (time.monotonic() - msg.logMonoTime) < 0.1 + assert (time.perf_counter() - msg.logMonoTime) < 0.1 assert not msg.valid assert evt == msg.which() diff --git a/openpilot/cereal/messaging/tests/test_pub_sub_master.py b/openpilot/cereal/messaging/tests/test_pub_sub_master.py index 24ee68d4fdc966..61ac4bf4a1feb7 100644 --- a/openpilot/cereal/messaging/tests/test_pub_sub_master.py +++ b/openpilot/cereal/messaging/tests/test_pub_sub_master.py @@ -61,9 +61,9 @@ def test_update_timeout(self): sock = random_sock() sm = messaging.SubMaster([sock,]) timeout = random.randrange(10, 30) - start_time = time.monotonic() + start_time = time.perf_counter() sm.update(timeout) - t = time.monotonic() - start_time + t = time.perf_counter() - start_time assert t >= timeout/1000. assert t < 0.1 assert not any(sm.updated.values()) From 7014ac39cd87c6603554fa8d5bf110bad4d3b30a Mon Sep 17 00:00:00 2001 From: Amy Jeanes Date: Wed, 9 Sep 2026 05:50:09 +0100 Subject: [PATCH 07/15] swaglog: skip the zmq teardown at process exit on Windows SwaglogState's destructor runs from DllMain during ExitProcess, after the zmq I/O thread is gone, and zmq_ctx_destroy() then waits forever. The process is going away, so leak the context. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_016AgqYZYWLpwE2T5vS3nVEt --- openpilot/common/swaglog.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpilot/common/swaglog.cc b/openpilot/common/swaglog.cc index 74c617fa611008..d7c9f2b68ca6ce 100644 --- a/openpilot/common/swaglog.cc +++ b/openpilot/common/swaglog.cc @@ -62,6 +62,10 @@ class SwaglogState { } ~SwaglogState() { +#ifdef _WIN32 + // runs from DllMain at process exit, after the zmq I/O thread is gone: zmq_ctx_destroy() would wait forever + return; +#endif zmq_close(sock); zmq_ctx_destroy(zctx); } From 1cacfcfef404b4fe07422d716e458d49adca6d2e Mon Sep 17 00:00:00 2001 From: Amy Jeanes Date: Wed, 9 Sep 2026 05:50:10 +0100 Subject: [PATCH 08/15] tests: make the tool tests pass on Windows Windows spawns instead of forking, so child targets live at module level; NamedTemporaryFile handles close before another process opens the file; binaries are launched by absolute path (CreateProcess resolves ./name against the parent's cwd) and test_native adds the .exe suffix; the hardcoded /tmp becomes TMP_DIR; manager.helpers imports fcntl lazily, since common/test.py imports the manager; a sleep measured with the 15.6 ms monotonic tick can read short, so recv_one_retry's timing uses perf_counter. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_016AgqYZYWLpwE2T5vS3nVEt --- openpilot/cereal/messaging/tests/test_messaging.py | 11 ++++++++--- openpilot/cereal/messaging/tests/test_services.py | 3 ++- openpilot/common/tests/test_file_helpers.py | 3 ++- openpilot/system/manager/helpers.py | 3 ++- openpilot/test_native.py | 3 ++- openpilot/tools/cabana/tests/test_cabana_ui.py | 2 +- openpilot/tools/jotpluggler/test_jotpluggler.py | 2 +- openpilot/tools/lib/tests/test_logreader.py | 3 ++- 8 files changed, 20 insertions(+), 10 deletions(-) diff --git a/openpilot/cereal/messaging/tests/test_messaging.py b/openpilot/cereal/messaging/tests/test_messaging.py index 1856ee8dcffc37..73d23c42fa9f39 100644 --- a/openpilot/cereal/messaging/tests/test_messaging.py +++ b/openpilot/cereal/messaging/tests/test_messaging.py @@ -41,6 +41,11 @@ def assert_carstate(cs1, cs2): if isinstance(val1, numbers.Number): assert val1 == val2, f"{f}: sent '{val1}' vs recvd '{val2}'" +def recv_one_retry_process(sock, timeout): + # module level: Windows spawns the process, and a socket cannot be pickled into it + messaging.recv_one_retry(messaging.sub_sock(sock, timeout=round(timeout * 1000))) + + def delayed_send(delay, sock, dat): def send_func(): sock.send(dat) @@ -148,7 +153,7 @@ def test_recv_one_retry(self): sub_sock = messaging.sub_sock(sock, timeout=round(sock_timeout*1000)) # wait 5 socket timeouts and make sure it's still retrying - p = multiprocessing.Process(target=messaging.recv_one_retry, args=(sub_sock,)) + p = multiprocessing.Process(target=recv_one_retry_process, args=(sock, sock_timeout)) p.start() time.sleep(sock_timeout*5) assert p.is_alive() @@ -156,9 +161,9 @@ def test_recv_one_retry(self): # wait 5 socket timeouts before sending msg = random_carstate() - start_time = time.monotonic() + start_time = time.perf_counter() delayed_send(sock_timeout*5, pub_sock, msg.to_bytes()) recvd = messaging.recv_one_retry(sub_sock) - assert (time.monotonic() - start_time) >= sock_timeout*5 + assert (time.perf_counter() - start_time) >= sock_timeout*5 assert isinstance(recvd, capnp._DynamicStructReader) assert_carstate(msg.carState, recvd.carState) diff --git a/openpilot/cereal/messaging/tests/test_services.py b/openpilot/cereal/messaging/tests/test_services.py index f4c1b81e4f1c6d..ebc306fc876e0b 100644 --- a/openpilot/cereal/messaging/tests/test_services.py +++ b/openpilot/cereal/messaging/tests/test_services.py @@ -17,6 +17,7 @@ def test_services(self, s): assert service.decimation != 0 def test_generated_header(self): - with tempfile.NamedTemporaryFile(suffix=".h") as f: + with tempfile.NamedTemporaryFile(suffix=".h", delete_on_close=False) as f: + f.close() # Windows: other processes cannot open the file while it is open here ret = subprocess.run(f"python3 {services.__file__} > {f.name} && clang++ {f.name} -std=c++11", shell=True).returncode assert ret == 0, "generated services header is not valid C" diff --git a/openpilot/common/tests/test_file_helpers.py b/openpilot/common/tests/test_file_helpers.py index 09b6990ed60554..91cdcd4ab3dbfa 100644 --- a/openpilot/common/tests/test_file_helpers.py +++ b/openpilot/common/tests/test_file_helpers.py @@ -1,13 +1,14 @@ import os from uuid import uuid4 +from openpilot.common.hardware.hw import TMP_DIR from openpilot.common.test import OpenpilotTestCase from openpilot.common.utils import atomic_write class TestFileHelpers(OpenpilotTestCase): def run_atomic_write_func(self, atomic_write_func): - path = f"/tmp/tmp{uuid4()}" + path = os.path.join(TMP_DIR, f"tmp{uuid4()}") with atomic_write_func(path) as f: f.write("test") assert not os.path.exists(path) diff --git a/openpilot/system/manager/helpers.py b/openpilot/system/manager/helpers.py index 453e13184de911..534e7d2d20b232 100644 --- a/openpilot/system/manager/helpers.py +++ b/openpilot/system/manager/helpers.py @@ -1,5 +1,4 @@ import errno -import fcntl import os import sys import pathlib @@ -13,6 +12,8 @@ from openpilot.common.params import Params def unblock_stdout() -> None: + import fcntl # POSIX only, keep the module importable on Windows + # get a non-blocking stdout child_pid, child_pty = os.forkpty() if child_pid != 0: # parent diff --git a/openpilot/test_native.py b/openpilot/test_native.py index eed549f4e47b4b..51a35805952bc2 100644 --- a/openpilot/test_native.py +++ b/openpilot/test_native.py @@ -1,5 +1,6 @@ import os import subprocess +import sysconfig from openpilot.common.basedir import BASEDIR from openpilot.common.parameterized import parameterized @@ -16,7 +17,7 @@ class TestNative(OpenpilotTestCase): @parameterized.expand(NATIVE_TESTS) def test_native(self, executable): - path = os.path.join(BASEDIR, executable) + path = os.path.join(BASEDIR, executable) + sysconfig.get_config_var("EXE") # .exe on Windows if not os.path.exists(path): self.skipTest(f"optional native test was not built: {executable}") subprocess.run([path], check=True) diff --git a/openpilot/tools/cabana/tests/test_cabana_ui.py b/openpilot/tools/cabana/tests/test_cabana_ui.py index aecd7e6ed7f369..17b6b38d53f75e 100644 --- a/openpilot/tools/cabana/tests/test_cabana_ui.py +++ b/openpilot/tools/cabana/tests/test_cabana_ui.py @@ -8,6 +8,6 @@ class TestCabanaUi(OpenpilotTestCase): def test_help(self): - result = subprocess.run(["./cabana", "-h"], cwd=CABANA_DIR, capture_output=True, text=True) + result = subprocess.run([str(CABANA_DIR / "cabana"), "-h"], cwd=CABANA_DIR, capture_output=True, text=True) assert result.returncode == 0, result.stderr assert "Usage:" in result.stderr diff --git a/openpilot/tools/jotpluggler/test_jotpluggler.py b/openpilot/tools/jotpluggler/test_jotpluggler.py index cbcc0a81687316..282f28549d4298 100644 --- a/openpilot/tools/jotpluggler/test_jotpluggler.py +++ b/openpilot/tools/jotpluggler/test_jotpluggler.py @@ -8,6 +8,6 @@ from openpilot.common.test import OpenpilotTestCase class TestJotpluggler(OpenpilotTestCase): def test_help(self): - result = subprocess.run(["./jotpluggler", "-h"], cwd=JOTPLUGGLER_DIR, capture_output=True, text=True) + result = subprocess.run([str(JOTPLUGGLER_DIR / "jotpluggler"), "-h"], cwd=JOTPLUGGLER_DIR, capture_output=True, text=True) assert result.returncode == 0, result.stderr assert "Usage:" in result.stderr diff --git a/openpilot/tools/lib/tests/test_logreader.py b/openpilot/tools/lib/tests/test_logreader.py index 9d5691abd881cd..3c78ab467f8ad6 100644 --- a/openpilot/tools/lib/tests/test_logreader.py +++ b/openpilot/tools/lib/tests/test_logreader.py @@ -259,7 +259,8 @@ def test_sort_by_time(self): assert msgs == sorted(msgs, key=lambda m: m.logMonoTime) def test_only_union_types(self): - with tempfile.NamedTemporaryFile() as qlog: + with tempfile.NamedTemporaryFile(delete_on_close=False) as qlog: + qlog.close() # Windows: the file cannot be reopened while this handle is open # write valid Event messages num_msgs = 100 with open(qlog.name, "wb") as f: From a6a89fffa46238c6aaaf1ec1f90f3c49e24f18d9 Mon Sep 17 00:00:00 2001 From: Amy Jeanes Date: Wed, 9 Sep 2026 05:50:11 +0100 Subject: [PATCH 09/15] tools: set up openpilot on Windows with op.sh The MSYS2 CLANG64 shell is the build environment: pacman installs the toolchain, uv and MSYS2's native git (the venv's git-lfs mis-resolves the POSIX paths the Cygwin-style git package reports). The venv lives in Scripts/ and gets a python3.exe for the build's shebangs. pycapnp's wheel links the Visual C++ runtime, so the setup checks for it. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_016AgqYZYWLpwE2T5vS3nVEt --- tools/README.md | 6 ++++-- tools/op.sh | 36 ++++++++++++++++++++++++++---------- tools/setup_dependencies.sh | 19 ++++++++++++++++++- 3 files changed, 48 insertions(+), 13 deletions(-) diff --git a/tools/README.md b/tools/README.md index ae36282828fcea..423f60e0fdc83c 100644 --- a/tools/README.md +++ b/tools/README.md @@ -4,12 +4,14 @@ openpilot is developed and tested on **Ubuntu 24.04**, which is the primary development target aside from the [supported embedded hardware](https://github.com/commaai/openpilot#running-on-a-dedicated-device-in-a-car). -Most of openpilot should work natively on macOS. On Windows you can use WSL for a nearly native Ubuntu experience. Running natively on any other system is not currently recommended and will likely require modifications. +Most of openpilot should work natively on macOS and, for development only, on Windows. On Windows you can also use WSL for a nearly native Ubuntu experience. Running natively on any other system is not currently recommended and will likely require modifications. -## Native setup on Ubuntu 24.04 and macOS +## Native setup on Ubuntu 24.04, macOS and Windows Follow these instructions for a fully managed setup experience. If you'd like to manage the dependencies yourself, just read the setup scripts in this directory. +On Windows, the tools (cabana, replay, jotpluggler, the UI) build natively for development only, from an [MSYS2](https://www.msys2.org/) CLANG64 shell: install MSYS2, run `pacman -S mingw-w64-clang-x86_64-git` in that shell and follow the same steps there, activating the venv with `source .venv/Scripts/activate`. The setup also needs the [Visual C++ Redistributable](https://aka.ms/vc14/vc_redist.x64.exe), which most machines already have. + **1. Clone openpilot** ``` bash git clone https://github.com/commaai/openpilot.git diff --git a/tools/op.sh b/tools/op.sh index f17714e620d259..9722bf9293bd76 100755 --- a/tools/op.sh +++ b/tools/op.sh @@ -20,6 +20,13 @@ if [ "$(uname)" == "Darwin" ] && [ $SHELL == "/bin/bash" ]; then RC_FILE="$HOME/.bash_profile" fi +# Windows builds run in an MSYS2 CLANG64 shell with a native Python, whose venv keeps its scripts in Scripts/ +VENV_BIN="bin" +if [[ "$(uname -s)" == MINGW* || "$(uname -s)" == MSYS* ]]; then + VENV_BIN="Scripts" + export PYTHONUTF8=1 # redirected output would use the ANSI code page otherwise +fi + function retry() { local attempts=$1 shift @@ -126,6 +133,12 @@ function op_check_os() { echo -e " ↳ [${GREEN}✔${NC}] Linux detected." elif [[ "$OSTYPE" == "darwin"* ]]; then echo -e " ↳ [${GREEN}✔${NC}] macOS detected." + elif [[ "$(uname -s)" == MINGW* || "$(uname -s)" == MSYS* ]]; then + if [[ "${MSYSTEM:-}" != "CLANG64" ]]; then + echo -e " ↳ [${RED}✗${NC}] Windows needs an MSYS2 CLANG64 shell, this is ${MSYSTEM:-not MSYS2}!" + return 1 + fi + echo -e " ↳ [${GREEN}✔${NC}] Windows (MSYS2 CLANG64) detected." else echo -e " ↳ [${RED}✗${NC}] OS type $OSTYPE not supported!" return 1 @@ -134,7 +147,7 @@ function op_check_os() { function op_check_venv() { echo "Checking for venv..." - if [[ -f $OPENPILOT_ROOT/.venv/bin/activate ]]; then + if [[ -f $OPENPILOT_ROOT/.venv/$VENV_BIN/activate ]]; then echo -e " ↳ [${GREEN}✔${NC}] venv detected." else echo -e " ↳ [${RED}✗${NC}] Can't activate venv in $OPENPILOT_ROOT. Assuming global env!" @@ -201,16 +214,17 @@ EOF fi et="$(date +%s)" echo -e " ↳ [${GREEN}✔${NC}] Dependencies installed successfully in $((et - st)) seconds." + hash -r # setup may have installed a git ahead of the one this shell already found op_activate_venv echo "Pulling git lfs files..." st="$(date +%s)" - git config --local filter.lfs.clean ".venv/bin/git-lfs clean -- %f" - git config --local filter.lfs.smudge ".venv/bin/git-lfs smudge -- %f" - git config --local filter.lfs.process ".venv/bin/git-lfs filter-process" + git config --local filter.lfs.clean ".venv/$VENV_BIN/git-lfs clean -- %f" + git config --local filter.lfs.smudge ".venv/$VENV_BIN/git-lfs smudge -- %f" + git config --local filter.lfs.process ".venv/$VENV_BIN/git-lfs filter-process" git config --local filter.lfs.required true - printf '#!/bin/sh\nexec .venv/bin/git-lfs pre-push "$@"\n' > "$(git rev-parse --git-path hooks)/pre-push" + printf '#!/bin/sh\nexec .venv/%s/git-lfs pre-push "$@"\n' "$VENV_BIN" > "$(git rev-parse --git-path hooks)/pre-push" chmod +x "$(git rev-parse --git-path hooks)/pre-push" if ! retry 3 git lfs pull; then echo -e " ↳ [${RED}✗${NC}] Pulling git lfs files failed!" @@ -230,19 +244,21 @@ function op_auth() { function op_activate_venv() { # bash 3.2 can't handle this without the 'set +e' set +e - source $OPENPILOT_ROOT/.venv/bin/activate &> /dev/null || true + source $OPENPILOT_ROOT/.venv/$VENV_BIN/activate &> /dev/null || true set -e # persist venv on PATH across GitHub Actions steps if [ -n "$GITHUB_PATH" ]; then - echo "$OPENPILOT_ROOT/.venv/bin" >> "$GITHUB_PATH" + VENV_PATH="$OPENPILOT_ROOT/.venv/$VENV_BIN" + command -v cygpath > /dev/null && VENV_PATH="$(cygpath -w "$VENV_PATH")" # the runner's PATH is a Windows one + echo "$VENV_PATH" >> "$GITHUB_PATH" fi } function op_venv() { op_before_cmd - if [[ ! -f $OPENPILOT_ROOT/.venv/bin/activate ]]; then + if [[ ! -f $OPENPILOT_ROOT/.venv/$VENV_BIN/activate ]]; then echo -e "No venv found in $OPENPILOT_ROOT" return 1 fi @@ -250,10 +266,10 @@ function op_venv() { case $SHELL_NAME in "zsh") ZSHRC_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'tmp_zsh') - echo "source $RC_FILE; source $OPENPILOT_ROOT/.venv/bin/activate" >> $ZSHRC_DIR/.zshrc + echo "source $RC_FILE; source $OPENPILOT_ROOT/.venv/$VENV_BIN/activate" >> $ZSHRC_DIR/.zshrc ZDOTDIR=$ZSHRC_DIR zsh ;; *) - bash --rcfile <(echo "source $RC_FILE; source $OPENPILOT_ROOT/.venv/bin/activate") ;; + bash --rcfile <(echo "source $RC_FILE; source $OPENPILOT_ROOT/.venv/$VENV_BIN/activate") ;; esac } diff --git a/tools/setup_dependencies.sh b/tools/setup_dependencies.sh index 7af2180686cacc..6fcc5042d3ab05 100755 --- a/tools/setup_dependencies.sh +++ b/tools/setup_dependencies.sh @@ -4,6 +4,9 @@ set -e DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" ROOT="$(git -C "$DIR" rev-parse --show-toplevel)" +VENV_BIN="bin" +case "$(uname -s)" in MINGW*|MSYS*) VENV_BIN="Scripts" ;; esac # native Python venv layout on Windows + function retry() { local attempts=$1 shift @@ -97,6 +100,16 @@ function install_linux_deps() { fi } +function install_windows_deps() { + [[ "${MSYSTEM:-}" == "CLANG64" ]] || { echo "Windows builds need an MSYS2 CLANG64 shell, this is ${MSYSTEM:-not MSYS2}"; exit 1; } + # pycapnp's wheel links the MSVC C++ runtime, which neither Python nor a fresh Windows ships + [[ -f "$(cygpath -u "$SYSTEMROOT")/System32/msvcp140.dll" ]] || { echo "install the Visual C++ Redistributable https://aka.ms/vc14/vc_redist.x64.exe and rerun"; exit 1; } + # clang/lld/libc++ (MSVC cannot build openpilot's GNU C), dlfcn, MSYS2's native git and uv (the venv's git-lfs mis-resolves msys git's POSIX paths) + pacman -S --needed --noconfirm \ + "$MINGW_PACKAGE_PREFIX-toolchain" "$MINGW_PACKAGE_PREFIX-pkgconf" "$MINGW_PACKAGE_PREFIX-ccache" \ + "$MINGW_PACKAGE_PREFIX-dlfcn" "$MINGW_PACKAGE_PREFIX-git" "$MINGW_PACKAGE_PREFIX-uv" file +} + function install_python_deps() { # Increase the pip timeout to handle TimeoutError export PIP_DEFAULT_TIMEOUT=200 @@ -117,7 +130,8 @@ function install_python_deps() { echo "installing python packages..." uv sync --frozen --all-extras - source .venv/bin/activate + [[ $VENV_BIN == bin ]] || cp -n .venv/Scripts/python.exe .venv/Scripts/python3.exe # scons commands and shebangs say python3 + source .venv/$VENV_BIN/activate } # --- Main --- @@ -131,6 +145,9 @@ elif [[ "$OSTYPE" == "darwin"* ]]; then elif [[ $SHELL == "/bin/bash" ]]; then RC_FILE="$HOME/.bash_profile" fi +elif [[ "$(uname -s)" == MINGW* || "$(uname -s)" == MSYS* ]]; then + install_windows_deps + echo "[ ] installed system dependencies t=$SECONDS" fi if [ -f "$ROOT/pyproject.toml" ]; then From c8013d8cfc6fbe092ac05122e96b87554200e4ec Mon Sep 17 00:00:00 2001 From: Amy Jeanes Date: Wed, 9 Sep 2026 05:50:11 +0100 Subject: [PATCH 10/15] CI: build and run the tool tests on windows-latest A build_windows job next to build_mac: op.sh setup and build in the MSYS2 CLANG64 shell the README describes, then the tests that cover what Windows is for (common, messaging, cabana, jotpluggler, tools/lib, the UI library). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_016AgqYZYWLpwE2T5vS3nVEt --- .github/workflows/tests.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index b8b1ace97aa27d..5dbb2038bbe222 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -76,6 +76,24 @@ jobs: - name: Building openpilot run: scons + build_windows: + name: build Windows + runs-on: windows-latest + defaults: + run: + shell: msys2 {0} + steps: + - uses: actions/checkout@v7 + - uses: msys2/setup-msys2@v2 + with: + msystem: CLANG64 + install: mingw-w64-clang-x86_64-git # the README's first step, the setup installs the rest + - run: ./tools/op.sh setup + - name: Building openpilot + run: tools/op.sh build # activates the venv: the msys2 shell puts its own python3 ahead of it + - name: Run the tool tests + timeout-minutes: 15 + run: tools/op.sh test openpilot/common openpilot/cereal/messaging openpilot/tools/cabana openpilot/tools/jotpluggler openpilot/tools/lib openpilot/system/ui openpilot/test_native.py static_analysis: name: static analysis runs-on: ${{ From ca1087b832b54e2a19d7857459ae05aaed5c3634 Mon Sep 17 00:00:00 2001 From: Amy Jeanes Date: Thu, 10 Sep 2026 11:55:44 +0100 Subject: [PATCH 11/15] build, tools: handle spaces in the install path Quote paths so an install directory containing spaces works. op.sh and setup.sh quote $OPENPILOT_ROOT; the cabana and jotpluggler string defines use escaped double quotes rather than '"..."', whose outer single quotes SCons' argument quoting drops and splits the path; the acados and modeld build commands quote their embedded paths. Cross-platform: the same paths break on Linux, spaces are just far more common on Windows. The PC tools (cabana, replay, jotpluggler, the UI) build and run from a spaced path; on Linux a full build, modeld and acados codegen included, does too. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_016AgqYZYWLpwE2T5vS3nVEt --- .../lib/longitudinal_mpc_lib/SConscript | 12 +++++----- openpilot/selfdrive/modeld/SConscript | 14 ++++++------ openpilot/tools/cabana/SConscript | 6 ++--- openpilot/tools/jotpluggler/SConscript | 4 ++-- tools/op.sh | 22 +++++++++---------- tools/setup.sh | 6 ++--- 6 files changed, 33 insertions(+), 31 deletions(-) diff --git a/openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript b/openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript index 7a3a12b85558b8..e25c692d037a90 100644 --- a/openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript +++ b/openpilot/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript @@ -1,3 +1,5 @@ +import os + Import('env', 'envCython', 'arch', 'msgq_python', 'common_python', 'np_version', 'acados') gen = "c_generated_code" @@ -73,7 +75,7 @@ elif arch == "Darwin": lenv.Clean(generated_files, Dir(gen)) generated_long = lenv.Command(generated_files, source_list, - f"cd {Dir('.').abspath} && python3 long_mpc.py") + f'cd "{Dir(".").abspath.replace(os.sep, "/")}" && python3 long_mpc.py') lenv.Depends(generated_long, [msgq_python, common_python]) lenv["CFLAGS"].append("-DACADOS_WITH_QPOASES") @@ -100,10 +102,10 @@ lenv2["RPATH"] += [lenv2.Literal('\\$$ORIGIN')] lenv2.Command(libacados_ocp_solver_c, [acados_ocp_solver_pyx, acados_ocp_solver_common, libacados_ocp_solver_pxd], f'cython' + \ - f' -o {libacados_ocp_solver_c.abspath}' + \ - f' -I {libacados_ocp_solver_pxd.get_dir().abspath}' + \ - f' -I {acados_ocp_solver_common.get_dir().abspath}' + \ - f' {acados_ocp_solver_pyx.abspath}') + f' -o "{libacados_ocp_solver_c.abspath.replace(os.sep, "/")}"' + \ + f' -I "{libacados_ocp_solver_pxd.get_dir().abspath.replace(os.sep, "/")}"' + \ + f' -I "{acados_ocp_solver_common.get_dir().abspath.replace(os.sep, "/")}"' + \ + f' "{acados_ocp_solver_pyx.abspath.replace(os.sep, "/")}"') lib_cython = lenv2.Program(f'{gen}/acados_ocp_solver_pyx.so', [libacados_ocp_solver_c], LIBS=lenv2["LIBS"] + ['acados_ocp_solver_long']) lenv2.Depends(lib_cython, lib_solver) lenv2.Depends(lib_cython, copied_acados_libs) diff --git a/openpilot/selfdrive/modeld/SConscript b/openpilot/selfdrive/modeld/SConscript index 9a10fed585ac3d..6ed0a1a3e67678 100644 --- a/openpilot/selfdrive/modeld/SConscript +++ b/openpilot/selfdrive/modeld/SConscript @@ -75,11 +75,11 @@ for chestnut in [False, True] if CHESTNUT else [False]: camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in camera_configs) # CPU 7 is isolated with isolcpus on AGNOS, so explicitly pin the compiler to it. taskset = 'taskset -c 7 ' if arch == 'comma_arm64' else '' - cmd = (f'{cmd_flags} {mac_brew_string} {taskset}python3 {modeld_dir}/compile_modeld.py ' + cmd = (f'{cmd_flags} {mac_brew_string} {taskset}python3 "{modeld_dir}/compile_modeld.py" ' f'--model-size {model_w}x{model_h} ' f'--camera-resolutions {camera_res_args} ' - f'--onnx {File(f"models/{file_prefix}driving_supercombo.onnx").abspath} ' - f'--output {target_pkl_path} --frame-skip {frame_skip}') + f'--onnx "{File(f"models/{file_prefix}driving_supercombo.onnx").abspath}" ' + f'--output "{target_pkl_path}" --frame-skip {frame_skip}') onnx_sizes_sum = sum(os.path.getsize(f) for f in driving_onnx_deps) chunk_targets = get_chunk_targets(target_pkl_path, estimate_pickle_max_size(onnx_sizes_sum * len(camera_configs))) def do_compile(target, source, env, command=cmd, pkl=target_pkl_path, chunks=chunk_targets): @@ -109,16 +109,16 @@ for chestnut in [False, True] if CHESTNUT else [False]: # get model metadata fn = File(f"models/dmonitoring_model").abspath script_files = [File(Dir("#openpilot/selfdrive/modeld").File("get_model_metadata.py").abspath)] -cmd = f'{tg_flags} {mac_brew_string} python3 {Dir("#openpilot/selfdrive/modeld").abspath}/get_model_metadata.py {fn}.onnx' +cmd = f'{tg_flags} {mac_brew_string} python3 "{Dir("#openpilot/selfdrive/modeld").abspath}/get_model_metadata.py" "{fn}.onnx"' lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"] + tinygrad_files + script_files + [tg_devices_node], cmd) dm_w, dm_h = DM_INPUT_SIZE compile_dm_warp_script = [File(f"{modeld_dir}/compile_dm_warp.py")] for cam_w, cam_h in camera_configs: dm_pkl_path = File(f"models/dm_warp_{cam_w}x{cam_h}_tinygrad.pkl").abspath - cmd = (f'{tg_flags} {mac_brew_string} python3 {modeld_dir}/compile_dm_warp.py ' + cmd = (f'{tg_flags} {mac_brew_string} python3 "{modeld_dir}/compile_dm_warp.py" ' f'--camera-resolution {cam_w}x{cam_h} --warp-to {dm_w}x{dm_h} ' - f'--output {dm_pkl_path}') + f'--output "{dm_pkl_path}"') lenv.Command(dm_pkl_path, tinygrad_files + compile_dm_warp_script + compile_modeld_script + [tg_devices_node], cmd) def tg_compile(flags, model_name): @@ -132,7 +132,7 @@ def tg_compile(flags, model_name): return lenv.Command( chunk_targets, [onnx_path] + tinygrad_files + [Value(chunk_targets), chunker_file, tg_devices_node], - [f'{pythonpath_string} {flags} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {pkl}', + [f'{pythonpath_string} {flags} python3 "{Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py" "{fn}.onnx" "{pkl}"', Action(do_chunk, " [CHUNK] $TARGET")], ) diff --git a/openpilot/tools/cabana/SConscript b/openpilot/tools/cabana/SConscript index 5117f1d6a7d466..9878e47ae9955e 100644 --- a/openpilot/tools/cabana/SConscript +++ b/openpilot/tools/cabana/SConscript @@ -7,7 +7,7 @@ from openpilot.common.basedir import BASEDIR Import('env', 'arch', 'common', 'messaging', 'visionipc', 'cereal', 'replay_lib', 'ffmpeg_libs') -opendbc_path = '-DOPENDBC_FILE_PATH=\'"%s"\'' % (env.Dir("../../../opendbc_repo/opendbc/dbc").abspath.replace(os.sep, "/")) +opendbc_path = '-DOPENDBC_FILE_PATH=\\"%s\\"' % (env.Dir("../../../opendbc_repo/opendbc/dbc").abspath.replace(os.sep, "/")) # embed the bootstrap icons SVG into the binary def build_bootstrap_icons_src(target, source, env): @@ -39,8 +39,8 @@ ui_env['LIBPATH'] += [imgui.MESA_DIR, libusb.LIB_DIR] ui_env['CXXFLAGS'] += [ opendbc_path, "-DGLFW_INCLUDE_NONE", - '-DCABANA_FONTS_DIR=\'"%s"\'' % os.path.join(os.path.realpath(BASEDIR), "openpilot", "selfdrive", "assets", "fonts").replace(os.sep, "/"), - '-DBOOTSTRAP_ICONS_TTF=\'"%s"\'' % bootstrap_icons.TTF_PATH.as_posix(), + '-DCABANA_FONTS_DIR=\\"%s\\"' % os.path.join(os.path.realpath(BASEDIR), "openpilot", "selfdrive", "assets", "fonts").replace(os.sep, "/"), + '-DBOOTSTRAP_ICONS_TTF=\\"%s\\"' % bootstrap_icons.TTF_PATH.as_posix(), ] ui_objs = [ui_env.Object('ui/obj/' + src.replace('/', '_')[:-3], src) for src in core_srcs] ui_objs += [ui_env.Object('ui/obj/bootstrap_icons', bootstrap_icons_src)] diff --git a/openpilot/tools/jotpluggler/SConscript b/openpilot/tools/jotpluggler/SConscript index c1e62e6e4b713b..d7f477547f901d 100644 --- a/openpilot/tools/jotpluggler/SConscript +++ b/openpilot/tools/jotpluggler/SConscript @@ -16,8 +16,8 @@ jot_env["LIBPATH"] += [imgui.MESA_DIR, libusb.LIB_DIR] jot_env["CPPPATH"] += [imgui.INCLUDE_DIR, libusb.INCLUDE_DIR] jot_env["CXXFLAGS"] += [ "-DGLFW_INCLUDE_NONE", - '-DJOTP_REPO_ROOT=\'"%s"\'' % os.path.realpath(BASEDIR).replace(os.sep, "/"), - '-DBOOTSTRAP_ICONS_TTF=\'"%s"\'' % bootstrap_icons.TTF_PATH.as_posix(), + '-DJOTP_REPO_ROOT=\\"%s\\"' % os.path.realpath(BASEDIR).replace(os.sep, "/"), + '-DBOOTSTRAP_ICONS_TTF=\\"%s\\"' % bootstrap_icons.TTF_PATH.as_posix(), ] def materialize_generated_dbcs(target, source, env): diff --git a/tools/op.sh b/tools/op.sh index 9722bf9293bd76..b82eb8cc079986 100755 --- a/tools/op.sh +++ b/tools/op.sh @@ -81,10 +81,10 @@ function op_get_openpilot_dir() { function op_install_post_commit() { op_get_openpilot_dir if [[ ! -d $OPENPILOT_ROOT/.git/hooks/post-commit.d ]]; then - mkdir $OPENPILOT_ROOT/.git/hooks/post-commit.d - mv $OPENPILOT_ROOT/.git/hooks/post-commit $OPENPILOT_ROOT/.git/hooks/post-commit.d 2>/dev/null || true + mkdir "$OPENPILOT_ROOT/.git/hooks/post-commit.d" + mv "$OPENPILOT_ROOT/.git/hooks/post-commit" "$OPENPILOT_ROOT/.git/hooks/post-commit.d" 2>/dev/null || true fi - cd $OPENPILOT_ROOT/.git/hooks + cd "$OPENPILOT_ROOT/.git/hooks" ln -sf ../../scripts/post-commit post-commit } @@ -110,7 +110,7 @@ function op_check_git() { fi echo "Checking for git lfs files..." - if [[ $(file -b $OPENPILOT_ROOT/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx) == "data" ]]; then + if [[ $(file -b "$OPENPILOT_ROOT/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx") == "data" ]]; then echo -e " ↳ [${GREEN}✔${NC}] git lfs files found." else echo -e " ↳ [${RED}✗${NC}] git lfs files not found! Run 'git lfs pull'" @@ -119,7 +119,7 @@ function op_check_git() { echo "Checking for git submodules..." for name in $(git config --file .gitmodules --get-regexp path | awk '{ print $2 }' | tr '\n' ' '); do - if [[ -z $(ls $OPENPILOT_ROOT/$name) ]]; then + if [[ -z $(ls "$OPENPILOT_ROOT/$name") ]]; then echo -e " ↳ [${RED}✗${NC}] git submodule $name not found! Run 'git submodule update --init --recursive'" return 1 fi @@ -160,7 +160,7 @@ function op_before_cmd() { fi op_get_openpilot_dir - cd $OPENPILOT_ROOT + cd "$OPENPILOT_ROOT" result="$((op_check_openpilot_dir ) 2>&1)" || (echo -e "$result" && return 1) result="${result}\n$(( op_check_git ) 2>&1)" || (echo -e "$result" && return 1) @@ -189,7 +189,7 @@ EOF echo -e " ↳ [${GREEN}✔${NC}] op installed successfully. Open a new shell to use it." op_get_openpilot_dir - cd $OPENPILOT_ROOT + cd "$OPENPILOT_ROOT" op_check_openpilot_dir op_check_os @@ -208,7 +208,7 @@ EOF echo "Installing dependencies..." st="$(date +%s)" SETUP_SCRIPT="tools/setup_dependencies.sh" - if ! $OPENPILOT_ROOT/$SETUP_SCRIPT; then + if ! "$OPENPILOT_ROOT/$SETUP_SCRIPT"; then echo -e " ↳ [${RED}✗${NC}] Dependencies installation failed!" return 1 fi @@ -244,7 +244,7 @@ function op_auth() { function op_activate_venv() { # bash 3.2 can't handle this without the 'set +e' set +e - source $OPENPILOT_ROOT/.venv/$VENV_BIN/activate &> /dev/null || true + source "$OPENPILOT_ROOT/.venv/$VENV_BIN/activate" &> /dev/null || true set -e # persist venv on PATH across GitHub Actions steps @@ -266,10 +266,10 @@ function op_venv() { case $SHELL_NAME in "zsh") ZSHRC_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'tmp_zsh') - echo "source $RC_FILE; source $OPENPILOT_ROOT/.venv/$VENV_BIN/activate" >> $ZSHRC_DIR/.zshrc + echo "source \"$RC_FILE\"; source \"$OPENPILOT_ROOT/.venv/$VENV_BIN/activate\"" >> "$ZSHRC_DIR/.zshrc" ZDOTDIR=$ZSHRC_DIR zsh ;; *) - bash --rcfile <(echo "source $RC_FILE; source $OPENPILOT_ROOT/.venv/$VENV_BIN/activate") ;; + bash --rcfile <(echo "source \"$RC_FILE\"; source \"$OPENPILOT_ROOT/.venv/$VENV_BIN/activate\"") ;; esac } diff --git a/tools/setup.sh b/tools/setup.sh index ced451ab11adfc..054d7f7755321a 100755 --- a/tools/setup.sh +++ b/tools/setup.sh @@ -127,10 +127,10 @@ function git_clone() { } function install_with_op() { - cd $OPENPILOT_ROOT - $OPENPILOT_ROOT/tools/op.sh post-commit + cd "$OPENPILOT_ROOT" + "$OPENPILOT_ROOT/tools/op.sh" post-commit - if ! $OPENPILOT_ROOT/tools/op.sh setup; then + if ! "$OPENPILOT_ROOT/tools/op.sh" setup; then echo -e "\n[${RED}✗${NC}] failed to install openpilot!" return 1 fi From 169e7313fdb2ee171da10c889fe39674fa1a4a55 Mon Sep 17 00:00:00 2001 From: Amy Jeanes Date: Thu, 10 Sep 2026 15:34:42 +0100 Subject: [PATCH 12/15] lint: make op lint work on Windows Three things broke op lint on Windows, all latent on Linux too: - lint.sh passed the whole openpilot/ file list (~48k of paths) as one argv, over Windows' ~32k command-line limit, so the file-list checks failed with "Argument list too long". Batch the arguments through xargs. - check_shebang_format flagged the LFS-tracked updater zipapp, whose real "#!/usr/bin/env python3" line sits over a ZIP payload, as a bad shebang because grep reported the binary as a match. Skip binaries with grep -I. - A spaced install path broke `cd $ROOT` and the check invocations. Quote $ROOT and $DIR. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_016AgqYZYWLpwE2T5vS3nVEt --- scripts/lint/check_shebang_format.sh | 5 +++-- scripts/lint/lint.sh | 21 +++++++++++++-------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/scripts/lint/check_shebang_format.sh b/scripts/lint/check_shebang_format.sh index 89b95d5929a3b2..29965ac15d35b1 100755 --- a/scripts/lint/check_shebang_format.sh +++ b/scripts/lint/check_shebang_format.sh @@ -2,12 +2,13 @@ FAIL=0 -if grep '^#!.*python' $@ | grep -v '#!/usr/bin/env python3$'; then +# -I skips binaries: a smudged LFS zipapp has a real python3 shebang over binary data. +if grep -I '^#!.*python' $@ | grep -v '#!/usr/bin/env python3$'; then echo -e "Invalid shebang! Must use '#!/usr/bin/env python3'\n" FAIL=1 fi -if grep '^#!.*bash' $@ | grep -v '#!/usr/bin/env bash$'; then +if grep -I '^#!.*bash' $@ | grep -v '#!/usr/bin/env bash$'; then echo -e "Invalid shebang! Must use '#!/usr/bin/env bash'" FAIL=1 fi diff --git a/scripts/lint/lint.sh b/scripts/lint/lint.sh index fe3644d67086ff..51122ebf15fef3 100755 --- a/scripts/lint/lint.sh +++ b/scripts/lint/lint.sh @@ -9,7 +9,7 @@ NC='\033[0m' DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" ROOT="$DIR/../../" -cd $ROOT +cd "$ROOT" FAILED=0 @@ -41,21 +41,26 @@ function run() { set -e } +# Batch the file list so it stays under the command-line length limit (~32k on Windows). +function batch() { + printf '%s' "$1" | tr '\n' '\0' | xargs -0 -r -s 20000 "${@:2}" +} + function run_tests() { ALL_FILES=$1 PYTHON_FILES=$2 run "ruff" ruff check openpilot --quiet - run "check_dependencies" python3 $DIR/check_dependencies.py - run "check_indentation" $DIR/check_indentation.py $PYTHON_FILES - run "check_added_large_files" $DIR/check_added_large_files.py --maxkb=120 $ALL_FILES - run "check_shebang_scripts_are_executable" $DIR/check_shebang_scripts_are_executable.py $ALL_FILES - run "check_shebang_format" $DIR/check_shebang_format.sh $ALL_FILES - run "check_nomerge_comments" $DIR/check_nomerge_comments.sh $ALL_FILES + run "check_dependencies" "python3 \"$DIR/check_dependencies.py\"" + run "check_indentation" "batch \"\$PYTHON_FILES\" \"$DIR/check_indentation.py\"" + run "check_added_large_files" "batch \"\$ALL_FILES\" \"$DIR/check_added_large_files.py\" --maxkb=120" + run "check_shebang_scripts_are_executable" "batch \"\$ALL_FILES\" \"$DIR/check_shebang_scripts_are_executable.py\"" + run "check_shebang_format" "batch \"\$ALL_FILES\" \"$DIR/check_shebang_format.sh\"" + run "check_nomerge_comments" "batch \"\$ALL_FILES\" \"$DIR/check_nomerge_comments.sh\"" if [[ -z "$FAST" ]]; then run "ty" ty check openpilot - run "codespell" codespell $ALL_FILES + run "codespell" "batch \"\$ALL_FILES\" codespell" fi return $FAILED From 2a24e19b2ba9148dbb4e8bbd0bc4a3bfab9bb6ac Mon Sep 17 00:00:00 2001 From: Amy Jeanes Date: Wed, 9 Sep 2026 05:50:12 +0100 Subject: [PATCH 13/15] TEMP: Windows wheels from a pre-release index until commaai/dependencies#107 publishes --- TODO REMOVE AFTER DEPENDENCY PR MERGES (commaai/dependencies#107) --- Until the win_amd64 wheels are on PyPI, uv resolves the comma-deps packages openpilot names for sys_platform == 'win32' from a GitHub release of the same wheels on my fork. Linux and macOS resolve from PyPI as before. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_016AgqYZYWLpwE2T5vS3nVEt --- pyproject.toml | 21 ++++ uv.lock | 270 ++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 263 insertions(+), 28 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index de9574d8dda5b0..2c4b2a7fa58e8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -154,6 +154,14 @@ override-dependencies = [ "opendbc", # panda pins opendbc from git for standalone use; always use our submodule ] +# --- TODO REMOVE AFTER DEPENDENCY PR MERGES (commaai/dependencies#107): fork-only index of the comma-deps Windows wheels until PyPI has them; not for upstream --- +# (publish_windows.sh there). Point at a local `dist/` directory instead to use wheels built with its build.sh. +[[tool.uv.index]] +name = "comma-deps-windows" +url = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" +format = "flat" +explicit = true + [tool.uv.sources] msgq-ipc = { path = "msgq_repo", editable = true } opendbc = { path = "opendbc_repo", editable = true } @@ -161,3 +169,16 @@ pandacan = { path = "panda", editable = true } rednose = { path = "rednose_repo", editable = true } teleoprtc = { path = "teleoprtc_repo", editable = true } tinygrad = { path = "tinygrad_repo", editable = true } +comma-deps-acados = { index = "comma-deps-windows", marker = "sys_platform == 'win32'" } +comma-deps-bootstrap-icons = { index = "comma-deps-windows", marker = "sys_platform == 'win32'" } +comma-deps-capnproto = { index = "comma-deps-windows", marker = "sys_platform == 'win32'" } +comma-deps-ffmpeg = { index = "comma-deps-windows", marker = "sys_platform == 'win32'" } +comma-deps-gcc-arm-none-eabi = { index = "comma-deps-windows", marker = "sys_platform == 'win32'" } +comma-deps-git-lfs = { index = "comma-deps-windows", marker = "sys_platform == 'win32'" } +comma-deps-imgui = { index = "comma-deps-windows", marker = "sys_platform == 'win32'" } +comma-deps-json11 = { index = "comma-deps-windows", marker = "sys_platform == 'win32'" } +comma-deps-libusb = { index = "comma-deps-windows", marker = "sys_platform == 'win32'" } +comma-deps-ncurses = { index = "comma-deps-windows", marker = "sys_platform == 'win32'" } +comma-deps-raylib = { index = "comma-deps-windows", marker = "sys_platform == 'win32'" } +comma-deps-zeromq = { index = "comma-deps-windows", marker = "sys_platform == 'win32'" } +comma-deps-zstd = { index = "comma-deps-windows", marker = "sys_platform == 'win32'" } diff --git a/uv.lock b/uv.lock index 3d2a6be4f48f71..ce775da2811af4 100644 --- a/uv.lock +++ b/uv.lock @@ -104,8 +104,11 @@ wheels = [ name = "comma-deps-acados" version = "0.2.2.post98" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform != 'win32'", +] dependencies = [ - { name = "numpy" }, + { name = "numpy", marker = "sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/3d/13/1190aed06e91a9f9024b16fb44a4184842e56ac39dbaec8e6aea83cb1d7e/comma_deps_acados-0.2.2.post98-py3-none-macosx_11_0_arm64.whl", hash = "sha256:64c002e0d6170c7bdec300159bbbe07cf3e05fa54ae5890fe736190fc3543fe7", size = 10635996, upload-time = "2026-07-23T17:01:04.136Z" }, @@ -113,24 +116,66 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/9d/24377b731093e015a44fff043dd7ea5b77b0de62acf48b5a0e7d5a662a15/comma_deps_acados-0.2.2.post98-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:e55ac429d848415930a0b82ab100a310e1848b48cdbaa8dda574b561b43c50d0", size = 13124767, upload-time = "2026-07-23T17:01:13.091Z" }, ] +[[package]] +name = "comma-deps-acados" +version = "0.2.2.post116" +source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" } +resolution-markers = [ + "sys_platform == 'win32'", +] +dependencies = [ + { name = "numpy", marker = "sys_platform == 'win32'" }, +] +wheels = [ + { url = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/comma_deps_acados-0.2.2.post116-py3-none-win_amd64.whl", hash = "sha256:2123b992e68a4c7ba5c6bac4e10f2bd36d9f251660eb8aa829eb0560dc9b56b9" }, +] + [[package]] name = "comma-deps-bootstrap-icons" version = "1.10.5.0.post98" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform != 'win32'", +] wheels = [ { url = "https://files.pythonhosted.org/packages/aa/69/da1a72b8b7783b0caf9a54b27c7124bad11768b8bce2c656ef3b700ab831/comma_deps_bootstrap_icons-1.10.5.0.post98-py3-none-any.whl", hash = "sha256:cabaeecea398eb867b96a6c653c6078691a437c0eff2364530194a218d94cb99", size = 385998, upload-time = "2026-07-23T17:01:17.476Z" }, ] +[[package]] +name = "comma-deps-bootstrap-icons" +version = "1.10.5.0.post116" +source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" } +resolution-markers = [ + "sys_platform == 'win32'", +] +wheels = [ + { url = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/comma_deps_bootstrap_icons-1.10.5.0.post116-py3-none-any.whl", hash = "sha256:21ca4cb414b13f7e3507a61d123257ae48c975d1021fd700f08203d2def3fc49" }, +] + [[package]] name = "comma-deps-capnproto" version = "1.0.1.post98" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform != 'win32'", +] wheels = [ { url = "https://files.pythonhosted.org/packages/ca/83/d3e6346a31491be1d378e4585f37a7979eb772018616abfa74fb27750f1e/comma_deps_capnproto-1.0.1.post98-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9f4d08682df92411b360bec855cb6475990313cd0ecd8ed5c6ee02befb9db913", size = 2407343, upload-time = "2026-07-23T17:01:21.247Z" }, { url = "https://files.pythonhosted.org/packages/b0/8b/6f2a29d50ed4c8741dbf0a34ab109899268d09753518cd693e881bbf1a9d/comma_deps_capnproto-1.0.1.post98-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:6cdf838a8d415ac71e1f52306624ab3ab6f27777f6ce89c0059c4129ea7b7f62", size = 2506355, upload-time = "2026-07-23T17:01:25.254Z" }, { url = "https://files.pythonhosted.org/packages/08/24/e91f2203d62e4db9de7dae06dd0cdefb1e000d8b3ba0bde48367be7e5b63/comma_deps_capnproto-1.0.1.post98-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:3d95993c9aff0c89e39ca965e995021dd3dccdfc3d5d85152916cf4bf651b7ec", size = 2590764, upload-time = "2026-07-23T17:01:29.062Z" }, ] +[[package]] +name = "comma-deps-capnproto" +version = "1.0.1.post116" +source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" } +resolution-markers = [ + "sys_platform == 'win32'", +] +wheels = [ + { url = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/comma_deps_capnproto-1.0.1.post116-py3-none-win_amd64.whl", hash = "sha256:12c7d088a5e46995550f94010716743778c7bc0091c8825a22f52d91f250c33e" }, +] + [[package]] name = "comma-deps-eigen" version = "3.4.0.post98" @@ -145,78 +190,179 @@ wheels = [ name = "comma-deps-ffmpeg" version = "7.1.0.post98" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform != 'win32'", +] wheels = [ { url = "https://files.pythonhosted.org/packages/20/59/4899ac0fa54905f43e237fff122008d6c591918b41e36880eeb18cd6279c/comma_deps_ffmpeg-7.1.0.post98-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7816c5adc9c6a7462209ccf1d023c42d7e38a4eee47aa2f43d45dfc8320063f8", size = 7326312, upload-time = "2026-07-23T17:01:55.975Z" }, { url = "https://files.pythonhosted.org/packages/29/cb/6e047c19c39977c5ae322ad698b91d8d9fce43314cb86563de91bb161982/comma_deps_ffmpeg-7.1.0.post98-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:45ad401b4058e3f7efb8d6841e1f187e8c265e942e56e7fb01db1ba9096e6b78", size = 4437675, upload-time = "2026-07-23T17:01:59.971Z" }, { url = "https://files.pythonhosted.org/packages/76/3d/cda4b19fa5a7b26921a518143c94fd3632030a34b157cab6d6f10f2c86bc/comma_deps_ffmpeg-7.1.0.post98-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:9e7034739d45a45254c4200a555d343b22ce117429bebc2de2c1b05f050bfc8c", size = 4681499, upload-time = "2026-07-23T17:02:03.963Z" }, ] +[[package]] +name = "comma-deps-ffmpeg" +version = "7.1.0.post116" +source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" } +resolution-markers = [ + "sys_platform == 'win32'", +] +wheels = [ + { url = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/comma_deps_ffmpeg-7.1.0.post116-py3-none-win_amd64.whl", hash = "sha256:103c92183aa6f518075a8a5f359b8d5e8f8f592acdab73fe55003d0ac4c6c9e1" }, +] + [[package]] name = "comma-deps-gcc-arm-none-eabi" version = "13.2.1.post98" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform != 'win32'", +] wheels = [ { url = "https://files.pythonhosted.org/packages/0e/e5/a4cd9faa80bf419c6a7052c99dfe565c283a5c966e90ce35b1b4040b24b8/comma_deps_gcc_arm_none_eabi-13.2.1.post98-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d0e6991b845636ab19e46199bc5cb9dd056611bc6b93c6bca4f2bb5002783533", size = 15238810, upload-time = "2026-07-23T17:02:08.588Z" }, { url = "https://files.pythonhosted.org/packages/5b/81/690ce48945aecf58e475cb728a8d2f6c034493afd87b5b0381a85dd324b4/comma_deps_gcc_arm_none_eabi-13.2.1.post98-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:41fef00d033e0f6c12e1748829d95e53942826e0085f57d918351b2de69530d7", size = 17367240, upload-time = "2026-07-23T17:02:13.637Z" }, { url = "https://files.pythonhosted.org/packages/41/a9/6af914145bd5c9ce3468a95500558bdc0a438f69700daedd37535945294e/comma_deps_gcc_arm_none_eabi-13.2.1.post98-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:ed3630aac06b3a1db78ba5a29a33b900a51dd1c0b37f86cbfe3c1591993178f8", size = 16941137, upload-time = "2026-07-23T17:02:18.976Z" }, ] +[[package]] +name = "comma-deps-gcc-arm-none-eabi" +version = "13.2.1.post116" +source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" } +resolution-markers = [ + "sys_platform == 'win32'", +] +wheels = [ + { url = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/comma_deps_gcc_arm_none_eabi-13.2.1.post116-py3-none-win_amd64.whl", hash = "sha256:8ff8c7cffd81c46a500c8b8b7b396abf7b3f1d2d4c388077482306efd3818c88" }, +] + [[package]] name = "comma-deps-git-lfs" version = "3.6.1.post98" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform != 'win32'", +] wheels = [ { url = "https://files.pythonhosted.org/packages/79/27/ecfda511eb334822d9bc464ec2d9b74d3c553784811a885baba34a27eaf6/comma_deps_git_lfs-3.6.1.post98-py3-none-macosx_11_0_arm64.whl", hash = "sha256:259b1f4859bb3ab20fdcc012be0a9868a3649e1087d5cec07eaade7adea44c78", size = 4685104, upload-time = "2026-07-23T17:02:23.67Z" }, { url = "https://files.pythonhosted.org/packages/00/a5/9631b4a676279b353f82d4e2da62eb567c70fff628133a62d7f70fbf5924/comma_deps_git_lfs-3.6.1.post98-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:60d39254138b2c7c3f15cc512c14504c11881e79885492321e1ffc3ecb840f93", size = 4485276, upload-time = "2026-07-23T17:02:27.751Z" }, { url = "https://files.pythonhosted.org/packages/09/5a/7ef6bc209d8ec15c40b1f988345e2e59c535a215284366916422ba0d0c30/comma_deps_git_lfs-3.6.1.post98-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:9586057ca9c6e77e9068128f3e96f0ca03db29a757414b1b251bab4ca31ee6b8", size = 4889582, upload-time = "2026-07-23T17:02:31.454Z" }, ] +[[package]] +name = "comma-deps-git-lfs" +version = "3.6.1.post116" +source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" } +resolution-markers = [ + "sys_platform == 'win32'", +] +wheels = [ + { url = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/comma_deps_git_lfs-3.6.1.post116-py3-none-win_amd64.whl", hash = "sha256:e7af591b2a790e01b3ccc2be14111a37db21931ef2059a50411c7f62544e05e5" }, +] + [[package]] name = "comma-deps-imgui" version = "1.92.7.post98" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform != 'win32'", +] wheels = [ { url = "https://files.pythonhosted.org/packages/f4/e4/b9f4b68973bfd529314c28fcd87cb2f52b5dc7d9fdeb3be2d3d15b7cea25/comma_deps_imgui-1.92.7.post98-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6fddf76138b1e54fe33e9f5ea3cbcb650f92fef8fbcf7e5080800ea851d68b98", size = 1688011, upload-time = "2026-07-23T17:02:35.416Z" }, { url = "https://files.pythonhosted.org/packages/7f/46/92030abf6e42e9813f144d10bcf541b39a246c5ca2d63d049478deac650b/comma_deps_imgui-1.92.7.post98-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9f7eed0f759e59afcf289967edb3d94c48a38fe97527f2909dbbc70a850dc2cc", size = 2522785, upload-time = "2026-07-23T17:02:39.092Z" }, { url = "https://files.pythonhosted.org/packages/7d/57/d41e76559a553565413976695fb63a768d3446d12eccc9e736a12b53e662/comma_deps_imgui-1.92.7.post98-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:07eeb105cce73ec3b27789501c78dcd059d75e736a99f1953358ef5097e7036f", size = 2655476, upload-time = "2026-07-23T17:02:42.925Z" }, ] +[[package]] +name = "comma-deps-imgui" +version = "1.92.7.post116" +source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" } +resolution-markers = [ + "sys_platform == 'win32'", +] +wheels = [ + { url = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/comma_deps_imgui-1.92.7.post116-py3-none-win_amd64.whl", hash = "sha256:3f19551eb46b20a2fa8f6681f1df800712d0a6c523744fbe6c7990157d08c422" }, +] + [[package]] name = "comma-deps-json11" version = "20170411.0.post98" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform != 'win32'", +] wheels = [ { url = "https://files.pythonhosted.org/packages/c1/f4/50411c9134a8347831a72f90318b7b7d91ce566e63575b8a4a821be50ca4/comma_deps_json11-20170411.0.post98-py3-none-macosx_11_0_arm64.whl", hash = "sha256:20d666897062487e4cd93b8e4eb9c53ecabf706864be8d8cbb60a56f0113c452", size = 34034, upload-time = "2026-07-23T17:02:46.595Z" }, { url = "https://files.pythonhosted.org/packages/1f/54/0c87fae682ee52e6aec371336ac980921ad34cedabb69e576cc9f83c40a7/comma_deps_json11-20170411.0.post98-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9b4a03909609b832dc99b06503fb8c77419399a99e0db982d4c3f51f1563aa73", size = 41848, upload-time = "2026-07-23T17:02:50.039Z" }, { url = "https://files.pythonhosted.org/packages/7b/71/dd100992e13f2c7a01f68eebcd1e3cf43f1d169e4b80b0577f330e5f5c12/comma_deps_json11-20170411.0.post98-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:1da9030908f3a6631a0a254f493c382ba061c2e8ddb280a30d64430af29f4638", size = 42602, upload-time = "2026-07-23T17:02:53.233Z" }, ] +[[package]] +name = "comma-deps-json11" +version = "20170411.0.post116" +source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" } +resolution-markers = [ + "sys_platform == 'win32'", +] +wheels = [ + { url = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/comma_deps_json11-20170411.0.post116-py3-none-win_amd64.whl", hash = "sha256:16597ade2966c89a9dd859bbf8ff97e23d6cfc91c9b841ea9c4c8e52e6d677bd" }, +] + [[package]] name = "comma-deps-libusb" version = "1.0.29.post98" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform != 'win32'", +] wheels = [ { url = "https://files.pythonhosted.org/packages/47/fb/f7d342a8f785fc1c0fd5d6883e1a5a7d424a1899b888f78f9091f4b98049/comma_deps_libusb-1.0.29.post98-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a10b82a946c33c23152cee3e330ce76d398ddce1f38fea62e33773bef8f56164", size = 102339, upload-time = "2026-07-23T17:02:56.567Z" }, { url = "https://files.pythonhosted.org/packages/b7/fe/1b21692cc03078219a3946aae56086a109168a4b4dcfba3a22ce1cd01064/comma_deps_libusb-1.0.29.post98-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:39567eeef6170ece389526780f90c3b75cbcfdf427f648f6b1539c203f7387a8", size = 94431, upload-time = "2026-07-23T17:03:00.01Z" }, { url = "https://files.pythonhosted.org/packages/49/d2/d93aac76b94ae87f7a37ce88f2e7e1184e19e67d10c068c1aac209075450/comma_deps_libusb-1.0.29.post98-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:5e5b86be94b4a355c6be933ed21586d01b1d7ba597298dafd33b871a1ae66416", size = 93462, upload-time = "2026-07-23T17:03:03.218Z" }, ] +[[package]] +name = "comma-deps-libusb" +version = "1.0.29.post116" +source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" } +resolution-markers = [ + "sys_platform == 'win32'", +] +wheels = [ + { url = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/comma_deps_libusb-1.0.29.post116-py3-none-win_amd64.whl", hash = "sha256:43105d4bedf06fed14f5a218c64cb3e730caa102628c52f339ee42d00cebe374" }, +] + [[package]] name = "comma-deps-ncurses" version = "6.5.post98" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform != 'win32'", +] wheels = [ { url = "https://files.pythonhosted.org/packages/d0/d4/03e62b2a0be92ad420653ff1cf4396de9840c4c59cdc6e000ea5614f7744/comma_deps_ncurses-6.5.post98-py3-none-macosx_11_0_arm64.whl", hash = "sha256:22ade1596deb18538d4bf59708aa2747c28c2e0e7048f13f5bfce3e5588f7417", size = 264921, upload-time = "2026-07-23T17:03:06.576Z" }, { url = "https://files.pythonhosted.org/packages/1a/ab/295b428ef473dbe0d7088ff02daf5ba17b924f3b915ac69a8a9c45a6eb2b/comma_deps_ncurses-6.5.post98-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:6724d3e8c1f2e59d2475f7588d86494bcc4001e993fd7dff4c91ecb046edc97f", size = 260844, upload-time = "2026-07-23T17:03:10.329Z" }, { url = "https://files.pythonhosted.org/packages/cd/db/75afb33eaa86425d9bee68153f6141be2645cf5699b2cab5a7cbf7a36099/comma_deps_ncurses-6.5.post98-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:d85e70e98f4b0a969d63a4a61a1a5b85db3c648ea58422401b55f386813f7d12", size = 248352, upload-time = "2026-07-23T17:03:13.786Z" }, ] +[[package]] +name = "comma-deps-ncurses" +version = "6.5.post116" +source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" } +resolution-markers = [ + "sys_platform == 'win32'", +] +wheels = [ + { url = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/comma_deps_ncurses-6.5.post116-py3-none-win_amd64.whl", hash = "sha256:ae25aa8ca50f5e7691b778ed7a2c4560e572a1e5b5aa703d77557ba09d2c8671" }, +] + [[package]] name = "comma-deps-raylib" version = "6.0.0.1.post101" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform != 'win32'", +] dependencies = [ - { name = "cffi" }, + { name = "cffi", marker = "sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/ff/90/e289acd1725d71c792c33399422f1052c4b0c38aeb4222a866d30c4a2cad/comma_deps_raylib-6.0.0.1.post101-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fa69d5093a92d7d2bfd2714a1afccca63b94d4c55fae88e61b80e8841de6a6cd", size = 1885392, upload-time = "2026-08-27T18:24:37.25Z" }, @@ -224,26 +370,68 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/ca/ef33aff790b37dfc925f94fbb3a86da19f67929c15b3725ddb18afb91e96/comma_deps_raylib-6.0.0.1.post101-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:8f2a1ffe5f60cac06170b144d6ce84925c91657e7ada3ae35bc5df6ccbe0b461", size = 20722616, upload-time = "2026-08-27T18:24:45.803Z" }, ] +[[package]] +name = "comma-deps-raylib" +version = "6.0.0.1.post116" +source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" } +resolution-markers = [ + "sys_platform == 'win32'", +] +dependencies = [ + { name = "cffi", marker = "sys_platform == 'win32'" }, +] +wheels = [ + { url = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/comma_deps_raylib-6.0.0.1.post116-py3-none-win_amd64.whl", hash = "sha256:774c27749e7ffa2c7f68db341f4338ddb8e3073f9dfa9266d5184e2bb9ce7fd6" }, +] + [[package]] name = "comma-deps-zeromq" version = "4.3.5.post98" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform != 'win32'", +] wheels = [ { url = "https://files.pythonhosted.org/packages/12/b7/b0070e091dae4be2cecccfb2921167b568d0e7bb9ea600b5814603e0590f/comma_deps_zeromq-4.3.5.post98-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0ecde97133d657024bf99ac7302130905a131dabedbc8425b6666b2058ce6acb", size = 815150, upload-time = "2026-07-23T17:03:29.517Z" }, { url = "https://files.pythonhosted.org/packages/2f/9a/d6a381b079516eca1b8a86aa3e972e550a48ff2f069ccc722b118ab53d60/comma_deps_zeromq-4.3.5.post98-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:110a628ea440ea75707ad29fd90341d300414b0092137b1d43e8f19d100cf2fa", size = 833389, upload-time = "2026-07-23T17:03:33.25Z" }, { url = "https://files.pythonhosted.org/packages/af/d7/504649efc8dbe8ce4c0cbec085178d1c3298f6950bd4bbca1683a90c49ed/comma_deps_zeromq-4.3.5.post98-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:cd16a515f00f5fb679c2883970c2e7f446ad84e65d7fcf045d325952f9cc3607", size = 798894, upload-time = "2026-07-23T17:03:36.788Z" }, ] +[[package]] +name = "comma-deps-zeromq" +version = "4.3.5.post116" +source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" } +resolution-markers = [ + "sys_platform == 'win32'", +] +wheels = [ + { url = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/comma_deps_zeromq-4.3.5.post116-py3-none-win_amd64.whl", hash = "sha256:194f5a87061cab28afb95601fa16cfb9560ab454406ea906923f25943b5d1456" }, +] + [[package]] name = "comma-deps-zstd" version = "1.5.6.post98" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform != 'win32'", +] wheels = [ { url = "https://files.pythonhosted.org/packages/ef/66/fd1098b4514e759d85e19d604d446ecec1f67e2f452df9e280d01a2449f7/comma_deps_zstd-1.5.6.post98-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2b6acdd50e71ec67a1426423cda5116a7cb43900dd2e4fca2cbcbb7d588be171", size = 1065140, upload-time = "2026-07-23T17:03:40.465Z" }, { url = "https://files.pythonhosted.org/packages/0c/ba/1d61aae97577bbf13c9c02e7e69d4c9391947d8d0d09ca4ad78f2e3d0faa/comma_deps_zstd-1.5.6.post98-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:04825faf902754a0945ebac374d01d676b7e5f98a68e460452319de509b369f3", size = 1006145, upload-time = "2026-07-23T17:03:44.144Z" }, { url = "https://files.pythonhosted.org/packages/a9/22/94d164407b579090eb3aceeeb63fbd8c540f6972a11e5b72fe3ca3139333/comma_deps_zstd-1.5.6.post98-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:316200a52c9ac1aeb6b22030480ffebaa1d91756c18a3f3cf216368a5fb35bfd", size = 1030359, upload-time = "2026-07-23T17:03:47.999Z" }, ] +[[package]] +name = "comma-deps-zstd" +version = "1.5.6.post116" +source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" } +resolution-markers = [ + "sys_platform == 'win32'", +] +wheels = [ + { url = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/comma_deps_zstd-1.5.6.post116-py3-none-win_amd64.whl", hash = "sha256:c4815a3c73aa62c7c9907947c831352b696528b296593efc36d3eff83df6c75f" }, +] + [[package]] name = "contourpy" version = "1.3.3" @@ -596,15 +784,24 @@ name = "openpilot" version = "0.1.0" source = { editable = "." } dependencies = [ - { name = "comma-deps-acados" }, - { name = "comma-deps-capnproto" }, - { name = "comma-deps-ffmpeg" }, - { name = "comma-deps-gcc-arm-none-eabi" }, - { name = "comma-deps-git-lfs" }, - { name = "comma-deps-json11" }, - { name = "comma-deps-raylib" }, - { name = "comma-deps-zeromq" }, - { name = "comma-deps-zstd" }, + { name = "comma-deps-acados", version = "0.2.2.post98", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'win32'" }, + { name = "comma-deps-acados", version = "0.2.2.post116", source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, marker = "sys_platform == 'win32'" }, + { name = "comma-deps-capnproto", version = "1.0.1.post98", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'win32'" }, + { name = "comma-deps-capnproto", version = "1.0.1.post116", source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, marker = "sys_platform == 'win32'" }, + { name = "comma-deps-ffmpeg", version = "7.1.0.post98", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'win32'" }, + { name = "comma-deps-ffmpeg", version = "7.1.0.post116", source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, marker = "sys_platform == 'win32'" }, + { name = "comma-deps-gcc-arm-none-eabi", version = "13.2.1.post98", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'win32'" }, + { name = "comma-deps-gcc-arm-none-eabi", version = "13.2.1.post116", source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, marker = "sys_platform == 'win32'" }, + { name = "comma-deps-git-lfs", version = "3.6.1.post98", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'win32'" }, + { name = "comma-deps-git-lfs", version = "3.6.1.post116", source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, marker = "sys_platform == 'win32'" }, + { name = "comma-deps-json11", version = "20170411.0.post98", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'win32'" }, + { name = "comma-deps-json11", version = "20170411.0.post116", source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, marker = "sys_platform == 'win32'" }, + { name = "comma-deps-raylib", version = "6.0.0.1.post101", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'win32'" }, + { name = "comma-deps-raylib", version = "6.0.0.1.post116", source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, marker = "sys_platform == 'win32'" }, + { name = "comma-deps-zeromq", version = "4.3.5.post98", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'win32'" }, + { name = "comma-deps-zeromq", version = "4.3.5.post116", source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, marker = "sys_platform == 'win32'" }, + { name = "comma-deps-zstd", version = "1.5.6.post98", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'win32'" }, + { name = "comma-deps-zstd", version = "1.5.6.post116", source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, marker = "sys_platform == 'win32'" }, { name = "inputs" }, { name = "jeepney" }, { name = "numpy" }, @@ -636,10 +833,14 @@ testing = [ { name = "ty" }, ] tools = [ - { name = "comma-deps-bootstrap-icons" }, - { name = "comma-deps-imgui" }, - { name = "comma-deps-libusb" }, - { name = "comma-deps-ncurses" }, + { name = "comma-deps-bootstrap-icons", version = "1.10.5.0.post98", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'win32'" }, + { name = "comma-deps-bootstrap-icons", version = "1.10.5.0.post116", source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, marker = "sys_platform == 'win32'" }, + { name = "comma-deps-imgui", version = "1.92.7.post98", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'win32'" }, + { name = "comma-deps-imgui", version = "1.92.7.post116", source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, marker = "sys_platform == 'win32'" }, + { name = "comma-deps-libusb", version = "1.0.29.post98", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'win32'" }, + { name = "comma-deps-libusb", version = "1.0.29.post116", source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, marker = "sys_platform == 'win32'" }, + { name = "comma-deps-ncurses", version = "6.5.post98", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'win32'" }, + { name = "comma-deps-ncurses", version = "6.5.post116", source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, marker = "sys_platform == 'win32'" }, { name = "matplotlib" }, ] @@ -651,19 +852,32 @@ standalone = [ [package.metadata] requires-dist = [ { name = "codespell", marker = "extra == 'testing'" }, - { name = "comma-deps-acados" }, - { name = "comma-deps-bootstrap-icons", marker = "extra == 'tools'" }, - { name = "comma-deps-capnproto" }, - { name = "comma-deps-ffmpeg" }, - { name = "comma-deps-gcc-arm-none-eabi" }, - { name = "comma-deps-git-lfs" }, - { name = "comma-deps-imgui", marker = "extra == 'tools'" }, - { name = "comma-deps-json11" }, - { name = "comma-deps-libusb", marker = "extra == 'tools'" }, - { name = "comma-deps-ncurses", marker = "extra == 'tools'" }, - { name = "comma-deps-raylib" }, - { name = "comma-deps-zeromq" }, - { name = "comma-deps-zstd" }, + { name = "comma-deps-acados", marker = "sys_platform != 'win32'" }, + { name = "comma-deps-acados", marker = "sys_platform == 'win32'", index = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, + { name = "comma-deps-bootstrap-icons", marker = "sys_platform == 'win32' and extra == 'tools'", index = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, + { name = "comma-deps-bootstrap-icons", marker = "sys_platform != 'win32' and extra == 'tools'" }, + { name = "comma-deps-capnproto", marker = "sys_platform != 'win32'" }, + { name = "comma-deps-capnproto", marker = "sys_platform == 'win32'", index = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, + { name = "comma-deps-ffmpeg", marker = "sys_platform != 'win32'" }, + { name = "comma-deps-ffmpeg", marker = "sys_platform == 'win32'", index = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, + { name = "comma-deps-gcc-arm-none-eabi", marker = "sys_platform != 'win32'" }, + { name = "comma-deps-gcc-arm-none-eabi", marker = "sys_platform == 'win32'", index = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, + { name = "comma-deps-git-lfs", marker = "sys_platform != 'win32'" }, + { name = "comma-deps-git-lfs", marker = "sys_platform == 'win32'", index = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, + { name = "comma-deps-imgui", marker = "sys_platform == 'win32' and extra == 'tools'", index = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, + { name = "comma-deps-imgui", marker = "sys_platform != 'win32' and extra == 'tools'" }, + { name = "comma-deps-json11", marker = "sys_platform != 'win32'" }, + { name = "comma-deps-json11", marker = "sys_platform == 'win32'", index = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, + { name = "comma-deps-libusb", marker = "sys_platform == 'win32' and extra == 'tools'", index = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, + { name = "comma-deps-libusb", marker = "sys_platform != 'win32' and extra == 'tools'" }, + { name = "comma-deps-ncurses", marker = "sys_platform == 'win32' and extra == 'tools'", index = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, + { name = "comma-deps-ncurses", marker = "sys_platform != 'win32' and extra == 'tools'" }, + { name = "comma-deps-raylib", marker = "sys_platform != 'win32'" }, + { name = "comma-deps-raylib", marker = "sys_platform == 'win32'", index = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, + { name = "comma-deps-zeromq", marker = "sys_platform != 'win32'" }, + { name = "comma-deps-zeromq", marker = "sys_platform == 'win32'", index = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, + { name = "comma-deps-zstd", marker = "sys_platform != 'win32'" }, + { name = "comma-deps-zstd", marker = "sys_platform == 'win32'", index = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, { name = "coverage", marker = "extra == 'testing'" }, { name = "inputs" }, { name = "jeepney" }, From e57aee443029b070778a3287768b6219cef8fc57 Mon Sep 17 00:00:00 2001 From: Amy Jeanes Date: Wed, 9 Sep 2026 05:51:45 +0100 Subject: [PATCH 14/15] TEMP: submodules from the fork branches until the series PRs merge --- TODO REMOVE AFTER THE SUBMODULE PRS MERGE (commaai/msgq#709, commaai/panda#2427, commaai/rednose#61, commaai/opendbc#3724) --- Until the four submodule PRs merge, the pointers move to the windows-tidy branches on my fork and .gitmodules fetches them from there: panda and opendbc just below their own TEMP index commits (their uv sources would leak into this lockfile), rednose at its TEMP commit (its source supplies comma-deps-eigen for Windows), msgq at its tip. The lock follows the submodules' metadata. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_016AgqYZYWLpwE2T5vS3nVEt --- .gitmodules | 12 ++++++++---- msgq_repo | 2 +- opendbc_repo | 2 +- panda | 2 +- rednose_repo | 2 +- uv.lock | 26 +++++++++++++++++++++----- 6 files changed, 33 insertions(+), 13 deletions(-) diff --git a/.gitmodules b/.gitmodules index ad6530de9ac910..ab350cbea65dee 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,15 +1,19 @@ [submodule "panda"] path = panda - url = ../../commaai/panda.git + # --- TODO REMOVE AFTER THE SUBMODULE PRS MERGE (commaai/panda#2427): the pinned commit is on the fork branch behind that PR --- + url = https://github.com/AmyJeanes/panda.git [submodule "opendbc"] path = opendbc_repo - url = ../../commaai/opendbc.git + # --- TODO REMOVE AFTER THE SUBMODULE PRS MERGE (commaai/opendbc#3724): the pinned commit is on the fork branch behind that PR --- + url = https://github.com/AmyJeanes/opendbc.git [submodule "msgq"] path = msgq_repo - url = ../../commaai/msgq.git + # --- TODO REMOVE AFTER THE SUBMODULE PRS MERGE (commaai/msgq#709): the pinned commit is on the fork branch behind that PR --- + url = https://github.com/AmyJeanes/msgq.git [submodule "rednose_repo"] path = rednose_repo - url = ../../commaai/rednose.git + # --- TODO REMOVE AFTER THE SUBMODULE PRS MERGE (commaai/rednose#61): the pinned commit is on the fork branch behind that PR --- + url = https://github.com/AmyJeanes/rednose.git [submodule "teleoprtc_repo"] path = teleoprtc_repo url = ../../commaai/teleoprtc diff --git a/msgq_repo b/msgq_repo index 326a9f5aa6cf63..eeb774311b0ef6 160000 --- a/msgq_repo +++ b/msgq_repo @@ -1 +1 @@ -Subproject commit 326a9f5aa6cf630f647fd6996106aa267dd01c2a +Subproject commit eeb774311b0ef6407934dcb8bfe9104ef57ffb97 diff --git a/opendbc_repo b/opendbc_repo index b4ef5e1cf406ff..ea0b85c655a061 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit b4ef5e1cf406ff143fa67bdbfb154739d43279c9 +Subproject commit ea0b85c655a06113854f14f0c21312640ce4c2b3 diff --git a/panda b/panda index 75aa44bec91408..290e79f3d8a9bb 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 75aa44bec9140849868239b1f1e3f22624adb8fe +Subproject commit 290e79f3d8a9bb134dab518b6253119bec9ac3db diff --git a/rednose_repo b/rednose_repo index 28d4a7f69e80e1..a411cf24917eed 160000 --- a/rednose_repo +++ b/rednose_repo @@ -1 +1 @@ -Subproject commit 28d4a7f69e80e1c3e0d24ca0733d7daeaeade3d0 +Subproject commit a411cf24917eed738b050a572acfcded90390df1 diff --git a/uv.lock b/uv.lock index ce775da2811af4..6bcf35607b2a7a 100644 --- a/uv.lock +++ b/uv.lock @@ -180,12 +180,26 @@ wheels = [ name = "comma-deps-eigen" version = "3.4.0.post98" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "sys_platform != 'win32'", +] wheels = [ { url = "https://files.pythonhosted.org/packages/3e/2f/89011c71976da6e1c3d7be315afa3d86ff25deeada1ad2319ac6be0e18ea/comma_deps_eigen-3.4.0.post98-py3-none-macosx_11_0_arm64.whl", hash = "sha256:bd182634cf4fa537e7815238d3135e6e89be5826421a495be92258c6c388b527", size = 2275893, upload-time = "2026-07-23T17:01:44.622Z" }, { url = "https://files.pythonhosted.org/packages/2c/61/fcd4ad536c51437ee73ac255f3b8a23fb5f21bb1f96e834d8036c3bbcf08/comma_deps_eigen-3.4.0.post98-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:250c15f6217c37736a2f54298d01f6e32f6e977faa87cc358de67ea3d121725e", size = 2275896, upload-time = "2026-07-23T17:01:48.397Z" }, { url = "https://files.pythonhosted.org/packages/d3/a2/2b7633fe5a5a2914900933393c315e9bd86e8fb7bbbe328d3a220eaf2027/comma_deps_eigen-3.4.0.post98-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:dee9eb6c6c7e58201d7a36611857b7b3f3ba70f888ee07493fef2ea41d0d2cae", size = 2275898, upload-time = "2026-07-23T17:01:52.179Z" }, ] +[[package]] +name = "comma-deps-eigen" +version = "3.4.0.post116" +source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" } +resolution-markers = [ + "sys_platform == 'win32'", +] +wheels = [ + { url = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/comma_deps_eigen-3.4.0.post116-py3-none-win_amd64.whl", hash = "sha256:4ad9b58b90d9df72b5b32940fbab6cfb2cd3727f9ee16930e4c36134abf4d0c4" }, +] + [[package]] name = "comma-deps-ffmpeg" version = "7.1.0.post98" @@ -776,7 +790,7 @@ provides-extras = ["testing", "examples"] [package.metadata.requires-dev] testing = [ { name = "comma-car-segments", url = "https://huggingface.co/datasets/commaai/commaCarSegments/resolve/main/dist/comma_car_segments-0.1.0-py3-none-any.whl" }, - { name = "cppcheck", git = "https://github.com/commaai/dependencies.git?subdirectory=cppcheck&rev=release-cppcheck" }, + { name = "comma-deps-cppcheck", marker = "python_full_version >= '3.12'" }, ] [[package]] @@ -930,9 +944,9 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "cffi", marker = "extra == 'dev'" }, - { name = "cppcheck", marker = "extra == 'dev'", git = "https://github.com/commaai/dependencies.git?subdirectory=cppcheck&rev=release-cppcheck" }, + { name = "comma-deps-cppcheck", marker = "python_full_version >= '3.12' and extra == 'dev'" }, + { name = "comma-deps-gcc-arm-none-eabi", marker = "python_full_version >= '3.12' and extra == 'dev'" }, { name = "flaky", marker = "extra == 'dev'" }, - { name = "gcc-arm-none-eabi", marker = "extra == 'dev'", git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=release-gcc-arm-none-eabi" }, { name = "libusb-package" }, { name = "libusb1" }, { name = "opendbc", git = "https://github.com/commaai/opendbc.git?rev=master" }, @@ -1074,7 +1088,8 @@ version = "0.0.1" source = { editable = "rednose_repo" } dependencies = [ { name = "cffi" }, - { name = "comma-deps-eigen" }, + { name = "comma-deps-eigen", version = "3.4.0.post98", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'win32'" }, + { name = "comma-deps-eigen", version = "3.4.0.post116", source = { registry = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, marker = "sys_platform == 'win32'" }, { name = "cython" }, { name = "numpy" }, { name = "scons" }, @@ -1085,7 +1100,8 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "cffi" }, - { name = "comma-deps-eigen" }, + { name = "comma-deps-eigen", marker = "sys_platform != 'win32'" }, + { name = "comma-deps-eigen", marker = "sys_platform == 'win32'", index = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" }, { name = "cython" }, { name = "numpy" }, { name = "ruff", marker = "extra == 'dev'" }, From 7d658c781db2688ceeb0cc859fbeacf1b1fb179f Mon Sep 17 00:00:00 2001 From: dzid26 Date: Fri, 11 Sep 2026 15:04:41 +0100 Subject: [PATCH 15/15] Update window dimensions in console UI window sizes for TimelineDesc, CarState, and DownloadBar relative to max_width. --- openpilot/tools/replay/consoleui.cc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpilot/tools/replay/consoleui.cc b/openpilot/tools/replay/consoleui.cc index 846e26a67ce86d..f07803ceb1381a 100644 --- a/openpilot/tools/replay/consoleui.cc +++ b/openpilot/tools/replay/consoleui.cc @@ -108,9 +108,9 @@ void ConsoleUI::initWindows() { w[Win::Title] = newwin(1, max_width, 0, 0); w[Win::Stats] = newwin(2, max_width - 2 * BORDER_SIZE, 2, BORDER_SIZE); w[Win::Timeline] = newwin(4, max_width - 2 * BORDER_SIZE, 5, BORDER_SIZE); - w[Win::TimelineDesc] = newwin(1, 100, 10, BORDER_SIZE); - w[Win::CarState] = newwin(3, 100, 12, BORDER_SIZE); - w[Win::DownloadBar] = newwin(1, 100, 16, BORDER_SIZE); + w[Win::TimelineDesc] = newwin(1, max_width - 2 * (BORDER_SIZE - 1), 10, BORDER_SIZE); + w[Win::CarState] = newwin(3, max_width - 2 * (BORDER_SIZE - 1), 12, BORDER_SIZE); + w[Win::DownloadBar] = newwin(1, max_width - 2 * (BORDER_SIZE - 1), 16, BORDER_SIZE); if (int log_height = max_height - 27; log_height > 4) { w[Win::LogBorder] = newwin(log_height, max_width - 2 * (BORDER_SIZE - 1), 17, BORDER_SIZE - 1); box(w[Win::LogBorder], 0, 0);