IQ.Pilot Release Commit @ e46d557

This commit is contained in:
IQ.Lvbs CI [bot]
2026-08-07 00:13:39 -05:00
parent 03c3158b81
commit 824bb9ddfd
216 changed files with 7457 additions and 3151 deletions
+2
View File
@@ -176,3 +176,5 @@ selfdrive/ui/tests/test_ui/nav_demo_report/
# fetcher: closed-source precompiled component — ship only the obfuscated .so
/iqpilot/models_private_src/fetcher.py
/tools/iqmacvisiond/macos/dist/
+16 -2
View File
@@ -10,6 +10,13 @@ import SCons.Errors
SCons.Warnings.warningAsException(True)
# scons only auto-loads a site dir named site_scons at the repo root; ours lives under tools/,
# so replicate what _load_site_scons_dir does (sys.path for site_tools imports + run site_init)
SITE_DIR = Dir('#tools/scons').abspath
if SITE_DIR not in sys.path:
sys.path.insert(0, SITE_DIR)
import site_init # noqa: F401
# capnp's kj library warns when $PWD is stale (doesn't match the real cwd); keep them in sync
os.environ.pop('PWD', None)
@@ -49,6 +56,13 @@ assert arch in [
"Darwin", # macOS arm64 (x86 not supported)
]
# ffmpeg comes from the system (brew on macOS, distro packages elsewhere) rather than
# a vendored wheel, so it always needs the static-link deps. Exported so tools/ can
# take upstream's `ffmpeg_libs` form instead of hand-listing codecs per SConscript.
ffmpeg_libs = ['avformat', 'avcodec', 'avutil', 'x264', 'z']
if arch != "Darwin":
ffmpeg_libs += ['va', 'va-drm', 'drm']
env = Environment(
ENV={
"PATH": os.environ['PATH'],
@@ -100,7 +114,7 @@ env = Environment(
COMPILATIONDB_USE_ABSPATH=True,
REDNOSE_ROOT="#",
tools=["default", "cython", "compilation_db", "rednose_filter"],
toolpath=["#site_scons/site_tools", "#rednose_repo/site_scons/site_tools"],
toolpath=["#tools/scons/site_tools", "#rednose_repo/site_scons/site_tools"],
)
# Arch-specific flags and paths
@@ -187,7 +201,7 @@ else:
np_version = SCons.Script.Value(np.__version__)
Export('envCython', 'np_version')
Export('env', 'arch')
Export('env', 'arch', 'ffmpeg_libs')
# Setup cache dir
default_cache_dir = os.environ.get('SCONS_CACHE_DIR') or ('/data/scons_cache' if arch == "larch64" else '/tmp/scons_cache')
+433
View File
@@ -0,0 +1,433 @@
# Upstream sync plan — `tools/cabana` and `tools/jotpluggler`
Hand-port the current upstream (commaai/openpilot) versions of Cabana and jotpluggler into
iqpilot without losing konn3kt integration. iqpilot does not use comma connect: every route
list, route-file listing, JWT and live-CAN path goes through `api-iqlabs.konn3kt.com`, and the
DBC source is `iqdbc`, not `opendbc_repo`.
Reference clone used for this analysis: `/tmp/openpilot-upstream` @ `e7b1ee3a5`
("jp: fix segment range parsing", 2026-08-05).
---
## 1. Where we actually are
Baselines were established by blob-matching every iqpilot file against upstream history, not by
reading commit messages.
| Tool | iqpilot baseline | Upstream HEAD | Gap |
|---|---|---|---|
| `tools/cabana` | `bcdeec313` (2025-12-16, #36884) | `e7b1ee3a5` (2026-08-05) | **53 commits, ~8 months** |
| `tools/jotpluggler` | `61608db78` (2026-07-15, #38353) | `e7b1ee3a5` | **6 commits, ~3 weeks** |
| `tools/plotjuggler` | `a46ff01ca` (2026-01-19) | `e7b1ee3a5` | 14 commits (secondary scope) |
Cabana is the real work. jotpluggler was hand-ported in MayJuly 2026 (`4b68073c4`, `b26218e85`,
`2cd89a198`) and is nearly current — most of its files are **byte-identical** to upstream HEAD.
A critical structural fact: upstream moved the whole tree under a nested `openpilot/` package
(`#38219`/`#38220`/`#38223`, 2026-06-21) and moved the HAL to `common/hardware/` (`#38202`).
iqpilot has **not** adopted either. Every ported file needs a de-prefix pass:
- `#include "openpilot/cereal/..."``"cereal/..."`
- `#include "common/hardware/hw.h"``"system/hardware/hw.h"`
- `#include "json11/json11.hpp"``"third_party/json11/json11.hpp"`
- `repo_root() / "openpilot" / "tools" / ...``repo_root() / "tools" / ...`
- SCons nodes `#openpilot/common`, `#openpilot/cereal``#common`, `#cereal`
- `opendbc_repo/opendbc/dbc``iqdbc/dbc`; `#opendbc_repo``#iqdbc_repo`
---
## 2. konn3kt invariants — the contract the port must not break
These are the only genuinely IQ-owned surfaces. Everything else in both trees is upstream code we
are behind on. **Anything not on this list should be taken from upstream verbatim** (modulo §1).
### Cabana
| File | What is IQ-owned | Disposition |
|---|---|---|
| `utils/api.{cc,h}` | `CommaApi::BASE_URL = API_HOST ?: https://api-iqlabs.konn3kt.com`; RSA-signed device JWT; Qt `HttpRequest` | **Delete** — fold into `tools/replay/api.{cc,h}` (§3.A) |
| `streams/routes.{cc,h}` | konn3kt device list + route listing | Rewrite on upstream HEAD shape, konn3kt endpoints |
| `konn3kt_canproxy.py` | `canlived → konn3kt relay → local ZMQ → Cabana` proxy | Keep; must stay wired to the Device/ZMQ stream (§3.C) |
| `SConscript` | iqdbc paths, non-nested paths, `PrettyAction`, extra libs | Rebase onto upstream, re-apply IQ deltas |
| `mainwin.cc` | "Load DBC from commaai/**iqdbc**" menu | Re-apply rename only |
| `dbc/generate_dbc_json.py` | `from iqdbc.car import ...` | Re-apply rename only |
| `assets/assets.cc` (tracked) | bootstrap-icons LFS bypass | See §3.K |
| `dbc/car_fingerprint_to_dbc.json` (+`.log`) | tracked build product | Keep tracked |
| `.gitignore`, `README.md` | binary name, iqdbc, konn3kt route examples | Re-apply |
### jotpluggler
All IQ deltas are already carrying `// IQ.Pilot patch:` comments — preserve that convention.
| File | IQ patch |
|---|---|
| `SConscript` | `.venv` site-packages injection; `iqdbc.dbc.generator.generator.create_all()` instead of upstream `get_generated_dbcs()`; no `bootstrap_icons` module; explicit `avformat/avcodec/avutil/x264/yuv/z/bz2/zstd/curl/ssl/crypto` + `OpenCL`/`va`/`va-drm`/`drm`; `PrettyAction`; guards `imgui.MESA_DIR` existence |
| `icons.cc` | vendored `tools/jotpluggler/assets/bootstrap-icons.ttf` (third_party/bootstrap is LFS, TTF not checked in) |
| `common.cc` | iqpilot `common/util.h` has no `check_system`; inline `std::system` + stderr |
| `runtime.cc` | no `common/yuv.h`; vendored libyuv lacks `NV12ToABGR``NV12ToARGB` + `ARGBToABGR` |
| `sketch_layout.cc` | `PyDownloader::getRouteFiles``CommaApi2::httpGet(BASE_URL + "/v1/route/<name>/files")`; `androidLog` (cereal not renamed); iqdbc paths |
| `app.{cc,h}`, `layout.cc`, `layout_io.cc`, `map.cc`, `custom_series.cc` | path/include de-prefixing; `LogOrigin::Android`; iqdbc |
| `dbc.h` | empty-multiplex guard (`mux.empty() ? 0 : std::stoi(mux)`) — a genuine bugfix, **upstream this** |
| `generate_event_extractors.py` | `getBusTimeDEPRECATED()` (iqpilot cereal keeps a flat field, not upstream's `deprecated :group`) |
| `pluggle.py` | iqpilot-only launcher (upstream has none) |
---
## 3. Conflict matrix — upstream change vs konn3kt
### A. File downloading / API moved from C++ to Python (`#37497`, `#38430`) — **highest impact**
Upstream cabana HEAD calls `PyDownloader::getDevices()`, `getDeviceRoutes()`, `getRouteFiles()`
(`tools/replay/py_downloader.h`), which shell into `openpilot/tools/lib/`. iqpilot's replay has
**no** `py_downloader`; it keeps a C++/libcurl path in `tools/replay/api.{cc,h}`
(`CommaApi2::BASE_URL`, `create_token(use_jwt)`, `httpGet`). jotpluggler already bridges this gap
in `sketch_layout.cc`**reuse that pattern**, don't invent a second one.
Resolution: extend `tools/replay/api.{cc,h}` with
```
std::string getDevices(); // GET /v1/me/devices/
std::string getDeviceRoutes(dongle_id, start_ms, end_ms, preserved);
// GET /v1/devices/<id>/routes_segments?start=&end=
// GET /v1/devices/<id>/routes/preserved
std::string getRouteFiles(route); // GET /v1/route/<name>/files (already reachable via httpGet)
```
returning the same JSON shapes upstream's `PyDownloader` returns, then port upstream's `routes.cc`
verbatim with `PyDownloader::``CommaApi2::`. Upstream's HEAD `routes.cc` is already Qt-free
(json11 + `strptime`/`strftime` for the preserved-route ISO timestamps) — that code ports as-is.
Auth semantics to preserve: `create_token(!Hardware::PC())` — device RSA-JWT on device,
`~/.comma/auth.json` `access_token` on a PC. Upstream's Python path only ever does the latter.
Consequence: `tools/cabana/utils/api.{cc,h}` (Qt `QNetworkAccessManager` + duplicate JWT code)
**gets deleted**, which drops `Qt5Network` from the cabana link and converges with §3.B.
### B. de-Qt: `#37519`, `#37521`, `#37522`, `#37523`, `#38357`, `#38359`, `#38360`
The bulk of the 53-commit gap. `QString``std::string` through the DBC core, new Qt-free
`core/{can_data,color,message_id,settings}.h`, Qt/core split in `dbc/dbcqt.{cc,h}`, QtXml dropped
(bootstrap SVG symbol extraction hand-rolled in `utils/util.cc`), QtConcurrent dropped, QtSerialBus
dropped (SocketCAN now raw `linux/can` sockets, `#37553` excludes it on macOS).
**Zero konn3kt content.** Pure upstream adoption. It is also why almost every cabana file shows a
large diff — those are not IQ changes, they are upstream changes we lack.
Net effect on `SConscript`: `qt_modules` goes from
`["Widgets","Gui","Core","Network","Concurrent","DBus","Xml"]``["Widgets","Gui","Core"]`,
and `Qt5SerialBus` / `QtSerialBus` framework drop out.
### C. `--zmq` bridge path (`#38484`) — **breaks konn3kt_canproxy if ported blind**
Upstream HEAD `DeviceStream::start()` forks `cereal/messaging/bridge <addr> /"can/"` and
`streamThread()` then always subscribes on `127.0.0.1`. `bridge <ip> <whitelist>` is
`zmq_to_msgq` (see `cereal/messaging/bridge.cc:60`) — it ZMQ-subscribes from `<ip>` and republishes
to local msgq.
iqpilot's documented remote-CAN workflow is the opposite direction:
`konn3kt_canproxy.py` sets `ZMQ=1` and **publishes** capnp `Event` frames on a local ZMQ `can`
socket; the user then picks *Live → Device → ZMQ → 127.0.0.1*. Under upstream HEAD semantics that
forks a bridge which subscribes to the same loopback endpoint the proxy publishes on and
republishes into msgq, while cabana reads ZMQ — the proxy path stops being the thing cabana reads.
Resolution: keep the pre-`#38484` direct-attach semantics as an explicit IQ escape hatch. Port
upstream's bridge-forking code, but subscribe to `zmq_address` directly (no bridge fork) when the
stream is started in "attach to an existing ZMQ publisher" mode — either by treating loopback
addresses as attach-mode, or by adding a third radio button next to MSGQ/ZMQ. Mark it
`// IQ.Pilot patch:` and state why (konn3kt_canproxy). **This must be re-verified end-to-end on a
real device before the sync is called done** (§5.3).
### D. libyuv removed, `common/yuv.h` added (`#38306`)
jotpluggler already patches around this. Cabana's `cameraview.cc` does NV12 conversion too.
Decision: port `common/yuv.h` into iqpilot once, then **delete** the `runtime.cc` IQ patch and take
upstream's `cameraview.cc` verbatim. One-time cost, removes a recurring patch site.
### E. Vendored native deps via `comma-deps-*` wheels (`#37327`, `#37994`, `#37681`, `#38308`)
Upstream `SConstruct` now exports `ffmpeg_libs` and pulls capnproto/ffmpeg/zstd/zeromq/json11/
libusb/imgui/bootstrap-icons from pip wheels. iqpilot pins `imgui`/`libusb` from
`git.konn3kt.com/IQ.Lvbs/dependencies`, vendors json11 in `third_party/json11`, and has **no**
`bootstrap_icons` module and **no** `ffmpeg_libs` export.
Resolution: add an `ffmpeg_libs` list + `Export` to iqpilot's `SConstruct` mirroring upstream's
(`avformat avcodec swresample avutil` [+ `x264 z` [+ `va va-drm drm`]]) so both tool SConscripts can
take upstream's form nearly verbatim instead of hand-listing libs. Keep `third_party/json11`.
Do **not** chase the wheels.
### F. `androidLog` → `operatingSystemLog` (`#38209`)
iqpilot cereal still has `androidLog`. Keep the existing `sketch_layout.cc` / `app.h` IQ patch.
Renaming cereal is out of scope (it is a schema change with device-side blast radius).
### G. Tests: catch2 dropped (`#38408`), unittest conversion (`#38387`, `#38384`)
Upstream deleted `tools/cabana/tests/test_runner.cc` and split a deliberately Qt-free
`tests/test_dbc_core` target (objects built from the **base** env, linking no Qt — it exists
specifically to stop Qt creeping back into the DBC core). Adopt that shape; drop `test_runner.cc`.
jotpluggler gains `test_jotpluggler.py`.
### H. `cabana` launcher script + `_cabana` binary (`#37814`, `c02cf706a`, `05cf8023a`)
Upstream builds `Program('_cabana')` and ships `tools/cabana/cabana` as a bash launcher that
installs Qt if missing and runs `scons -u openpilot/tools/cabana/_cabana openpilot/cereal/messaging/bridge`.
iqpilot builds `Program('cabana')` — so `tools/cabana/cabana` is currently a **committed binary**.
Adopting upstream's split means: `.gitignore` `cabana``_cabana`, and the launcher's scons
targets de-prefixed to `tools/cabana/_cabana cereal/messaging/bridge`.
### I. `assets.cc` / bootstrap icons (`#37994`, `71290f380`, `15267e408`)
Upstream generates `assets/assets.cc` at build time (`rcc` over an `assets.generated.qrc` that
interpolates the packaged bootstrap-icons SVG path) and gitignores it. iqpilot tracks the generated
14.9k-line `assets.cc` because the icons package isn't available.
Recommended: vendor the bootstrap-icons **SVG** next to the already-vendored TTF
(`tools/jotpluggler/assets/bootstrap-icons.ttf`), add a tiny local shim module exposing
`SVG_PATH`/`TTF_PATH`, adopt upstream's generation, and untrack `assets.cc`. This also lets
jotpluggler's `icons.cc` IQ patch collapse back to upstream's `BOOTSTRAP_ICONS_TTF` define.
Acceptable fallback: keep tracking `assets.cc` and keep both patches.
---
## Status
**Phases 02 are done and landed.** Both tools are at upstream `e7b1ee3a5` (2026-08-05) with
konn3kt preserved. Departures from the plan as written, and what remains:
- §3.I resolved better than planned: `third_party/bootstrap/bootstrap-icons.svg` turned out to be
**tracked already**, so upstream's build-time embedding was adopted as-is — no vendored SVG, and
the 14.9k-line generated `assets.cc` is gone (it was never tracked; it was a gitignored build
product).
- §3.D done both ways: `common/yuv.{cc,h}` ported, and `tools/replay/framereader.cc` moved off
libyuv too, so the jotpluggler `runtime.cc` patch disappeared entirely.
- Unplanned dependency found during the port: upstream's `#38430` also added download/decompress/
parse instrumentation to `LogReader`, which jotpluggler's load-stats panel reads. Ported into
iqpilot's `LogReader` around its own FileReader/decompress steps (so decompress is timed
separately, unlike upstream where the Python downloader does it inline).
- §3.C landed as a **three-mode** `DeviceStream` (`Msgq` / `Zmq` / `Bridge`) rather than an
escape hatch. Upstream's ZMQ path forks a bridge that republishes to **msgq** while still
setting `ZMQ=1` and subscribing over **ZMQ** — that combination reads nothing. Keeping the modes
explicit preserves upstream's bridge convenience *and* the direct attach `konn3kt_canproxy.py`
needs, and fixes that inconsistency. `--bridge <ip>` is a new flag.
- Also de-comma'd beyond the plan: jotpluggler's "Useradmin" / "comma connect" buttons became a
single **konn3kt** link (`KONN3KT_APP_HOST`, default `https://konn3kt.com`, same
`<dongle_id>/<log_id>` path shape `tools/lib/logreader.py` already parses).
### Phase 3 verification actually performed (macOS arm64, live konn3kt)
- **konn3kt API, live, real credentials** — every endpoint the port builds returns 200 with real
data: `/v1/me/devices/` (2 devices), `/v1/devices/<id>/routes_segments?start=&end=` (1776
routes), `/v1/devices/<id>/routes/preserved` (16), `/v1/route/<name>/files` (48 logs / 48 qlogs
/ 48 qcameras).
- **Real route in the real GUI** — `_cabana --qcam 0f53129ed44f6920|000001a5--07db1da02e` loaded
**48 valid segments** and came up as a foreground Cocoa app (not offscreen).
- **Live CAN over the ZMQ attach** — a local publisher mimicking `konn3kt_canproxy.py` fed
`_cabana --zmq 127.0.0.1`; cabana ingested **2179 CAN frames** and wrote them to
`~/cabana_live_stream/.../rlog`. This is the exact path `#38484` would have broken.
- **Fixed while verifying:** `utils::icon()` painted a null QPixmap whenever an empty icon id was
requested (`ToolButton("")` in `chartswidget.cc`), logging two `QPainter` warnings on every
dark-theme macOS start. Pre-existing upstream bug, not a port regression; guarded here and worth
upstreaming alongside the `dbc.h` mux fix.
**Still outstanding:**
- **Phase 3.3 (device half):** the konn3kt relay accepts the websocket and authorizes the stream,
but `0f53129ed44f6920` emits no frames because `canlived` only runs while the `CanLiveStreaming`
param is set. Needs a device with that enabled to close the loop.
- **Phase 3.5:** cabana has not been built for larch64.
- Charts/video widgets were not visually inspected — `screencapture` is blocked by macOS
screen-recording permission for the terminal, so GUI rendering is evidenced by process state and
the ingested-frame log rather than by a screenshot.
Tests added: `tools/replay/tests/test_api.cc` (konn3kt endpoint paths + error envelope),
`tools/cabana/test_cabana_konn3kt.py`, `tools/jotpluggler/test_jotpluggler.py`, and
`tools/test_tools_local_route.py` (synthesizes a local route and drives both tools headlessly).
## 4. Execution plan
Land each phase as its own commit. Build after every sub-step — do not batch §4.2.
### Phase 0 — infrastructure (no tool code)
0.1 `SConstruct`: add and `Export` `ffmpeg_libs`.
0.2 Port `common/yuv.h`; drop the `runtime.cc` libyuv patch.
0.3 Vendor bootstrap-icons SVG + local `bootstrap_icons` shim (`SVG_PATH`, `TTF_PATH`).
0.4 `tools/replay/api.{cc,h}`: add `getDevices()`, `getDeviceRoutes()`, `getRouteFiles()` against
the konn3kt endpoints, matching upstream `PyDownloader` JSON shapes.
**Gate:** `scons -u -j8 tools/replay` clean; a scratch binary lists real devices from
`api-iqlabs.konn3kt.com` with both a device JWT and a PC `auth.json` token.
### Phase 1 — jotpluggler (6 commits; do first, it validates the whole pattern cheaply)
1.1 `3f49e2d33` thumbnail source — add `thumbnail.{cc,h}`; `common.h` `kSpecialItemSpecs` 5→6;
`common.cc` `PaneKind::Thumbnail`; `layout_io.cc` `"thumbnail"`; `app.{cc,h}` `ThumbnailView`
+ `ThumbnailFrame`; `session.cc` `setThumbnails`. Re-apply the json11/path/`Android` patches.
1.2 `576de9c7e` build speedup — take upstream `generate_event_extractors.py` **wholesale**
(iqpilot's copy has diverged: `event_base_slots`, `static_enums`, single-use scalar getters)
and re-apply only the `getBusTimeDEPRECATED()` patch.
1.3 `e7b1ee3a5` segment-range parsing — empty-begin guard in `sketch_layout.cc`.
1.4 `6b47a5b6b` decompress-in-downloader — iqpilot has no `PyDownloader`; confirm the replay-side
decompression path is unaffected (expected no-op).
1.5 `fef29ad22`/`911f07ee8` — add `test_jotpluggler.py`.
**Gate:** `scons -u -j8 tools/jotpluggler`; `./tools/jotpluggler/pluggle.py <konn3kt route>` opens,
thumbnails render, CAN series decode against iqdbc, a saved layout round-trips.
### Phase 2 — cabana (53 commits). Bottom-up, one layer per build.
2.1 **DBC core**`dbc/{dbc,dbcfile,dbcmanager}.{cc,h}`, new `core/{can_data,color,message_id,settings}.h`,
new `dbc/dbcqt.{cc,h}`. Re-apply iqdbc rename in `dbc/generate_dbc_json.py`.
2.2 **Streams**`abstractstream`, `livestream`, `replaystream`, `pandastream`, `socketcanstream`
(raw `linux/can`, macOS-excluded), `devicestream`. Re-apply the §3.C canproxy escape hatch.
2.3 **Routes** — port upstream HEAD `streams/routes.{cc,h}`, swap `PyDownloader::``CommaApi2::`.
**Delete `utils/api.{cc,h}`.**
2.4 **Widgets**`binaryview`, `historylog`, `videowidget`, `signalview`, `detailwidget`,
`messageswidget`, `mainwin` (re-apply the iqdbc menu label), `streamselector`, `commands`,
`settings`, `cameraview`, `chart/*`, `tools/*`, `utils/{util,export,elidedlabel}`.
2.5 **SConscript** — rebase on upstream; re-apply: non-nested paths, `iqdbc/dbc` +
`#iqdbc_repo`, `PrettyAction` wrappers, `third_party/json11`, `ffmpeg_libs` from Phase 0.1.
Qt modules collapse to `Widgets/Gui/Core` (+Charts).
2.6 **Tests** — adopt the Qt-free `tests/test_dbc_core` target; drop `tests/test_runner.cc`.
2.7 **Launcher** — adopt `cabana` bash script + `Program('_cabana')`; de-prefix its scons targets;
fix `.gitignore` (`cabana``_cabana`, `*.generated.qrc`, `bootstrap_icons.cc`).
**Gate (each sub-step):** `scons -u -j8 tools/cabana` clean, no new warnings.
### Phase 3 — konn3kt verification (the part that actually proves the sync)
3.1 Remote-routes dialog against `api-iqlabs.konn3kt.com`: device list populates; all five periods
(7d / 14d / 30d / 6mo / **Preserved**) return routes with correct start times and durations.
Preserved uses ISO timestamps, the rest use `*_utc_millis` — both paths must be exercised.
3.2 Open a konn3kt route end-to-end: route-file listing, log download + decompression, qcam/fcam
video, iqdbc DBC load, chart, CSV export.
3.3 **Remote live CAN:** `canlived` on device → konn3kt relay → `konn3kt_canproxy.py` on laptop →
Cabana *Live → Device → ZMQ → 127.0.0.1*. Frames must decode. Per prior work this is the
untested leg of the canlive feature — run it on `gutek-a1`.
3.4 Local live CAN: panda stream, MSGQ device stream, SocketCAN (Linux only).
3.5 larch64 device build, if cabana is still expected to build on-device.
### Phase 4 — plotjuggler (secondary, 14 commits)
Only `juggle.py`, `layouts/`, `README.md` diverge, and the only IQ delta is the `iqdbc.car.fingerprints`
import plus non-nested paths. Cheap; fold in after Phase 3 or skip.
---
## 5. Risks and rules
- **Don't batch Phase 2.** ~40 files change; a single "port everything then build" pass produces an
unbisectable wall of link errors. One layer, one build.
- **`git add` only ported files.** The tree currently contains built artifacts (`*.o`, `*.a`,
`moc_*.cc`, `tools/cabana/cabana`, `tools/jotpluggler/jotpluggler`,
`generated_event_extractors.h`, `generated_dbcs/`). Never `git add -A` here.
- `dbc/car_fingerprint_to_dbc.json` is a tracked build product regenerated by scons — expect churn
on every build; keep it tracked so cabana doesn't need iqdbc importable at runtime.
- Dropping `Qt5Network/Concurrent/DBus/Xml/SerialBus` changes the larch64 link set; verify the
device build before assuming the SConscript is done.
- §3.C is the one place where a faithful upstream port actively regresses a konn3kt feature. If
Phase 3.3 can't be run on hardware in this pass, **do not** land 2.2 with upstream's bridge fork
as the only ZMQ path.
- Upstream candidate: the `dbc.h` empty-multiplex-indicator guard is a real upstream bug fix worth
sending back.
## 6. Explicit non-goals
- Adopting upstream's nested `openpilot/` tree layout.
- Migrating iqpilot's replay to `PyDownloader` / `tools/lib` Python downloading.
- Renaming cereal `androidLog``operatingSystemLog`.
- Converging on the `comma-deps-*` pip wheels.
---
## Appendix A — cabana commits to port (`bcdeec313..e7b1ee3a5`, 53)
```
4cfdcea1d 2026-07-28 cabana: fix macOS build and --zmq bridge path (#38484) <- §3.C
6b47a5b6b 2026-07-23 tools: decompress in the python downloader (#38430) <- §3.A
75d590fb9 2026-07-21 replace catch2 tests for 20% faster builds (#38408) <- §3.G
e124d6df9 2026-07-19 more dead code gc
a04c045cd 2026-07-17 cabana: de-Qt, part 3 (#38360) <- §3.B
5d23a78c7 2026-07-16 cabana: de-Qt, part 2 (#38359) <- §3.B
06a73f538 2026-07-16 cabana: de-Qt, part 1 (#38357) <- §3.B
39117a587 2026-07-08 remove submodule symlinks (#38312)
f283f6703 2026-07-08 Revert "try removing submodule symlinks (#38310)"
b827c0f55 2026-07-08 try removing submodule symlinks (#38310)
1e49eac4d 2026-07-08 rm libyuv (#38306) <- §3.D
9877f6ac0 2026-07-08 ffmpeg: use shared libraries (#38308) <- §3.E
05cf8023a 2026-07-07 fix cabana <- §3.H
c02cf706a 2026-07-07 fix cabana launch script <- §3.H
5edc0bd89 2026-06-21 mv root dirs into nested openpilot (#38219) <- §1 (de-prefix)
20e0f21b5 2026-06-21 prefix paths with openpilot (#38223) <- §1 (de-prefix)
37eda06c9 2026-06-21 move cereal/ into nested openpilot (#38220) <- §1 (de-prefix)
bfd8d4868 2026-06-06 cabana: fix "seperated" typo in findsignal placeholders (#38142)
5408c86b7 2026-05-28 Cabana: Fixed internal typos and method casing (#38099)
15267e408 2026-05-11 cabana: gitignore generated file <- §3.I
bea893820 2026-05-10 use packaged bootstrap icons (#37994) <- §3.I
bd1c7f39e 2026-05-07 scons build cleanups (#37981)
0584a5f5e 2026-04-12 add bridge target to cabana run script (#37814) <- §3.H
31e4fe55a 2026-03-22 tools: setup ffmpeg hwaccel (#37718)
a8b5c7450 2026-03-21 prep for imgui tools (#37712)
240e0036d 2026-03-20 macOS: fix build (#37686)
a68ea44af 2026-03-14 cabana: use vendored libusb from commaai/dependencies (#37681) <- §3.E
5e7f5dd84 2026-03-14 replay/cabana: remove unused openssl dependency (#37680)
ee9da82aa 2026-03-13 cleanup build paths (#37667)
71290f380 2026-03-08 cabana: gitignore assets.cc <- §3.I
e42ee228c 2026-03-08 gitignore cleanups (#37615)
5e1a576f3 2026-03-07 cabana: exclude SocketCAN on macOS (#37553) <- §3.B
0c452dbaf 2026-03-03 cabana: fix right pane width limitation (#37527)
06b2c68e0 2026-03-01 macOS: fix cabana builds (#37518)
3478ac133 2026-03-01 cabana: remove QtSerialBus (#37523) <- §3.B
ce04d25f7 2026-03-01 cabana: remove QtConcurrent (#37522) <- §3.B
0c7abf385 2026-03-01 cabana: remove QtXml (#37521) <- §3.B
0b9ab8bb9 2026-03-01 cabana: replace Qt types with stdlib (#37519) <- §3.B (largest)
885658512 2026-02-28 new demo route (#37457)
e7cc70f3f 2026-02-28 consolidate file downloading from C++ to Python (#37497) <- §3.A
276713ddf 2026-02-27 add back bz2 support with vendored bzip2 (#37459)
238fca233 2026-02-25 tools: fix darwin compile errors (#37399)
8810948ec 2026-02-24 CI: ensure no brew (#37387)
76d084d87 2026-02-23 switch to system compilers (GCC on Linux, Apple Clang on macOS) (#37355)
f4a36f7f7 2026-02-22 rm cpp bz2 (#37332)
4bffe422e 2026-02-22 vendor capnproto and ffmpeg via dependencies repo (#37327) <- §3.E
c98ba4ff4 2026-02-20 Qt is optional (#37295)
037e6e749 2026-02-17 cabana: fix crash when zmq address is used (#37222)
af1583cdf 2026-02-12 Reapply tgwarp w NV12 fix (#37168)
45099e7fc 2026-02-10 Revert tgwarp again (#37161)
667f3bb32 2026-02-07 Revert "revert tg calib and opencl cleanup (#37113)" (#37115)
51312afd3 2026-02-07 revert tg calib and opencl cleanup (#37113)
d5cbb89d8 2026-02-06 Remove all the OpenCL (#37105)
```
## Appendix B — jotpluggler commits to port (`61608db78..e7b1ee3a5`, 6)
```
e7b1ee3a5 2026-08-05 jp: fix segment range parsing
6b47a5b6b 2026-07-23 tools: decompress in the python downloader (#38430)
576de9c7e 2026-07-21 speed up jotpluggler build (#38406)
911f07ee8 2026-07-21 convert tests to unittest (#38387)
fef29ad22 2026-07-19 start porting tests to unittest style (#38384)
3f49e2d33 2026-07-18 jp: add thumbnail source (#38363)
```
## Appendix C — how the baselines were established
Blob-hash matching, not commit archaeology: for each iqpilot file, walk upstream history
newest-first and report the newest commit whose blob for that path is byte-identical. Files with no
match are IQ-modified or IQ-new; the minimum over the untouched files bounds the fork point.
```
cd /tmp/openpilot-upstream
git log --format='%H %ad %s' --date=short -30 -- openpilot/tools/cabana tools/cabana > /tmp/cab_commits.txt
for f in $(cd $LOCAL/tools/cabana && find . -type f \( -name '*.cc' -o -name '*.h' \) -not -name 'moc_*' | sed 's|^\./||' | sort); do
h=$(git hash-object "$LOCAL/tools/cabana/$f") || continue
while read -r c d rest; do
b=$(git rev-parse "$c:openpilot/tools/cabana/$f" 2>/dev/null || git rev-parse "$c:tools/cabana/$f" 2>/dev/null)
[ "$b" = "$h" ] && { echo "$f -> ${c:0:9} $d $rest"; break; }
done < /tmp/cab_commits.txt
done
```
Cross-checked against the last upstream-authored commits in iqpilot's own history
(`bcdeec313 Reduce pub-sub memory usage by 10x (#36884)` for cabana) and against feature markers
(`virtual std::string routeName` first appears in `0b9ab8bb9`, 2026-03-01 — absent from iqpilot,
confirming the fork predates it).
@@ -16,27 +16,27 @@
},
"python/iqpilot_private/konn3kt/backups/archive_codec.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "2d4cfb58875fee44a7baea50946fb8b31feb5ceb0daf61598b032634f336363e",
"sha256": "9368086c4cdd6cf46b6a6d0a1e1e83fdc7e7dff8d3bd67a42b3bffa91b45dc26",
"size": 135536
},
"python/iqpilot_private/konn3kt/backups/backup_keys.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "903174fad1ee61fe959be5c9401b8d10e2edd05eadfa02e19e4cd599aa1e464d",
"sha256": "291789f912304c72cd1e0da955bd58fa903cd99e7d6504e6c3c51d39befaf6e8",
"size": 67664
},
"python/iqpilot_private/konn3kt/backups/backup_orchestrator.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "fd38412866cefa4be7777b7573b46e57665756f0d4d7decdfb189a8dafae8067",
"sha256": "c47ec079040efee31673f55752a6215b3a8908c63c8373625ce5cb4cb1177aff",
"size": 204624
},
"python/iqpilot_private/konn3kt/backups/cbc_vault.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "c95f786dbff8484c2ad63ed04fa37bcf0bdaf16f0b9f80d37e5d1e8e5b224554",
"sha256": "a0d5710938b32e56f2fc55c86ff8947787a3ebeb43850a94a90202c3773ff74e",
"size": 69752
},
"python/iqpilot_private/konn3kt/backups/imahelper.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "88b9a944da9cfbf6fe043ee685d4cd04e900b34996e69ab2eb7cedd807798854",
"sha256": "ff345aa71ac599bdd16747368ceb92fae7b5b35657f81466fc0660b76d44aa22",
"size": 203072
},
"python/iqpilot_private/konn3kt/flockd/__init__.py": {
@@ -46,12 +46,12 @@
},
"python/iqpilot_private/konn3kt/flockd/flockd.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "83336a6b02ffdb451297e3d4f6b586fa764f87195dfe6094805a4a18fcd9d279",
"sha256": "2aa26e4bc71870f639fca6e8e273deb59f50cf6a6d963ba77ce0949c65a52d63",
"size": 202168
},
"python/iqpilot_private/konn3kt/flockd/signatures.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "563e6dd93009fd5e99de5d6abebbed20a7020683a8a0266e3c1a1f9d6b5f2889",
"sha256": "96b47ec40dd7e3430cebf3979b4fdfb0096e80029b40da6445cdf44dbcdb8917",
"size": 68080
},
"python/iqpilot_private/konn3kt/hephaestus/__init__.py": {
@@ -61,67 +61,67 @@
},
"python/iqpilot_private/konn3kt/hephaestus/_vendor/localapi_runtime.zip": {
"mode": 420,
"sha256": "dc0184fd4971a040ca4ca332b7bcf6b2ee6b8040804c90d3efc3f2806359106c",
"sha256": "f12e44b7a986616d6b3c4e127e4eeae793e346f31d5b821ad9c44249e4731ebd",
"size": 2100238
},
"python/iqpilot_private/konn3kt/hephaestus/ble_auth.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "8036f724630003afda855cf293f098cc3bb50801a5ca7626c3b57d4556605f5a",
"sha256": "4ac192d102bde18d9b3215ff637e1c316a08eb0eb00c595c07eaa07284d1f28c",
"size": 268064
},
"python/iqpilot_private/konn3kt/hephaestus/ble_gatt.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "a97b390dd697dbba69ac83663c49f65c6408bbcb03be05c889bdee622d9e5efa",
"sha256": "6f2dccd9f85db9d4948ddb31f62b6059d9359f7f16df6d55455b05c6ae4f37b7",
"size": 340584
},
"python/iqpilot_private/konn3kt/hephaestus/ble_rpc_dispatch.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "a3fea60218cd759a37d4ca423b6b55d02fd5e36cb762e8333604d7fb322b4923",
"sha256": "abb3eb9033205d1c2903b619dbbab7e3de10b859493239c35533b75d00bc3305",
"size": 68048
},
"python/iqpilot_private/konn3kt/hephaestus/ble_transportd.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "548e4910fd18dfc8058c17bfabfbb5f69db15481169efd4fe68841f20c8a0692",
"sha256": "a631270d167cbd9a67db6111de57e5d6ddcdd554706fd9fd919d8f116733bf66",
"size": 335728
},
"python/iqpilot_private/konn3kt/hephaestus/bt_gamepad.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "3ed0ce5cfeec2f8cc3b6a0a85b6137d3bf7219904e5e76f6490f027e9d6346a0",
"sha256": "c5c4f480275ddc00e2d7d44aee76808f2b4184b6a6354ef2e757d8b3a7ae8e44",
"size": 271680
},
"python/iqpilot_private/konn3kt/hephaestus/cloud_routes.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "a8667a7a12c8a096e68b8716b0f309fad8d98e5cf056121a8f84371908421402",
"sha256": "a9d9fdfcd6f154e093ed1cc678bf9a1e12c7a3e475a358c19941af7777c40807",
"size": 134408
},
"python/iqpilot_private/konn3kt/hephaestus/hephaestusd.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "af175ad8b662828c25f2e53dd4af1e323318f4b7df51276914327ae110a8283d",
"size": 3514480
"sha256": "d3468f09df47814c161937306c776df5fe9a6a6d819859a596f7e33be6d253e8",
"size": 3581928
},
"python/iqpilot_private/konn3kt/hephaestus/kwp2000.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "0be3eb36bd0fcc798ad602ff7be133366d82c3b112df9663a94347e431dd6486",
"sha256": "c7cf16d3b2297deafd87c53c8b61031d2af84ff04848bab34d9dde37ba939ed5",
"size": 136448
},
"python/iqpilot_private/konn3kt/hephaestus/manage_hephaestusd.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "fa70ebb1569dde9f474040476c495d170874217cac0611e8057a92c9bfee0c92",
"sha256": "113890a2fa588f0c3d6d0c6723aef63adb58fc3ad5355174effe5ac2d3d6d364",
"size": 68136
},
"python/iqpilot_private/konn3kt/hephaestus/motd.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "674acf92f744733857437a9993e6e7bb41190e924ec894a45adb4acf396b560f",
"sha256": "e97f1650766b15b231657afd6530c019d09352dfa0d96deb2643893acf56efa4",
"size": 67840
},
"python/iqpilot_private/konn3kt/hephaestus/tp20.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "0b4e93091aaf56a2ae1b0a22dab3e7c07e2172df80308f97edb793e412310a3d",
"sha256": "7be8a68c56f6ff37ef887b87275c78e517065995243e88d1ab6a10f3b028abe5",
"size": 135648
},
"python/iqpilot_private/konn3kt/hephaestus/vw_pq_flasher.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "0b6ddf3b89bb78d1d2c68ef0c3f93796867c150606269f7666bdca093ecc856f",
"sha256": "2a1f6cbb8794a29a55decf6d04915e752360eac7701ed55c826355bb348e39a6",
"size": 269568
},
"python/iqpilot_private/konn3kt/uploaderd/__init__.py": {
@@ -131,7 +131,7 @@
},
"python/iqpilot_private/konn3kt/uploaderd/iquploaderd.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "9104feff3435925c9235b8424f08dc52eee877ae546093ecea83687b3fd6478a",
"sha256": "613141bdba6cabaf47fa07b438fe964e90796b7e0309c8f0fbfa60b6bf54e6ad",
"size": 204184
},
"runtime": {
@@ -171,25 +171,25 @@
}
},
"signatures": {
"python/iqpilot_private/konn3kt/backups/archive_codec.cpython-312-aarch64-linux-gnu.so": "P79t7+Txevk0MV7hDKGcDPlx2RH9CACuI89MdIuouzwjTIPyi/XZxb2z5qukLp/SENm8tIO/uvDqNMGg1zsNDQ==",
"python/iqpilot_private/konn3kt/backups/backup_keys.cpython-312-aarch64-linux-gnu.so": "9iVq7Mj2pYNsFG/663bMRD1XH2IEk6+Xbpd41jT83rhtYCi83aHzXcXyDcqNmyYhQpPeu4It9sp1eSj42w5DBg==",
"python/iqpilot_private/konn3kt/backups/backup_orchestrator.cpython-312-aarch64-linux-gnu.so": "f3SUgO2s1N9juez3vvXlhi4KV9gibraZs0XKdtlTCHS9TVpUPAforrIv+fiiv+CnXuNQaFxOsVE1nsVxYUFVCg==",
"python/iqpilot_private/konn3kt/backups/cbc_vault.cpython-312-aarch64-linux-gnu.so": "+Em9DTW7a6uzqgeZlLCFFjIjN3su8P7SImlAiLLc9Bc8ylsmvaEqfqLewnblANN0E3fPefIWPy2SXtE/sh8DDw==",
"python/iqpilot_private/konn3kt/backups/imahelper.cpython-312-aarch64-linux-gnu.so": "adDfmCgrUH/7F/tctcIfy7PMLkdnEj0Y8dJO6q7/Ds34WpyNZy0JousS007TGsryrIAjZsfujMieVei7DCvbDA==",
"python/iqpilot_private/konn3kt/flockd/flockd.cpython-312-aarch64-linux-gnu.so": "khtz+JnG262yIsCgaqzeSxqlkbyXbzVbZrVxuXV6SQhhl+Ob/LL1ZgSs6Wpca48STo2NMYjglzeaM5jwj847Bg==",
"python/iqpilot_private/konn3kt/flockd/signatures.cpython-312-aarch64-linux-gnu.so": "ZFG+EQgMvwqq8T6VNUqqgQJmIgAC1AIYNF/QWYU7MxYWWWfnx9s+G5Mz2EwLMUIdW16pGXsqkn02u4kHNxpcCg==",
"python/iqpilot_private/konn3kt/hephaestus/ble_auth.cpython-312-aarch64-linux-gnu.so": "KW+OPawOOOKdeIl4l7gc1hjV8sdd0QXk8tIBl5UXwXgQSdbGwE+H8mCc2EKEhDb5wR1Zl/Cb6pC7D8ksNZcFCQ==",
"python/iqpilot_private/konn3kt/hephaestus/ble_gatt.cpython-312-aarch64-linux-gnu.so": "Q+2toj33o3kivPIA152qAIRGdUMFI9jKJ97W9bf2cNXCWAs04p8XvHIxjD1JChJMfvcZLHIk9HlumRSalcT1Dg==",
"python/iqpilot_private/konn3kt/hephaestus/ble_rpc_dispatch.cpython-312-aarch64-linux-gnu.so": "s7qLI/aus4eexP6XEqrreU/stI781n0T0F1yeerxo0BoaUc5oT0HTQkrZTgSagFfGnPT228MKEhB7xW2JOuAAg==",
"python/iqpilot_private/konn3kt/hephaestus/ble_transportd.cpython-312-aarch64-linux-gnu.so": "ESRJsvz0NplwLhzY/oGMGP4ZD3S70fd07QSl/xLdFcX36Scelic5pJO9ClBSeoMd1uA1iWmSMARK4vugJHTBBQ==",
"python/iqpilot_private/konn3kt/hephaestus/bt_gamepad.cpython-312-aarch64-linux-gnu.so": "DPbD2qSAUTkR0JmZT+9bPYDZlSeAmZaKnRJiC9Av+uLpX/lCnbIeLMEgRFrOdho/gf4CzmDcbfboULYwgd06AQ==",
"python/iqpilot_private/konn3kt/hephaestus/cloud_routes.cpython-312-aarch64-linux-gnu.so": "ut5z43F74P4rq51YQBaPU35hrOsTphthrkAfYv7ezFUwSfZei0jUBvJAujU9n6fbg58NiUYrIgwe8/AnntpaCw==",
"python/iqpilot_private/konn3kt/hephaestus/hephaestusd.cpython-312-aarch64-linux-gnu.so": "IUzori/U8oSQPTO9KSMvR0/B3zBmpwmSUNdnb2Idop3gGaGBrebNaRv4Ilbjp6+xVNArvEsOybVxPPSapVsjDg==",
"python/iqpilot_private/konn3kt/hephaestus/kwp2000.cpython-312-aarch64-linux-gnu.so": "7aEw/FLCYQuH+VqyeV88nnfHG9xFrfP0XK9rNltmT6YXAWKN6PtnpXVqPTmrBuuOlkl+pCIKLIvKVm52dbAOAg==",
"python/iqpilot_private/konn3kt/hephaestus/manage_hephaestusd.cpython-312-aarch64-linux-gnu.so": "EDpN12qdopPVzYu4aHbrNwYjN3vfRrEJhln5XNrVnh/RsZNkM0HKTJJdcVauJz//lEwIHTn1KLvOgiKzn5rlBA==",
"python/iqpilot_private/konn3kt/hephaestus/motd.cpython-312-aarch64-linux-gnu.so": "0ud+tsJGU2wew33iJmC6bJ+Pg9f4Fl0znsbXhO4lUXmuqCWTfaCWYbAASggUVF0MFYccJ9aZsARON2S/24igAw==",
"python/iqpilot_private/konn3kt/hephaestus/tp20.cpython-312-aarch64-linux-gnu.so": "+3ahkAwXUIluEv86JNK0HpnsrprT8iPdKDRBWdCWc4ux+k8YN7bCDQSl+B1gkGRxB/2hCQq0ncF3EKb3+yTCCQ==",
"python/iqpilot_private/konn3kt/hephaestus/vw_pq_flasher.cpython-312-aarch64-linux-gnu.so": "ch5zd8KmXfi/vyI0see68Qdcr7PUBIyJ2iJ2KMGGfIXufa57DBDuyzcwfkyeA6btC8mt+mokIeRap/gUhyraAg==",
"python/iqpilot_private/konn3kt/uploaderd/iquploaderd.cpython-312-aarch64-linux-gnu.so": "iLrfT53m4UBggK+ql3XR2FYQrPtuIxnfGe7jSLUd3skttpG6FNL9g+dEzbU4IPoc9wgE6kvxorjz7KejFq7WBw=="
"python/iqpilot_private/konn3kt/backups/archive_codec.cpython-312-aarch64-linux-gnu.so": "nSuJmlS6Uu7Z2hCUfo8ia0v3TtLy0k7vXuUtkRteuXr283FbjSL/SAnGZtotLam9QqWcKUZiX5AY7iKRxdJRDA==",
"python/iqpilot_private/konn3kt/backups/backup_keys.cpython-312-aarch64-linux-gnu.so": "b4kzh2Ih+crCs71fFdIku1vWUarJheL2EFkX9K85ppio/t27Vml4wjdRXt6G9ImEIwXb0FwZQ5TO290oTS81DQ==",
"python/iqpilot_private/konn3kt/backups/backup_orchestrator.cpython-312-aarch64-linux-gnu.so": "Y4Z6VxELzmD2+MZKv/Uk7mE03ZFxxjXFiV21zHXbGTTQ8x+3OWSs+c43kfEFO7+Y0GRLAV/pFVdS4TJotL1bCA==",
"python/iqpilot_private/konn3kt/backups/cbc_vault.cpython-312-aarch64-linux-gnu.so": "uKdDonv7l3RV5Rk0p18w2B7OFo56b61Gdzj4Q2HCa4MkjzVv81Pe7kf+2AbRXJ+tV74P1Xg/S4uFYH9B03D7Cw==",
"python/iqpilot_private/konn3kt/backups/imahelper.cpython-312-aarch64-linux-gnu.so": "+HMqTUeIHFUo9Fr7/yzkI9tQ+/uCJI1dH06oRDg1xgU8YjuiI8XUZCpgsUR4nMn7O497qiCdL5TfiYJw/0j/AQ==",
"python/iqpilot_private/konn3kt/flockd/flockd.cpython-312-aarch64-linux-gnu.so": "EOxdzTbj5/I7gOu48kM3Z0G54cJ1pII4tikrDqYhVSFRiJopUPtjb2IAG/Lg0D6kwZaIzbkEVpRKq707MF2BAw==",
"python/iqpilot_private/konn3kt/flockd/signatures.cpython-312-aarch64-linux-gnu.so": "fZ8UhZhwmPn4IMxudbJkmrCsBj/A+EOH4cdzT8Vb75kI7SD9V5rR7gRCPFuVEL1nQ6lnlF2QW4Osmik2WOqeDA==",
"python/iqpilot_private/konn3kt/hephaestus/ble_auth.cpython-312-aarch64-linux-gnu.so": "bptAbc1grlaQ8cQoR1RZ1p5GKEvnHUGOpyRoCmkK2xW2HMRD9YmY+t7HrARFfr14Bkp0dgdVKswzZWa6aUR3Cg==",
"python/iqpilot_private/konn3kt/hephaestus/ble_gatt.cpython-312-aarch64-linux-gnu.so": "4Q9ZzJhFaTqnaj66o+CHE/vtjY9qWOHO7UFYZrxs3uQdUaeIO5JUkXHtr4Fl7mDrZy0zaT8hL+R6g7zj8VLCCg==",
"python/iqpilot_private/konn3kt/hephaestus/ble_rpc_dispatch.cpython-312-aarch64-linux-gnu.so": "bzGWFkSPQg9xj1Rl8jBkGx3coMF6srLe509MLng4gQMqo8hHHWZTFI3235UsSJCkbnPjOolUWzVLtbDp5bMmDQ==",
"python/iqpilot_private/konn3kt/hephaestus/ble_transportd.cpython-312-aarch64-linux-gnu.so": "pIB/KThS9GZ+HYf+SSd14rxawltaKKAZ1cFyYRPWpAnpEZ4q/Vz7G2o6bdEZ2r8bJsfBMMkzyPLRJQwpX0jODQ==",
"python/iqpilot_private/konn3kt/hephaestus/bt_gamepad.cpython-312-aarch64-linux-gnu.so": "FEbOGV+d8BvV4yurjBNz74nK0CT+yQSReXPRliMt9uuMNLKVuRMlUibmgD63sx7tv0hz7Z49VwYYZVlwEPNrDw==",
"python/iqpilot_private/konn3kt/hephaestus/cloud_routes.cpython-312-aarch64-linux-gnu.so": "x17n17jSDgrTCCphHNWZQ4ibHMGITtrhaDK1wBmWFqj6uzr3cWWXV6THxNJy4qbj9/yHVLICGNuW7wIOb0tzDQ==",
"python/iqpilot_private/konn3kt/hephaestus/hephaestusd.cpython-312-aarch64-linux-gnu.so": "ldjMNYlSmOa3XjWrcqlDMrNybvFZ3T9aVVZevwt9TYxVgUrz/5qMui7KIAPdEins39c4fu0jayqM/1V0kjsUCA==",
"python/iqpilot_private/konn3kt/hephaestus/kwp2000.cpython-312-aarch64-linux-gnu.so": "Cvq80OZLZ0rIywx1RjkV5cd6jKnpL0Rq9evyOxGqYwc0xGCGL1t2YqsoiyyODMeMphEdvdK+w+OMBge6OGk1DQ==",
"python/iqpilot_private/konn3kt/hephaestus/manage_hephaestusd.cpython-312-aarch64-linux-gnu.so": "17P0y74P0Ug92QlhioE6XJJ6p3tz8atS5Wp2SihaQZfnERG8CCjQ6RjVBSwINdvJuU05sGexOlb29Cjebc92DQ==",
"python/iqpilot_private/konn3kt/hephaestus/motd.cpython-312-aarch64-linux-gnu.so": "wcXjEUKfLHopsy91qnShxa8/1D6wChOsFZcOkPnA0b61vPY7ceKlVMXcv+ECr35BiMsW4gHQG9MSMoyXo1ZJAw==",
"python/iqpilot_private/konn3kt/hephaestus/tp20.cpython-312-aarch64-linux-gnu.so": "8nTnxWCYCQ2mtzd8JL8kcRLsU6JrKVrfMNfI84ovoejxH7kAXCM6d3f1u6TFNPqrQjlIFlc1AyUoL7UoFsQJCA==",
"python/iqpilot_private/konn3kt/hephaestus/vw_pq_flasher.cpython-312-aarch64-linux-gnu.so": "fs6ZLA1aFyUMWRpeSduc0nnv4cJjb3OIdxmUGaCr58D5GAGBSOOUQszzMWDwrEOCQ0JOsW27TM3VmliIhP1cBA==",
"python/iqpilot_private/konn3kt/uploaderd/iquploaderd.cpython-312-aarch64-linux-gnu.so": "YleZT+ZmBpdryTlsH4k+yoLYhyIJ1DMPYnEC8L2ixxJejAjcUBmn2GjXFSS+7JzK6g8aokV6NswVSqbuIVOJDQ=="
}
}
+42 -7
View File
@@ -6,7 +6,7 @@
},
"python/iqpilot_private/iqvd/__init__.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "d36af86bf1fe721522e729b6a48e3a962ce033dcfe89f9e00e3d6208b1801591",
"sha256": "48bad6268fabc23a9be62fcd7f61240ebcbabd5e3c6afcf7d3328bd33d93d3a2",
"size": 67616
},
"python/iqpilot_private/iqvd/__init__.py": {
@@ -16,12 +16,42 @@
},
"python/iqpilot_private/iqvd/iqvd.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "f81036080eb7bc0c91bae10b11f52064041311a5fc67e7dc0fba32f585318e41",
"size": 201584
"sha256": "674c4ecd09e36af6c2b9a41285e78914ee94a0e8c4de728904171dd59e02edda",
"size": 135576
},
"python/iqpilot_private/iqvd/offload/__init__.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "7ce45dfb9a6ffe8e5426f261cf7e9b3a3825e3d284a0675013d43697e73d56d9",
"size": 67616
},
"python/iqpilot_private/iqvd/offload/__init__.py": {
"mode": 420,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"size": 0
},
"python/iqpilot_private/iqvd/offload/client.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "439f88b68d220912db2f7e9a43d41827d298f68b113e7c5e5858adea3a05e963",
"size": 135296
},
"python/iqpilot_private/iqvd/offload/geometry.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "0199f81ce365fb956d729c61c20c8eaa0120448171db6b83544f3dd1a0861877",
"size": 68232
},
"python/iqpilot_private/iqvd/offload/perception.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "678ebcdd9a97fe527970bbb93021d2ae8f4f9935444b6558dd61b399ab3fe3e9",
"size": 135976
},
"python/iqpilot_private/iqvd/offload/protocol.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "a8d24107433a037d19876b2240e980b09c89f560ba960dda5e28c16665a03ca6",
"size": 68120
},
"python/iqpilot_private/iqvd/yolov8_net.cpython-312-aarch64-linux-gnu.so": {
"mode": 493,
"sha256": "976be8806f006a175a14b0c63e9a1c2c2b845ee2dab77a41eeb6759c7feab641",
"sha256": "943e0ef218eed109289d208ea76407e223a4e8b81659f0ec79dc090fbd7ed329",
"size": 204568
},
"runtime": {
@@ -33,8 +63,13 @@
}
},
"signatures": {
"python/iqpilot_private/iqvd/__init__.cpython-312-aarch64-linux-gnu.so": "bMboPEsPzZjh0Y1Zk3Brvc6fyWFF5CxuzN9KQr2RK0Hn+l8XiD6l/ym2Y10nFxvj8+uLikIZ0xSAovJlJcRUAA==",
"python/iqpilot_private/iqvd/iqvd.cpython-312-aarch64-linux-gnu.so": "seyE6m33mqlKZ4H7b1JL6pxxz+r/+dKP1PzcfXxhdhHvsl8Ka8Noi6+ln5z+QSvBgt5ulxQeBJMBP5qdu6lOCw==",
"python/iqpilot_private/iqvd/yolov8_net.cpython-312-aarch64-linux-gnu.so": "RvzkJLNxPA+u7yEnH5jd2jecT02PJCQkIDj/LiWvBCE4lFmFFzOf1Q+ZCGGjrFQzhLQZlm75SvW80mlqJ9BJCw=="
"python/iqpilot_private/iqvd/__init__.cpython-312-aarch64-linux-gnu.so": "YPDPJiSbuWXyg3MpOJXzZtejR5sazfra6c/AFVm+tzE+i9OA9/OPS5qvpFSaHOBPaVZYtsmxkOFq4Q5GJlYOAg==",
"python/iqpilot_private/iqvd/iqvd.cpython-312-aarch64-linux-gnu.so": "H+2OPwV0XS7biX05P6fBzRBCN1Cjnq9eH6YSLk+9BX8OPbdiyJTbDpt8YZYk4nU0LO5FF4dMkEXzFRsewB7BAw==",
"python/iqpilot_private/iqvd/offload/__init__.cpython-312-aarch64-linux-gnu.so": "vbv8RlHm7LGdrojZuRhs+2StjASHEC1GbkZspXh6FCbmyKmW7F9B5GzHjIwr7HVFSNX6u+ELPPKebfnIsOkmAQ==",
"python/iqpilot_private/iqvd/offload/client.cpython-312-aarch64-linux-gnu.so": "pfDu4/OifTpFWqMD/hyQN+p+9syWaiAavHCkvxG9++0VvHDwKsBPJFxRk8r3vBdylSz4zkIFXlqPPt/hNuzJAg==",
"python/iqpilot_private/iqvd/offload/geometry.cpython-312-aarch64-linux-gnu.so": "Ll6azxnD3jGmyPPjd+yTmAVCl+CwM2iSdSqpYtTtzqPIb6GFBp0FfF5w+FTpacKjoQ4i3cXV4gdUVOf3qRWzCg==",
"python/iqpilot_private/iqvd/offload/perception.cpython-312-aarch64-linux-gnu.so": "ww5BVRLiGB+LkqREN1f7njOrrgOcWghyTXNXTJTJHYeLx5AgIrvsqg1PH7+nq1LuSvk+LHDTOSUAcs7JIDh3DQ==",
"python/iqpilot_private/iqvd/offload/protocol.cpython-312-aarch64-linux-gnu.so": "3OzAWo66E+qe/H+HtGEP1i73WLW9SPa9+zeb8fOWBe7Nlsx7WrgGcURBq0VTycv2v3QLNLEluei/rnQEYBRMCA==",
"python/iqpilot_private/iqvd/yolov8_net.cpython-312-aarch64-linux-gnu.so": "C64cBnuSM2wm6vCEO9W3cNqbB4P7Yuy78pFUXxn8iesCQD0hbZ1+w8IuH4VuxB88ubLRLQt2GZ8LYIESb+HYAg=="
}
}
+28 -1
View File
@@ -333,6 +333,7 @@ struct IQCarParams @0xd4189b5c8aca9f78 {
iqLateralNet @4 :LateralNet;
longitudinalStoppingSpeedOverride @5 :Float32; # m/s; zero keeps the upstream default
stoppingDecelRateOverride @6 :Float32; # m/s^3; zero keeps the upstream default
longActiveWithGasOverride @7 :Bool; # keep long control active while the driver is on the gas
struct LateralNet {
fuzzyFingerprint @0 :Bool;
@@ -762,7 +763,33 @@ struct IQVehicleTracks @0xb877ef4b20a4ae22 {
}
}
struct CustomReserved13 @0xfd960244a79e2804 {
struct IQEnvironment @0xfd960244a79e2804 {
frameId @0 :UInt32;
offloaded @1 :Bool;
modelValid @2 :Bool;
objects @3 :List(Object);
struct Object {
x @0 :Float32;
y @1 :Float32;
z @2 :Float32;
width @3 :Float32;
height @4 :Float32;
length @5 :Float32;
prob @6 :Float32;
label @7 :Label;
enum Label {
car @0;
motorcycle @1;
bus @2;
truck @3;
person @4;
bicycle @5;
stopSign @6;
trafficLight @7;
}
}
}
struct CustomReserved14 @0xa6e5a1ce8ca5258e {
+10 -1
View File
@@ -177,6 +177,15 @@ struct InitData {
wallTimeNanos @20 :UInt64;
ufsHealth @25 :UfsHealth;
struct UfsHealth {
preEolInfo @0 :UInt8;
lifeTimeEstimateA @1 :UInt8;
lifeTimeEstimateB @2 :UInt8;
vendorHealthReport @3 :Data;
}
enum DeviceType {
unknown @0;
neo @1;
@@ -2670,7 +2679,7 @@ struct Event {
iqPerfTrace @136 :Custom.IQPerfTrace;
iqConstructionZone @137 :Custom.IQConstructionZone;
iqVehicleTracks @138 :Custom.IQVehicleTracks;
customReserved13 @139 :Custom.CustomReserved13;
iqEnvironment @139 :Custom.IQEnvironment;
customReserved14 @140 :Custom.CustomReserved14;
customReserved15 @141 :Custom.CustomReserved15;
customReserved16 @142 :Custom.CustomReserved16;
+1
View File
@@ -100,6 +100,7 @@ _services: dict[str, tuple] = {
"iqLiveData": (True, 1., 1),
"iqConstructionZone": (True, 2., 2),
"iqVehicleTracks": (True, 4., 4),
"iqEnvironment": (True, 4., 4),
"mapdOut": (True, 20., 20, QueueSize.MEDIUM),
"mapdExtendedOut": (False, 1., -1, QueueSize.MEDIUM),
"mapdIn": (False, 1., -1, QueueSize.MEDIUM),
+1
View File
@@ -6,6 +6,7 @@ common_libs = [
'util.cc',
'ratekeeper.cc',
'clutil.cc',
'yuv.cc',
]
_common = env.Library('common', common_libs, LIBS="json11")
+13 -1
View File
@@ -3,6 +3,7 @@ import os
import requests
import unicodedata
from datetime import datetime, timedelta, UTC
from functools import lru_cache
from openpilot.system.hardware.hw import Paths
from openpilot.system.version import get_version
@@ -11,6 +12,16 @@ KEYS = {"id_rsa": "RS256",
"id_ecdsa": "ES256"}
@lru_cache(maxsize=4)
def load_signing_key(private_key: str):
# PyJWT re-parses a PEM string on every encode; an RSA parse is ~40ms, so cache the key object
try:
from cryptography.hazmat.primitives.serialization import load_pem_private_key
return load_pem_private_key(private_key.encode(), password=None)
except Exception:
return private_key
class BaseApi:
def __init__(self, dongle_id, api_host, user_agent="openpilot-"):
self.dongle_id = dongle_id
@@ -38,7 +49,8 @@ class BaseApi:
}
if payload_extra is not None:
payload.update(payload_extra)
token = jwt.encode(payload, self.private_key, algorithm=self.jwt_algorithm)
key = load_signing_key(self.private_key) if self.private_key else self.private_key
token = jwt.encode(payload, key, algorithm=self.jwt_algorithm)
if isinstance(token, bytes):
token = token.decode('utf8')
return token
+4
View File
@@ -21,6 +21,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"CalibrationParams", {PERSISTENT, BYTES}},
{"CameraDebugExpGain", {CLEAR_ON_MANAGER_START, STRING}},
{"CameraDebugExpTime", {CLEAR_ON_MANAGER_START, STRING}},
{"CanLiveStreaming", {CLEAR_ON_MANAGER_START, BOOL}},
{"CarBatteryCapacity", {PERSISTENT, INT}},
{"CarParams", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BYTES}},
{"CarParamsCache", {CLEAR_ON_MANAGER_START, BYTES}},
@@ -199,6 +200,8 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"OSMapsHeadingUp", {PERSISTENT, BOOL, "1"}},
{"OfflineTilesBaseUrl", {PERSISTENT, STRING}},
{"OnroadUploads", {PERSISTENT, BOOL, "1"}},
{"IQAutoUnits", {PERSISTENT, BOOL, "1"}},
{"IQAutoUnitsRegion", {PERSISTENT, STRING}},
{"IQAlertSilence", {PERSISTENT, BOOL, "0"}},
{"IQAccelMeter", {PERSISTENT, BOOL, "0"}},
{"IQBlinkerIndicators", {PERSISTENT, BOOL, "0"}},
@@ -349,6 +352,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ConstructionZoneAssist", {PERSISTENT, BOOL, "0"}},
{"VisionVehicleTracks", {PERSISTENT, BOOL, "0"}},
{"AmbientTrackDots", {PERSISTENT, BOOL, "1"}},
{"EnvironmentView", {PERSISTENT, INT, "0"}},
{"ConstructionZoneSpeed", {PERSISTENT, INT, "60"}},
{"ShowSpeedLimits", {PERSISTENT, BOOL, "0"}},
{"SLCPolicy", {PERSISTENT, INT, "1"}},
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include <iostream>
#include <stdexcept>
#include <string>
inline void native_test_check(bool condition, const char *expression, const char *file, int line) {
if (!condition) {
throw std::runtime_error(std::string(file) + ":" + std::to_string(line) + ": check failed: " + expression);
}
}
#define CHECK(condition) native_test_check(static_cast<bool>(condition), #condition, __FILE__, __LINE__)
#define REQUIRE(...) CHECK((__VA_ARGS__))
template <typename Function>
int run_native_test(Function &&function) {
try {
function();
return 0;
} catch (const std::exception &error) {
std::cerr << error.what() << '\n';
return 1;
}
}
+132
View File
@@ -0,0 +1,132 @@
#include "common/yuv.h"
#include <algorithm>
#include <cstring>
namespace yuv {
namespace {
inline uint8_t clamp_u8(int v) {
return static_cast<uint8_t>(std::clamp(v, 0, 255));
}
void copy_plane(const uint8_t *src, int src_stride,
uint8_t *dst, int dst_stride,
int width, int height) {
if (src_stride == width && dst_stride == width) {
std::memcpy(dst, src, static_cast<size_t>(width) * height);
return;
}
for (int y = 0; y < height; ++y) {
std::memcpy(dst + y * dst_stride, src + y * src_stride, width);
}
}
void scale_plane_point(const uint8_t *src, int src_stride, int src_width, int src_height,
uint8_t *dst, int dst_stride, int dst_width, int dst_height) {
if (src_width == dst_width && src_height == dst_height) {
copy_plane(src, src_stride, dst, dst_stride, dst_width, dst_height);
return;
}
for (int y = 0; y < dst_height; ++y) {
const int sy = y * src_height / dst_height;
const uint8_t *src_row = src + sy * src_stride;
uint8_t *dst_row = dst + y * dst_stride;
for (int x = 0; x < dst_width; ++x) {
dst_row[x] = src_row[x * src_width / dst_width];
}
}
}
// BT.601 limited range → RGB (integer form used widely, incl. similar to libyuv).
inline void yuv_to_rgb(int y, int u, int v, uint8_t *r, uint8_t *g, uint8_t *b) {
const int c = (y - 16) * 298;
const int d = u - 128;
const int e = v - 128;
*r = clamp_u8((c + 409 * e + 128) >> 8);
*g = clamp_u8((c - 100 * d - 208 * e + 128) >> 8);
*b = clamp_u8((c + 516 * d + 128) >> 8);
}
} // namespace
void nv12_to_i420(const uint8_t *src_y, int src_stride_y,
const uint8_t *src_uv, int src_stride_uv,
uint8_t *dst_y, int dst_stride_y,
uint8_t *dst_u, int dst_stride_u,
uint8_t *dst_v, int dst_stride_v,
int width, int height) {
copy_plane(src_y, src_stride_y, dst_y, dst_stride_y, width, height);
const int uv_width = width / 2;
const int uv_height = height / 2;
for (int y = 0; y < uv_height; ++y) {
const uint8_t *uv = src_uv + y * src_stride_uv;
uint8_t *u = dst_u + y * dst_stride_u;
uint8_t *v = dst_v + y * dst_stride_v;
for (int x = 0; x < uv_width; ++x) {
u[x] = uv[2 * x];
v[x] = uv[2 * x + 1];
}
}
}
void i420_to_nv12(const uint8_t *src_y, int src_stride_y,
const uint8_t *src_u, int src_stride_u,
const uint8_t *src_v, int src_stride_v,
uint8_t *dst_y, int dst_stride_y,
uint8_t *dst_uv, int dst_stride_uv,
int width, int height) {
copy_plane(src_y, src_stride_y, dst_y, dst_stride_y, width, height);
const int uv_width = width / 2;
const int uv_height = height / 2;
for (int y = 0; y < uv_height; ++y) {
const uint8_t *u = src_u + y * src_stride_u;
const uint8_t *v = src_v + y * src_stride_v;
uint8_t *uv = dst_uv + y * dst_stride_uv;
for (int x = 0; x < uv_width; ++x) {
uv[2 * x] = u[x];
uv[2 * x + 1] = v[x];
}
}
}
void i420_scale(const uint8_t *src_y, int src_stride_y,
const uint8_t *src_u, int src_stride_u,
const uint8_t *src_v, int src_stride_v,
int src_width, int src_height,
uint8_t *dst_y, int dst_stride_y,
uint8_t *dst_u, int dst_stride_u,
uint8_t *dst_v, int dst_stride_v,
int dst_width, int dst_height) {
scale_plane_point(src_y, src_stride_y, src_width, src_height,
dst_y, dst_stride_y, dst_width, dst_height);
scale_plane_point(src_u, src_stride_u, src_width / 2, src_height / 2,
dst_u, dst_stride_u, dst_width / 2, dst_height / 2);
scale_plane_point(src_v, src_stride_v, src_width / 2, src_height / 2,
dst_v, dst_stride_v, dst_width / 2, dst_height / 2);
}
void nv12_to_rgba(const uint8_t *src_y, int src_stride_y,
const uint8_t *src_uv, int src_stride_uv,
uint8_t *dst_rgba, int dst_stride_rgba,
int width, int height) {
for (int y = 0; y < height; ++y) {
const uint8_t *y_row = src_y + y * src_stride_y;
const uint8_t *uv_row = src_uv + (y / 2) * src_stride_uv;
uint8_t *dst = dst_rgba + y * dst_stride_rgba;
for (int x = 0; x < width; ++x) {
const int uv_x = (x & ~1);
uint8_t r, g, b;
yuv_to_rgb(y_row[x], uv_row[uv_x], uv_row[uv_x + 1], &r, &g, &b);
dst[4 * x + 0] = r;
dst[4 * x + 1] = g;
dst[4 * x + 2] = b;
dst[4 * x + 3] = 255;
}
}
}
} // namespace yuv
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include <cstdint>
// NV12: Y plane + interleaved UV. I420: planar Y, U, V.
namespace yuv {
// Deinterleave NV12 UV into planar I420.
void nv12_to_i420(const uint8_t *src_y, int src_stride_y,
const uint8_t *src_uv, int src_stride_uv,
uint8_t *dst_y, int dst_stride_y,
uint8_t *dst_u, int dst_stride_u,
uint8_t *dst_v, int dst_stride_v,
int width, int height);
// Interleave planar I420 UV into NV12.
void i420_to_nv12(const uint8_t *src_y, int src_stride_y,
const uint8_t *src_u, int src_stride_u,
const uint8_t *src_v, int src_stride_v,
uint8_t *dst_y, int dst_stride_y,
uint8_t *dst_uv, int dst_stride_uv,
int width, int height);
// Point-sample scale I420 (equivalent to libyuv::I420Scale + kFilterNone).
void i420_scale(const uint8_t *src_y, int src_stride_y,
const uint8_t *src_u, int src_stride_u,
const uint8_t *src_v, int src_stride_v,
int src_width, int src_height,
uint8_t *dst_y, int dst_stride_y,
uint8_t *dst_u, int dst_stride_u,
uint8_t *dst_v, int dst_stride_v,
int dst_width, int dst_height);
// Convert NV12 to packed RGBA (R,G,B,A bytes — suitable for GL_RGBA).
// BT.601 limited-range, matching common libyuv defaults.
void nv12_to_rgba(const uint8_t *src_y, int src_stride_y,
const uint8_t *src_uv, int src_stride_uv,
uint8_t *dst_rgba, int dst_stride_rgba,
int width, int height);
}
+1
View File
@@ -208,6 +208,7 @@ struct CarState {
blockPcmEnable @60 :Bool; # whether to allow PCM to enable this frame
lateralAvailable @61 :Bool; # lateral control is available even if cruise is faulted
cruiseFaultLateralMode @62 :Bool; # cruise is faulted but lateral control is still active
carNotReady @95 :Bool; # car is transiently refusing engagement, not a fault
radarDisableFailed @66 :Bool;
# Physical vehicle odometer in kilometers. Zero means unavailable on this platform.
odometer @67 :Float64;
+1
View File
@@ -53,6 +53,7 @@ class IQCarParams:
enableGasInterceptor: bool = auto_field()
longitudinalStoppingSpeedOverride: float = auto_field()
stoppingDecelRateOverride: float = auto_field()
longActiveWithGasOverride: bool = auto_field()
iqLateralNet: 'IQCarParams.LateralNet' = field(default_factory=lambda: IQCarParams.LateralNet())
+14 -3
View File
@@ -29,6 +29,8 @@ class CarState(CarStateBase, IQCarState):
self.hands_on_level = 0
self.acc_state_last = 0
self.das_accCancel = False
self.das_cancel_last = True
self.das_control = None
self.das_body_controls_dat = b""
self._odometer_store = vehicle_state.VehicleOdometerStore(CP, Params())
@@ -93,15 +95,24 @@ class CarState(CarStateBase, IQCarState):
cruise_state = self.can_define.dv["DI_state"]["DI_cruiseState"].get(int(cp_party.vl["DI_state"]["DI_cruiseState"]), None)
speed_units = self.can_define.dv["DI_state"]["DI_speedUnits"].get(int(cp_party.vl["DI_state"]["DI_speedUnits"]), None)
acc_state = cp_ap_party.vl["DAS_control"]["DAS_accState"]
# Respect all stock DAS cancel states, not just ACC_CANCEL_GENERIC_SILENT(13).
# ELDA/ELK triggers ACC_CANCEL_GENERIC(0) which must also be forwarded.
self.das_accCancel = acc_state in (0, 1, 2, 12, 13, 14, 15)
summon_state = self.can_define.dv["DI_state"]["DI_autoparkState"].get(int(cp_party.vl["DI_state"]["DI_autoparkState"]), None)
cruise_enabled = cruise_state in ("ENABLED", "STANDSTILL", "OVERRIDE", "PRE_FAULT", "PRE_CANCEL")
self.cruise_override = cruise_state in ("OVERRIDE")
self.update_summon_state(summon_state, cruise_enabled)
# Respect all stock DAS cancel states, not just ACC_CANCEL_GENERIC_SILENT(13).
# ELDA/ELK triggers ACC_CANCEL_GENERIC(0) which must also be forwarded.
# The stock AP is isolated from the party bus while the relay is closed, so its accState
# free-runs between ACC_ON and ACC_CANCEL_GENERIC. Only a rising edge while ACC is engaged
# is a real cancel; level-forwarding it pins DI_cruiseState to UNAVAILABLE and blocks engaging.
das_cancel = acc_state in (0, 1, 2, 12, 13, 14, 15)
if not cruise_enabled:
self.das_accCancel = False
elif das_cancel and not self.das_cancel_last:
self.das_accCancel = True
self.das_cancel_last = das_cancel
# Match panda safety cruise engaged logic
ret.cruiseState.enabled = cruise_enabled and not self.summon
if speed_units == "KPH":
@@ -44,6 +44,7 @@ FW_VERSIONS = {
b'TeMYG4_Legacy3Y_0.0.0 (6),Y4003.04.0',
b'TeMYG4_Main_0.0.0 (77),Y4003.05.4',
b'TeMYG4_Main_0.0.0 (78),Y4003.06.0',
b'TeMYG4_Main_0.0.0 (87),Y4003.09.3',
],
},
CAR.TESLA_MODEL_X: {
+2 -2
View File
@@ -2,7 +2,7 @@ from iqdbc.car import Bus, get_safety_config, structs
from iqdbc.car.interfaces import CarInterfaceBase
from iqdbc.car.tesla.carcontroller import CarController
from iqdbc.car.tesla.carstate import CarState
from iqdbc.car.tesla.values import TeslaSafetyFlags, TeslaFlags, CANBUS, CAR, DBC, LEGACY_DAS_STEERING_FW, Ecu
from iqdbc.car.tesla.values import TeslaSafetyFlags, TeslaFlags, CANBUS, CAR, DBC, Ecu, is_legacy_das_steering
from iqdbc.car.tesla.radar_interface import RadarInterface, RADAR_START_ADDR
from iqdbc.lvbs.car.tesla.values import TeslaFlagsIQ, TeslaSafetyFlagsIQ
@@ -41,7 +41,7 @@ class CarInterface(CarInterfaceBase):
ret.openpilotLongitudinalControl = True
ret.safetyConfigs[0].safetyParam |= TeslaSafetyFlags.LONG_CONTROL.value
legacy_das = any(fw.ecu == Ecu.eps and fw.fwVersion in LEGACY_DAS_STEERING_FW.get(candidate, []) for fw in car_fw)
legacy_das = any(fw.ecu == Ecu.eps and is_legacy_das_steering(candidate, fw.fwVersion) for fw in car_fw)
if legacy_das:
ret.flags |= TeslaFlags.LEGACY_DAS_STEERING.value
ret.safetyConfigs[0].safetyParam |= TeslaSafetyFlags.LEGACY_DAS_STEERING.value
+100 -1
View File
@@ -1,11 +1,110 @@
from collections import defaultdict
import pytest
from iqdbc.car import gen_empty_fingerprint, structs
from iqdbc.car.structs import CarParams
from iqdbc.car.fw_versions import match_fw_to_car
from iqdbc.car.tesla.fingerprints import FW_VERSIONS
from iqdbc.car.tesla.interface import CarInterface
from iqdbc.car.tesla.teslacan import TeslaCAN
from iqdbc.car.tesla.radar_interface import RADAR_START_ADDR
from iqdbc.car.tesla.carcontroller import CarController
from iqdbc.car.tesla.values import CAR
from iqdbc.car.tesla.values import (CAR, FW_PATTERN, LEGACY_DAS_STEERING_FW, TeslaFlags, TeslaSafetyFlags,
get_platform_codes, is_legacy_das_steering)
from iqdbc.can import CANPacker, CANParser
Ecu = CarParams.Ecu
EPS_ADDR = 0x730
def fw_match(fw: bytes):
car_fw = [CarParams.CarFw(ecu=Ecu.eps, fwVersion=fw, address=EPS_ADDR, subAddress=0, brand='tesla')]
exact, matches = match_fw_to_car(car_fw, '0' * 17, log=False)
return exact, matches
class TestTeslaFwPattern:
def test_all_known_fw_parses(self):
for car, ecus in FW_VERSIONS.items():
for fws in ecus.values():
for fw in fws:
assert FW_PATTERN.match(fw) is not None, f'{car}: unparsed FW version: {fw}'
def test_model_code_identifies_one_platform(self):
# a new platform reusing an existing model code would silently misfingerprint
platforms = defaultdict(set)
for car, ecus in FW_VERSIONS.items():
for fws in ecus.values():
for model, _, _ in get_platform_codes(fws):
platforms[model].add(car)
for model, cars in platforms.items():
assert len(cars) == 1, f'model code {model} maps to multiple platforms: {cars}'
def test_exact_match_still_wins(self):
for car, ecus in FW_VERSIONS.items():
for fws in ecus.values():
for fw in fws:
exact, matches = fw_match(fw)
assert exact, f'{fw} fell back to fuzzy matching'
assert matches == {car}, f'{fw} matched {matches}, expected {car}'
@pytest.mark.parametrize("fw, expected", [
# a firmware bump within a known series, the case that used to fingerprint as MOCK
(b'TeMYG4_Main_0.0.0 (99),Y4003.14.0', CAR.TESLA_MODEL_Y),
(b'TeMYG4_Main_0.0.0 (99),E4H015.09.0', CAR.TESLA_MODEL_3),
(b'TeM3_SP_XP002p2_0.0.0 (40),XPR003.12.0', CAR.TESLA_MODEL_X),
# Tesla bumps the series within a platform (E4014 -> E4015, Y4002 -> Y4003)
(b'TeMYG4_Main_0.0.0 (12),Y4004.01.0', CAR.TESLA_MODEL_Y),
# an unknown model code is a car we don't support
(b'TeCT_Main_0.0.0 (1),CT001.01.0', None),
(b'garbage', None),
])
def test_unknown_fw_fuzzy_match(self, fw, expected):
exact, matches = fw_match(fw)
if expected is None:
assert matches == set(), f'{fw} unexpectedly matched {matches}'
else:
assert not exact
assert matches == {expected}
class TestTeslaLegacyDasSteering:
def test_reproduces_known_table(self):
for car, ecus in FW_VERSIONS.items():
for fws in ecus.values():
for fw in fws:
expected = fw in LEGACY_DAS_STEERING_FW.get(car, [])
assert is_legacy_das_steering(car, fw) == expected, f'{car}: wrong legacy DAS verdict for {fw}'
@pytest.mark.parametrize("car, fw, expected", [
# below a family's known modern cutoff (Y4/003 splits at 003.04.0)
(CAR.TESLA_MODEL_Y, b'TeMYG4_Legacy3Y_0.0.0 (5),Y4003.03.9', True),
(CAR.TESLA_MODEL_Y, b'TeMYG4_Main_0.0.0 (99),Y4003.14.0', False),
# E4/015 splits at 015.04.5
(CAR.TESLA_MODEL_3, b'TeMYG4_Main_0.0.0 (68),E4H015.03.9', True),
(CAR.TESLA_MODEL_3, b'TeMYG4_Main_0.0.0 (99),E4H015.09.0', False),
# families with no known modern FW: interpolate legacy, extrapolate modern
(CAR.TESLA_MODEL_3, b'TeM3_E014p10_0.0.0 (16),E014.18.00', True),
(CAR.TESLA_MODEL_3, b'TeM3_E014p10_0.0.0 (30),E014.22.0', False),
# numeric, not lexical, version compare (XPR003.10.0 > XPR003.6.0)
(CAR.TESLA_MODEL_X, b'TeM3_SP_XP002p2_0.0.0 (30),XPR003.9.0', True),
# an unknown series has no history to compare against
(CAR.TESLA_MODEL_Y, b'TeMYG4_Main_0.0.0 (12),Y4004.01.0', False),
(CAR.TESLA_MODEL_Y, b'garbage', False),
])
def test_unknown_fw(self, car, fw, expected):
assert is_legacy_das_steering(car, fw) == expected
def test_flag_set_from_fuzzy_match(self):
for fw, legacy in ((b'TeMYG4_Legacy3Y_0.0.0 (5),Y4003.03.9', True),
(b'TeMYG4_Main_0.0.0 (99),Y4003.14.0', False)):
car_fw = [CarParams.CarFw(ecu=Ecu.eps, fwVersion=fw, address=EPS_ADDR, subAddress=0, brand='tesla')]
CP = CarInterface.get_params(CAR.TESLA_MODEL_Y, gen_empty_fingerprint(), car_fw, False, False, False)
assert bool(CP.flags & TeslaFlags.LEGACY_DAS_STEERING) == legacy
assert bool(CP.safetyConfigs[0].safetyParam & TeslaSafetyFlags.LEGACY_DAS_STEERING) == legacy
class TestTeslaFingerprint:
def test_radar_detection(self):
+97 -11
View File
@@ -1,10 +1,13 @@
import re
from collections import defaultdict
from dataclasses import dataclass, field
from enum import Enum, IntFlag
from functools import cache
from iqdbc.car import ACCELERATION_DUE_TO_GRAVITY, Bus, CarSpecs, DbcDict, PlatformConfig, Platforms
from iqdbc.car.lateral import AngleSteeringLimits, ISO_LATERAL_ACCEL
from iqdbc.car.structs import CarParams, CarState
from iqdbc.car.docs_definitions import CarDocs, CarFootnote, CarHarness, CarParts, Column, SupportType
from iqdbc.car.fw_query_definitions import FwQueryConfig, Request, StdQueries
from iqdbc.car.fw_query_definitions import FwQueryConfig, LiveFwVersions, OfflineFwVersions, Request, StdQueries
Ecu = CarParams.Ecu
@@ -69,16 +72,6 @@ class CAR(Platforms):
)
FW_QUERY_CONFIG = FwQueryConfig(
requests=[
Request(
[StdQueries.TESTER_PRESENT_REQUEST, StdQueries.SUPPLIER_SOFTWARE_VERSION_REQUEST],
[StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.SUPPLIER_SOFTWARE_VERSION_RESPONSE],
bus=0,
)
]
)
# Cars with this EPS FW have a 2-bit DAS_steeringControlType and use TeslaFlags.LEGACY_DAS_STEERING
LEGACY_DAS_STEERING_FW = {
CAR.TESLA_MODEL_3: [
@@ -116,6 +109,99 @@ LEGACY_DAS_STEERING_FW = {
],
}
# e.g. TeMYG4_Main_0.0.0 (87),Y4003.09.3
# 11111_22222_______33____45__666666
# 1 = EPS firmware program, 2 = build lineage, 3 = build number, 4 = model code,
# 5 = trim/hardware variant, 6 = series and software version
#
# Only the model code identifies the vehicle: 1 and 2 are shared across models (Model 3 and
# Model Y both ship TeM3_ and TeMYG4_ firmware) and 3 is only monotone within one lineage.
FW_PATTERN = re.compile(rb'^Te[A-Z0-9]+_[A-Za-z0-9_]+_0\.0\.0 \(\d+\),' +
rb'(?P<model>E4|E|Y4|Y|XP)[A-Z]{0,2}(?P<series>\d{3})\.(?P<version>\d+(?:\.\d+)*)$')
def get_platform_codes(fw_versions: list[bytes] | set[bytes]) -> set[tuple[bytes, bytes, tuple[int, ...]]]:
codes = set()
for fw in fw_versions:
match = FW_PATTERN.match(fw)
if match is not None:
codes.add((match.group('model'), match.group('series'),
tuple(int(v) for v in match.group('version').split(b'.'))))
return codes
@cache
def _das_steering_cutoffs() -> dict[tuple[str, bytes, bytes], tuple[tuple[int, ...] | None, tuple[int, ...] | None]]:
"""Per (platform, model code, series) family, the oldest known modern version and the newest
known legacy version. Tesla only ever moves a family forward, so these bound the split."""
# imported here because fingerprints.py imports this module
from iqdbc.car.tesla.fingerprints import FW_VERSIONS
legacy: defaultdict[tuple, set] = defaultdict(set)
modern: defaultdict[tuple, set] = defaultdict(set)
for platform, ecus in FW_VERSIONS.items():
known_legacy = LEGACY_DAS_STEERING_FW.get(platform, [])
for fws in ecus.values():
for fw in fws:
for model, series, version in get_platform_codes([fw]):
(legacy if fw in known_legacy else modern)[(platform, model, series)].add(version)
return {k: (min(modern[k]) if k in modern else None, max(legacy[k]) if k in legacy else None)
for k in set(legacy) | set(modern)}
def is_legacy_das_steering(candidate: str, fw: bytes) -> bool:
"""Whether an EPS FW uses the 2-bit DAS_steeringControlType. Unknown firmware newer than
anything in a family is treated as modern: cars only move forward, and someone left behind
on legacy software can force the platform with CarPlatformBundle."""
if fw in LEGACY_DAS_STEERING_FW.get(candidate, []):
return True
codes = get_platform_codes([fw])
if not len(codes):
return False
model, series, version = next(iter(codes))
first_modern, last_legacy = _das_steering_cutoffs().get((candidate, model, series), (None, None))
if first_modern is not None:
return version < first_modern
return last_legacy is not None and version <= last_legacy
def match_fw_to_car_fuzzy(live_fw_versions: LiveFwVersions, vin: str, offline_fw_versions: OfflineFwVersions) -> set[str]:
# Tesla fingerprints on the EPS alone and Ecu.eps is in FUZZY_EXCLUDE_ECUS, so the generic fuzzy
# matcher can never match a Tesla. Match on the model code, which survives the EPS version bumps
# that ship with Tesla software updates. The series is deliberately not required to be known:
# Tesla bumps it within a platform (E4014 -> E4015, Y4002 -> Y4003).
offline_codes: defaultdict[bytes, set[str]] = defaultdict(set)
for candidate, ecus in offline_fw_versions.items():
for fws in ecus.values():
for model, _, _ in get_platform_codes(fws):
offline_codes[model].add(candidate)
candidates: set[str] = set()
for ecu, addr, sub_addr in {e for ecus in offline_fw_versions.values() for e in ecus}:
if ecu != Ecu.eps:
continue
for model, _, _ in get_platform_codes(live_fw_versions.get((addr, sub_addr), set())):
candidates |= offline_codes[model]
return candidates if len(candidates) == 1 else set()
FW_QUERY_CONFIG = FwQueryConfig(
requests=[
Request(
[StdQueries.TESTER_PRESENT_REQUEST, StdQueries.SUPPLIER_SOFTWARE_VERSION_REQUEST],
[StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.SUPPLIER_SOFTWARE_VERSION_RESPONSE],
bus=0,
)
],
match_fw_to_car_fuzzy=match_fw_to_car_fuzzy,
)
class CANBUS:
party = 0
@@ -381,6 +381,9 @@ class CarState(CarStateBase):
ret.cruiseState.available = pt_cp.vl["Motor_51"]["TSK_Status"] in (2, 3, 4, 5)
ret.cruiseState.enabled = pt_cp.vl["Motor_51"]["TSK_Status"] in (3, 4, 5)
# TSK winds its braking down through brake_only after a driver brake. Requesting drive-off in this
# state can fault TSK, and stock refuses to engage here as well, so block entry until it clears.
ret.carNotReady = pt_cp.vl["Motor_51"]["TSK_Status"] == 5 # brake_only
acc_values = ext_cp.vl.get("MEB_ACC_01", ext_cp.vl.get("ACC_19", {}))
ret.cruiseState.nonAdaptive = bool(acc_values.get("ACC_Limiter_Mode", 0)) if self.CP.pcmCruise else bool(pt_cp.vl["Motor_51"]["TSK_Limiter_ausgewaehlt"])
@@ -585,6 +588,7 @@ class CarState(CarStateBase):
ret.cruiseFaultLateralMode = allow_lat_only and cruise_faulted and cruise_main_available
ret.lateralAvailable = ret.cruiseState.available or ret.cruiseFaultLateralMode
ret.blockPcmEnable = ret.cruiseFaultLateralMode
ret.carNotReady = bool(pt_cp.vl["Bremse_8"]["BR8_Sta_VerzReg"])
# Update ACC setpoint. When the setpoint reads as 255, the driver has not
# yet established an ACC setpoint, so treat it as zero.
+3 -1
View File
@@ -4,7 +4,9 @@ from pathlib import Path
env = Environment(ENV=os.environ)
# short colored build output, if the top-level pretty tool is present (main-repo build)
_pretty = Dir('#site_scons/site_tools').File('pretty.py')
_pretty = Dir('#tools/scons/site_tools').File('pretty.py')
if not _pretty.exists():
_pretty = Dir('#site_scons/site_tools').File('pretty.py')
if _pretty.exists():
env.Tool('pretty', toolpath=[_pretty.dir.abspath])
if not hasattr(env, 'PrettyAction'):
@@ -26,7 +26,9 @@ env = Environment(
)
# short colored build output, if the top-level pretty tool is present (main-repo build)
_pretty = Dir('#site_scons/site_tools').File('pretty.py')
_pretty = Dir('#tools/scons/site_tools').File('pretty.py')
if not _pretty.exists():
_pretty = Dir('#site_scons/site_tools').File('pretty.py')
if _pretty.exists():
env.Tool('pretty', toolpath=[_pretty.dir.abspath])
+58
View File
@@ -0,0 +1,58 @@
import time
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.iqpilot.common.geo_regions import UNKNOWN_REGION, region_for_position, region_is_metric
CHECK_INTERVAL = 10.0
CONFIRMATIONS = 3
class AutoUnits:
def __init__(self, params: Params | None = None):
self.params = params or Params()
self._next_check = 0.0
self._candidate = UNKNOWN_REGION
self._confirmations = 0
def _position(self) -> tuple[float, float, bool]:
from openpilot.selfdrive.ui.lib.nav_helpers import current_or_last_gps_position
lat, lon, _, valid = current_or_last_gps_position(self.params)
return lat, lon, valid
def update(self, now: float | None = None) -> None:
if not self.params.get_bool("IQAutoUnits"):
self._candidate = UNKNOWN_REGION
self._confirmations = 0
return
now = time.monotonic() if now is None else now
if now < self._next_check:
return
self._next_check = now + CHECK_INTERVAL
lat, lon, valid = self._position()
region = region_for_position(lat, lon) if valid else UNKNOWN_REGION
if region == UNKNOWN_REGION:
self._confirmations = 0
return
if region != self._candidate:
self._candidate = region
self._confirmations = 1
return
self._confirmations += 1
if self._confirmations < CONFIRMATIONS:
return
if region == self.params.get("IQAutoUnitsRegion"):
return
self.params.put("IQAutoUnitsRegion", region)
metric = region_is_metric(region)
if metric != self.params.get_bool("IsMetric"):
self.params.put_bool("IsMetric", metric)
cloudlog.warning(f"auto units: {region} detected, switching to {'km/h' if metric else 'mph'}")
+140
View File
@@ -0,0 +1,140 @@
MPH_REGIONS = ("US", "GB", "LR")
METRIC_REGION = "METRIC"
UNKNOWN_REGION = ""
_US_CONUS = [
(-123.32, 49.00), (-117.03, 49.00), (-110.00, 49.00), (-104.05, 49.00), (-97.23, 49.00), (-95.15, 49.00),
(-95.15, 49.38), (-94.82, 49.30), (-94.68, 48.77), (-93.85, 48.63), (-93.35, 48.62), (-92.72, 48.54),
(-92.30, 48.24), (-91.55, 48.10), (-90.84, 48.24), (-89.99, 48.02), (-89.60, 48.02), (-89.10, 48.32),
(-88.40, 48.30), (-87.00, 47.80), (-85.60, 47.15), (-84.60, 46.75), (-84.42, 46.56), (-84.30, 46.49),
(-84.12, 46.28), (-83.90, 46.05), (-83.40, 45.75), (-82.90, 45.05), (-82.55, 44.00), (-82.42, 43.00),
(-82.70, 42.47), (-82.93, 42.34), (-83.00, 42.33), (-83.05, 42.32), (-83.075, 42.312), (-83.13, 42.25),
(-83.15, 42.18), (-83.11, 42.10), (-83.09, 42.02),
(-82.50, 41.70), (-81.50, 42.00), (-80.20, 42.40), (-79.06, 42.85), (-79.05, 43.27), (-78.00, 43.45),
(-77.00, 43.65), (-76.40, 44.10), (-75.80, 44.50), (-74.75, 45.00), (-73.35, 45.01), (-71.50, 45.01),
(-71.29, 45.30), (-70.90, 45.30), (-70.72, 45.42), (-70.31, 45.86), (-70.05, 46.44), (-69.99, 46.70),
(-69.24, 47.46), (-68.90, 47.20), (-68.38, 47.29), (-67.79, 47.07), (-67.78, 45.94), (-67.42, 45.60),
(-67.03, 44.80), (-68.00, 44.30), (-69.06, 43.80), (-70.20, 43.60), (-70.80, 42.85), (-70.00, 41.90),
(-70.00, 41.55), (-71.20, 41.30), (-72.00, 41.05), (-73.90, 40.55), (-74.20, 39.60), (-75.05, 38.45),
(-75.90, 37.05), (-75.50, 35.20), (-78.50, 33.85), (-80.90, 32.00), (-81.40, 30.70), (-80.03, 26.80),
(-80.15, 25.15), (-81.20, 24.55), (-82.00, 26.40), (-82.80, 27.80), (-83.00, 29.15), (-84.30, 29.90),
(-85.30, 29.65), (-87.50, 30.25), (-89.00, 29.15), (-89.40, 28.95), (-91.30, 29.10), (-93.80, 29.65),
(-95.00, 29.10), (-97.10, 27.80), (-97.14, 25.96), (-98.30, 26.05), (-99.10, 26.40), (-99.50, 27.60),
(-100.40, 28.50), (-101.40, 29.77), (-102.30, 29.88), (-102.90, 29.30), (-103.30, 29.00), (-104.37, 29.56),
(-104.68, 30.13), (-105.30, 30.80), (-105.85, 31.30), (-106.15, 31.50), (-106.30, 31.68), (-106.45, 31.755),
(-106.53, 31.786), (-108.21, 31.783), (-108.21, 31.33), (-111.07, 31.33), (-114.72, 32.72),
(-117.13, 32.53), (-118.40, 33.75), (-119.80, 34.40), (-120.65, 35.10), (-121.90, 36.60), (-122.52, 37.78),
(-123.75, 39.40), (-124.20, 40.45), (-124.15, 42.00), (-124.05, 43.35), (-123.95, 46.25), (-124.75, 48.40),
(-123.30, 48.25), (-123.15, 48.70),
]
_US_ALASKA = [
(-141.00, 70.20), (-141.00, 60.30), (-139.05, 60.35), (-137.45, 58.95), (-136.47, 59.63), (-135.03, 59.57),
(-134.30, 58.90), (-133.40, 58.20), (-132.20, 56.90), (-130.60, 56.20), (-130.01, 54.80), (-131.80, 54.70),
(-133.80, 55.90), (-136.60, 58.20), (-140.00, 59.70), (-145.00, 60.00), (-149.20, 59.10), (-152.30, 57.30),
(-155.20, 55.60), (-160.00, 54.60), (-164.50, 54.40), (-162.00, 57.50), (-165.00, 60.20), (-167.50, 62.50),
(-164.00, 64.50), (-168.10, 65.60), (-166.00, 68.30), (-161.00, 70.30), (-156.50, 71.40), (-150.00, 70.50),
]
_US_ALEUTIANS_EAST = [(-180.00, 51.00), (-158.50, 51.00), (-158.50, 56.00), (-180.00, 56.00)]
_US_ALEUTIANS_WEST = [(172.00, 51.00), (180.00, 51.00), (180.00, 54.00), (172.00, 54.00)]
_US_HAWAII = [(-160.50, 18.80), (-154.70, 18.80), (-154.70, 22.30), (-160.50, 22.30)]
_US_PUERTO_RICO = [(-67.35, 17.85), (-64.55, 17.85), (-64.55, 18.55), (-67.35, 18.55)]
_US_MARIANAS = [(144.50, 13.10), (146.20, 13.10), (146.20, 20.60), (144.50, 20.60)]
_US_SAMOA = [(-171.20, -14.60), (-168.10, -14.60), (-168.10, -11.00), (-171.20, -11.00)]
_GB_BRITAIN = [
(-5.72, 50.07), (-4.20, 50.32), (-3.41, 50.62), (-2.45, 50.52), (-1.80, 50.72), (-0.90, 50.77),
(0.58, 50.85), (1.35, 51.13), (1.38, 51.38), (1.15, 51.79), (1.35, 51.95), (1.75, 52.48),
(1.30, 52.94), (0.49, 52.94), (0.34, 53.15), (-0.08, 53.57), (-0.08, 54.12), (-0.61, 54.49),
(-1.18, 54.69), (-1.38, 54.91), (-1.50, 55.13), (-2.00, 55.77), (-2.52, 56.00), (-2.62, 56.28),
(-2.47, 56.55), (-2.21, 56.96), (-2.08, 57.14), (-1.77, 57.50), (-2.00, 57.70), (-2.96, 57.68),
(-3.90, 57.60), (-4.22, 57.48), (-4.05, 57.81), (-3.85, 58.01), (-3.65, 58.12), (-3.09, 58.44),
(-3.01, 58.67), (-3.35, 58.62), (-3.52, 58.60), (-4.99, 58.62), (-5.05, 58.45), (-5.16, 57.90), (-5.70, 57.72),
(-5.72, 57.28), (-5.83, 57.00), (-5.72, 56.65), (-5.47, 56.41), (-5.79, 55.60), (-5.62, 55.31),
(-4.82, 55.64), (-4.63, 55.46), (-4.85, 55.24), (-5.12, 54.84), (-4.86, 54.63), (-4.44, 54.87),
(-4.05, 54.83), (-3.26, 54.98), (-3.05, 54.90), (-3.50, 54.72), (-3.23, 54.07), (-3.05, 53.82),
(-3.40, 53.34), (-3.83, 53.33), (-4.63, 53.42), (-4.72, 53.28), (-4.35, 53.12), (-4.76, 52.80),
(-4.06, 52.72), (-4.09, 52.41), (-4.66, 52.09), (-5.31, 51.88), (-5.06, 51.70), (-4.70, 51.67),
(-4.30, 51.62), (-3.95, 51.56), (-3.70, 51.48), (-3.17, 51.45), (-2.99, 51.55), (-2.67, 51.62),
(-2.48, 51.72), (-2.30, 51.85), (-2.70, 51.50), (-2.98, 51.35), (-3.00, 51.20), (-3.47, 51.21),
(-4.12, 51.21), (-4.55, 50.83), (-5.08, 50.42), (-5.48, 50.21),
]
_GB_NORTHERN_IRELAND = [
(-6.03, 54.05), (-6.28, 54.10), (-6.65, 54.17), (-6.86, 54.33), (-7.16, 54.34), (-7.31, 54.12),
(-7.62, 54.14), (-8.00, 54.31), (-8.18, 54.47), (-8.20, 54.52), (-7.90, 54.55), (-7.85, 54.72), (-7.55, 54.75),
(-7.44, 54.94), (-7.25, 55.06), (-6.95, 55.22), (-6.50, 55.25), (-6.25, 55.31), (-6.03, 55.22),
(-5.43, 54.62), (-5.53, 54.24),
]
_GB_ISLE_OF_MAN = [(-4.85, 54.03), (-4.30, 54.03), (-4.30, 54.42), (-4.85, 54.42)]
_GB_CHANNEL_ISLANDS = [(-2.75, 49.15), (-1.95, 49.15), (-1.95, 49.80), (-2.75, 49.80)]
_GB_ISLE_OF_WIGHT = [(-1.60, 50.55), (-1.05, 50.55), (-1.05, 50.80), (-1.60, 50.80)]
_GB_OUTER_HEBRIDES = [(-7.75, 56.75), (-6.05, 56.75), (-6.05, 58.55), (-7.75, 58.55)]
_GB_INNER_HEBRIDES = [(-7.00, 55.45), (-5.55, 55.45), (-5.55, 57.85), (-7.00, 57.85)]
_GB_ORKNEY = [(-3.50, 58.70), (-2.35, 58.70), (-2.35, 59.45), (-3.50, 59.45)]
_GB_SHETLAND = [(-1.85, 59.80), (-0.65, 59.80), (-0.65, 60.90), (-1.85, 60.90)]
_LR_LIBERIA = [
(-11.46, 6.77), (-11.30, 6.95), (-11.16, 7.15), (-11.05, 7.40), (-10.85, 7.75), (-10.60, 8.00),
(-10.28, 8.49), (-9.70, 8.54), (-9.35, 7.80),
(-8.85, 7.40), (-8.48, 7.55), (-8.30, 6.90), (-7.95, 6.20), (-7.60, 5.20), (-7.40, 4.55),
(-7.74, 4.33), (-8.46, 4.61), (-9.06, 4.97), (-9.52, 5.36), (-10.08, 5.85), (-10.40, 6.11),
(-10.83, 6.27),
]
_REGION_RINGS = {
"US": (_US_CONUS, _US_ALASKA, _US_ALEUTIANS_EAST, _US_ALEUTIANS_WEST, _US_HAWAII, _US_PUERTO_RICO,
_US_MARIANAS, _US_SAMOA),
"GB": (_GB_BRITAIN, _GB_NORTHERN_IRELAND, _GB_ISLE_OF_MAN, _GB_CHANNEL_ISLANDS, _GB_ISLE_OF_WIGHT,
_GB_OUTER_HEBRIDES, _GB_INNER_HEBRIDES, _GB_ORKNEY, _GB_SHETLAND),
"LR": (_LR_LIBERIA,),
}
def _bounded(rings):
out = []
for ring in rings:
lons = [p[0] for p in ring]
lats = [p[1] for p in ring]
out.append(((min(lons), min(lats), max(lons), max(lats)), ring))
return tuple(out)
_REGIONS = tuple((region, _bounded(rings)) for region, rings in _REGION_RINGS.items())
def _point_in_ring(lat: float, lon: float, ring) -> bool:
inside = False
count = len(ring)
j = count - 1
for i in range(count):
lon_i, lat_i = ring[i]
lon_j, lat_j = ring[j]
if (lat_i > lat) != (lat_j > lat):
crossing = (lon_j - lon_i) * (lat - lat_i) / (lat_j - lat_i) + lon_i
if lon < crossing:
inside = not inside
j = i
return inside
def valid_position(lat: float, lon: float) -> bool:
return abs(lat) <= 90.0 and abs(lon) <= 180.0 and (abs(lat) > 1e-4 or abs(lon) > 1e-4)
def region_for_position(lat: float, lon: float) -> str:
if not valid_position(lat, lon):
return UNKNOWN_REGION
for region, rings in _REGIONS:
for (min_lon, min_lat, max_lon, max_lat), ring in rings:
if min_lon <= lon <= max_lon and min_lat <= lat <= max_lat and _point_in_ring(lat, lon, ring):
return region
return METRIC_REGION
def region_is_metric(region: str) -> bool:
return bool(region) and region not in MPH_REGIONS
+11 -16
View File
@@ -4,27 +4,22 @@ EARTH_RADIUS = 6378137
# Mapbox API limits
FREE_MAPBOX_REQUESTS = 100_000
# Speed limit offset maps for different unit systems
# Each entry is (min_speed_ms, max_speed_ms, param_name)
# Speed limit offset zones for different unit systems
# Each entry is (min_speed_ms, max_speed_ms, param_name); the param value is a
# percent offset applied to the resolved limit (e.g. 10 -> +10%), lower bound inclusive
OFFSET_PERCENT_MAX = 50.0
OFFSET_MAP_IMPERIAL = [
(0, 11.2, "speed_limit_offset1"), # 0-24 mph
(11.2, 15.2, "speed_limit_offset2"), # 25-34 mph
(15.2, 19.6, "speed_limit_offset3"), # 35-44 mph
(19.6, 24.1, "speed_limit_offset4"), # 45-54 mph
(24.1, 28.6, "speed_limit_offset5"), # 55-64 mph
(28.6, 33.1, "speed_limit_offset6"), # 65-74 mph
(33.1, 44.2, "speed_limit_offset7"), # 75-99 mph
(0, 8.94, "speed_limit_offset1"), # 0-20 mph
(8.94, 17.88, "speed_limit_offset2"), # 20-40 mph
(17.88, float("inf"), "speed_limit_offset3"), # 40+ mph
]
OFFSET_MAP_METRIC = [
(0, 8.1, "speed_limit_offset1"), # 0-29 km/h
(8.1, 13.6, "speed_limit_offset2"), # 30-49 km/h
(13.6, 16.4, "speed_limit_offset3"), # 50-59 km/h
(16.4, 21.9, "speed_limit_offset4"), # 60-79 km/h
(21.9, 27.5, "speed_limit_offset5"), # 80-99 km/h
(27.5, 33.1, "speed_limit_offset6"), # 100-119 km/h
(33.1, 38.9, "speed_limit_offset7"), # 120-140 km/h
(0, 8.33, "speed_limit_offset1"), # 0-30 km/h
(8.33, 16.67, "speed_limit_offset2"), # 30-60 km/h
(16.67, float("inf"), "speed_limit_offset3"), # 60+ km/h
]
# Speed limit filler constants
View File
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""
canlived — live CAN bridge to konn3kt.
Streams the device's live CAN bus to the konn3kt server so it can be viewed remotely
in Cabana (via a local ZMQ proxy on the laptop). This is the remote analogue of running
`./cereal/messaging/bridge` locally: instead of re-publishing CAN over a LAN ZMQ socket,
canlived opens its OWN websocket to konn3kt and forwards the raw capnp `Event` frames.
It is deliberately a separate daemon (not part of hephaestusd's control websocket):
* hephaestusd's send path fragments everything as TEXT frames through one queue, which
cannot carry binary capnp and would head-of-line-block the control plane at 100Hz.
* a dedicated socket means CAN traffic and control traffic never contend.
Lifecycle: the manager launches canlived only while the `CanLiveStreaming` param is set.
hephaestusd sets/clears that param via startCanLive/stopCanLive, which the konn3kt server
calls when the first viewer connects / the last viewer disconnects. So canlived runs only
during an active debug session — no idle connections, no battery/data cost otherwise.
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved.
"""
import os
import threading
from websocket import ABNF, create_connection
import cereal.messaging as messaging
from openpilot.common.api import Api
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
# Cabana's live "Device" stream subscribes only to "can", so that's all we forward to
# match the local experience exactly. (sendcan/TX is not shown by the live device view.)
CAN_SERVICES = ["can"]
# Reconnect backoff bounds (seconds).
RECONNECT_MIN = 1.0
RECONNECT_MAX = 10.0
def _api_host() -> str:
# Same host hephaestusd talks to; force the websocket scheme.
host = os.getenv("HEPHAESTUS_HOST") or os.getenv("KONN3KT_API_HOST") or "wss://api-iqlabs.konn3kt.com"
host = host.rstrip("/")
if host.startswith("https://"):
host = "wss://" + host[len("https://"):]
elif host.startswith("http://"):
host = "ws://" + host[len("http://"):]
return host
def _stream_once(dongle_id: str, ws_uri: str, token: str, exit_event: threading.Event) -> None:
"""Open one websocket and pump CAN until it drops or we're asked to exit."""
ws = create_connection(ws_uri, cookie="jwt=" + token, enable_multithread=True, timeout=30.0)
cloudlog.info("canlived: connected to %s", ws_uri)
try:
# Blocking receive with a short timeout so we periodically re-check exit_event and
# the socket stays responsive to shutdown even when the bus is quiet.
socks = [messaging.sub_sock(s, conflate=False, timeout=100) for s in CAN_SERVICES]
while not exit_event.is_set():
got_any = False
for sock in socks:
while True:
raw = sock.receive(non_blocking=True)
if raw is None:
break
got_any = True
# Forward the exact capnp Event bytes as a single binary frame. canlived owns
# this socket, so there is no fragmentation/interleaving to worry about.
ws.send_frame(ABNF.create_frame(raw, ABNF.OPCODE_BINARY, 1))
if not got_any:
# Nothing pending across any sub — yield briefly instead of busy-spinning.
exit_event.wait(0.005)
finally:
try:
ws.close()
except Exception:
pass
def main(exit_event: threading.Event | None = None) -> None:
if exit_event is None:
exit_event = threading.Event()
params = Params()
dongle_id = params.get("DongleId", encoding="utf-8")
if not dongle_id:
cloudlog.error("canlived: no DongleId, cannot stream")
return
api = Api(dongle_id)
host = _api_host()
ws_uri = f"{host}/ws/can/{dongle_id}"
backoff = RECONNECT_MIN
while not exit_event.is_set():
try:
token = api.get_token(expiry_hours=1)
_stream_once(dongle_id, ws_uri, token, exit_event)
backoff = RECONNECT_MIN # clean disconnect, reset backoff
except Exception as e:
cloudlog.exception("canlived: stream error: %s", e)
exit_event.wait(backoff)
backoff = min(backoff * 2, RECONNECT_MAX)
if __name__ == "__main__":
main()
@@ -17,7 +17,7 @@ from openpilot.common.realtime import DT_MDL
from openpilot.common.swaglog import cloudlog
from openpilot.iqpilot.common.k3_slc_log import k3_slc_log
from openpilot.iqpilot.common.slc_utilities import calculate_bearing_offset, is_url_pingable
from openpilot.iqpilot.common.slc_variables import FREE_MAPBOX_REQUESTS, OFFSET_MAP_IMPERIAL, OFFSET_MAP_METRIC
from openpilot.iqpilot.common.slc_variables import FREE_MAPBOX_REQUESTS, OFFSET_MAP_IMPERIAL, OFFSET_MAP_METRIC, OFFSET_PERCENT_MAX
try:
import requests
@@ -354,6 +354,8 @@ class SpeedLimitController:
pass
self.executor = ThreadPoolExecutor(max_workers=1)
self._offset_cache = {}
self._offset_cache_t = 0.0
self._last_mapbox_log_t = 0.0
self._last_mapbox_diag_t = 0.0
self._last_mapbox_diag_message = None
@@ -416,17 +418,33 @@ class SpeedLimitController:
return self._assist.output_a_target
def get_offset(self, is_metric):
target = self._assist.target
# offsets only apply to real limit sources: fallback set-speed publishes "None",
# construction clamps must never be inflated
if target <= 0 or self._assist.source in ("None", "Construction"):
return 0.0
offset_map = OFFSET_MAP_METRIC if is_metric else OFFSET_MAP_IMPERIAL
for low, high, offset_param in offset_map:
if low < self._assist.target < high:
offset_value = self.params.get(offset_param)
if offset_value is not None:
if isinstance(offset_value, bytes):
return float(offset_value.decode("utf-8"))
return float(offset_value)
return 0.0
if low <= target < high:
percent = float(np.clip(self._get_offset_percent(offset_param), -OFFSET_PERCENT_MAX, OFFSET_PERCENT_MAX))
return target * percent / 100.0
return 0.0
def _get_offset_percent(self, offset_param):
now_mono = time.monotonic()
if now_mono - self._offset_cache_t >= 5.0:
self._offset_cache.clear()
self._offset_cache_t = now_mono
if offset_param not in self._offset_cache:
offset_value = self.params.get(offset_param)
try:
if isinstance(offset_value, bytes):
offset_value = offset_value.decode("utf-8")
self._offset_cache[offset_param] = float(offset_value) if offset_value is not None else 0.0
except (ValueError, TypeError):
self._offset_cache[offset_param] = 0.0
return self._offset_cache[offset_param]
@staticmethod
def _is_alive(sm, key):
if hasattr(sm, "alive"):
@@ -2,9 +2,11 @@
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos
"""
from datetime import datetime
from types import SimpleNamespace
from openpilot.common.constants import CV
from openpilot.iqpilot.common.slc_variables import OFFSET_MAP_IMPERIAL
from openpilot.iqpilot.selfdrive.controls.lib.slc_vcruise import SLCVCruise, CRUISING_SPEED
from openpilot.iqpilot.selfdrive.controls.lib.speed_limit_controller import SpeedLimitController, POLICY_MAP_DATA_PRIORITY, POLICY_COMBINED
@@ -92,7 +94,7 @@ def test_speed_limit_controller_resolves_source_by_priority():
slc_params = _base_slc_params_controller()
slc_params["slc_policy"] = POLICY_MAP_DATA_PRIORITY
controller.update_limits(25.0, None, True, 30.0, 27.0, sm, slc_params)
controller.update_limits(25.0, datetime.now(), True, 30.0, 27.0, sm, slc_params)
assert controller.active_source == "Map Data"
assert controller.active_target == 18.0
@@ -112,7 +114,7 @@ def test_speed_limit_controller_combined_mode_prefers_smallest_limit():
slc_params = _base_slc_params_controller()
slc_params["slc_policy"] = POLICY_COMBINED
controller.update_limits(28.0, None, True, 31.0, 27.0, sm, slc_params)
controller.update_limits(28.0, datetime.now(), True, 31.0, 27.0, sm, slc_params)
assert controller.active_source == "Map Data"
assert controller.active_target == 16.0
@@ -128,6 +130,7 @@ def test_slc_vcruise_applies_target_without_increasing_cruise():
slc._get_slc_params = lambda: {
"speed_limit_controller": True,
"speed_limit_mode": 3,
"show_speed_limits": False,
"is_metric": True,
"slc_policy": POLICY_MAP_DATA_PRIORITY,
@@ -163,6 +166,7 @@ def test_slc_vcruise_show_only_does_not_modify_cruise():
slc.slc.active_source = "Map Data"
slc._get_slc_params = lambda: {
"speed_limit_controller": False,
"speed_limit_mode": 1,
"show_speed_limits": True,
"is_metric": True,
"slc_policy": POLICY_MAP_DATA_PRIORITY,
@@ -198,6 +202,7 @@ def test_slc_vcruise_auto_raises_for_higher_limit_when_confirmation_disabled():
slc._get_slc_params = lambda: {
"speed_limit_controller": True,
"speed_limit_mode": 3,
"show_speed_limits": False,
"is_metric": True,
"slc_policy": POLICY_MAP_DATA_PRIORITY,
@@ -232,6 +237,7 @@ def test_slc_vcruise_does_not_auto_raise_when_higher_confirmation_enabled():
slc._get_slc_params = lambda: {
"speed_limit_controller": True,
"speed_limit_mode": 3,
"show_speed_limits": False,
"is_metric": True,
"slc_policy": POLICY_MAP_DATA_PRIORITY,
@@ -392,6 +398,50 @@ def test_construction_zone_never_raises_cruise_even_with_auto_raise():
assert abs(out - 60.0 * CV.MPH_TO_MS) < 1e-6
def _offset_controller(pct1=10.0, pct2=5.0, pct3=8.0):
params = FakeParams()
params.put("speed_limit_offset1", pct1)
params.put("speed_limit_offset2", pct2)
params.put("speed_limit_offset3", pct3)
controller = SpeedLimitController(params)
controller._assist.source = "Map Data"
return controller
def test_get_offset_percent_per_zone():
controller = _offset_controller()
controller._assist.target = 6.7 # ~15 mph -> zone 1
assert abs(controller.get_offset(False) - 6.7 * 0.10) < 1e-9
controller._assist.target = 13.4 # ~30 mph -> zone 2
assert abs(controller.get_offset(False) - 13.4 * 0.05) < 1e-9
controller._assist.target = 31.3 # ~70 mph -> zone 3 (open-ended)
assert abs(controller.get_offset(False) - 31.3 * 0.08) < 1e-9
def test_get_offset_zone_lower_bound_inclusive():
controller = _offset_controller()
boundary = OFFSET_MAP_IMPERIAL[1][0]
controller._assist.target = boundary
assert abs(controller.get_offset(False) - boundary * 0.05) < 1e-9
def test_get_offset_zero_without_real_limit_source():
for source in ("None", "Construction"):
controller = _offset_controller()
controller._assist.source = source
controller._assist.target = 30.0
assert controller.get_offset(False) == 0.0
def test_get_offset_percent_clamped():
controller = _offset_controller(pct3=500.0)
controller._assist.target = 30.0
assert abs(controller.get_offset(False) - 30.0 * 0.50) < 1e-9
def test_construction_zone_fires_event_once_per_zone_entry():
from cereal import custom
event = custom.IQOnroadEvent.EventName.constructionZoneDetected
+91
View File
@@ -0,0 +1,91 @@
from openpilot.common.params import Params
from openpilot.iqpilot.common.auto_units import CONFIRMATIONS, AutoUnits
SEATTLE = (47.6062, -122.3321)
BERLIN = (52.5200, 13.4050)
LONDON = (51.5074, -0.1278)
class StubAutoUnits(AutoUnits):
def __init__(self, params):
super().__init__(params)
self.position = (0.0, 0.0, False)
def _position(self):
return self.position
def settle(auto_units, position, count=CONFIRMATIONS, start=0.0):
auto_units.position = position
for i in range(count):
auto_units.update(now=start + i * 100.0)
return start + count * 100.0
class TestAutoUnits:
def setup_method(self):
self.params = Params()
self.params.put_bool("IQAutoUnits", True)
self.params.remove("IQAutoUnitsRegion")
self.params.put_bool("IsMetric", False)
self.auto_units = StubAutoUnits(self.params)
def test_no_fix_does_nothing(self):
settle(self.auto_units, (0.0, 0.0, False))
assert self.params.get("IQAutoUnitsRegion") is None
assert not self.params.get_bool("IsMetric")
def test_disabled_does_nothing(self):
self.params.put_bool("IQAutoUnits", False)
settle(self.auto_units, (*BERLIN, True))
assert self.params.get("IQAutoUnitsRegion") is None
assert not self.params.get_bool("IsMetric")
def test_metric_region_switches_to_metric(self):
settle(self.auto_units, (*BERLIN, True))
assert self.params.get("IQAutoUnitsRegion") == "METRIC"
assert self.params.get_bool("IsMetric")
def test_mph_region_stays_imperial(self):
settle(self.auto_units, (*SEATTLE, True))
assert self.params.get("IQAutoUnitsRegion") == "US"
assert not self.params.get_bool("IsMetric")
def test_uk_stays_imperial(self):
self.params.put_bool("IsMetric", True)
settle(self.auto_units, (*LONDON, True))
assert self.params.get("IQAutoUnitsRegion") == "GB"
assert not self.params.get_bool("IsMetric")
def test_border_crossing_switches_units(self):
now = settle(self.auto_units, (*SEATTLE, True))
assert not self.params.get_bool("IsMetric")
settle(self.auto_units, (*BERLIN, True), start=now)
assert self.params.get("IQAutoUnitsRegion") == "METRIC"
assert self.params.get_bool("IsMetric")
def test_manual_override_is_kept_within_a_region(self):
now = settle(self.auto_units, (*SEATTLE, True))
self.params.put_bool("IsMetric", True)
settle(self.auto_units, (*SEATTLE, True), start=now)
assert self.params.get_bool("IsMetric")
def test_unconfirmed_region_is_not_applied(self):
settle(self.auto_units, (*BERLIN, True), count=CONFIRMATIONS - 1)
assert self.params.get("IQAutoUnitsRegion") is None
assert not self.params.get_bool("IsMetric")
def test_flapping_region_resets_confirmations(self):
now = 0.0
for position in (BERLIN, SEATTLE, BERLIN, SEATTLE):
now = settle(self.auto_units, (*position, True), count=1, start=now)
assert self.params.get("IQAutoUnitsRegion") is None
assert not self.params.get_bool("IsMetric")
def test_rate_limited(self):
self.auto_units.position = (*BERLIN, True)
for _ in range(CONFIRMATIONS * 4):
self.auto_units.update(now=1.0)
assert self.params.get("IQAutoUnitsRegion") is None
+165
View File
@@ -0,0 +1,165 @@
import pytest
from openpilot.iqpilot.common.geo_regions import METRIC_REGION, UNKNOWN_REGION, region_for_position, region_is_metric
US_POINTS = [
(47.6062, -122.3321, "Seattle"),
(42.3314, -83.0458, "Detroit"),
(42.8864, -78.8784, "Buffalo"),
(25.7617, -80.1918, "Miami"),
(29.7604, -95.3698, "Houston"),
(32.7157, -117.1611, "San Diego"),
(34.0522, -118.2437, "Los Angeles"),
(61.2181, -149.9003, "Anchorage"),
(58.3019, -134.4197, "Juneau"),
(64.8378, -147.7164, "Fairbanks"),
(21.3069, -157.8583, "Honolulu"),
(18.4655, -66.1057, "San Juan"),
(13.4757, 144.7489, "Guam"),
(44.9778, -93.2650, "Minneapolis"),
(40.7128, -74.0060, "New York"),
(41.8781, -87.6298, "Chicago"),
(31.7900, -106.4300, "El Paso"),
(26.2034, -98.2300, "McAllen"),
(44.8016, -68.7712, "Bangor"),
(48.7519, -122.4787, "Bellingham"),
(46.8772, -96.7898, "Fargo"),
(48.6023, -93.4093, "International Falls"),
(46.4953, -84.3453, "Sault Ste. Marie MI"),
(47.1211, -88.5694, "Houghton"),
(41.6528, -83.5379, "Toledo"),
(42.1370, -83.1930, "Trenton MI"),
(39.7392, -104.9903, "Denver"),
(33.4484, -112.0740, "Phoenix"),
(30.3322, -81.6557, "Jacksonville"),
(42.3601, -71.0589, "Boston"),
(38.9072, -77.0369, "Washington DC"),
]
GB_POINTS = [
(51.5074, -0.1278, "London"),
(54.5973, -5.9301, "Belfast"),
(55.8642, -4.2518, "Glasgow"),
(51.4816, -3.1791, "Cardiff"),
(51.4545, -2.5879, "Bristol"),
(53.4084, -2.9916, "Liverpool"),
(53.4808, -2.2426, "Manchester"),
(55.9533, -3.1883, "Edinburgh"),
(52.4862, -1.8904, "Birmingham"),
(53.8008, -1.5491, "Leeds"),
(57.4778, -4.2247, "Inverness"),
(57.1497, -2.0943, "Aberdeen"),
(56.4620, -2.9707, "Dundee"),
(58.6373, -3.0689, "John o' Groats"),
(54.1509, -4.4814, "Douglas"),
(49.1858, -2.1064, "St Helier"),
(55.0000, -7.3200, "Derry"),
(54.3438, -7.6315, "Enniskillen"),
(54.1751, -6.3402, "Newry"),
(54.4783, -8.0906, "Belleek"),
(54.5973, -7.3095, "Omagh"),
(55.2053, -6.6570, "Portrush"),
(58.2090, -6.3890, "Stornoway"),
(58.9809, -2.9605, "Kirkwall"),
(60.1546, -1.1494, "Lerwick"),
(50.7184, -3.5339, "Exeter"),
(50.3755, -4.1427, "Plymouth"),
(52.6309, 1.2974, "Norwich"),
(54.9783, -1.6178, "Newcastle"),
(51.8642, -2.2382, "Gloucester"),
(52.4140, -4.0810, "Aberystwyth"),
(51.6214, -3.9436, "Swansea"),
(50.6938, -1.3040, "Newport IoW"),
]
LR_POINTS = [
(6.3005, -10.7969, "Monrovia"),
(4.3750, -7.7169, "Harper"),
(6.9956, -9.4722, "Gbarnga"),
(6.0667, -8.1333, "Zwedru"),
(8.4219, -9.7478, "Voinjama"),
(5.8808, -10.0467, "Buchanan"),
(5.0100, -9.0400, "Greenville"),
(7.3500, -8.7200, "Ganta"),
]
METRIC_POINTS = [
(49.2827, -123.1207, "Vancouver"),
(48.4284, -123.3656, "Victoria"),
(43.6532, -79.3832, "Toronto"),
(42.3149, -83.0364, "Windsor"),
(46.5136, -84.3358, "Sault Ste. Marie ON"),
(42.9745, -82.4066, "Sarnia"),
(43.2557, -79.8711, "Hamilton"),
(42.9849, -81.2453, "London ON"),
(45.5019, -73.5674, "Montreal"),
(45.4765, -75.7013, "Gatineau"),
(46.8139, -71.2080, "Quebec City"),
(46.0878, -64.7782, "Moncton"),
(44.6488, -63.5752, "Halifax"),
(49.8951, -97.1384, "Winnipeg"),
(51.0447, -114.0719, "Calgary"),
(53.5461, -113.4938, "Edmonton"),
(52.1332, -106.6700, "Saskatoon"),
(50.6745, -120.3273, "Kamloops"),
(32.5149, -117.0382, "Tijuana"),
(31.7000, -106.4700, "Ciudad Juarez"),
(25.6866, -100.3161, "Monterrey"),
(27.5060, -99.5075, "Nuevo Laredo"),
(19.4326, -99.1332, "Mexico City"),
(53.3498, -6.2603, "Dublin"),
(51.8985, -8.4756, "Cork"),
(53.2707, -9.0568, "Galway"),
(54.9503, -7.7345, "Letterkenny"),
(54.0000, -6.4000, "Dundalk"),
(54.2489, -6.9683, "Monaghan"),
(54.2766, -8.4761, "Sligo"),
(54.6538, -8.1096, "Donegal"),
(52.5200, 13.4050, "Berlin"),
(52.2297, 21.0122, "Warsaw"),
(48.8566, 2.3522, "Paris"),
(60.1699, 24.9384, "Helsinki"),
(50.4501, 30.5234, "Kyiv"),
(-33.8688, 151.2093, "Sydney"),
(35.6762, 139.6503, "Tokyo"),
(8.4844, -13.2299, "Freetown"),
(7.8767, -11.1875, "Kenema"),
(8.2783, -10.5733, "Kailahun"),
(9.6412, -13.5784, "Conakry"),
(7.7562, -8.8179, "Nzerekore"),
(7.4125, -7.5539, "Man"),
(5.3600, -4.0083, "Abidjan"),
]
@pytest.mark.parametrize("lat, lon, name", US_POINTS)
def test_us_positions(lat, lon, name):
assert region_for_position(lat, lon) == "US", name
@pytest.mark.parametrize("lat, lon, name", GB_POINTS)
def test_gb_positions(lat, lon, name):
assert region_for_position(lat, lon) == "GB", name
@pytest.mark.parametrize("lat, lon, name", LR_POINTS)
def test_lr_positions(lat, lon, name):
assert region_for_position(lat, lon) == "LR", name
@pytest.mark.parametrize("lat, lon, name", METRIC_POINTS)
def test_metric_positions(lat, lon, name):
assert region_for_position(lat, lon) == METRIC_REGION, name
@pytest.mark.parametrize("lat, lon", [(0.0, 0.0), (0.0, 0.00001), (91.0, 10.0), (10.0, 181.0)])
def test_invalid_positions(lat, lon):
assert region_for_position(lat, lon) == UNKNOWN_REGION
def test_region_is_metric():
assert not region_is_metric("US")
assert not region_is_metric("GB")
assert not region_is_metric("LR")
assert region_is_metric(METRIC_REGION)
assert not region_is_metric(UNKNOWN_REGION)
+2
View File
@@ -47,6 +47,8 @@ class IQHudRenderer(HudRenderer):
has_limit = self.speed_limit_renderer.speed_limit_valid or self.speed_limit_renderer.speed_limit_last_valid
self.limit_available = has_limit
self.limit_speed_text = str(round(self.speed_limit_renderer.speed_limit_last)) if has_limit else "---"
offset = round(self.speed_limit_renderer.speed_limit_offset)
self.limit_offset_text = f"{offset:+d}" if has_limit and offset != 0 else ""
self.turn_signal_controller.update()
self.speed_renderer.update()
self.soft_warning_renderer.update()
+16 -4
View File
@@ -128,6 +128,16 @@ function launch {
fi
fi
# Best-effort: install the verified runtime before anything starts the services that
# need it. This only succeeds on a tree that has built _verified_import.so, so on a
# prebuilt release install it is a no-op -- there the runtime arrives with the IQ.OS
# flash, whose image bakes it in. What actually fixes the 20+ minute first-install hang
# is --no-block on the service starts below: hephaestusd/ble-transportd/flockd each
# ExecStartPre-wait up to 600s for /usr/libexec/iqpilot/iqpilot_bundle_runner.
if [ -x "$DIR/system/proprietary_runtime/install_verified_runtime.sh" ]; then
"$DIR/system/proprietary_runtime/install_verified_runtime.sh" || true
fi
# Install/update proprietary runtime bundles.
if [ -f "$DIR/artifacts/runtime/ensure_private_installed.sh" ]; then
bash "$DIR/artifacts/runtime/ensure_private_installed.sh" || true
@@ -161,10 +171,12 @@ function launch {
sudo mount -o remount,ro /
fi
sudo systemctl enable "${service_name}.service"
# --no-block: these units ExecStartPre-wait for the verified runtime. Blocking here
# made a missing runner stall the whole install for the unit's 600s timeout (x3 units).
if systemctl is-active --quiet "${service_name}.service"; then
sudo systemctl restart "${service_name}.service"
sudo systemctl restart --no-block "${service_name}.service"
else
sudo systemctl start "${service_name}.service"
sudo systemctl start --no-block "${service_name}.service"
fi
elif [ -f "$service_src" ]; then
if [ ! -f "$service_dst" ] || ! cmp -s "$service_src" "$service_dst"; then
@@ -173,9 +185,9 @@ function launch {
sudo systemctl daemon-reload
sudo systemctl enable "${service_name}.service"
sudo mount -o remount,ro /
sudo systemctl restart "${service_name}.service"
sudo systemctl restart --no-block "${service_name}.service"
elif ! systemctl is-active --quiet "${service_name}.service"; then
sudo systemctl start "${service_name}.service"
sudo systemctl start --no-block "${service_name}.service"
fi
fi
done
+1 -1
View File
@@ -23,7 +23,7 @@ if [ -z "$AGNOS_VERSION" ]; then
case "$DEVICE_MODEL" in
*)
export AGNOS_VERSION="IQ.OS 4.9"
export AGNOS_VERSION="IQ.OS 4.9.3"
;;
esac
fi
+3 -1
View File
@@ -93,7 +93,9 @@ def build_project(project_name, project, main, extra_flags):
)
# short colored build output shared with the top-level build (if present)
_pretty = Dir('#site_scons/site_tools').File('pretty.py')
_pretty = Dir('#tools/scons/site_tools').File('pretty.py')
if not _pretty.exists():
_pretty = Dir('#site_scons/site_tools').File('pretty.py')
if _pretty.exists():
env.Tool('pretty', toolpath=[_pretty.dir.abspath])
if not hasattr(env, 'PrettyAction'):
+4
View File
@@ -74,6 +74,10 @@ void init_interrupts(bool check_rate_limit){
for(uint16_t i=0U; i<NUM_INTERRUPTS; i++){
interrupts[i].handler = unused_interrupt_handler;
// Default priority, lowered so the comms link can preempt everything else and
// re-arm its DMA (see IRQ_PRIORITY_COMMS). Shared state is guarded by
// ENTER_CRITICAL, which masks all interrupts regardless of priority.
NVIC_SetPriority((IRQn_Type)i, IRQ_PRIORITY_DEFAULT);
}
// Init interrupt timer for a 1s interval
@@ -1,5 +1,12 @@
#pragma once
// The SPI slave must re-arm its RX DMA at every protocol turnaround before the
// master clocks the next phase. Without preemption that re-arm waits behind any
// in-flight handler (CAN RX under bus load), the master clocks into an unarmed
// peripheral, and the transfer fails its checksum -> NACK retry storms.
#define IRQ_PRIORITY_COMMS 0U
#define IRQ_PRIORITY_DEFAULT 2U
typedef struct interrupt {
IRQn_Type irq_type;
void (*handler)(void);
+6
View File
@@ -101,6 +101,12 @@ void llspi_init(void) {
register_set(&(SPI4->CR1), SPI_CR1_SPE, 0xFFFFU);
register_set(&(SPI4->CR2), 0, 0xFFFFU);
// preempt other handlers so the RX DMA is re-armed before the master clocks
// the next phase of a transfer
NVIC_SetPriority(DMA2_Stream2_IRQn, IRQ_PRIORITY_COMMS);
NVIC_SetPriority(DMA2_Stream3_IRQn, IRQ_PRIORITY_COMMS);
NVIC_SetPriority(SPI4_IRQn, IRQ_PRIORITY_COMMS);
NVIC_EnableIRQ(DMA2_Stream2_IRQn);
NVIC_EnableIRQ(DMA2_Stream3_IRQn);
NVIC_EnableIRQ(SPI4_IRQn);
+3 -1
View File
@@ -27,7 +27,9 @@ if system == "Darwin":
env.PrependENVPath('PATH', '/opt/homebrew/bin')
# short colored build output, if the top-level pretty tool is present (main-repo build)
_pretty = Dir('#site_scons/site_tools').File('pretty.py')
_pretty = Dir('#tools/scons/site_tools').File('pretty.py')
if not _pretty.exists():
_pretty = Dir('#site_scons/site_tools').File('pretty.py')
if _pretty.exists():
env.Tool('pretty', toolpath=[_pretty.dir.abspath])
+5 -2
View File
@@ -168,8 +168,11 @@ class Controls(IQControlsLayer):
CC.latActive = _lat_active and not CS.steerFaultTemporary and not CS.steerFaultPermanent and \
(not standstill or self.CP.steerAtStandstill)
# long control may stay active through a gas override on platforms that opt in
override_longitudinal = any(e.overrideLongitudinal for e in self.sm['onroadEvents'])
long_through_override = self.CP_IQ.longActiveWithGasOverride and self.CP.openpilotLongitudinalControl
CC.longActive = CC.enabled and not getattr(CS, 'cruiseFaultLateralMode', False) and \
not any(e.overrideLongitudinal for e in self.sm['onroadEvents']) and \
(not override_longitudinal or long_through_override) and \
(self.CP.openpilotLongitudinalControl or not self.CP_IQ.pcmCruiseSpeed)
actuators = CC.actuators
@@ -184,7 +187,7 @@ class Controls(IQControlsLayer):
# accel PID loop
pid_accel_limits = self.CI.get_pid_accel_limits(self.CP, self.CP_IQ, CS.vEgo, CS.vCruise * CV.KPH_TO_MS)
actuators.accel = float(self.LoC.update(CC.longActive, CS, long_plan.aTarget, long_plan.shouldStop, pid_accel_limits,
long_plan.leadDistance, long_plan.hasLead))
long_plan.leadDistance, long_plan.hasLead, gas_override=override_longitudinal))
# Steering PID loop and lateral MPC
# Reset desired curvature to current to avoid violating the limits on engage
+7 -2
View File
@@ -53,7 +53,7 @@ class LongControl:
def reset(self):
self.pid.reset()
def update(self, active, CS, a_target, should_stop, accel_limits, lead_distance=0.0, has_lead=False):
def update(self, active, CS, a_target, should_stop, accel_limits, lead_distance=0.0, has_lead=False, gas_override=False):
"""Update longitudinal control. This updates the state machine and runs a PID loop"""
self.pid.neg_limit = accel_limits[0]
self.pid.pos_limit = accel_limits[1]
@@ -87,8 +87,13 @@ class LongControl:
else:
error = a_target - CS.aEgo
output_accel = self.pid.update(error, speed=CS.vEgo,
feedforward=a_target)
feedforward=a_target,
freeze_integrator=gas_override)
self.smooth.reset()
if gas_override:
# safety blocks braking while the gas is pressed, and a blocked tx drops the whole frame
output_accel = max(output_accel, 0.0)
self.last_output_accel = np.clip(output_accel, accel_limits[0], accel_limits[1])
return self.last_output_accel
+2 -1
View File
@@ -26,7 +26,8 @@ class ExcessiveActuationCheck:
# CS.aEgo can be noisy to bumps in the road, transitioning from standstill, losing traction, etc.
# longitudinal
accel_calibrated = calibrated_pose.acceleration.x
excessive_long_actuation = sm['carControl'].longActive and (accel_calibrated > ACCEL_MAX * 2 or accel_calibrated < ACCEL_MIN * 2)
excessive_long_actuation = sm['carControl'].longActive and ((not CS.gasPressed and accel_calibrated > ACCEL_MAX * 2) or
accel_calibrated < ACCEL_MIN * 2)
# lateral
yaw_rate = calibrated_pose.angular_velocity.yaw
+12
View File
@@ -41,6 +41,10 @@ DESCRIPTIONS = {
"DashcamEnabled": tr_noop("Record and upload driving data and video. Disabling this stops all recording! No logs, no video, no audio."),
'RecordFront': tr_noop("Upload data from the driver facing camera and help improve the driver monitoring algorithm."),
"IsMetric": tr_noop("Display speed in km/h instead of mph."),
"IQAutoUnits": tr_noop(
"Set the units from the device location. Speeds switch to km/h everywhere except the United States, " +
"the United Kingdom and Liberia, and are re-checked when you cross a border."
),
"RecordAudio": tr_noop("Record and store microphone audio while driving. The audio will be included in the dashcam video in Konn3kt."),
"LongitudinalControlMode": tr_noop(
"Choose longitudinal behavior: IQ.Pilot (IQ longitudinal + end-to-end), "
@@ -95,6 +99,12 @@ class TogglesLayout(Widget):
"metric.png",
False,
),
"IQAutoUnits": (
lambda: tr("Set Units From Location"),
DESCRIPTIONS["IQAutoUnits"],
"metric.png",
False,
),
}
self._long_personality_setting = multiple_button_item(
@@ -296,6 +306,8 @@ class TogglesLayout(Widget):
def _toggle_callback(self, state: bool, param: str):
self._params.put_bool(param, state)
if param == "IQAutoUnits" and state:
self._params.remove("IQAutoUnitsRegion")
if self._toggle_defs[param][3]:
self._params.put_bool("OnroadCycleRequested", True)
@@ -18,13 +18,15 @@ class TogglesLayoutMici(NavScroller):
disengage = BigParamControl("disengage on accelerator", "DisengageOnAccelerator")
ldw = BigParamControl("lane departure warnings", "IsLdwEnabled")
is_metric = BigParamControl("use metric units", "IsMetric")
auto_units = BigParamControl("set units from location", "IQAutoUnits", toggle_callback=self._auto_units_callback)
self._scroller.add_widgets([disengage, ldw, is_metric])
self._scroller.add_widgets([disengage, ldw, is_metric, auto_units])
self._refresh_toggles = (
("DisengageOnAccelerator", disengage),
("IsLdwEnabled", ldw),
("IsMetric", is_metric),
("IQAutoUnits", auto_units),
)
if ui_state.params.get_bool("ShowDebugInfo"):
@@ -35,6 +37,10 @@ class TogglesLayoutMici(NavScroller):
super().show_event()
self._update_toggles()
def _auto_units_callback(self, state: bool):
if state:
ui_state.params.remove("IQAutoUnitsRegion")
def _update_toggles(self):
ui_state.update_params()
for key, item in self._refresh_toggles:
+14 -14
View File
@@ -4,7 +4,7 @@ import pyray as rl
from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf
from openpilot.common.swaglog import cloudlog
from openpilot.system.hardware import TICI
from openpilot.system.hardware import EGL_DMA_BUF_SUPPORTED
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage
from openpilot.system.ui.widgets import Widget
@@ -38,7 +38,7 @@ void main() {
"""
# Choose fragment shader based on platform capabilities
if TICI:
if EGL_DMA_BUF_SUPPORTED:
FRAME_FRAGMENT_SHADER = """
#version 300 es
#extension GL_OES_EGL_image_external_essl3 : enable
@@ -121,7 +121,7 @@ class CameraView(Widget):
self._texture_needs_update = True
self.last_connection_attempt: float = 0.0
self.shader = rl.load_shader_from_memory(VERTEX_SHADER, FRAME_FRAGMENT_SHADER)
self._texture1_loc: int = rl.get_shader_location(self.shader, "texture1") if not TICI else -1
self._texture1_loc: int = rl.get_shader_location(self.shader, "texture1") if not EGL_DMA_BUF_SUPPORTED else -1
self._engaged_loc = rl.get_shader_location(self.shader, "engaged")
self._engaged_val = rl.ffi.new("int[1]", [1])
self._enhance_driver_loc = rl.get_shader_location(self.shader, "enhance_driver")
@@ -137,8 +137,8 @@ class CameraView(Widget):
self._placeholder_color: rl.Color | None = None
# Initialize EGL for zero-copy rendering on TICI
if TICI:
# Initialize EGL for zero-copy rendering on comma 3/3X.
if EGL_DMA_BUF_SUPPORTED:
if not init_egl():
raise RuntimeError("Failed to initialize EGL")
@@ -189,7 +189,7 @@ class CameraView(Widget):
self._clear_textures()
# Clean up EGL texture
if TICI and self.egl_texture:
if EGL_DMA_BUF_SUPPORTED and self.egl_texture:
rl.unload_texture(self.egl_texture)
self.egl_texture = None
@@ -264,7 +264,7 @@ class CameraView(Widget):
dst_rect = rl.Rectangle(x_offset, y_offset, scale_x, scale_y)
# Render with appropriate method
if TICI:
if EGL_DMA_BUF_SUPPORTED:
self._render_egl(src_rect, dst_rect)
else:
self._render_textures(src_rect, dst_rect)
@@ -387,12 +387,12 @@ class CameraView(Widget):
self._initialize_textures()
def _initialize_textures(self):
self._clear_textures()
if not TICI:
self.texture_y = rl.load_texture_from_image(rl.Image(None, int(self.client.stride),
int(self.client.height), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE))
self.texture_uv = rl.load_texture_from_image(rl.Image(None, int(self.client.stride // 2),
int(self.client.height // 2), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA))
self._clear_textures()
if not EGL_DMA_BUF_SUPPORTED:
self.texture_y = rl.load_texture_from_image(rl.Image(None, int(self.client.stride),
int(self.client.height), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE))
self.texture_uv = rl.load_texture_from_image(rl.Image(None, int(self.client.stride // 2),
int(self.client.height // 2), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA))
def _clear_textures(self):
if self.texture_y and self.texture_y.id:
@@ -404,7 +404,7 @@ class CameraView(Widget):
self.texture_uv = None
# Clean up EGL resources
if TICI:
if EGL_DMA_BUF_SUPPORTED:
for data in self.egl_images.values():
destroy_egl_image(data)
self.egl_images = {}
@@ -9,6 +9,7 @@ from openpilot.selfdrive.ui.onroad.alert_renderer import AlertRenderer
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer as BaseDriverStateRenderer, BTN_SIZE
from openpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer as BaseHudRenderer
from openpilot.selfdrive.ui.onroad.model_renderer import ModelRenderer
from openpilot.selfdrive.ui.onroad.environment_renderer import EnvironmentRenderer
from openpilot.selfdrive.ui.onroad.cameraview import CameraView
from openpilot.system.ui.lib.application import gui_app
from openpilot.common.issue_debug import log_issue_limited
@@ -55,6 +56,7 @@ class AugmentedRoadView(CameraView, AugmentedRoadViewIQ):
self._split_nav_available = False
self.model_renderer = ModelRenderer()
self.environment_renderer = EnvironmentRenderer()
self.alert_renderer = AlertRenderer()
self._hud_renderer = IQHudRenderer()
self.driver_state_renderer = DriverStateRendererIQ()
@@ -114,6 +116,7 @@ class AugmentedRoadView(CameraView, AugmentedRoadViewIQ):
# Draw all UI overlays
self.model_renderer.render(camera_rect)
self.environment_renderer.render(camera_rect)
AugmentedRoadViewIQ.update_fade_out_bottom_overlay(self, camera_rect)
self._hud_renderer.render(camera_rect)
@@ -280,6 +283,7 @@ class AugmentedRoadView(CameraView, AugmentedRoadViewIQ):
])
self.model_renderer.set_transform(video_transform @ calib_transform)
self.model_renderer.set_frame_transform(video_transform, is_wide_camera)
self.environment_renderer.set_transform(video_transform @ calib_transform)
return self._cached_matrix
+9 -9
View File
@@ -4,7 +4,7 @@ import pyray as rl
from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf
from openpilot.common.swaglog import cloudlog
from openpilot.system.hardware import TICI
from openpilot.system.hardware import EGL_DMA_BUF_SUPPORTED
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage
from openpilot.system.ui.widgets import Widget
@@ -38,7 +38,7 @@ void main() {
"""
# Choose fragment shader based on platform capabilities
if TICI:
if EGL_DMA_BUF_SUPPORTED:
FRAME_FRAGMENT_SHADER = """
#version 300 es
#extension GL_OES_EGL_image_external_essl3 : enable
@@ -82,7 +82,7 @@ class CameraView(Widget):
self._texture_needs_update = True
self.last_connection_attempt: float = 0.0
self.shader = rl.load_shader_from_memory(VERTEX_SHADER, FRAME_FRAGMENT_SHADER)
self._texture1_loc: int = rl.get_shader_location(self.shader, "texture1") if not TICI else -1
self._texture1_loc: int = rl.get_shader_location(self.shader, "texture1") if not EGL_DMA_BUF_SUPPORTED else -1
self.frame: VisionBuf | None = None
self.texture_y: rl.Texture | None = None
@@ -94,8 +94,8 @@ class CameraView(Widget):
self._placeholder_color: rl.Color | None = None
# Initialize EGL for zero-copy rendering on TICI
if TICI:
# Initialize EGL for zero-copy rendering on comma 3/3X.
if EGL_DMA_BUF_SUPPORTED:
if not init_egl():
raise RuntimeError("Failed to initialize EGL")
@@ -146,7 +146,7 @@ class CameraView(Widget):
self._clear_textures()
# Clean up EGL texture
if TICI and self.egl_texture:
if EGL_DMA_BUF_SUPPORTED and self.egl_texture:
rl.unload_texture(self.egl_texture)
self.egl_texture = None
@@ -220,7 +220,7 @@ class CameraView(Widget):
dst_rect = rl.Rectangle(x_offset, y_offset, scale_x, scale_y)
# Render with appropriate method
if TICI:
if EGL_DMA_BUF_SUPPORTED:
self._render_egl(src_rect, dst_rect)
else:
self._render_textures(src_rect, dst_rect)
@@ -337,7 +337,7 @@ class CameraView(Widget):
def _initialize_textures(self):
self._clear_textures()
if not TICI:
if not EGL_DMA_BUF_SUPPORTED:
self.texture_y = rl.load_texture_from_image(rl.Image(None, int(self.client.stride),
int(self.client.height), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE))
self.texture_uv = rl.load_texture_from_image(rl.Image(None, int(self.client.stride // 2),
@@ -353,7 +353,7 @@ class CameraView(Widget):
self.texture_uv = None
# Clean up EGL resources
if TICI:
if EGL_DMA_BUF_SUPPORTED:
for data in self.egl_images.values():
destroy_egl_image(data)
self.egl_images = {}
+141
View File
@@ -0,0 +1,141 @@
import numpy as np
import pyray as rl
from cereal import custom
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.widgets import Widget
MODE_OFF = 0
MODE_OVERLAY = 1
MODE_REPLACE = 2
_ENV_LABEL = custom.IQEnvironment.Object.Label
_OBJECT_COLORS = {
_ENV_LABEL.car: (40, 210, 200),
_ENV_LABEL.truck: (40, 210, 200),
_ENV_LABEL.bus: (40, 210, 200),
_ENV_LABEL.motorcycle: (90, 220, 255),
_ENV_LABEL.bicycle: (90, 220, 255),
_ENV_LABEL.person: (255, 210, 90),
_ENV_LABEL.stopSign: (255, 60, 45),
_ENV_LABEL.trafficLight: (255, 190, 0),
}
_BOX_EDGES = (
(0, 1), (1, 3), (3, 2), (2, 0),
(4, 5), (5, 7), (7, 6), (6, 4),
(0, 4), (1, 5), (2, 6), (3, 7),
)
GRID_HALF_WIDTH = 12.0
GRID_MAX_DISTANCE = 90.0
GRID_STEP = 6.0
class EnvironmentRenderer(Widget):
def __init__(self):
Widget.__init__(self)
self._car_space_transform = np.zeros((3, 3), dtype=np.float32)
self._mode = MODE_OFF
self._counter = 0
def set_transform(self, transform: np.ndarray):
self._car_space_transform = transform.astype(np.float32)
def _project(self, pt: np.ndarray):
p = self._car_space_transform @ pt
if abs(p[2]) < 1e-6:
return None
return p[0] / p[2], p[1] / p[2]
def _in_rect(self, x: float, y: float) -> bool:
r = self._rect
return r.x - 400 <= x <= r.x + r.width + 400 and r.y - 400 <= y <= r.y + r.height + 400
def _render(self, rect: rl.Rectangle):
sm = ui_state.sm
if self._counter % 30 == 0:
self._mode = int(ui_state.params.get("EnvironmentView", return_default=True) or 0) if ui_state.active_bundle else 0
self._counter += 1
if self._mode == MODE_OFF:
return
if sm.recv_frame["liveCalibration"] < ui_state.started_frame:
return
if self._mode == MODE_REPLACE:
self._draw_backdrop(rect)
self._draw_ground_grid()
if sm.valid["modelV2"]:
self._draw_model_scene(sm["modelV2"])
if sm.alive["iqEnvironment"] and sm.valid["iqEnvironment"]:
self._draw_objects(sm["iqEnvironment"])
def _draw_backdrop(self, rect: rl.Rectangle):
rl.draw_rectangle_gradient_v(int(rect.x), int(rect.y), int(rect.width), int(rect.height),
rl.Color(14, 17, 22, 255), rl.Color(6, 8, 11, 255))
def _draw_ground_grid(self):
col = rl.Color(60, 70, 82, 90)
dist = GRID_STEP
while dist <= GRID_MAX_DISTANCE:
a = self._project(np.array([dist, -GRID_HALF_WIDTH, 0.0]))
b = self._project(np.array([dist, GRID_HALF_WIDTH, 0.0]))
if a and b and self._in_rect(*a) and self._in_rect(*b):
rl.draw_line_ex(rl.Vector2(*a), rl.Vector2(*b), 1.5, col)
dist += GRID_STEP
for off in np.arange(-GRID_HALF_WIDTH, GRID_HALF_WIDTH + 0.1, 3.0):
a = self._project(np.array([GRID_STEP, float(off), 0.0]))
b = self._project(np.array([GRID_MAX_DISTANCE, float(off), 0.0]))
if a and b and self._in_rect(*a) and self._in_rect(*b):
rl.draw_line_ex(rl.Vector2(*a), rl.Vector2(*b), 1.5, col)
def _draw_polyline(self, xs, ys, zs, color, thick):
pts = []
for x, y, z in zip(xs, ys, zs, strict=False):
if x < 0:
continue
s = self._project(np.array([x, y, z], dtype=np.float32))
if s and self._in_rect(*s):
pts.append(rl.Vector2(*s))
for i in range(len(pts) - 1):
rl.draw_line_ex(pts[i], pts[i + 1], thick, color)
def _draw_model_scene(self, model):
for i, lane in enumerate(model.laneLines):
a = int(np.clip(model.laneLineProbs[i], 0.0, 0.9) * 255)
self._draw_polyline(lane.x, lane.y, lane.z, rl.Color(235, 235, 235, a), 3.0)
for edge in model.roadEdges:
self._draw_polyline(edge.x, edge.y, edge.z, rl.Color(230, 70, 70, 180), 3.0)
pos = model.position
self._draw_polyline(pos.x, pos.y, pos.z, rl.Color(40, 210, 200, 220), 6.0)
def _draw_objects(self, env):
for obj in env.objects:
self._draw_box(obj)
def _draw_box(self, obj):
hx, hy = obj.length / 2.0, obj.width / 2.0
base = np.array([
[obj.x - hx, obj.y - hy, obj.z], [obj.x - hx, obj.y + hy, obj.z],
[obj.x + hx, obj.y - hy, obj.z], [obj.x + hx, obj.y + hy, obj.z],
[obj.x - hx, obj.y - hy, obj.z + obj.height], [obj.x - hx, obj.y + hy, obj.z + obj.height],
[obj.x + hx, obj.y - hy, obj.z + obj.height], [obj.x + hx, obj.y + hy, obj.z + obj.height],
], dtype=np.float32)
screen = []
for corner in base:
s = self._project(corner)
if s is None or not self._in_rect(*s):
return
screen.append(s)
r, g, b = _OBJECT_COLORS.get(obj.label, (40, 210, 200))
a = int(np.clip(obj.prob, 0.3, 1.0) * 210)
floor = [rl.Vector2(*screen[i]) for i in (0, 1, 3, 2)]
rl.draw_triangle(floor[0], floor[1], floor[2], rl.Color(r, g, b, a // 5))
rl.draw_triangle(floor[0], floor[2], floor[3], rl.Color(r, g, b, a // 5))
for i, j in _BOX_EDGES:
rl.draw_line_ex(rl.Vector2(*screen[i]), rl.Vector2(*screen[j]), 2.0, rl.Color(r, g, b, a))
+17 -1
View File
@@ -32,6 +32,7 @@ class FontSizes:
max_speed: int = 28
set_speed: int = 74
limit_speed: int = 64
limit_offset: int = 30
limit_unit: int = 22
limit_label: int = 24
@@ -70,6 +71,7 @@ class HudRenderer(Widget):
self.speed: float = 0.0
self.v_ego_cluster_seen: bool = False
self.limit_speed_text: str = "---"
self.limit_offset_text: str = ""
self.limit_available: bool = False
self._font_semi_bold: rl.Font = gui_app.font(FontWeight.SEMI_BOLD)
@@ -168,14 +170,28 @@ class HudRenderer(Widget):
else:
limit_value_size = 48
limit_value_width = measure_text_cached(self._font_bold, limit_value_text, limit_value_size).x
limit_offset_text = self.limit_offset_text if self.limit_available else ""
limit_offset_width = 0.0
if limit_offset_text:
limit_offset_width = measure_text_cached(self._font_semi_bold, limit_offset_text, FONT_SIZES.limit_offset).x + 8
limit_value_x = x + (set_speed_width - limit_value_width - limit_offset_width) / 2
rl.draw_text_ex(
self._font_bold,
limit_value_text,
rl.Vector2(x + (set_speed_width - limit_value_width) / 2, y + 14),
rl.Vector2(limit_value_x, y + 14),
limit_value_size,
0,
limit_value_color,
)
if limit_offset_text:
rl.draw_text_ex(
self._font_semi_bold,
limit_offset_text,
rl.Vector2(limit_value_x + limit_value_width + 8, y + 22),
FONT_SIZES.limit_offset,
0,
COLORS.WHITE_TRANSLUCENT,
)
if self.limit_available:
limit_unit_text = tr("LIMIT")
+11
View File
@@ -224,6 +224,17 @@ class Soundd(AlertSoundFilter):
sm = messaging.SubMaster(['selfdriveState', 'soundPressure'])
threading.Thread(target=self.webrtc_audio_thread, daemon=True).start()
while True:
try:
self._stream_loop(sd, sm)
except Exception:
# Some A1s wedge the audio DSP (ALSA EINVAL / ADSP_EFAILED until reboot). Dying here
# crash-loops the process and selfdrived raises a takeover alert mid-drive over alert
# sounds - stay alive and keep retrying instead; recovers if the DSP comes back.
cloudlog.exception("soundd: audio stream unavailable, retrying")
time.sleep(10)
def _stream_loop(self, sd, sm):
with self.get_stream(sd) as stream:
rk = Ratekeeper(20)
+4 -1
View File
@@ -8,6 +8,7 @@ from cereal import messaging, car, log, custom
from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.iqpilot.common.auto_units import AutoUnits
from openpilot.selfdrive.ui.lib.prime_state import PrimeState
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.hardware import HARDWARE, PC
@@ -42,12 +43,13 @@ class IQUIState:
self.update_params()
self.auto_units = AutoUnits(self.params)
self.onroad_brightness_timer: int = 0
self.custom_interactive_timeout: int = self.params.get("InteractivityTimeout", return_default=True)
self.reset_onroad_sleep_timer()
def update(self) -> None:
pass
self.auto_units.update()
def onroad_brightness_handle_alerts(self, started: bool, alert):
# while an alert is on screen the dim countdown is frozen and re-armed; otherwise it ticks down
@@ -254,6 +256,7 @@ class UIState(IQUIState):
"radarState",
"liveTracks",
"iqVehicleTracks",
"iqEnvironment",
"deviceState",
"pandaStates",
"carParams",
+2 -2
View File
@@ -596,7 +596,7 @@ void SpectraCamera::config_bps(int idx, int request_id) {
tmp.header = CAM_ICP_CMD_GENERIC_BLOB_CLK;
tmp.header |= (sizeof(cam_icp_clk_bw_request)) << 8;
tmp.clk.budget_ns = 0x1fca058;
tmp.clk.frame_cycles = 2329024; // comes from the striping lib
tmp.clk.frame_cycles = 20000000; // force max BPS clock (600 MHz)
tmp.clk.rt_flag = 0x0;
tmp.clk.uncompressed_bw = 0x38512180;
tmp.clk.compressed_bw = 0x38512180;
@@ -843,7 +843,7 @@ void SpectraCamera::config_bps_downscale(int idx, int request_id) {
tmp.header = CAM_ICP_CMD_GENERIC_BLOB_CLK;
tmp.header |= (sizeof(cam_icp_clk_bw_request)) << 8;
tmp.clk.budget_ns = 0x1fca058;
tmp.clk.frame_cycles = sensor->frame_width * sensor->frame_height; // matches striping lib pixelCount
tmp.clk.frame_cycles = 20000000; // force max BPS clock (600 MHz)
tmp.clk.rt_flag = 0x0;
tmp.clk.uncompressed_bw = 0x38512180;
tmp.clk.compressed_bw = 0x38512180;
+6
View File
@@ -14,3 +14,9 @@ if TICI:
HARDWARE = cast(HardwareBase, Tici())
else:
HARDWARE = cast(HardwareBase, Pc())
# Only comma 3/3X expose the DMA-BUF EGL extensions used by the zero-copy
# camera renderer and the direct EGL frame-pacing calls. /TICI is also present
# on comma 4, so it identifies the AGNOS hardware family rather than this GPU
# capability.
EGL_DMA_BUF_SUPPORTED = TICI and HARDWARE.get_device_type() in ("tici", "tizi")
+12
View File
@@ -1,15 +1,25 @@
#pragma once
#include <cstdint>
#include <cstdlib>
#include <fstream>
#include <map>
#include <optional>
#include <string>
#include <vector>
#include "cereal/gen/cpp/log.capnp.h"
// no-op base hw class
class HardwareNone {
public:
struct UfsHealth {
uint8_t pre_eol_info;
uint8_t life_time_estimate_a;
uint8_t life_time_estimate_b;
std::vector<uint8_t> vendor_health_report;
};
static std::string get_name() { return ""; }
static cereal::InitData::DeviceType get_device_type() { return cereal::InitData::DeviceType::UNKNOWN; }
static int get_voltage() { return 0; }
@@ -21,6 +31,8 @@ public:
return {};
}
static std::optional<UfsHealth> get_ufs_health() { return std::nullopt; }
static void set_ir_power(int percentage) {}
static bool PC() { return false; }
+10 -10
View File
@@ -67,28 +67,28 @@
},
{
"name": "boot",
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/boot-179865564b1c196897c77edc41d99a8d4831a3a95564cac6868e7942cd11fd25.img.xz",
"hash": "179865564b1c196897c77edc41d99a8d4831a3a95564cac6868e7942cd11fd25",
"hash_raw": "179865564b1c196897c77edc41d99a8d4831a3a95564cac6868e7942cd11fd25",
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/boot-50b102d3dfa5d0eea506d9586745470cd59bcea9a723408a6abc20ffd6493a89.img.xz",
"hash": "50b102d3dfa5d0eea506d9586745470cd59bcea9a723408a6abc20ffd6493a89",
"hash_raw": "50b102d3dfa5d0eea506d9586745470cd59bcea9a723408a6abc20ffd6493a89",
"size": 18216960,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "2c969938fdd59528e6244820b77ec6b2cef9ab49cc9fedd490b0b3d195c189e4"
"ondevice_hash": "fdcb135e8412a896c4a882738a7022f6b7779939d8716f24b9a566b0b3ff3757"
},
{
"name": "system",
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a.img.xz",
"hash": "0c9e7dee6c7365600c77b33c4996456459d04aad005e3e41bf6ee2e8af74ceb9",
"hash_raw": "03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a",
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-a473962357e3c0c7639af17006def4567f02e64d578b00e2e0037012755be5cd.img.xz",
"hash": "62a08bdf79f6dc0075eba8b93b55cbac6eeee0ecd2b6e8ab05d51ef61a9a880a",
"hash_raw": "a473962357e3c0c7639af17006def4567f02e64d578b00e2e0037012755be5cd",
"size": 6291456000,
"sparse": true,
"full_check": false,
"has_ab": true,
"ondevice_hash": "1ee589d3d728a03561718383bf640171f1aef048573e46fe17239617c7c19fff",
"ondevice_hash": "9ed5bc147a6d4c2e0a89562f31096a7802485ee8e740defeab7674abbf6d4baa",
"alt": {
"hash": "03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a",
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a.img",
"hash": "a473962357e3c0c7639af17006def4567f02e64d578b00e2e0037012755be5cd",
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-a473962357e3c0c7639af17006def4567f02e64d578b00e2e0037012755be5cd.img",
"size": 6291456000
}
}
+1 -1
View File
@@ -1 +1 @@
+LyP/p4uBBxbTwRwPrwR/2qPaHkAXmlFkg0GdxOBuujPY89GxP4t5SNKxirqiw9LmMcfA0JfVwMeR3QD9rQ6Dg==
LxtrHkl5DFbSWnd1DZCkinmXiKFUVUausf0oYpa97dyAp3QGpWx7wrEAUZuJbrERQtdIV72ZU2eKOaQwGMBQBg==
+10 -10
View File
@@ -56,28 +56,28 @@
},
{
"name": "boot",
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/boot-179865564b1c196897c77edc41d99a8d4831a3a95564cac6868e7942cd11fd25.img.xz",
"hash": "179865564b1c196897c77edc41d99a8d4831a3a95564cac6868e7942cd11fd25",
"hash_raw": "179865564b1c196897c77edc41d99a8d4831a3a95564cac6868e7942cd11fd25",
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/boot-50b102d3dfa5d0eea506d9586745470cd59bcea9a723408a6abc20ffd6493a89.img.xz",
"hash": "50b102d3dfa5d0eea506d9586745470cd59bcea9a723408a6abc20ffd6493a89",
"hash_raw": "50b102d3dfa5d0eea506d9586745470cd59bcea9a723408a6abc20ffd6493a89",
"size": 18216960,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "2c969938fdd59528e6244820b77ec6b2cef9ab49cc9fedd490b0b3d195c189e4"
"ondevice_hash": "fdcb135e8412a896c4a882738a7022f6b7779939d8716f24b9a566b0b3ff3757"
},
{
"name": "system",
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a.img.xz",
"hash": "0c9e7dee6c7365600c77b33c4996456459d04aad005e3e41bf6ee2e8af74ceb9",
"hash_raw": "03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a",
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-a473962357e3c0c7639af17006def4567f02e64d578b00e2e0037012755be5cd.img.xz",
"hash": "62a08bdf79f6dc0075eba8b93b55cbac6eeee0ecd2b6e8ab05d51ef61a9a880a",
"hash_raw": "a473962357e3c0c7639af17006def4567f02e64d578b00e2e0037012755be5cd",
"size": 6291456000,
"sparse": true,
"full_check": false,
"has_ab": true,
"ondevice_hash": "1ee589d3d728a03561718383bf640171f1aef048573e46fe17239617c7c19fff",
"ondevice_hash": "9ed5bc147a6d4c2e0a89562f31096a7802485ee8e740defeab7674abbf6d4baa",
"alt": {
"hash": "03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a",
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a.img",
"hash": "a473962357e3c0c7639af17006def4567f02e64d578b00e2e0037012755be5cd",
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-a473962357e3c0c7639af17006def4567f02e64d578b00e2e0037012755be5cd.img",
"size": 6291456000
}
}
@@ -1 +1 @@
RCYk34Ifjx7S9PIhPm6p49vFMhamPwvw8Lfbb6a6HO8sUOi3J2PcY4VDUcqQZWU6+Am++HP3nJSea8OmAsckAg==
mKCTfzNHr1d0BYPdmXQRPGpnVuLaDLGZGLbq+86ZJQaGIK1ekcKeYeycoX8BTMNFr+89Tnd77M/CvmjM8yteCg==
+47 -1
View File
@@ -1,17 +1,63 @@
#pragma once
#include <cstdlib>
#include <array>
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <fcntl.h>
#include <fstream>
#include <map>
#include <string>
#include <algorithm> // for std::clamp
#include <sys/ioctl.h>
#include <unistd.h>
#include "common/util.h"
#include "system/hardware/base.h"
class HardwareTici : public HardwareNone {
public:
static std::optional<UfsHealth> get_ufs_health() {
constexpr unsigned long UFS_IOCTL_QUERY = 0x5388;
constexpr uint32_t UPIU_QUERY_OPCODE_READ_DESC = 0x1;
constexpr uint8_t QUERY_DESC_IDN_HEALTH = 0x9;
constexpr uint16_t QUERY_DESC_HEALTH_SIZE = 0x25;
struct UfsQuery {
uint32_t opcode;
uint8_t idn;
uint8_t reserved;
uint16_t buf_size;
std::array<uint8_t, QUERY_DESC_HEALTH_SIZE> buffer;
};
static_assert(offsetof(UfsQuery, buffer) == 8);
UfsQuery query = {};
query.opcode = UPIU_QUERY_OPCODE_READ_DESC;
query.idn = QUERY_DESC_IDN_HEALTH;
query.buf_size = QUERY_DESC_HEALTH_SIZE;
int fd = open("/dev/sda", O_RDONLY | O_CLOEXEC);
if (fd < 0) {
return std::nullopt;
}
int ret = ioctl(fd, UFS_IOCTL_QUERY, &query);
close(fd);
if (ret != 0 || query.buf_size < 5 || query.buf_size > query.buffer.size() ||
query.buffer[0] != query.buf_size || query.buffer[1] != QUERY_DESC_IDN_HEALTH) {
return std::nullopt;
}
return UfsHealth{
query.buffer[2],
query.buffer[3],
query.buffer[4],
std::vector<uint8_t>(query.buffer.begin() + 5, query.buffer.begin() + query.buf_size),
};
}
static std::string get_name() {
std::string model = util::read_file("/sys/firmware/devicetree/base/model");
return util::strip(model.substr(std::string("comma ").size()));
+6 -5
View File
@@ -78,9 +78,10 @@ void V4LEncoder::dequeue_handler(V4LEncoder *e) {
uint32_t idx = -1;
bool exit = false;
// POLLIN is capture, POLLOUT is frame
// POLLIN is capture, POLLOUT is frame. Qualcomm's reference client also
// requests the corresponding normal-data bits.
struct pollfd pfd;
pfd.events = POLLIN | POLLOUT;
pfd.events = POLLIN | POLLRDNORM | POLLOUT | POLLWRNORM;
pfd.fd = e->fd;
// save the header
@@ -105,7 +106,7 @@ void V4LEncoder::dequeue_handler(V4LEncoder *e) {
}
int frame_id = -1;
if (pfd.revents & POLLIN) {
if (pfd.revents & (POLLIN | POLLRDNORM)) {
unsigned int bytesused, flags, index;
struct timeval timestamp;
dequeue_buffer(e->fd, V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, &index, &bytesused, &flags, &timestamp);
@@ -136,7 +137,7 @@ void V4LEncoder::dequeue_handler(V4LEncoder *e) {
queue_buffer(e->fd, V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, index, &e->buf_out[index]);
}
if (pfd.revents & POLLOUT) {
if (pfd.revents & (POLLOUT | POLLWRNORM)) {
unsigned int index;
dequeue_buffer(e->fd, V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, &index);
e->free_buf_in.push(index);
@@ -244,7 +245,7 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei
{ .id = V4L2_CID_MPEG_VIDEO_H264_LEVEL, .value = V4L2_MPEG_VIDEO_H264_LEVEL_UNKNOWN},
{ .id = V4L2_CID_MPEG_VIDEO_H264_ENTROPY_MODE, .value = V4L2_MPEG_VIDEO_H264_ENTROPY_MODE_CABAC},
{ .id = V4L2_CID_MPEG_VIDC_VIDEO_H264_CABAC_MODEL, .value = V4L2_CID_MPEG_VIDC_VIDEO_H264_CABAC_MODEL_0},
{ .id = V4L2_CID_MPEG_VIDEO_H264_LOOP_FILTER_MODE, .value = 0},
{ .id = V4L2_CID_MPEG_VIDEO_H264_LOOP_FILTER_MODE, .value = V4L2_MPEG_VIDEO_H264_LOOP_FILTER_MODE_ENABLED},
{ .id = V4L2_CID_MPEG_VIDEO_H264_LOOP_FILTER_ALPHA, .value = 0},
{ .id = V4L2_CID_MPEG_VIDEO_H264_LOOP_FILTER_BETA, .value = 0},
{ .id = V4L2_CID_MPEG_VIDEO_MULTI_SLICE_MODE, .value = 0},
+8
View File
@@ -46,6 +46,14 @@ kj::Array<capnp::word> logger_build_init_data() {
init.setKernelVersion(util::read_file("/proc/version"));
init.setOsVersion(util::read_file("/VERSION"));
if (auto health = Hardware::get_ufs_health()) {
auto ufs_health = init.initUfsHealth();
ufs_health.setPreEolInfo(health->pre_eol_info);
ufs_health.setLifeTimeEstimateA(health->life_time_estimate_a);
ufs_health.setLifeTimeEstimateB(health->life_time_estimate_b);
ufs_health.setVendorHealthReport(capnp::Data::Reader(health->vendor_health_report.data(), health->vendor_health_report.size()));
}
// log params
Params params(util::getenv("PARAMS_COPY_PATH", ""));
std::map<std::string, std::string> params_map = params.readAll();
+9
View File
@@ -170,6 +170,15 @@ class TestLoggerd:
assert initData.dirty != bool(os.environ["CLEAN"])
assert initData.version == get_version()
if TICI:
assert initData._has("ufsHealth")
assert initData.ufsHealth.preEolInfo in (1, 2, 3)
assert 1 <= initData.ufsHealth.lifeTimeEstimateA <= 11
assert 1 <= initData.ufsHealth.lifeTimeEstimateB <= 11
assert len(initData.ufsHealth.vendorHealthReport) == 32
else:
assert not initData._has("ufsHealth")
if os.path.isfile("/proc/cmdline"):
with open("/proc/cmdline") as f:
assert list(initData.kernelArgs) == f.read().strip().split(" ")
+31 -3
View File
@@ -17,6 +17,10 @@ from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
MAX_CRASH_BACKOFF = 300.0
CRASH_RESET_TIME = 60.0
CRASH_LOOP_THRESHOLD = 6
try:
from openpilot.system.proprietary_runtime.runtime_paths import preferred_runner_path
except ModuleNotFoundError:
@@ -82,6 +86,10 @@ class ManagerProcess(ABC):
name = ""
shutting_down = False
restart_if_crash = False
crash_count = 0
last_restart_time = 0.0
last_alive_time = 0.0
crash_loop_logged = False
@abstractmethod
def prepare(self) -> None:
@@ -318,13 +326,33 @@ def ensure_running(procs: ValuesView[ManagerProcess], started: bool, params=None
not_run = []
running = []
now = time.monotonic()
for p in procs:
if p.enabled and p.name not in not_run and p.should_run(started, params, CP):
if p.restart_if_crash and p.proc is not None and not p.proc.is_alive():
cloudlog.error(f'Restarting {p.name} (exitcode {p.proc.exitcode})')
p.restart()
if p.restart_if_crash and p.proc is not None and p.proc.is_alive():
p.last_alive_time = now
elif p.restart_if_crash and p.proc is not None:
# uptime, not time-since-restart: the latter also counts the backoff wait,
# which would reset the counter as soon as backoff exceeds CRASH_RESET_TIME
if p.last_alive_time - p.last_restart_time > CRASH_RESET_TIME:
p.crash_count = 0
p.crash_loop_logged = False
backoff = 0.0 if not p.crash_count else min(MAX_CRASH_BACKOFF, 2.0 ** (p.crash_count - 1))
if now - p.last_restart_time >= backoff:
p.crash_count += 1
p.last_restart_time = now
cloudlog.error(f'Restarting {p.name} (exitcode {p.proc.exitcode}) [crash {p.crash_count}]')
if p.crash_count >= CRASH_LOOP_THRESHOLD and not p.crash_loop_logged:
# never stop retrying: giving up on hardwared or ui is worse than restarting slowly
cloudlog.error(f'{p.name} is in a crash loop, backing off to {MAX_CRASH_BACKOFF}s between restarts')
p.crash_loop_logged = True
p.restart()
running.append(p)
else:
p.crash_count = 0
p.crash_loop_logged = False
p.last_alive_time = 0.0
p.stop(block=False)
for p in running:
+10 -1
View File
@@ -98,7 +98,9 @@ def constructiond_onroad(started: bool, params: Params, CP: car.CarParams) -> bo
return started and params.get_bool("ConstructionZoneAssist")
def iqvd_onroad(started: bool, params: Params, CP: car.CarParams) -> bool:
return started and params.get_bool("VisionVehicleTracks")
# held for 1.0d: iqvd runs a detector per frame and the added load is not
# something 1.0c needs to carry. re-enable by restoring the param check.
return False
def only_offroad(started: bool, params: Params, CP: car.CarParams) -> bool:
return not started
@@ -109,6 +111,12 @@ def livestream(started: bool, params: Params, CP: car.CarParams) -> bool:
# down cleanly when the session ends — no subprocess management inside hephaestusd.
return params.get_bool("IsLiveStreaming")
def canlive(started: bool, params: Params, CP: car.CarParams) -> bool:
# Remote live CAN debugging via konn3kt. hephaestusd sets CanLiveStreaming when a viewer
# connects (startCanLive) and clears it when the last one leaves (stopCanLive), so canlived
# runs only during an active debug session — no idle connection or battery cost otherwise.
return params.get_bool("CanLiveStreaming")
def is_tinygrad_model(started, params, CP: car.CarParams) -> bool:
"""Check if the active model runner is tinygrad."""
return bool(get_active_model_runner(params, not started) == custom.IQModelManager.Runner.tinygrad)
@@ -179,6 +187,7 @@ procs = [
# debug procs
NativeProcess("bridge", "cereal/messaging", ["./bridge"], notcar),
PythonProcess("webrtcd", "system.webrtc.webrtcd", or_(iscar, livestream)),
PythonProcess("canlived", "iqpilot.konn3kt.canlive.canlived", canlive),
PythonProcess("webjoystick", "tools.bodyteleop.web", notcar),
]
+13 -4
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
import numpy as np
import os
import time
from functools import cache
import threading
@@ -136,10 +137,18 @@ class Mic:
self.last_device = f"{device}: {sd.query_devices(device)['name']}" if isinstance(device, int) else str(device)
cloudlog.info(f"micd selecting input device {self.last_device}")
with self.get_stream(sd) as stream:
cloudlog.info(f"micd stream started: {stream.samplerate=} {stream.channels=} {stream.dtype=} {stream.device=}, {stream.blocksize=}")
while True:
self.update()
while True:
try:
with self.get_stream(sd) as stream:
cloudlog.info(f"micd stream started: {stream.samplerate=} {stream.channels=} {stream.dtype=} {stream.device=}, {stream.blocksize=}")
while True:
self.update()
except Exception:
# Some A1s wedge the audio DSP (ALSA EINVAL / ADSP_EFAILED until reboot). Dying here
# crash-loops the process and selfdrived raises a takeover alert mid-drive over a
# microphone - stay alive and keep retrying instead; recovers if the DSP comes back.
cloudlog.exception("micd: audio stream unavailable, retrying")
time.sleep(10)
def main():
+2 -2
View File
@@ -12,7 +12,7 @@ from openpilot.common.gps import get_gps_location_service
def set_time(new_time):
diff = datetime.datetime.now() - new_time
diff = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) - new_time
if abs(diff) < datetime.timedelta(seconds=10):
cloudlog.debug(f"Time diff too small: {diff}")
return
@@ -47,7 +47,7 @@ def main() -> NoReturn:
pm.send('clocks', msg)
gps = sm[gps_location_service]
gps_time = datetime.datetime.fromtimestamp(gps.unixTimestampMillis / 1000.)
gps_time = datetime.datetime.fromtimestamp(gps.unixTimestampMillis / 1000., datetime.UTC).replace(tzinfo=None)
if not sm.updated[gps_location_service] or (time.monotonic() - sm.logMonoTime[gps_location_service] / 1e9) > 2.0:
continue
if not gps.hasFix:
+6 -6
View File
@@ -22,7 +22,7 @@ from pathlib import Path
from typing import NamedTuple
from importlib.resources import as_file, files
from openpilot.common.swaglog import cloudlog
from openpilot.system.hardware import HARDWARE, PC
from openpilot.system.hardware import EGL_DMA_BUF_SUPPORTED, HARDWARE, PC
from openpilot.system.ui.lib.multilang import multilang
from openpilot.common.realtime import Ratekeeper
@@ -326,7 +326,7 @@ class GuiApplication(IQAppHooks):
rl.glfw_swap_interval(0)
except Exception:
pass
if not PC and rl.is_window_ready() and not OFFSCREEN:
if EGL_DMA_BUF_SUPPORTED and rl.is_window_ready() and not OFFSCREEN:
try:
from openpilot.system.ui.lib.egl import set_swap_interval
set_swap_interval(0)
@@ -349,7 +349,7 @@ class GuiApplication(IQAppHooks):
glfw_vsync = False
egl_vsync = False
if not PC:
if EGL_DMA_BUF_SUPPORTED:
try:
from openpilot.system.ui.lib.egl import set_swap_interval
egl_vsync = set_swap_interval(interval)
@@ -367,7 +367,7 @@ class GuiApplication(IQAppHooks):
rl.rl_draw_render_batch_active()
def _apply_display_sync_before_swap(self) -> None:
if PC or OFFSCREEN or not DISPLAY_SYNC_BEFORE_SWAP or self._display_sync_available is False:
if not EGL_DMA_BUF_SUPPORTED or OFFSCREEN or not DISPLAY_SYNC_BEFORE_SWAP or self._display_sync_available is False:
return
try:
self._flush_raylib_batch()
@@ -395,7 +395,7 @@ class GuiApplication(IQAppHooks):
# intermittent screen tearing when scrolling (the content moves, so a mid-scanout swap is
# visible). eglSwapInterval is a trivial call, so re-assert it every frame to keep FIFO vsync
# pinned instead of relying on the slow (VSYNC_REAPPLY_INTERVAL) full re-apply below.
if self._paced_by_vsync and self._vsync_interval > 0 and not PC and rl.is_window_ready():
if self._paced_by_vsync and self._vsync_interval > 0 and EGL_DMA_BUF_SUPPORTED and rl.is_window_ready():
try:
from openpilot.system.ui.lib.egl import set_swap_interval
set_swap_interval(self._vsync_interval)
@@ -850,7 +850,7 @@ class GuiApplication(IQAppHooks):
# full-res frame texture can make the Adreno/GBM driver silently reset the swap interval to 0
# *after* that call but before this swap, which reintroduced tearing. This is the swap that
# actually matters, so pin the interval here too.
if (ENABLE_VSYNC and not OFFSCREEN and not PC and self._paced_by_vsync
if (ENABLE_VSYNC and not OFFSCREEN and EGL_DMA_BUF_SUPPORTED and self._paced_by_vsync
and self._vsync_interval > 0 and rl.is_window_ready()):
try:
from openpilot.system.ui.lib.egl import set_swap_interval
+46 -1
View File
@@ -961,6 +961,10 @@ class WifiManager:
settings['connection'] = {}
changes = False
# NetworkManager updates an active modem connection in place. Quectel
# modems keep the existing PDP bearer in that case, so an APN change
# does not take effect until LTE is disconnected and reactivated.
apn_settings_changed = False
auto_config = apn == ""
initial_eps_apn = apn if not auto_config else ""
@@ -968,11 +972,13 @@ class WifiManager:
cloudlog.warning(f'Changing gsm.auto-config to {auto_config}')
settings['gsm']['auto-config'] = ('b', auto_config)
changes = True
apn_settings_changed = True
if settings['gsm'].get('apn', ('s', ''))[1] != apn:
cloudlog.warning(f'Changing gsm.apn to {apn}')
settings['gsm']['apn'] = ('s', apn)
changes = True
apn_settings_changed = True
if settings['gsm'].get('home-only', ('b', False))[1] == roaming:
cloudlog.warning(f'Changing gsm.home-only to {not roaming}')
@@ -983,11 +989,13 @@ class WifiManager:
cloudlog.warning(f'Changing gsm.initial-eps-bearer-configure to {bool(initial_eps_apn)}')
settings['gsm']['initial-eps-bearer-configure'] = ('b', bool(initial_eps_apn))
changes = True
apn_settings_changed = True
if settings['gsm'].get('initial-eps-bearer-apn', ('s', ''))[1] != initial_eps_apn:
cloudlog.warning(f'Changing gsm.initial-eps-bearer-apn to {initial_eps_apn}')
settings['gsm']['initial-eps-bearer-apn'] = ('s', initial_eps_apn)
changes = True
apn_settings_changed = True
# Unknown means NetworkManager decides
metered_int = int(MeteredType.UNKNOWN if metered else MeteredType.NO)
@@ -1005,7 +1013,10 @@ class WifiManager:
cloudlog.warning(f"Failed to update GSM settings: {reply}")
return
self._activate_modem_connection(lte_connection_path)
if apn_settings_changed:
self._restart_modem_connection(lte_connection_path)
else:
self._activate_modem_connection(lte_connection_path)
except Exception as e:
cloudlog.exception(f"Error updating GSM settings: {e}")
@@ -1034,6 +1045,40 @@ class WifiManager:
except Exception as e:
cloudlog.exception(f"Error activating modem connection: {e}")
def _restart_modem_connection(self, connection_path: str):
"""Reconnect LTE so a changed APN is used for a new PDP bearer."""
try:
for active_conn in self._get_active_connections():
conn_addr = DBusAddress(active_conn, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE)
active_conn_path = self._router_main.send_and_get_reply(Properties(conn_addr).get('Connection')).body[0][1]
if active_conn_path == connection_path:
cloudlog.warning("Restarting LTE connection to apply APN settings")
reply = self._router_main.send_and_get_reply(new_method_call(self._nm, 'DeactivateConnection', 'o', (active_conn,)))
if reply.header.message_type == MessageType.error:
cloudlog.warning(f"Failed to deactivate LTE connection: {reply}")
return
for _ in range(20):
if not self._is_connection_active(connection_path):
break
time.sleep(0.25)
else:
cloudlog.warning("LTE connection did not deactivate after APN change")
return
break
self._activate_modem_connection(connection_path)
except Exception as e:
cloudlog.exception(f"Error restarting modem connection: {e}")
def _is_connection_active(self, connection_path: str) -> bool:
for active_conn in self._get_active_connections():
conn_addr = DBusAddress(active_conn, bus_name=NM, interface=NM_ACTIVE_CONNECTION_IFACE)
active_conn_path = self._router_main.send_and_get_reply(Properties(conn_addr).get('Connection')).body[0][1]
if active_conn_path == connection_path:
return True
return False
def stop(self):
if not self._exit:
self._exit = True

Some files were not shown because too many files have changed in this diff Show More