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
.gitignore vendored
View File

@@ -176,3 +176,5 @@ selfdrive/ui/tests/test_ui/nav_demo_report/
# fetcher: closed-source precompiled component — ship only the obfuscated .so # fetcher: closed-source precompiled component — ship only the obfuscated .so
/iqpilot/models_private_src/fetcher.py /iqpilot/models_private_src/fetcher.py
/tools/iqmacvisiond/macos/dist/

View File

@@ -10,6 +10,13 @@ import SCons.Errors
SCons.Warnings.warningAsException(True) 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 # capnp's kj library warns when $PWD is stale (doesn't match the real cwd); keep them in sync
os.environ.pop('PWD', None) os.environ.pop('PWD', None)
@@ -49,6 +56,13 @@ assert arch in [
"Darwin", # macOS arm64 (x86 not supported) "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 = Environment(
ENV={ ENV={
"PATH": os.environ['PATH'], "PATH": os.environ['PATH'],
@@ -100,7 +114,7 @@ env = Environment(
COMPILATIONDB_USE_ABSPATH=True, COMPILATIONDB_USE_ABSPATH=True,
REDNOSE_ROOT="#", REDNOSE_ROOT="#",
tools=["default", "cython", "compilation_db", "rednose_filter"], 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 # Arch-specific flags and paths
@@ -187,7 +201,7 @@ else:
np_version = SCons.Script.Value(np.__version__) np_version = SCons.Script.Value(np.__version__)
Export('envCython', 'np_version') Export('envCython', 'np_version')
Export('env', 'arch') Export('env', 'arch', 'ffmpeg_libs')
# Setup cache dir # Setup cache dir
default_cache_dir = os.environ.get('SCONS_CACHE_DIR') or ('/data/scons_cache' if arch == "larch64" else '/tmp/scons_cache') default_cache_dir = os.environ.get('SCONS_CACHE_DIR') or ('/data/scons_cache' if arch == "larch64" else '/tmp/scons_cache')

433
TOOLS_UPSTREAM_SYNC_PLAN.md Normal file
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).

View File

@@ -16,27 +16,27 @@
}, },
"python/iqpilot_private/konn3kt/backups/archive_codec.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/backups/archive_codec.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "2d4cfb58875fee44a7baea50946fb8b31feb5ceb0daf61598b032634f336363e", "sha256": "9368086c4cdd6cf46b6a6d0a1e1e83fdc7e7dff8d3bd67a42b3bffa91b45dc26",
"size": 135536 "size": 135536
}, },
"python/iqpilot_private/konn3kt/backups/backup_keys.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/backups/backup_keys.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "903174fad1ee61fe959be5c9401b8d10e2edd05eadfa02e19e4cd599aa1e464d", "sha256": "291789f912304c72cd1e0da955bd58fa903cd99e7d6504e6c3c51d39befaf6e8",
"size": 67664 "size": 67664
}, },
"python/iqpilot_private/konn3kt/backups/backup_orchestrator.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/backups/backup_orchestrator.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "fd38412866cefa4be7777b7573b46e57665756f0d4d7decdfb189a8dafae8067", "sha256": "c47ec079040efee31673f55752a6215b3a8908c63c8373625ce5cb4cb1177aff",
"size": 204624 "size": 204624
}, },
"python/iqpilot_private/konn3kt/backups/cbc_vault.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/backups/cbc_vault.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "c95f786dbff8484c2ad63ed04fa37bcf0bdaf16f0b9f80d37e5d1e8e5b224554", "sha256": "a0d5710938b32e56f2fc55c86ff8947787a3ebeb43850a94a90202c3773ff74e",
"size": 69752 "size": 69752
}, },
"python/iqpilot_private/konn3kt/backups/imahelper.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/backups/imahelper.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "88b9a944da9cfbf6fe043ee685d4cd04e900b34996e69ab2eb7cedd807798854", "sha256": "ff345aa71ac599bdd16747368ceb92fae7b5b35657f81466fc0660b76d44aa22",
"size": 203072 "size": 203072
}, },
"python/iqpilot_private/konn3kt/flockd/__init__.py": { "python/iqpilot_private/konn3kt/flockd/__init__.py": {
@@ -46,12 +46,12 @@
}, },
"python/iqpilot_private/konn3kt/flockd/flockd.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/flockd/flockd.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "83336a6b02ffdb451297e3d4f6b586fa764f87195dfe6094805a4a18fcd9d279", "sha256": "2aa26e4bc71870f639fca6e8e273deb59f50cf6a6d963ba77ce0949c65a52d63",
"size": 202168 "size": 202168
}, },
"python/iqpilot_private/konn3kt/flockd/signatures.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/flockd/signatures.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "563e6dd93009fd5e99de5d6abebbed20a7020683a8a0266e3c1a1f9d6b5f2889", "sha256": "96b47ec40dd7e3430cebf3979b4fdfb0096e80029b40da6445cdf44dbcdb8917",
"size": 68080 "size": 68080
}, },
"python/iqpilot_private/konn3kt/hephaestus/__init__.py": { "python/iqpilot_private/konn3kt/hephaestus/__init__.py": {
@@ -61,67 +61,67 @@
}, },
"python/iqpilot_private/konn3kt/hephaestus/_vendor/localapi_runtime.zip": { "python/iqpilot_private/konn3kt/hephaestus/_vendor/localapi_runtime.zip": {
"mode": 420, "mode": 420,
"sha256": "dc0184fd4971a040ca4ca332b7bcf6b2ee6b8040804c90d3efc3f2806359106c", "sha256": "f12e44b7a986616d6b3c4e127e4eeae793e346f31d5b821ad9c44249e4731ebd",
"size": 2100238 "size": 2100238
}, },
"python/iqpilot_private/konn3kt/hephaestus/ble_auth.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/hephaestus/ble_auth.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "8036f724630003afda855cf293f098cc3bb50801a5ca7626c3b57d4556605f5a", "sha256": "4ac192d102bde18d9b3215ff637e1c316a08eb0eb00c595c07eaa07284d1f28c",
"size": 268064 "size": 268064
}, },
"python/iqpilot_private/konn3kt/hephaestus/ble_gatt.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/hephaestus/ble_gatt.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "a97b390dd697dbba69ac83663c49f65c6408bbcb03be05c889bdee622d9e5efa", "sha256": "6f2dccd9f85db9d4948ddb31f62b6059d9359f7f16df6d55455b05c6ae4f37b7",
"size": 340584 "size": 340584
}, },
"python/iqpilot_private/konn3kt/hephaestus/ble_rpc_dispatch.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/hephaestus/ble_rpc_dispatch.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "a3fea60218cd759a37d4ca423b6b55d02fd5e36cb762e8333604d7fb322b4923", "sha256": "abb3eb9033205d1c2903b619dbbab7e3de10b859493239c35533b75d00bc3305",
"size": 68048 "size": 68048
}, },
"python/iqpilot_private/konn3kt/hephaestus/ble_transportd.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/hephaestus/ble_transportd.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "548e4910fd18dfc8058c17bfabfbb5f69db15481169efd4fe68841f20c8a0692", "sha256": "a631270d167cbd9a67db6111de57e5d6ddcdd554706fd9fd919d8f116733bf66",
"size": 335728 "size": 335728
}, },
"python/iqpilot_private/konn3kt/hephaestus/bt_gamepad.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/hephaestus/bt_gamepad.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "3ed0ce5cfeec2f8cc3b6a0a85b6137d3bf7219904e5e76f6490f027e9d6346a0", "sha256": "c5c4f480275ddc00e2d7d44aee76808f2b4184b6a6354ef2e757d8b3a7ae8e44",
"size": 271680 "size": 271680
}, },
"python/iqpilot_private/konn3kt/hephaestus/cloud_routes.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/hephaestus/cloud_routes.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "a8667a7a12c8a096e68b8716b0f309fad8d98e5cf056121a8f84371908421402", "sha256": "a9d9fdfcd6f154e093ed1cc678bf9a1e12c7a3e475a358c19941af7777c40807",
"size": 134408 "size": 134408
}, },
"python/iqpilot_private/konn3kt/hephaestus/hephaestusd.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/hephaestus/hephaestusd.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "af175ad8b662828c25f2e53dd4af1e323318f4b7df51276914327ae110a8283d", "sha256": "d3468f09df47814c161937306c776df5fe9a6a6d819859a596f7e33be6d253e8",
"size": 3514480 "size": 3581928
}, },
"python/iqpilot_private/konn3kt/hephaestus/kwp2000.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/hephaestus/kwp2000.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "0be3eb36bd0fcc798ad602ff7be133366d82c3b112df9663a94347e431dd6486", "sha256": "c7cf16d3b2297deafd87c53c8b61031d2af84ff04848bab34d9dde37ba939ed5",
"size": 136448 "size": 136448
}, },
"python/iqpilot_private/konn3kt/hephaestus/manage_hephaestusd.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/hephaestus/manage_hephaestusd.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "fa70ebb1569dde9f474040476c495d170874217cac0611e8057a92c9bfee0c92", "sha256": "113890a2fa588f0c3d6d0c6723aef63adb58fc3ad5355174effe5ac2d3d6d364",
"size": 68136 "size": 68136
}, },
"python/iqpilot_private/konn3kt/hephaestus/motd.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/hephaestus/motd.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "674acf92f744733857437a9993e6e7bb41190e924ec894a45adb4acf396b560f", "sha256": "e97f1650766b15b231657afd6530c019d09352dfa0d96deb2643893acf56efa4",
"size": 67840 "size": 67840
}, },
"python/iqpilot_private/konn3kt/hephaestus/tp20.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/hephaestus/tp20.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "0b4e93091aaf56a2ae1b0a22dab3e7c07e2172df80308f97edb793e412310a3d", "sha256": "7be8a68c56f6ff37ef887b87275c78e517065995243e88d1ab6a10f3b028abe5",
"size": 135648 "size": 135648
}, },
"python/iqpilot_private/konn3kt/hephaestus/vw_pq_flasher.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/hephaestus/vw_pq_flasher.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "0b6ddf3b89bb78d1d2c68ef0c3f93796867c150606269f7666bdca093ecc856f", "sha256": "2a1f6cbb8794a29a55decf6d04915e752360eac7701ed55c826355bb348e39a6",
"size": 269568 "size": 269568
}, },
"python/iqpilot_private/konn3kt/uploaderd/__init__.py": { "python/iqpilot_private/konn3kt/uploaderd/__init__.py": {
@@ -131,7 +131,7 @@
}, },
"python/iqpilot_private/konn3kt/uploaderd/iquploaderd.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/konn3kt/uploaderd/iquploaderd.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "9104feff3435925c9235b8424f08dc52eee877ae546093ecea83687b3fd6478a", "sha256": "613141bdba6cabaf47fa07b438fe964e90796b7e0309c8f0fbfa60b6bf54e6ad",
"size": 204184 "size": 204184
}, },
"runtime": { "runtime": {
@@ -171,25 +171,25 @@
} }
}, },
"signatures": { "signatures": {
"python/iqpilot_private/konn3kt/backups/archive_codec.cpython-312-aarch64-linux-gnu.so": "P79t7+Txevk0MV7hDKGcDPlx2RH9CACuI89MdIuouzwjTIPyi/XZxb2z5qukLp/SENm8tIO/uvDqNMGg1zsNDQ==", "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": "9iVq7Mj2pYNsFG/663bMRD1XH2IEk6+Xbpd41jT83rhtYCi83aHzXcXyDcqNmyYhQpPeu4It9sp1eSj42w5DBg==", "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": "f3SUgO2s1N9juez3vvXlhi4KV9gibraZs0XKdtlTCHS9TVpUPAforrIv+fiiv+CnXuNQaFxOsVE1nsVxYUFVCg==", "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": "+Em9DTW7a6uzqgeZlLCFFjIjN3su8P7SImlAiLLc9Bc8ylsmvaEqfqLewnblANN0E3fPefIWPy2SXtE/sh8DDw==", "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": "adDfmCgrUH/7F/tctcIfy7PMLkdnEj0Y8dJO6q7/Ds34WpyNZy0JousS007TGsryrIAjZsfujMieVei7DCvbDA==", "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": "khtz+JnG262yIsCgaqzeSxqlkbyXbzVbZrVxuXV6SQhhl+Ob/LL1ZgSs6Wpca48STo2NMYjglzeaM5jwj847Bg==", "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": "ZFG+EQgMvwqq8T6VNUqqgQJmIgAC1AIYNF/QWYU7MxYWWWfnx9s+G5Mz2EwLMUIdW16pGXsqkn02u4kHNxpcCg==", "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": "KW+OPawOOOKdeIl4l7gc1hjV8sdd0QXk8tIBl5UXwXgQSdbGwE+H8mCc2EKEhDb5wR1Zl/Cb6pC7D8ksNZcFCQ==", "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": "Q+2toj33o3kivPIA152qAIRGdUMFI9jKJ97W9bf2cNXCWAs04p8XvHIxjD1JChJMfvcZLHIk9HlumRSalcT1Dg==", "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": "s7qLI/aus4eexP6XEqrreU/stI781n0T0F1yeerxo0BoaUc5oT0HTQkrZTgSagFfGnPT228MKEhB7xW2JOuAAg==", "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": "ESRJsvz0NplwLhzY/oGMGP4ZD3S70fd07QSl/xLdFcX36Scelic5pJO9ClBSeoMd1uA1iWmSMARK4vugJHTBBQ==", "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": "DPbD2qSAUTkR0JmZT+9bPYDZlSeAmZaKnRJiC9Av+uLpX/lCnbIeLMEgRFrOdho/gf4CzmDcbfboULYwgd06AQ==", "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": "ut5z43F74P4rq51YQBaPU35hrOsTphthrkAfYv7ezFUwSfZei0jUBvJAujU9n6fbg58NiUYrIgwe8/AnntpaCw==", "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": "IUzori/U8oSQPTO9KSMvR0/B3zBmpwmSUNdnb2Idop3gGaGBrebNaRv4Ilbjp6+xVNArvEsOybVxPPSapVsjDg==", "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": "7aEw/FLCYQuH+VqyeV88nnfHG9xFrfP0XK9rNltmT6YXAWKN6PtnpXVqPTmrBuuOlkl+pCIKLIvKVm52dbAOAg==", "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": "EDpN12qdopPVzYu4aHbrNwYjN3vfRrEJhln5XNrVnh/RsZNkM0HKTJJdcVauJz//lEwIHTn1KLvOgiKzn5rlBA==", "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": "0ud+tsJGU2wew33iJmC6bJ+Pg9f4Fl0znsbXhO4lUXmuqCWTfaCWYbAASggUVF0MFYccJ9aZsARON2S/24igAw==", "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": "+3ahkAwXUIluEv86JNK0HpnsrprT8iPdKDRBWdCWc4ux+k8YN7bCDQSl+B1gkGRxB/2hCQq0ncF3EKb3+yTCCQ==", "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": "ch5zd8KmXfi/vyI0see68Qdcr7PUBIyJ2iJ2KMGGfIXufa57DBDuyzcwfkyeA6btC8mt+mokIeRap/gUhyraAg==", "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": "iLrfT53m4UBggK+ql3XR2FYQrPtuIxnfGe7jSLUd3skttpG6FNL9g+dEzbU4IPoc9wgE6kvxorjz7KejFq7WBw==" "python/iqpilot_private/konn3kt/uploaderd/iquploaderd.cpython-312-aarch64-linux-gnu.so": "YleZT+ZmBpdryTlsH4k+yoLYhyIJ1DMPYnEC8L2ixxJejAjcUBmn2GjXFSS+7JzK6g8aokV6NswVSqbuIVOJDQ=="
} }
} }

View File

@@ -6,7 +6,7 @@
}, },
"python/iqpilot_private/iqvd/__init__.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/iqvd/__init__.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "d36af86bf1fe721522e729b6a48e3a962ce033dcfe89f9e00e3d6208b1801591", "sha256": "48bad6268fabc23a9be62fcd7f61240ebcbabd5e3c6afcf7d3328bd33d93d3a2",
"size": 67616 "size": 67616
}, },
"python/iqpilot_private/iqvd/__init__.py": { "python/iqpilot_private/iqvd/__init__.py": {
@@ -16,12 +16,42 @@
}, },
"python/iqpilot_private/iqvd/iqvd.cpython-312-aarch64-linux-gnu.so": { "python/iqpilot_private/iqvd/iqvd.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "f81036080eb7bc0c91bae10b11f52064041311a5fc67e7dc0fba32f585318e41", "sha256": "674c4ecd09e36af6c2b9a41285e78914ee94a0e8c4de728904171dd59e02edda",
"size": 201584 "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": { "python/iqpilot_private/iqvd/yolov8_net.cpython-312-aarch64-linux-gnu.so": {
"mode": 493, "mode": 493,
"sha256": "976be8806f006a175a14b0c63e9a1c2c2b845ee2dab77a41eeb6759c7feab641", "sha256": "943e0ef218eed109289d208ea76407e223a4e8b81659f0ec79dc090fbd7ed329",
"size": 204568 "size": 204568
}, },
"runtime": { "runtime": {
@@ -33,8 +63,13 @@
} }
}, },
"signatures": { "signatures": {
"python/iqpilot_private/iqvd/__init__.cpython-312-aarch64-linux-gnu.so": "bMboPEsPzZjh0Y1Zk3Brvc6fyWFF5CxuzN9KQr2RK0Hn+l8XiD6l/ym2Y10nFxvj8+uLikIZ0xSAovJlJcRUAA==", "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": "seyE6m33mqlKZ4H7b1JL6pxxz+r/+dKP1PzcfXxhdhHvsl8Ka8Noi6+ln5z+QSvBgt5ulxQeBJMBP5qdu6lOCw==", "python/iqpilot_private/iqvd/iqvd.cpython-312-aarch64-linux-gnu.so": "H+2OPwV0XS7biX05P6fBzRBCN1Cjnq9eH6YSLk+9BX8OPbdiyJTbDpt8YZYk4nU0LO5FF4dMkEXzFRsewB7BAw==",
"python/iqpilot_private/iqvd/yolov8_net.cpython-312-aarch64-linux-gnu.so": "RvzkJLNxPA+u7yEnH5jd2jecT02PJCQkIDj/LiWvBCE4lFmFFzOf1Q+ZCGGjrFQzhLQZlm75SvW80mlqJ9BJCw==" "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=="
} }
} }

View File

@@ -333,6 +333,7 @@ struct IQCarParams @0xd4189b5c8aca9f78 {
iqLateralNet @4 :LateralNet; iqLateralNet @4 :LateralNet;
longitudinalStoppingSpeedOverride @5 :Float32; # m/s; zero keeps the upstream default longitudinalStoppingSpeedOverride @5 :Float32; # m/s; zero keeps the upstream default
stoppingDecelRateOverride @6 :Float32; # m/s^3; 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 { struct LateralNet {
fuzzyFingerprint @0 :Bool; 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 { struct CustomReserved14 @0xa6e5a1ce8ca5258e {

View File

@@ -177,6 +177,15 @@ struct InitData {
wallTimeNanos @20 :UInt64; wallTimeNanos @20 :UInt64;
ufsHealth @25 :UfsHealth;
struct UfsHealth {
preEolInfo @0 :UInt8;
lifeTimeEstimateA @1 :UInt8;
lifeTimeEstimateB @2 :UInt8;
vendorHealthReport @3 :Data;
}
enum DeviceType { enum DeviceType {
unknown @0; unknown @0;
neo @1; neo @1;
@@ -2670,7 +2679,7 @@ struct Event {
iqPerfTrace @136 :Custom.IQPerfTrace; iqPerfTrace @136 :Custom.IQPerfTrace;
iqConstructionZone @137 :Custom.IQConstructionZone; iqConstructionZone @137 :Custom.IQConstructionZone;
iqVehicleTracks @138 :Custom.IQVehicleTracks; iqVehicleTracks @138 :Custom.IQVehicleTracks;
customReserved13 @139 :Custom.CustomReserved13; iqEnvironment @139 :Custom.IQEnvironment;
customReserved14 @140 :Custom.CustomReserved14; customReserved14 @140 :Custom.CustomReserved14;
customReserved15 @141 :Custom.CustomReserved15; customReserved15 @141 :Custom.CustomReserved15;
customReserved16 @142 :Custom.CustomReserved16; customReserved16 @142 :Custom.CustomReserved16;

View File

@@ -100,6 +100,7 @@ _services: dict[str, tuple] = {
"iqLiveData": (True, 1., 1), "iqLiveData": (True, 1., 1),
"iqConstructionZone": (True, 2., 2), "iqConstructionZone": (True, 2., 2),
"iqVehicleTracks": (True, 4., 4), "iqVehicleTracks": (True, 4., 4),
"iqEnvironment": (True, 4., 4),
"mapdOut": (True, 20., 20, QueueSize.MEDIUM), "mapdOut": (True, 20., 20, QueueSize.MEDIUM),
"mapdExtendedOut": (False, 1., -1, QueueSize.MEDIUM), "mapdExtendedOut": (False, 1., -1, QueueSize.MEDIUM),
"mapdIn": (False, 1., -1, QueueSize.MEDIUM), "mapdIn": (False, 1., -1, QueueSize.MEDIUM),

View File

@@ -6,6 +6,7 @@ common_libs = [
'util.cc', 'util.cc',
'ratekeeper.cc', 'ratekeeper.cc',
'clutil.cc', 'clutil.cc',
'yuv.cc',
] ]
_common = env.Library('common', common_libs, LIBS="json11") _common = env.Library('common', common_libs, LIBS="json11")

View File

@@ -3,6 +3,7 @@ import os
import requests import requests
import unicodedata import unicodedata
from datetime import datetime, timedelta, UTC from datetime import datetime, timedelta, UTC
from functools import lru_cache
from openpilot.system.hardware.hw import Paths from openpilot.system.hardware.hw import Paths
from openpilot.system.version import get_version from openpilot.system.version import get_version
@@ -11,6 +12,16 @@ KEYS = {"id_rsa": "RS256",
"id_ecdsa": "ES256"} "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: class BaseApi:
def __init__(self, dongle_id, api_host, user_agent="openpilot-"): def __init__(self, dongle_id, api_host, user_agent="openpilot-"):
self.dongle_id = dongle_id self.dongle_id = dongle_id
@@ -38,7 +49,8 @@ class BaseApi:
} }
if payload_extra is not None: if payload_extra is not None:
payload.update(payload_extra) 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): if isinstance(token, bytes):
token = token.decode('utf8') token = token.decode('utf8')
return token return token

View File

@@ -21,6 +21,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"CalibrationParams", {PERSISTENT, BYTES}}, {"CalibrationParams", {PERSISTENT, BYTES}},
{"CameraDebugExpGain", {CLEAR_ON_MANAGER_START, STRING}}, {"CameraDebugExpGain", {CLEAR_ON_MANAGER_START, STRING}},
{"CameraDebugExpTime", {CLEAR_ON_MANAGER_START, STRING}}, {"CameraDebugExpTime", {CLEAR_ON_MANAGER_START, STRING}},
{"CanLiveStreaming", {CLEAR_ON_MANAGER_START, BOOL}},
{"CarBatteryCapacity", {PERSISTENT, INT}}, {"CarBatteryCapacity", {PERSISTENT, INT}},
{"CarParams", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BYTES}}, {"CarParams", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BYTES}},
{"CarParamsCache", {CLEAR_ON_MANAGER_START, BYTES}}, {"CarParamsCache", {CLEAR_ON_MANAGER_START, BYTES}},
@@ -199,6 +200,8 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"OSMapsHeadingUp", {PERSISTENT, BOOL, "1"}}, {"OSMapsHeadingUp", {PERSISTENT, BOOL, "1"}},
{"OfflineTilesBaseUrl", {PERSISTENT, STRING}}, {"OfflineTilesBaseUrl", {PERSISTENT, STRING}},
{"OnroadUploads", {PERSISTENT, BOOL, "1"}}, {"OnroadUploads", {PERSISTENT, BOOL, "1"}},
{"IQAutoUnits", {PERSISTENT, BOOL, "1"}},
{"IQAutoUnitsRegion", {PERSISTENT, STRING}},
{"IQAlertSilence", {PERSISTENT, BOOL, "0"}}, {"IQAlertSilence", {PERSISTENT, BOOL, "0"}},
{"IQAccelMeter", {PERSISTENT, BOOL, "0"}}, {"IQAccelMeter", {PERSISTENT, BOOL, "0"}},
{"IQBlinkerIndicators", {PERSISTENT, BOOL, "0"}}, {"IQBlinkerIndicators", {PERSISTENT, BOOL, "0"}},
@@ -349,6 +352,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ConstructionZoneAssist", {PERSISTENT, BOOL, "0"}}, {"ConstructionZoneAssist", {PERSISTENT, BOOL, "0"}},
{"VisionVehicleTracks", {PERSISTENT, BOOL, "0"}}, {"VisionVehicleTracks", {PERSISTENT, BOOL, "0"}},
{"AmbientTrackDots", {PERSISTENT, BOOL, "1"}}, {"AmbientTrackDots", {PERSISTENT, BOOL, "1"}},
{"EnvironmentView", {PERSISTENT, INT, "0"}},
{"ConstructionZoneSpeed", {PERSISTENT, INT, "60"}}, {"ConstructionZoneSpeed", {PERSISTENT, INT, "60"}},
{"ShowSpeedLimits", {PERSISTENT, BOOL, "0"}}, {"ShowSpeedLimits", {PERSISTENT, BOOL, "0"}},
{"SLCPolicy", {PERSISTENT, INT, "1"}}, {"SLCPolicy", {PERSISTENT, INT, "1"}},

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
common/yuv.cc Normal file
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
common/yuv.h Normal file
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);
}

View File

@@ -208,6 +208,7 @@ struct CarState {
blockPcmEnable @60 :Bool; # whether to allow PCM to enable this frame blockPcmEnable @60 :Bool; # whether to allow PCM to enable this frame
lateralAvailable @61 :Bool; # lateral control is available even if cruise is faulted lateralAvailable @61 :Bool; # lateral control is available even if cruise is faulted
cruiseFaultLateralMode @62 :Bool; # cruise is faulted but lateral control is still active 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; radarDisableFailed @66 :Bool;
# Physical vehicle odometer in kilometers. Zero means unavailable on this platform. # Physical vehicle odometer in kilometers. Zero means unavailable on this platform.
odometer @67 :Float64; odometer @67 :Float64;

View File

@@ -53,6 +53,7 @@ class IQCarParams:
enableGasInterceptor: bool = auto_field() enableGasInterceptor: bool = auto_field()
longitudinalStoppingSpeedOverride: float = auto_field() longitudinalStoppingSpeedOverride: float = auto_field()
stoppingDecelRateOverride: float = auto_field() stoppingDecelRateOverride: float = auto_field()
longActiveWithGasOverride: bool = auto_field()
iqLateralNet: 'IQCarParams.LateralNet' = field(default_factory=lambda: IQCarParams.LateralNet()) iqLateralNet: 'IQCarParams.LateralNet' = field(default_factory=lambda: IQCarParams.LateralNet())

View File

@@ -29,6 +29,8 @@ class CarState(CarStateBase, IQCarState):
self.hands_on_level = 0 self.hands_on_level = 0
self.acc_state_last = 0 self.acc_state_last = 0
self.das_accCancel = False
self.das_cancel_last = True
self.das_control = None self.das_control = None
self.das_body_controls_dat = b"" self.das_body_controls_dat = b""
self._odometer_store = vehicle_state.VehicleOdometerStore(CP, Params()) 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) 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) 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"] 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) 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") cruise_enabled = cruise_state in ("ENABLED", "STANDSTILL", "OVERRIDE", "PRE_FAULT", "PRE_CANCEL")
self.cruise_override = cruise_state in ("OVERRIDE") self.cruise_override = cruise_state in ("OVERRIDE")
self.update_summon_state(summon_state, cruise_enabled) 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 # Match panda safety cruise engaged logic
ret.cruiseState.enabled = cruise_enabled and not self.summon ret.cruiseState.enabled = cruise_enabled and not self.summon
if speed_units == "KPH": if speed_units == "KPH":

View File

@@ -44,6 +44,7 @@ FW_VERSIONS = {
b'TeMYG4_Legacy3Y_0.0.0 (6),Y4003.04.0', 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 (77),Y4003.05.4',
b'TeMYG4_Main_0.0.0 (78),Y4003.06.0', b'TeMYG4_Main_0.0.0 (78),Y4003.06.0',
b'TeMYG4_Main_0.0.0 (87),Y4003.09.3',
], ],
}, },
CAR.TESLA_MODEL_X: { CAR.TESLA_MODEL_X: {

View File

@@ -2,7 +2,7 @@ from iqdbc.car import Bus, get_safety_config, structs
from iqdbc.car.interfaces import CarInterfaceBase from iqdbc.car.interfaces import CarInterfaceBase
from iqdbc.car.tesla.carcontroller import CarController from iqdbc.car.tesla.carcontroller import CarController
from iqdbc.car.tesla.carstate import CarState 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.car.tesla.radar_interface import RadarInterface, RADAR_START_ADDR
from iqdbc.lvbs.car.tesla.values import TeslaFlagsIQ, TeslaSafetyFlagsIQ from iqdbc.lvbs.car.tesla.values import TeslaFlagsIQ, TeslaSafetyFlagsIQ
@@ -41,7 +41,7 @@ class CarInterface(CarInterfaceBase):
ret.openpilotLongitudinalControl = True ret.openpilotLongitudinalControl = True
ret.safetyConfigs[0].safetyParam |= TeslaSafetyFlags.LONG_CONTROL.value 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: if legacy_das:
ret.flags |= TeslaFlags.LEGACY_DAS_STEERING.value ret.flags |= TeslaFlags.LEGACY_DAS_STEERING.value
ret.safetyConfigs[0].safetyParam |= TeslaSafetyFlags.LEGACY_DAS_STEERING.value ret.safetyConfigs[0].safetyParam |= TeslaSafetyFlags.LEGACY_DAS_STEERING.value

View File

@@ -1,11 +1,110 @@
from collections import defaultdict
import pytest
from iqdbc.car import gen_empty_fingerprint, structs 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.interface import CarInterface
from iqdbc.car.tesla.teslacan import TeslaCAN from iqdbc.car.tesla.teslacan import TeslaCAN
from iqdbc.car.tesla.radar_interface import RADAR_START_ADDR from iqdbc.car.tesla.radar_interface import RADAR_START_ADDR
from iqdbc.car.tesla.carcontroller import CarController 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 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: class TestTeslaFingerprint:
def test_radar_detection(self): def test_radar_detection(self):

View File

@@ -1,10 +1,13 @@
import re
from collections import defaultdict
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum, IntFlag 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 import ACCELERATION_DUE_TO_GRAVITY, Bus, CarSpecs, DbcDict, PlatformConfig, Platforms
from iqdbc.car.lateral import AngleSteeringLimits, ISO_LATERAL_ACCEL from iqdbc.car.lateral import AngleSteeringLimits, ISO_LATERAL_ACCEL
from iqdbc.car.structs import CarParams, CarState from iqdbc.car.structs import CarParams, CarState
from iqdbc.car.docs_definitions import CarDocs, CarFootnote, CarHarness, CarParts, Column, SupportType 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 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 # Cars with this EPS FW have a 2-bit DAS_steeringControlType and use TeslaFlags.LEGACY_DAS_STEERING
LEGACY_DAS_STEERING_FW = { LEGACY_DAS_STEERING_FW = {
CAR.TESLA_MODEL_3: [ 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: class CANBUS:
party = 0 party = 0

View File

@@ -381,6 +381,9 @@ class CarState(CarStateBase):
ret.cruiseState.available = pt_cp.vl["Motor_51"]["TSK_Status"] in (2, 3, 4, 5) 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) 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", {})) 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"]) 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.cruiseFaultLateralMode = allow_lat_only and cruise_faulted and cruise_main_available
ret.lateralAvailable = ret.cruiseState.available or ret.cruiseFaultLateralMode ret.lateralAvailable = ret.cruiseState.available or ret.cruiseFaultLateralMode
ret.blockPcmEnable = 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 # Update ACC setpoint. When the setpoint reads as 255, the driver has not
# yet established an ACC setpoint, so treat it as zero. # yet established an ACC setpoint, so treat it as zero.

View File

@@ -4,7 +4,9 @@ from pathlib import Path
env = Environment(ENV=os.environ) env = Environment(ENV=os.environ)
# short colored build output, if the top-level pretty tool is present (main-repo build) # 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(): if _pretty.exists():
env.Tool('pretty', toolpath=[_pretty.dir.abspath]) env.Tool('pretty', toolpath=[_pretty.dir.abspath])
if not hasattr(env, 'PrettyAction'): if not hasattr(env, 'PrettyAction'):

View File

@@ -26,7 +26,9 @@ env = Environment(
) )
# short colored build output, if the top-level pretty tool is present (main-repo build) # 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(): if _pretty.exists():
env.Tool('pretty', toolpath=[_pretty.dir.abspath]) env.Tool('pretty', toolpath=[_pretty.dir.abspath])

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'}")

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

View File

@@ -4,27 +4,22 @@ EARTH_RADIUS = 6378137
# Mapbox API limits # Mapbox API limits
FREE_MAPBOX_REQUESTS = 100_000 FREE_MAPBOX_REQUESTS = 100_000
# Speed limit offset maps for different unit systems # Speed limit offset zones for different unit systems
# Each entry is (min_speed_ms, max_speed_ms, param_name) # 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 = [ OFFSET_MAP_IMPERIAL = [
(0, 11.2, "speed_limit_offset1"), # 0-24 mph (0, 8.94, "speed_limit_offset1"), # 0-20 mph
(11.2, 15.2, "speed_limit_offset2"), # 25-34 mph (8.94, 17.88, "speed_limit_offset2"), # 20-40 mph
(15.2, 19.6, "speed_limit_offset3"), # 35-44 mph (17.88, float("inf"), "speed_limit_offset3"), # 40+ 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
] ]
OFFSET_MAP_METRIC = [ OFFSET_MAP_METRIC = [
(0, 8.1, "speed_limit_offset1"), # 0-29 km/h (0, 8.33, "speed_limit_offset1"), # 0-30 km/h
(8.1, 13.6, "speed_limit_offset2"), # 30-49 km/h (8.33, 16.67, "speed_limit_offset2"), # 30-60 km/h
(13.6, 16.4, "speed_limit_offset3"), # 50-59 km/h (16.67, float("inf"), "speed_limit_offset3"), # 60+ 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
] ]
# Speed limit filler constants # Speed limit filler constants

View File

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()

View File

@@ -17,7 +17,7 @@ from openpilot.common.realtime import DT_MDL
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
from openpilot.iqpilot.common.k3_slc_log import k3_slc_log 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_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: try:
import requests import requests
@@ -354,6 +354,8 @@ class SpeedLimitController:
pass pass
self.executor = ThreadPoolExecutor(max_workers=1) 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_log_t = 0.0
self._last_mapbox_diag_t = 0.0 self._last_mapbox_diag_t = 0.0
self._last_mapbox_diag_message = None self._last_mapbox_diag_message = None
@@ -416,17 +418,33 @@ class SpeedLimitController:
return self._assist.output_a_target return self._assist.output_a_target
def get_offset(self, is_metric): 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 offset_map = OFFSET_MAP_METRIC if is_metric else OFFSET_MAP_IMPERIAL
for low, high, offset_param in offset_map: for low, high, offset_param in offset_map:
if low < self._assist.target < high: if low <= target < high:
offset_value = self.params.get(offset_param) percent = float(np.clip(self._get_offset_percent(offset_param), -OFFSET_PERCENT_MAX, OFFSET_PERCENT_MAX))
if offset_value is not None: return target * percent / 100.0
if isinstance(offset_value, bytes):
return float(offset_value.decode("utf-8"))
return float(offset_value)
return 0.0
return 0.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 @staticmethod
def _is_alive(sm, key): def _is_alive(sm, key):
if hasattr(sm, "alive"): if hasattr(sm, "alive"):

View File

@@ -2,9 +2,11 @@
Copyright © IQ.Lvbs, apart of Project Teal Lvbs, All Rights Reserved, licensed under https://konn3kt.com/tos 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 types import SimpleNamespace
from openpilot.common.constants import CV 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.slc_vcruise import SLCVCruise, CRUISING_SPEED
from openpilot.iqpilot.selfdrive.controls.lib.speed_limit_controller import SpeedLimitController, POLICY_MAP_DATA_PRIORITY, POLICY_COMBINED 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 = _base_slc_params_controller()
slc_params["slc_policy"] = POLICY_MAP_DATA_PRIORITY 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_source == "Map Data"
assert controller.active_target == 18.0 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 = _base_slc_params_controller()
slc_params["slc_policy"] = POLICY_COMBINED 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_source == "Map Data"
assert controller.active_target == 16.0 assert controller.active_target == 16.0
@@ -128,6 +130,7 @@ def test_slc_vcruise_applies_target_without_increasing_cruise():
slc._get_slc_params = lambda: { slc._get_slc_params = lambda: {
"speed_limit_controller": True, "speed_limit_controller": True,
"speed_limit_mode": 3,
"show_speed_limits": False, "show_speed_limits": False,
"is_metric": True, "is_metric": True,
"slc_policy": POLICY_MAP_DATA_PRIORITY, "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.slc.active_source = "Map Data"
slc._get_slc_params = lambda: { slc._get_slc_params = lambda: {
"speed_limit_controller": False, "speed_limit_controller": False,
"speed_limit_mode": 1,
"show_speed_limits": True, "show_speed_limits": True,
"is_metric": True, "is_metric": True,
"slc_policy": POLICY_MAP_DATA_PRIORITY, "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: { slc._get_slc_params = lambda: {
"speed_limit_controller": True, "speed_limit_controller": True,
"speed_limit_mode": 3,
"show_speed_limits": False, "show_speed_limits": False,
"is_metric": True, "is_metric": True,
"slc_policy": POLICY_MAP_DATA_PRIORITY, "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: { slc._get_slc_params = lambda: {
"speed_limit_controller": True, "speed_limit_controller": True,
"speed_limit_mode": 3,
"show_speed_limits": False, "show_speed_limits": False,
"is_metric": True, "is_metric": True,
"slc_policy": POLICY_MAP_DATA_PRIORITY, "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 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(): def test_construction_zone_fires_event_once_per_zone_entry():
from cereal import custom from cereal import custom
event = custom.IQOnroadEvent.EventName.constructionZoneDetected event = custom.IQOnroadEvent.EventName.constructionZoneDetected

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

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)

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 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_available = has_limit
self.limit_speed_text = str(round(self.speed_limit_renderer.speed_limit_last)) if has_limit else "---" 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.turn_signal_controller.update()
self.speed_renderer.update() self.speed_renderer.update()
self.soft_warning_renderer.update() self.soft_warning_renderer.update()

View File

@@ -128,6 +128,16 @@ function launch {
fi fi
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. # Install/update proprietary runtime bundles.
if [ -f "$DIR/artifacts/runtime/ensure_private_installed.sh" ]; then if [ -f "$DIR/artifacts/runtime/ensure_private_installed.sh" ]; then
bash "$DIR/artifacts/runtime/ensure_private_installed.sh" || true bash "$DIR/artifacts/runtime/ensure_private_installed.sh" || true
@@ -161,10 +171,12 @@ function launch {
sudo mount -o remount,ro / sudo mount -o remount,ro /
fi fi
sudo systemctl enable "${service_name}.service" 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 if systemctl is-active --quiet "${service_name}.service"; then
sudo systemctl restart "${service_name}.service" sudo systemctl restart --no-block "${service_name}.service"
else else
sudo systemctl start "${service_name}.service" sudo systemctl start --no-block "${service_name}.service"
fi fi
elif [ -f "$service_src" ]; then elif [ -f "$service_src" ]; then
if [ ! -f "$service_dst" ] || ! cmp -s "$service_src" "$service_dst"; then if [ ! -f "$service_dst" ] || ! cmp -s "$service_src" "$service_dst"; then
@@ -173,9 +185,9 @@ function launch {
sudo systemctl daemon-reload sudo systemctl daemon-reload
sudo systemctl enable "${service_name}.service" sudo systemctl enable "${service_name}.service"
sudo mount -o remount,ro / 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 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
fi fi
done done

View File

@@ -23,7 +23,7 @@ if [ -z "$AGNOS_VERSION" ]; then
case "$DEVICE_MODEL" in case "$DEVICE_MODEL" in
*) *)
export AGNOS_VERSION="IQ.OS 4.9" export AGNOS_VERSION="IQ.OS 4.9.3"
;; ;;
esac esac
fi fi

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) # 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(): if _pretty.exists():
env.Tool('pretty', toolpath=[_pretty.dir.abspath]) env.Tool('pretty', toolpath=[_pretty.dir.abspath])
if not hasattr(env, 'PrettyAction'): if not hasattr(env, 'PrettyAction'):

View File

@@ -74,6 +74,10 @@ void init_interrupts(bool check_rate_limit){
for(uint16_t i=0U; i<NUM_INTERRUPTS; i++){ for(uint16_t i=0U; i<NUM_INTERRUPTS; i++){
interrupts[i].handler = unused_interrupt_handler; 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 // Init interrupt timer for a 1s interval

View File

@@ -1,5 +1,12 @@
#pragma once #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 { typedef struct interrupt {
IRQn_Type irq_type; IRQn_Type irq_type;
void (*handler)(void); void (*handler)(void);

View File

@@ -101,6 +101,12 @@ void llspi_init(void) {
register_set(&(SPI4->CR1), SPI_CR1_SPE, 0xFFFFU); register_set(&(SPI4->CR1), SPI_CR1_SPE, 0xFFFFU);
register_set(&(SPI4->CR2), 0, 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_Stream2_IRQn);
NVIC_EnableIRQ(DMA2_Stream3_IRQn); NVIC_EnableIRQ(DMA2_Stream3_IRQn);
NVIC_EnableIRQ(SPI4_IRQn); NVIC_EnableIRQ(SPI4_IRQn);

View File

@@ -27,7 +27,9 @@ if system == "Darwin":
env.PrependENVPath('PATH', '/opt/homebrew/bin') env.PrependENVPath('PATH', '/opt/homebrew/bin')
# short colored build output, if the top-level pretty tool is present (main-repo build) # 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(): if _pretty.exists():
env.Tool('pretty', toolpath=[_pretty.dir.abspath]) env.Tool('pretty', toolpath=[_pretty.dir.abspath])

View File

@@ -168,8 +168,11 @@ class Controls(IQControlsLayer):
CC.latActive = _lat_active and not CS.steerFaultTemporary and not CS.steerFaultPermanent and \ CC.latActive = _lat_active and not CS.steerFaultTemporary and not CS.steerFaultPermanent and \
(not standstill or self.CP.steerAtStandstill) (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 \ 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) (self.CP.openpilotLongitudinalControl or not self.CP_IQ.pcmCruiseSpeed)
actuators = CC.actuators actuators = CC.actuators
@@ -184,7 +187,7 @@ class Controls(IQControlsLayer):
# accel PID loop # 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) 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, 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 # Steering PID loop and lateral MPC
# Reset desired curvature to current to avoid violating the limits on engage # Reset desired curvature to current to avoid violating the limits on engage

View File

@@ -53,7 +53,7 @@ class LongControl:
def reset(self): def reset(self):
self.pid.reset() 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""" """Update longitudinal control. This updates the state machine and runs a PID loop"""
self.pid.neg_limit = accel_limits[0] self.pid.neg_limit = accel_limits[0]
self.pid.pos_limit = accel_limits[1] self.pid.pos_limit = accel_limits[1]
@@ -87,8 +87,13 @@ class LongControl:
else: else:
error = a_target - CS.aEgo error = a_target - CS.aEgo
output_accel = self.pid.update(error, speed=CS.vEgo, output_accel = self.pid.update(error, speed=CS.vEgo,
feedforward=a_target) feedforward=a_target,
freeze_integrator=gas_override)
self.smooth.reset() 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]) self.last_output_accel = np.clip(output_accel, accel_limits[0], accel_limits[1])
return self.last_output_accel return self.last_output_accel

View File

@@ -26,7 +26,8 @@ class ExcessiveActuationCheck:
# CS.aEgo can be noisy to bumps in the road, transitioning from standstill, losing traction, etc. # CS.aEgo can be noisy to bumps in the road, transitioning from standstill, losing traction, etc.
# longitudinal # longitudinal
accel_calibrated = calibrated_pose.acceleration.x 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 # lateral
yaw_rate = calibrated_pose.angular_velocity.yaw yaw_rate = calibrated_pose.angular_velocity.yaw

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."), "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."), '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."), "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."), "RecordAudio": tr_noop("Record and store microphone audio while driving. The audio will be included in the dashcam video in Konn3kt."),
"LongitudinalControlMode": tr_noop( "LongitudinalControlMode": tr_noop(
"Choose longitudinal behavior: IQ.Pilot (IQ longitudinal + end-to-end), " "Choose longitudinal behavior: IQ.Pilot (IQ longitudinal + end-to-end), "
@@ -95,6 +99,12 @@ class TogglesLayout(Widget):
"metric.png", "metric.png",
False, False,
), ),
"IQAutoUnits": (
lambda: tr("Set Units From Location"),
DESCRIPTIONS["IQAutoUnits"],
"metric.png",
False,
),
} }
self._long_personality_setting = multiple_button_item( self._long_personality_setting = multiple_button_item(
@@ -296,6 +306,8 @@ class TogglesLayout(Widget):
def _toggle_callback(self, state: bool, param: str): def _toggle_callback(self, state: bool, param: str):
self._params.put_bool(param, state) self._params.put_bool(param, state)
if param == "IQAutoUnits" and state:
self._params.remove("IQAutoUnitsRegion")
if self._toggle_defs[param][3]: if self._toggle_defs[param][3]:
self._params.put_bool("OnroadCycleRequested", True) self._params.put_bool("OnroadCycleRequested", True)

View File

@@ -18,13 +18,15 @@ class TogglesLayoutMici(NavScroller):
disengage = BigParamControl("disengage on accelerator", "DisengageOnAccelerator") disengage = BigParamControl("disengage on accelerator", "DisengageOnAccelerator")
ldw = BigParamControl("lane departure warnings", "IsLdwEnabled") ldw = BigParamControl("lane departure warnings", "IsLdwEnabled")
is_metric = BigParamControl("use metric units", "IsMetric") 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 = ( self._refresh_toggles = (
("DisengageOnAccelerator", disengage), ("DisengageOnAccelerator", disengage),
("IsLdwEnabled", ldw), ("IsLdwEnabled", ldw),
("IsMetric", is_metric), ("IsMetric", is_metric),
("IQAutoUnits", auto_units),
) )
if ui_state.params.get_bool("ShowDebugInfo"): if ui_state.params.get_bool("ShowDebugInfo"):
@@ -35,6 +37,10 @@ class TogglesLayoutMici(NavScroller):
super().show_event() super().show_event()
self._update_toggles() self._update_toggles()
def _auto_units_callback(self, state: bool):
if state:
ui_state.params.remove("IQAutoUnitsRegion")
def _update_toggles(self): def _update_toggles(self):
ui_state.update_params() ui_state.update_params()
for key, item in self._refresh_toggles: for key, item in self._refresh_toggles:

View File

@@ -4,7 +4,7 @@ import pyray as rl
from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf
from openpilot.common.swaglog import cloudlog 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.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.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
@@ -38,7 +38,7 @@ void main() {
""" """
# Choose fragment shader based on platform capabilities # Choose fragment shader based on platform capabilities
if TICI: if EGL_DMA_BUF_SUPPORTED:
FRAME_FRAGMENT_SHADER = """ FRAME_FRAGMENT_SHADER = """
#version 300 es #version 300 es
#extension GL_OES_EGL_image_external_essl3 : enable #extension GL_OES_EGL_image_external_essl3 : enable
@@ -121,7 +121,7 @@ class CameraView(Widget):
self._texture_needs_update = True self._texture_needs_update = True
self.last_connection_attempt: float = 0.0 self.last_connection_attempt: float = 0.0
self.shader = rl.load_shader_from_memory(VERTEX_SHADER, FRAME_FRAGMENT_SHADER) 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_loc = rl.get_shader_location(self.shader, "engaged")
self._engaged_val = rl.ffi.new("int[1]", [1]) self._engaged_val = rl.ffi.new("int[1]", [1])
self._enhance_driver_loc = rl.get_shader_location(self.shader, "enhance_driver") 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 self._placeholder_color: rl.Color | None = None
# Initialize EGL for zero-copy rendering on TICI # Initialize EGL for zero-copy rendering on comma 3/3X.
if TICI: if EGL_DMA_BUF_SUPPORTED:
if not init_egl(): if not init_egl():
raise RuntimeError("Failed to initialize EGL") raise RuntimeError("Failed to initialize EGL")
@@ -189,7 +189,7 @@ class CameraView(Widget):
self._clear_textures() self._clear_textures()
# Clean up EGL texture # 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) rl.unload_texture(self.egl_texture)
self.egl_texture = None self.egl_texture = None
@@ -264,7 +264,7 @@ class CameraView(Widget):
dst_rect = rl.Rectangle(x_offset, y_offset, scale_x, scale_y) dst_rect = rl.Rectangle(x_offset, y_offset, scale_x, scale_y)
# Render with appropriate method # Render with appropriate method
if TICI: if EGL_DMA_BUF_SUPPORTED:
self._render_egl(src_rect, dst_rect) self._render_egl(src_rect, dst_rect)
else: else:
self._render_textures(src_rect, dst_rect) self._render_textures(src_rect, dst_rect)
@@ -387,12 +387,12 @@ class CameraView(Widget):
self._initialize_textures() self._initialize_textures()
def _initialize_textures(self): def _initialize_textures(self):
self._clear_textures() 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), 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)) 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), 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)) int(self.client.height // 2), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAY_ALPHA))
def _clear_textures(self): def _clear_textures(self):
if self.texture_y and self.texture_y.id: if self.texture_y and self.texture_y.id:
@@ -404,7 +404,7 @@ class CameraView(Widget):
self.texture_uv = None self.texture_uv = None
# Clean up EGL resources # Clean up EGL resources
if TICI: if EGL_DMA_BUF_SUPPORTED:
for data in self.egl_images.values(): for data in self.egl_images.values():
destroy_egl_image(data) destroy_egl_image(data)
self.egl_images = {} self.egl_images = {}

View File

@@ -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.driver_state import DriverStateRenderer as BaseDriverStateRenderer, BTN_SIZE
from openpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer as BaseHudRenderer 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.model_renderer import ModelRenderer
from openpilot.selfdrive.ui.onroad.environment_renderer import EnvironmentRenderer
from openpilot.selfdrive.ui.onroad.cameraview import CameraView from openpilot.selfdrive.ui.onroad.cameraview import CameraView
from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app
from openpilot.common.issue_debug import log_issue_limited from openpilot.common.issue_debug import log_issue_limited
@@ -55,6 +56,7 @@ class AugmentedRoadView(CameraView, AugmentedRoadViewIQ):
self._split_nav_available = False self._split_nav_available = False
self.model_renderer = ModelRenderer() self.model_renderer = ModelRenderer()
self.environment_renderer = EnvironmentRenderer()
self.alert_renderer = AlertRenderer() self.alert_renderer = AlertRenderer()
self._hud_renderer = IQHudRenderer() self._hud_renderer = IQHudRenderer()
self.driver_state_renderer = DriverStateRendererIQ() self.driver_state_renderer = DriverStateRendererIQ()
@@ -114,6 +116,7 @@ class AugmentedRoadView(CameraView, AugmentedRoadViewIQ):
# Draw all UI overlays # Draw all UI overlays
self.model_renderer.render(camera_rect) self.model_renderer.render(camera_rect)
self.environment_renderer.render(camera_rect)
AugmentedRoadViewIQ.update_fade_out_bottom_overlay(self, camera_rect) AugmentedRoadViewIQ.update_fade_out_bottom_overlay(self, camera_rect)
self._hud_renderer.render(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_transform(video_transform @ calib_transform)
self.model_renderer.set_frame_transform(video_transform, is_wide_camera) self.model_renderer.set_frame_transform(video_transform, is_wide_camera)
self.environment_renderer.set_transform(video_transform @ calib_transform)
return self._cached_matrix return self._cached_matrix

View File

@@ -4,7 +4,7 @@ import pyray as rl
from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf
from openpilot.common.swaglog import cloudlog 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.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.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage
from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets import Widget
@@ -38,7 +38,7 @@ void main() {
""" """
# Choose fragment shader based on platform capabilities # Choose fragment shader based on platform capabilities
if TICI: if EGL_DMA_BUF_SUPPORTED:
FRAME_FRAGMENT_SHADER = """ FRAME_FRAGMENT_SHADER = """
#version 300 es #version 300 es
#extension GL_OES_EGL_image_external_essl3 : enable #extension GL_OES_EGL_image_external_essl3 : enable
@@ -82,7 +82,7 @@ class CameraView(Widget):
self._texture_needs_update = True self._texture_needs_update = True
self.last_connection_attempt: float = 0.0 self.last_connection_attempt: float = 0.0
self.shader = rl.load_shader_from_memory(VERTEX_SHADER, FRAME_FRAGMENT_SHADER) 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.frame: VisionBuf | None = None
self.texture_y: rl.Texture | None = None self.texture_y: rl.Texture | None = None
@@ -94,8 +94,8 @@ class CameraView(Widget):
self._placeholder_color: rl.Color | None = None self._placeholder_color: rl.Color | None = None
# Initialize EGL for zero-copy rendering on TICI # Initialize EGL for zero-copy rendering on comma 3/3X.
if TICI: if EGL_DMA_BUF_SUPPORTED:
if not init_egl(): if not init_egl():
raise RuntimeError("Failed to initialize EGL") raise RuntimeError("Failed to initialize EGL")
@@ -146,7 +146,7 @@ class CameraView(Widget):
self._clear_textures() self._clear_textures()
# Clean up EGL texture # 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) rl.unload_texture(self.egl_texture)
self.egl_texture = None self.egl_texture = None
@@ -220,7 +220,7 @@ class CameraView(Widget):
dst_rect = rl.Rectangle(x_offset, y_offset, scale_x, scale_y) dst_rect = rl.Rectangle(x_offset, y_offset, scale_x, scale_y)
# Render with appropriate method # Render with appropriate method
if TICI: if EGL_DMA_BUF_SUPPORTED:
self._render_egl(src_rect, dst_rect) self._render_egl(src_rect, dst_rect)
else: else:
self._render_textures(src_rect, dst_rect) self._render_textures(src_rect, dst_rect)
@@ -337,7 +337,7 @@ class CameraView(Widget):
def _initialize_textures(self): def _initialize_textures(self):
self._clear_textures() 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), 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)) 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), 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 self.texture_uv = None
# Clean up EGL resources # Clean up EGL resources
if TICI: if EGL_DMA_BUF_SUPPORTED:
for data in self.egl_images.values(): for data in self.egl_images.values():
destroy_egl_image(data) destroy_egl_image(data)
self.egl_images = {} self.egl_images = {}

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))

View File

@@ -32,6 +32,7 @@ class FontSizes:
max_speed: int = 28 max_speed: int = 28
set_speed: int = 74 set_speed: int = 74
limit_speed: int = 64 limit_speed: int = 64
limit_offset: int = 30
limit_unit: int = 22 limit_unit: int = 22
limit_label: int = 24 limit_label: int = 24
@@ -70,6 +71,7 @@ class HudRenderer(Widget):
self.speed: float = 0.0 self.speed: float = 0.0
self.v_ego_cluster_seen: bool = False self.v_ego_cluster_seen: bool = False
self.limit_speed_text: str = "---" self.limit_speed_text: str = "---"
self.limit_offset_text: str = ""
self.limit_available: bool = False self.limit_available: bool = False
self._font_semi_bold: rl.Font = gui_app.font(FontWeight.SEMI_BOLD) self._font_semi_bold: rl.Font = gui_app.font(FontWeight.SEMI_BOLD)
@@ -168,14 +170,28 @@ class HudRenderer(Widget):
else: else:
limit_value_size = 48 limit_value_size = 48
limit_value_width = measure_text_cached(self._font_bold, limit_value_text, limit_value_size).x 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( rl.draw_text_ex(
self._font_bold, self._font_bold,
limit_value_text, 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, limit_value_size,
0, 0,
limit_value_color, 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: if self.limit_available:
limit_unit_text = tr("LIMIT") limit_unit_text = tr("LIMIT")

View File

@@ -224,6 +224,17 @@ class Soundd(AlertSoundFilter):
sm = messaging.SubMaster(['selfdriveState', 'soundPressure']) sm = messaging.SubMaster(['selfdriveState', 'soundPressure'])
threading.Thread(target=self.webrtc_audio_thread, daemon=True).start() 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: with self.get_stream(sd) as stream:
rk = Ratekeeper(20) rk = Ratekeeper(20)

View File

@@ -8,6 +8,7 @@ from cereal import messaging, car, log, custom
from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog 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.selfdrive.ui.lib.prime_state import PrimeState
from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.application import gui_app
from openpilot.system.hardware import HARDWARE, PC from openpilot.system.hardware import HARDWARE, PC
@@ -42,12 +43,13 @@ class IQUIState:
self.update_params() self.update_params()
self.auto_units = AutoUnits(self.params)
self.onroad_brightness_timer: int = 0 self.onroad_brightness_timer: int = 0
self.custom_interactive_timeout: int = self.params.get("InteractivityTimeout", return_default=True) self.custom_interactive_timeout: int = self.params.get("InteractivityTimeout", return_default=True)
self.reset_onroad_sleep_timer() self.reset_onroad_sleep_timer()
def update(self) -> None: def update(self) -> None:
pass self.auto_units.update()
def onroad_brightness_handle_alerts(self, started: bool, alert): 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 # 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", "radarState",
"liveTracks", "liveTracks",
"iqVehicleTracks", "iqVehicleTracks",
"iqEnvironment",
"deviceState", "deviceState",
"pandaStates", "pandaStates",
"carParams", "carParams",

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 = CAM_ICP_CMD_GENERIC_BLOB_CLK;
tmp.header |= (sizeof(cam_icp_clk_bw_request)) << 8; tmp.header |= (sizeof(cam_icp_clk_bw_request)) << 8;
tmp.clk.budget_ns = 0x1fca058; 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.rt_flag = 0x0;
tmp.clk.uncompressed_bw = 0x38512180; tmp.clk.uncompressed_bw = 0x38512180;
tmp.clk.compressed_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 = CAM_ICP_CMD_GENERIC_BLOB_CLK;
tmp.header |= (sizeof(cam_icp_clk_bw_request)) << 8; tmp.header |= (sizeof(cam_icp_clk_bw_request)) << 8;
tmp.clk.budget_ns = 0x1fca058; 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.rt_flag = 0x0;
tmp.clk.uncompressed_bw = 0x38512180; tmp.clk.uncompressed_bw = 0x38512180;
tmp.clk.compressed_bw = 0x38512180; tmp.clk.compressed_bw = 0x38512180;

View File

@@ -14,3 +14,9 @@ if TICI:
HARDWARE = cast(HardwareBase, Tici()) HARDWARE = cast(HardwareBase, Tici())
else: else:
HARDWARE = cast(HardwareBase, Pc()) 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")

View File

@@ -1,15 +1,25 @@
#pragma once #pragma once
#include <cstdint>
#include <cstdlib> #include <cstdlib>
#include <fstream> #include <fstream>
#include <map> #include <map>
#include <optional>
#include <string> #include <string>
#include <vector>
#include "cereal/gen/cpp/log.capnp.h" #include "cereal/gen/cpp/log.capnp.h"
// no-op base hw class // no-op base hw class
class HardwareNone { class HardwareNone {
public: 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 std::string get_name() { return ""; }
static cereal::InitData::DeviceType get_device_type() { return cereal::InitData::DeviceType::UNKNOWN; } static cereal::InitData::DeviceType get_device_type() { return cereal::InitData::DeviceType::UNKNOWN; }
static int get_voltage() { return 0; } static int get_voltage() { return 0; }
@@ -21,6 +31,8 @@ public:
return {}; return {};
} }
static std::optional<UfsHealth> get_ufs_health() { return std::nullopt; }
static void set_ir_power(int percentage) {} static void set_ir_power(int percentage) {}
static bool PC() { return false; } static bool PC() { return false; }

View File

@@ -67,28 +67,28 @@
}, },
{ {
"name": "boot", "name": "boot",
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/boot-179865564b1c196897c77edc41d99a8d4831a3a95564cac6868e7942cd11fd25.img.xz", "url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/boot-50b102d3dfa5d0eea506d9586745470cd59bcea9a723408a6abc20ffd6493a89.img.xz",
"hash": "179865564b1c196897c77edc41d99a8d4831a3a95564cac6868e7942cd11fd25", "hash": "50b102d3dfa5d0eea506d9586745470cd59bcea9a723408a6abc20ffd6493a89",
"hash_raw": "179865564b1c196897c77edc41d99a8d4831a3a95564cac6868e7942cd11fd25", "hash_raw": "50b102d3dfa5d0eea506d9586745470cd59bcea9a723408a6abc20ffd6493a89",
"size": 18216960, "size": 18216960,
"sparse": false, "sparse": false,
"full_check": true, "full_check": true,
"has_ab": true, "has_ab": true,
"ondevice_hash": "2c969938fdd59528e6244820b77ec6b2cef9ab49cc9fedd490b0b3d195c189e4" "ondevice_hash": "fdcb135e8412a896c4a882738a7022f6b7779939d8716f24b9a566b0b3ff3757"
}, },
{ {
"name": "system", "name": "system",
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a.img.xz", "url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-a473962357e3c0c7639af17006def4567f02e64d578b00e2e0037012755be5cd.img.xz",
"hash": "0c9e7dee6c7365600c77b33c4996456459d04aad005e3e41bf6ee2e8af74ceb9", "hash": "62a08bdf79f6dc0075eba8b93b55cbac6eeee0ecd2b6e8ab05d51ef61a9a880a",
"hash_raw": "03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a", "hash_raw": "a473962357e3c0c7639af17006def4567f02e64d578b00e2e0037012755be5cd",
"size": 6291456000, "size": 6291456000,
"sparse": true, "sparse": true,
"full_check": false, "full_check": false,
"has_ab": true, "has_ab": true,
"ondevice_hash": "1ee589d3d728a03561718383bf640171f1aef048573e46fe17239617c7c19fff", "ondevice_hash": "9ed5bc147a6d4c2e0a89562f31096a7802485ee8e740defeab7674abbf6d4baa",
"alt": { "alt": {
"hash": "03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a", "hash": "a473962357e3c0c7639af17006def4567f02e64d578b00e2e0037012755be5cd",
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a.img", "url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-a473962357e3c0c7639af17006def4567f02e64d578b00e2e0037012755be5cd.img",
"size": 6291456000 "size": 6291456000
} }
} }

View File

@@ -1 +1 @@
+LyP/p4uBBxbTwRwPrwR/2qPaHkAXmlFkg0GdxOBuujPY89GxP4t5SNKxirqiw9LmMcfA0JfVwMeR3QD9rQ6Dg== LxtrHkl5DFbSWnd1DZCkinmXiKFUVUausf0oYpa97dyAp3QGpWx7wrEAUZuJbrERQtdIV72ZU2eKOaQwGMBQBg==

View File

@@ -56,28 +56,28 @@
}, },
{ {
"name": "boot", "name": "boot",
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/boot-179865564b1c196897c77edc41d99a8d4831a3a95564cac6868e7942cd11fd25.img.xz", "url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/boot-50b102d3dfa5d0eea506d9586745470cd59bcea9a723408a6abc20ffd6493a89.img.xz",
"hash": "179865564b1c196897c77edc41d99a8d4831a3a95564cac6868e7942cd11fd25", "hash": "50b102d3dfa5d0eea506d9586745470cd59bcea9a723408a6abc20ffd6493a89",
"hash_raw": "179865564b1c196897c77edc41d99a8d4831a3a95564cac6868e7942cd11fd25", "hash_raw": "50b102d3dfa5d0eea506d9586745470cd59bcea9a723408a6abc20ffd6493a89",
"size": 18216960, "size": 18216960,
"sparse": false, "sparse": false,
"full_check": true, "full_check": true,
"has_ab": true, "has_ab": true,
"ondevice_hash": "2c969938fdd59528e6244820b77ec6b2cef9ab49cc9fedd490b0b3d195c189e4" "ondevice_hash": "fdcb135e8412a896c4a882738a7022f6b7779939d8716f24b9a566b0b3ff3757"
}, },
{ {
"name": "system", "name": "system",
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a.img.xz", "url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-a473962357e3c0c7639af17006def4567f02e64d578b00e2e0037012755be5cd.img.xz",
"hash": "0c9e7dee6c7365600c77b33c4996456459d04aad005e3e41bf6ee2e8af74ceb9", "hash": "62a08bdf79f6dc0075eba8b93b55cbac6eeee0ecd2b6e8ab05d51ef61a9a880a",
"hash_raw": "03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a", "hash_raw": "a473962357e3c0c7639af17006def4567f02e64d578b00e2e0037012755be5cd",
"size": 6291456000, "size": 6291456000,
"sparse": true, "sparse": true,
"full_check": false, "full_check": false,
"has_ab": true, "has_ab": true,
"ondevice_hash": "1ee589d3d728a03561718383bf640171f1aef048573e46fe17239617c7c19fff", "ondevice_hash": "9ed5bc147a6d4c2e0a89562f31096a7802485ee8e740defeab7674abbf6d4baa",
"alt": { "alt": {
"hash": "03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a", "hash": "a473962357e3c0c7639af17006def4567f02e64d578b00e2e0037012755be5cd",
"url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-03397de3da2e9a6d2808b95b72c945b3af71ab79f7f0298624aed62b8749fc5a.img", "url": "https://sdn.konn3kt.com/agnos/16-iqlvbs/system-a473962357e3c0c7639af17006def4567f02e64d578b00e2e0037012755be5cd.img",
"size": 6291456000 "size": 6291456000
} }
} }

View File

@@ -1 +1 @@
RCYk34Ifjx7S9PIhPm6p49vFMhamPwvw8Lfbb6a6HO8sUOi3J2PcY4VDUcqQZWU6+Am++HP3nJSea8OmAsckAg== mKCTfzNHr1d0BYPdmXQRPGpnVuLaDLGZGLbq+86ZJQaGIK1ekcKeYeycoX8BTMNFr+89Tnd77M/CvmjM8yteCg==

View File

@@ -1,17 +1,63 @@
#pragma once #pragma once
#include <cstdlib> #include <array>
#include <cassert> #include <cassert>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <fcntl.h>
#include <fstream> #include <fstream>
#include <map> #include <map>
#include <string> #include <string>
#include <algorithm> // for std::clamp #include <algorithm> // for std::clamp
#include <sys/ioctl.h>
#include <unistd.h>
#include "common/util.h" #include "common/util.h"
#include "system/hardware/base.h" #include "system/hardware/base.h"
class HardwareTici : public HardwareNone { class HardwareTici : public HardwareNone {
public: 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() { static std::string get_name() {
std::string model = util::read_file("/sys/firmware/devicetree/base/model"); std::string model = util::read_file("/sys/firmware/devicetree/base/model");
return util::strip(model.substr(std::string("comma ").size())); return util::strip(model.substr(std::string("comma ").size()));

View File

@@ -78,9 +78,10 @@ void V4LEncoder::dequeue_handler(V4LEncoder *e) {
uint32_t idx = -1; uint32_t idx = -1;
bool exit = false; 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; struct pollfd pfd;
pfd.events = POLLIN | POLLOUT; pfd.events = POLLIN | POLLRDNORM | POLLOUT | POLLWRNORM;
pfd.fd = e->fd; pfd.fd = e->fd;
// save the header // save the header
@@ -105,7 +106,7 @@ void V4LEncoder::dequeue_handler(V4LEncoder *e) {
} }
int frame_id = -1; int frame_id = -1;
if (pfd.revents & POLLIN) { if (pfd.revents & (POLLIN | POLLRDNORM)) {
unsigned int bytesused, flags, index; unsigned int bytesused, flags, index;
struct timeval timestamp; struct timeval timestamp;
dequeue_buffer(e->fd, V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, &index, &bytesused, &flags, &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]); 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; unsigned int index;
dequeue_buffer(e->fd, V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, &index); dequeue_buffer(e->fd, V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, &index);
e->free_buf_in.push(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_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_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_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_ALPHA, .value = 0},
{ .id = V4L2_CID_MPEG_VIDEO_H264_LOOP_FILTER_BETA, .value = 0}, { .id = V4L2_CID_MPEG_VIDEO_H264_LOOP_FILTER_BETA, .value = 0},
{ .id = V4L2_CID_MPEG_VIDEO_MULTI_SLICE_MODE, .value = 0}, { .id = V4L2_CID_MPEG_VIDEO_MULTI_SLICE_MODE, .value = 0},

View File

@@ -46,6 +46,14 @@ kj::Array<capnp::word> logger_build_init_data() {
init.setKernelVersion(util::read_file("/proc/version")); init.setKernelVersion(util::read_file("/proc/version"));
init.setOsVersion(util::read_file("/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 // log params
Params params(util::getenv("PARAMS_COPY_PATH", "")); Params params(util::getenv("PARAMS_COPY_PATH", ""));
std::map<std::string, std::string> params_map = params.readAll(); std::map<std::string, std::string> params_map = params.readAll();

View File

@@ -170,6 +170,15 @@ class TestLoggerd:
assert initData.dirty != bool(os.environ["CLEAN"]) assert initData.dirty != bool(os.environ["CLEAN"])
assert initData.version == get_version() 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"): if os.path.isfile("/proc/cmdline"):
with open("/proc/cmdline") as f: with open("/proc/cmdline") as f:
assert list(initData.kernelArgs) == f.read().strip().split(" ") assert list(initData.kernelArgs) == f.read().strip().split(" ")

View File

@@ -17,6 +17,10 @@ from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog from openpilot.common.swaglog import cloudlog
MAX_CRASH_BACKOFF = 300.0
CRASH_RESET_TIME = 60.0
CRASH_LOOP_THRESHOLD = 6
try: try:
from openpilot.system.proprietary_runtime.runtime_paths import preferred_runner_path from openpilot.system.proprietary_runtime.runtime_paths import preferred_runner_path
except ModuleNotFoundError: except ModuleNotFoundError:
@@ -82,6 +86,10 @@ class ManagerProcess(ABC):
name = "" name = ""
shutting_down = False shutting_down = False
restart_if_crash = False restart_if_crash = False
crash_count = 0
last_restart_time = 0.0
last_alive_time = 0.0
crash_loop_logged = False
@abstractmethod @abstractmethod
def prepare(self) -> None: def prepare(self) -> None:
@@ -318,13 +326,33 @@ def ensure_running(procs: ValuesView[ManagerProcess], started: bool, params=None
not_run = [] not_run = []
running = [] running = []
now = time.monotonic()
for p in procs: for p in procs:
if p.enabled and p.name not in not_run and p.should_run(started, params, CP): 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(): if p.restart_if_crash and p.proc is not None and p.proc.is_alive():
cloudlog.error(f'Restarting {p.name} (exitcode {p.proc.exitcode})') p.last_alive_time = now
p.restart() 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) running.append(p)
else: else:
p.crash_count = 0
p.crash_loop_logged = False
p.last_alive_time = 0.0
p.stop(block=False) p.stop(block=False)
for p in running: for p in running:

View File

@@ -98,7 +98,9 @@ def constructiond_onroad(started: bool, params: Params, CP: car.CarParams) -> bo
return started and params.get_bool("ConstructionZoneAssist") return started and params.get_bool("ConstructionZoneAssist")
def iqvd_onroad(started: bool, params: Params, CP: car.CarParams) -> bool: 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: def only_offroad(started: bool, params: Params, CP: car.CarParams) -> bool:
return not started 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. # down cleanly when the session ends — no subprocess management inside hephaestusd.
return params.get_bool("IsLiveStreaming") 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: def is_tinygrad_model(started, params, CP: car.CarParams) -> bool:
"""Check if the active model runner is tinygrad.""" """Check if the active model runner is tinygrad."""
return bool(get_active_model_runner(params, not started) == custom.IQModelManager.Runner.tinygrad) return bool(get_active_model_runner(params, not started) == custom.IQModelManager.Runner.tinygrad)
@@ -179,6 +187,7 @@ procs = [
# debug procs # debug procs
NativeProcess("bridge", "cereal/messaging", ["./bridge"], notcar), NativeProcess("bridge", "cereal/messaging", ["./bridge"], notcar),
PythonProcess("webrtcd", "system.webrtc.webrtcd", or_(iscar, livestream)), PythonProcess("webrtcd", "system.webrtc.webrtcd", or_(iscar, livestream)),
PythonProcess("canlived", "iqpilot.konn3kt.canlive.canlived", canlive),
PythonProcess("webjoystick", "tools.bodyteleop.web", notcar), PythonProcess("webjoystick", "tools.bodyteleop.web", notcar),
] ]

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import numpy as np import numpy as np
import os import os
import time
from functools import cache from functools import cache
import threading 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) 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}") cloudlog.info(f"micd selecting input device {self.last_device}")
with self.get_stream(sd) as stream: while True:
cloudlog.info(f"micd stream started: {stream.samplerate=} {stream.channels=} {stream.dtype=} {stream.device=}, {stream.blocksize=}") try:
while True: with self.get_stream(sd) as stream:
self.update() 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(): def main():

View File

@@ -12,7 +12,7 @@ from openpilot.common.gps import get_gps_location_service
def set_time(new_time): 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): if abs(diff) < datetime.timedelta(seconds=10):
cloudlog.debug(f"Time diff too small: {diff}") cloudlog.debug(f"Time diff too small: {diff}")
return return
@@ -47,7 +47,7 @@ def main() -> NoReturn:
pm.send('clocks', msg) pm.send('clocks', msg)
gps = sm[gps_location_service] 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: if not sm.updated[gps_location_service] or (time.monotonic() - sm.logMonoTime[gps_location_service] / 1e9) > 2.0:
continue continue
if not gps.hasFix: if not gps.hasFix:

View File

@@ -22,7 +22,7 @@ from pathlib import Path
from typing import NamedTuple from typing import NamedTuple
from importlib.resources import as_file, files from importlib.resources import as_file, files
from openpilot.common.swaglog import cloudlog 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.system.ui.lib.multilang import multilang
from openpilot.common.realtime import Ratekeeper from openpilot.common.realtime import Ratekeeper
@@ -326,7 +326,7 @@ class GuiApplication(IQAppHooks):
rl.glfw_swap_interval(0) rl.glfw_swap_interval(0)
except Exception: except Exception:
pass 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: try:
from openpilot.system.ui.lib.egl import set_swap_interval from openpilot.system.ui.lib.egl import set_swap_interval
set_swap_interval(0) set_swap_interval(0)
@@ -349,7 +349,7 @@ class GuiApplication(IQAppHooks):
glfw_vsync = False glfw_vsync = False
egl_vsync = False egl_vsync = False
if not PC: if EGL_DMA_BUF_SUPPORTED:
try: try:
from openpilot.system.ui.lib.egl import set_swap_interval from openpilot.system.ui.lib.egl import set_swap_interval
egl_vsync = set_swap_interval(interval) egl_vsync = set_swap_interval(interval)
@@ -367,7 +367,7 @@ class GuiApplication(IQAppHooks):
rl.rl_draw_render_batch_active() rl.rl_draw_render_batch_active()
def _apply_display_sync_before_swap(self) -> None: 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 return
try: try:
self._flush_raylib_batch() 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 # 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 # 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. # 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: try:
from openpilot.system.ui.lib.egl import set_swap_interval from openpilot.system.ui.lib.egl import set_swap_interval
set_swap_interval(self._vsync_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 # 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 # *after* that call but before this swap, which reintroduced tearing. This is the swap that
# actually matters, so pin the interval here too. # 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()): and self._vsync_interval > 0 and rl.is_window_ready()):
try: try:
from openpilot.system.ui.lib.egl import set_swap_interval from openpilot.system.ui.lib.egl import set_swap_interval

View File

@@ -961,6 +961,10 @@ class WifiManager:
settings['connection'] = {} settings['connection'] = {}
changes = False 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 == "" auto_config = apn == ""
initial_eps_apn = apn if not auto_config else "" 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}') cloudlog.warning(f'Changing gsm.auto-config to {auto_config}')
settings['gsm']['auto-config'] = ('b', auto_config) settings['gsm']['auto-config'] = ('b', auto_config)
changes = True changes = True
apn_settings_changed = True
if settings['gsm'].get('apn', ('s', ''))[1] != apn: if settings['gsm'].get('apn', ('s', ''))[1] != apn:
cloudlog.warning(f'Changing gsm.apn to {apn}') cloudlog.warning(f'Changing gsm.apn to {apn}')
settings['gsm']['apn'] = ('s', apn) settings['gsm']['apn'] = ('s', apn)
changes = True changes = True
apn_settings_changed = True
if settings['gsm'].get('home-only', ('b', False))[1] == roaming: if settings['gsm'].get('home-only', ('b', False))[1] == roaming:
cloudlog.warning(f'Changing gsm.home-only to {not 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)}') 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)) settings['gsm']['initial-eps-bearer-configure'] = ('b', bool(initial_eps_apn))
changes = True changes = True
apn_settings_changed = True
if settings['gsm'].get('initial-eps-bearer-apn', ('s', ''))[1] != initial_eps_apn: 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}') cloudlog.warning(f'Changing gsm.initial-eps-bearer-apn to {initial_eps_apn}')
settings['gsm']['initial-eps-bearer-apn'] = ('s', initial_eps_apn) settings['gsm']['initial-eps-bearer-apn'] = ('s', initial_eps_apn)
changes = True changes = True
apn_settings_changed = True
# Unknown means NetworkManager decides # Unknown means NetworkManager decides
metered_int = int(MeteredType.UNKNOWN if metered else MeteredType.NO) 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}") cloudlog.warning(f"Failed to update GSM settings: {reply}")
return 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: except Exception as e:
cloudlog.exception(f"Error updating GSM settings: {e}") cloudlog.exception(f"Error updating GSM settings: {e}")
@@ -1034,6 +1045,40 @@ class WifiManager:
except Exception as e: except Exception as e:
cloudlog.exception(f"Error activating modem connection: {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): def stop(self):
if not self._exit: if not self._exit:
self._exit = True self._exit = True

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