CarPlay patch set for Audi MHI2 infotainment. (Based on MHI2Q firmware, but may need rebuild for different versions.)
Disclaimer: Use at your own risk. These patches modify firmware binaries and system configurations on your infotainment unit. Always back up all original files before making any changes. The authors are not responsible for any damage, bricked devices, or warranty issues resulting from use of these patches.
What this patch makes the head unit + cluster do that stock MHI2 doesn't:
- Full HUD route guidance from CarPlay nav (Maps, Waze, etc.) - maneuver icons, lanes, distance bargraph, ETA, destination
- Custom 3D maneuver overlay drawn into the cluster's MOST video stream (the same video plane the HU uses for its native map)
- Album cover art forwarded to the cluster's now-playing widget
- MMI touchpad -> DPAD bridging so finger drags navigate CarPlay menus
What it does not do, with explanations:
- CarPlay AltScreen on cluster - architecturally blocked on this HU generation, see deep dive below
iOS RGD messages enter through the iAP2 hook and fan out into two independent rendering branches on the VC:
- HUD branch (BAP LSG 50) - feeds the text/icon HUD widgets the cluster firmware already knows how to draw. Cheap, always-on.
- Render branch (custom MOST-video renderer) - draws full 3D maneuver scenes into the cluster's video pipeline. Spawned only while a route is active.
Both branches receive the same parsed state from BAPBridge and
update in lock-step.
flowchart LR
iOS["📱 iOS<br/>CarPlay nav app"]
subgraph hook["libcarplay_hook.so"]
recv["recv() hook<br/>+ RGD parser"]
bus["TCP bus :19810"]
recv -->|"EVT_RGD_UPDATE"| bus
end
subgraph jar["carplay_hook.jar"]
bus_cli["bus client"]
bridge["BAPBridge<br/>(maneuver state machine)"]
bus_cli --> bridge
end
iOS -->|"iAP2 RGD<br/>0x5200..0x5204"| recv
bus -.->|"TCP :19810"| bus_cli
bridge -->|"branch A"| hud_path
bridge -->|"branch B"| render_path
subgraph hud_path["Branch A - HUD (BAP LSG 50)"]
direction TB
hud_icons["Maneuver icons + lanes<br/>FctID 23, 24"]
hud_text["Text overlays<br/>FctID 19, 21, 22, 46"]
hud_dist["Distance + bargraph<br/>FctID 18"]
end
subgraph render_path["Branch B - Render (MOST video)"]
direction TB
spawn["spawn maneuver_render<br/>(slay -f -Q on stop)"]
rend["maneuver_render<br/>EGL/GLES2 3D scene"]
surf["displayable 200<br/>EGL surface"]
enc["cluster video encoder<br/>(H.264)"]
most["MOST video link"]
spawn --> rend --> surf --> enc --> most
end
hud_icons --> vc
hud_text --> vc
hud_dist --> vc
most --> vc
vc[("🚗 Virtual Cockpit<br/>HUD + native map area")]
style hook fill:#fff4e6,stroke:#cc7700
style jar fill:#e6f3ff,stroke:#0066cc
style hud_path fill:#fff4e6,stroke:#cc7700
style render_path fill:#e6ffe6,stroke:#008800
| Branch | Cost when idle | Cost per maneuver | When active |
|---|---|---|---|
| HUD (BAP) | ~0 | small BAP frames | always while CarPlay nav running |
| Render (MOST) | renderer process not spawned | EGL frame draws + H.264 encode | only while route is set |
The renderer is spawned by BAPBridge on the first maneuver and
killed on 0x5203 StopRouteGuidance, so the MOST-video branch costs
nothing when the user isn't actively navigating.
All data sent via BAP protocol (LSG 50) to the VC, which drives the HUD.
flowchart LR
iOS["📱 iAP2 RGD<br/>0x5201 RouteGuidanceUpdate<br/>0x5202 ManeuverUpdate<br/>0x5204 LaneGuidance"]
subgraph mapper["ManeuverMapper (Java)"]
direction TB
in1["iAP2 turn_type<br/>(54 distinct types)"]
in2["junction angle"]
in3["driving_side (L/R)"]
in4["lane_info[]"]
in5["distance_meters<br/>+ HU unit setting"]
out["BAP enums:<br/>ManeuverType, ExitNumber,<br/>SideStreets, LaneMask,<br/>distance + unit (m/ft)"]
in1 --> out
in2 --> out
in3 --> out
in4 --> out
in5 --> out
end
iOS --> mapper
subgraph bap["BAP LSG 50 (HUD-driving FctIDs)"]
direction TB
f23["FctID 23 - ManeuverDescriptor<br/>icon + side streets +<br/>exit number + multi-list (up to 3)"]
f24["FctID 24 - LaneGuidance<br/>lane arrows + active highlight"]
f18["FctID 18 - Distance + bargraph<br/>numeric value + fill +<br/>call-for-action blink"]
end
mapper --> f23
mapper --> f24
mapper --> f18
subgraph hud["🚗 VC HUD widgets (drawn by stock cluster firmware)"]
direction TB
w_arrow["Big maneuver arrow"]
w_extras["Side streets, exit number,<br/>upcoming maneuvers stack"]
w_lanes["Lane indicator strip"]
w_dist["Distance number + bargraph"]
end
f23 --> w_arrow
f23 --> w_extras
f24 --> w_lanes
f18 --> w_dist
style mapper fill:#e6f3ff,stroke:#0066cc
style bap fill:#fff4e6,stroke:#cc7700
style hud fill:#f5f5f5,stroke:#666
- Maneuver icons - iAP2 turn type mapped to BAP ManeuverDescriptor (FctID 23). Supports turns, roundabouts, highway exits, merges, U-turns, ferry, etc.
- Multi-maneuver list - up to 3 upcoming maneuvers sent in a single descriptor
- Side streets at intersection - computed from iAP2 junction type + turn angle
- Junction view - roundabout, highway interchange exit numbers
- Left-hand / right-hand driving - auto-detected from iAP2
driving_side, affects icon mirroring and side-street layout - Distance to next maneuver - numeric display when far (FctID 18)
- Distance bargraph - fills up on approach, with call-for-action blink at maneuver point (FctID 18)
- Auto distance units - reads current HU setting (metric/imperial), no manual switch needed
- Lane guidance - lane arrows with active-lane highlighting (FctID 24)
Sent via the same BAP path. VC renders these as text bars over the native map area.
- Turn-to street / exit name (FctID 23, part of ManeuverDescriptor)
- Current road name (FctID 19)
- Distance to destination (FctID 21)
- ETA / remaining travel time - timezone-adjusted to HU local time (FctID 22)
- Destination name (FctID 46)
Custom 3D renderer (c_render/) draws maneuver icons into the cluster's
MOST video pipeline (MOST150 isochronous channel - the same path
the HU uses to ship its native map render to the VC's Map tab).
Renders to QNX displayable 200 via EGL/GLES2; the frames are
captured by the HU video encoder (H.264) and shipped over MOST to the
VC, where the cluster's TVMRCapture pipeline decodes them into a
texture composited by the Kanzi scene. Stock displayable 20 (the
native route-guidance widget that fights for the same screen region)
is periodically re-claimed by re-declaring context 74 - without that
watchdog the stock widget eventually wins back the screen.
Note on naming. Earlier drafts of this README called this branch "LVDS video". That was wrong for MHI2Q - LVDS exists on the platform (
DISPLAYSTATUS_LVDS_DM_ACTIVE/LVDS_HMI_ACTIVEbits inDSIKombiSync) but is reserved for full-screen mirror modes (startup logo, standby). The HU->VC video for the Map tab on this generation rides MOST150, not LVDS.
flowchart LR
iOS[("📱 iOS<br/>CarPlay nav app")]
subgraph hookbox["libcarplay_hook.so"]
direction TB
recv["recv() hook"]
rgd["RGD parser<br/>(rgd_hook.c)"]
idpatch["Identify patcher<br/>+0x001E component<br/>+ msg IDs"]
bus["TCP bus :19810"]
end
subgraph javabox["carplay_hook.jar"]
direction TB
bridge["BAPBridge<br/>maneuver state machine"]
bus_cli["bus client<br/>(EVT_RGD_UPDATE)"]
end
subgraph rendbox["maneuver_render (separate process)"]
direction TB
atlas["flag_atlas.rgba<br/>14 frames x 128x128"]
render["EGL/GLES2 3D scene<br/>(road, arrow, flag)"]
wd["watchdog<br/>re-declare context 74<br/>(reclaims displayable 200)"]
end
surf[("displayable 200<br/>EGL surface")]
enc[/"cluster video encoder<br/>(H.264)"/]
most([MOST video link])
vc[("🚗 Virtual Cockpit")]
stock["[!] displayable 20<br/>stock RG widget<br/>(fights for region)"]:::stock
iOS -->|"0x5200<br/>StartRouteGuidance"| recv
iOS -->|"0x5201<br/>RouteGuidanceUpdate"| recv
iOS -->|"0x5202<br/>ManeuverUpdate"| recv
iOS -->|"0x5203<br/>StopRouteGuidance"| recv
iOS -->|"0x5204<br/>LaneGuidance"| recv
recv --> rgd
recv --> idpatch
rgd -->|"parsed state"| bus
bus -.->|"TCP :19810"| bus_cli
bus_cli --> bridge
bridge -->|"on first maneuver:<br/>spawn /mnt/app/root/hooks/<br/>maneuver_render &"| rendbox
bridge -->|"on stop:<br/>slay -f -Q maneuver_render"| rendbox
bridge -->|"TCP :19800<br/>CMD_MANEUVER (48-byte fixed)"| render
bridge -->|"BAP LSG 50<br/>FctID 18, 19, 21..24, 46"| vc
atlas --> render
render --> surf
surf --> enc --> most --> vc
wd -. "periodic reclaim" .-> surf
stock -. "competes for screen region" .-> surf
classDef stock fill:#fee,stroke:#900,stroke-dasharray: 5 5
style hookbox fill:#fff4e6,stroke:#cc7700
style javabox fill:#e6f3ff,stroke:#0066cc
style rendbox fill:#e6ffe6,stroke:#008800
- Procedural 3D maneuver icons (turns, roundabouts, U-turns, merges, lane changes, arrival)
- Side streets at junctions from iAP2 junction angles
- Animated route path with curvature-aware speed
- Smooth push transitions between consecutive maneuvers (crossfade + path chaining)
- Camera follow/settle during transitions
- Arrow-to-bulb tip morphing for arrival
- Animated destination flag sprite
- Distance bargraph overlay with blink mode
- Perspective/orthographic view with animated blend
- FXAA + 2x SSAA anti-aliasing
- Painter's algorithm road rendering (white outline + grey fill + blue active route)
- TCP command protocol (port 19800) - Java bridge sends maneuver updates, renderer handles all animation autonomously
Both branches above (HUD FctID 18 bargraph and the renderer's
on-screen bargraph overlay) need continuous fill + attention-grabbing
blink near the maneuver, but iOS sends 0x5202 ManeuverUpdate at
sparse intervals (~1-3 s, faster on approach). BAPBridge smooths
that gap with a small state machine + a dedicated blink timer thread.
Phases:
stateDiagram-v2
direction LR
[*] --> Far: route activated
Far --> Approach: distM <= prepareThreshold
Approach --> Blink: linBargraph% < 20<br/>(call-for-action zone)
Blink --> Approach: distM/maneuver changed<br/>(bargraph >= 20% again)
Approach --> Far: new maneuver pushed<br/>(distance jumps back up)
Blink --> Far: new maneuver pushed
Far --> [*]: route stopped (0x5203)
Approach --> [*]: route stopped
Blink --> [*]: route stopped
Key parameters (in BAPBridge.java):
| Constant | Value | Meaning |
|---|---|---|
CITY_PREPARE_THRESHOLD_M |
1500 m | enter Approach zone (city maneuver) |
HIGHWAY_PREPARE_THRESHOLD_M |
3000 m | enter Approach zone (highway maneuver) |
BARGRAPH_BLINK_PERCENT |
20% | enter Blink phase when bargraph drops below |
ACTION_BLINK_INTERVAL_MS |
600 ms | blink toggle period (50% duty) |
Linear fill formula: linBargraph% = distM * 100 / prepareThreshold
clamped to [0, 100]. Highway maneuvers use the wider 3000 m
denominator so the bar fills more gradually on a long approach.
Blink loop runs on a dedicated BAPActionBlink daemon thread
(spawned only while a route is active). Every 600 ms it toggles the
bargraph between 100% and 0% and re-sends FctID 18 (HUD) plus a
CMD_MANEUVER tick to the renderer (overlay). This is independent
of iAP2 update cadence - even if iOS goes silent for 2 seconds, both
HUD and renderer still see the blink animation in lock-step.
FSG-sync workaround. AppConnectorNavi.sendStatusIfChanged()
silently drops updates when nothing in {FctID 23, 18, 49} changed.
On every ManeuverDescriptor send we toggle the cosmetic
exitViewNum variant on FctID 49 to force a transmission - without
that toggle the cluster occasionally misses bargraph ticks during
fast approach.
Stock MHI2 CarPlay (TerminalMode) forwards track title, artist, and album to the VC, but the stock AppConnectorTerminalMode never pushes cover art to the BAP picture manager. The VC always shows a blank/default album icon.
%%{init: {'sequence': {'mirrorActors': false, 'showSequenceNumbers': true}}}%%
sequenceDiagram
participant iOS as 📱 iOS
participant recv as recv() hook<br/>(iAP2 thread)
participant worker as Async worker<br/>(pthread)
participant fs as coverart.png<br/>(tmpfs)
participant bus as TCP bus<br/>:19810
participant java as CoverArt +<br/>EventListener (Java)
participant bap as AppConnector<br/>TerminalMode
participant VC as 🚗 VC
Note over iOS,VC: Track change - incoming cover art
iOS->>recv: iAP2 transport frames<br/>(chunked JPEG)
activate recv
recv->>recv: append to per-fd buffer<br/>scan for FF 5A packets
recv->>recv: reassemble JPEG (SOI..EOI)
recv->>worker: enqueue raw JPEG<br/>(1-slot, latest wins)
deactivate recv
Note right of recv: hook returns in µs -<br/>iAP2 traffic not stalled
activate worker
rect rgba(255, 240, 200, 0.4)
Note right of worker: ~50-100 ms decode work,<br/>off the iAP2 hot path
worker->>worker: CRC32 of JPEG
alt CRC == last_crc (duplicate)
worker--xworker: skip - already saved
else new image
worker->>worker: stb_image decode<br/>+ resize 256x256<br/>+ PNG encode
worker->>fs: write coverart.png<br/>(atomic rename)
worker->>bus: EVT_COVERART<br/>crc:<u32> path:<str>
end
end
deactivate worker
bus->>java: deliver event
java->>java: dedup by CRC,<br/>track current art-id
java->>bap: push image<br/>(ResourceLocator)
bap->>VC: BAP picture mgr -><br/>responseCoverArt()
VC-->>VC: render album art
alt Late cover art<br/>(track info already pushed)
Note over java,VC: VC ignores second update<br/>with same picture id
java->>java: tweak picture id (id+1)
java->>bap: re-push now-playing<br/>with modified id
bap->>VC: forces refresh
end
The C hook intercepts iAP2 transport packets (read()/recv() hooks),
reassembles JPEG cover art from chunked transfers, hands the complete
JPEG to a dedicated async worker thread (single-slot pending queue,
latest-wins coalescing) which decodes and resizes to 256x256 PNG using
stb_image and writes to /var/app/icab/tmp/37/coverart.png (tmpfs -
regenerated each session, lost on reboot, which is fine since the
decode pipeline restarts with every CarPlay handshake). The async
hand-off keeps the recv()/read() hook thread free, avoiding ~50-100 ms
of synchronous decode latency per cover art that previously stalled
concurrent iAP2 traffic during handshake. A TCP bus event
(EVT_COVERART, port 19810) signals Java. The Java CoverArt module
subscribes to the bus, and TerminalModeBapCombi$EventListener pushes
the image through AppConnectorTerminalMode to the BAP picture manager
with proper ResourceLocator and responseCoverArt() calls,
mirroring the native AppConnectorMedia pattern. Late-arriving cover
art (after track info was already sent) triggers a re-send with a
modified picture ID to force the VC to refresh.
- Cover art forwarding to VC
Stock MHI2 forwards the rotary, knob press, back and softkey buttons to CarPlay natively - those work out of the box. The MMI touchpad is the only input device that stock leaves unbridged: finger gestures on the touchpad never reach the CarPlay session.
This patch adds the missing leg of the chain:
flowchart TD
subgraph mmi["🎛️ MMI input devices"]
direction LR
pad["🖐️ Touchpad"]
rotary["Rotary knob<br/>(turn left/right)"]
knob["Knob press"]
soft["▭ Softkeys / Back"]
end
subgraph dsi_layer["DSI input layer"]
direction TB
dsi_touch["updateTouchEvents<br/>(TouchEvent[])"]
dsi_key["postButtonEvent<br/>(rotary, press, softkeys)"]
end
subgraph patch["This patch (touchpad-only)"]
direction TB
hook["DSI key events controller<br/>(class-replacement)"]
ctrl["CursorController"]
accum["accumulate Δx, Δy"]
thresh{"|Δx| or |Δy|<br/>> threshold?"}
emit["emit DPAD tick<br/>press + release"]
end
sink["DSI sink<br/>postDpad / postButtonEvent"]
cp[("📲 CarPlay session")]
pad --> dsi_touch
rotary --> dsi_key
knob --> dsi_key
soft --> dsi_key
dsi_touch --> hook
dsi_key -.->|"stock - passes through"| sink
hook --> ctrl
ctrl --> accum
accum --> thresh
thresh -->|yes| emit
thresh -->|no| accum
emit --> sink
sink --> cp
style patch fill:#fff4e6,stroke:#cc7700
style hook fill:#fff4e6
style ctrl fill:#fff4e6
style accum fill:#fff4e6
style thresh fill:#fff4e6
style emit fill:#fff4e6
The orange box is what this patch adds. Rotary, knob press and softkeys already reach CarPlay through stock DSI (dotted path) - only the touchpad needed bridging.
Touchpad model. A finger drag accumulates Δx / Δy; whenever either
axis crosses a speed-adaptive threshold, a KEY_DPAD_* press+release
pair is emitted and the threshold is subtracted. A long drag emits
multiple ticks so the user can traverse several list items in one
gesture.
(The class is named CursorController for legacy reasons - it once
drove an on-screen cursor that was abandoned because MHI2Q's H.264
encoder ghosted the overlay through motion compensation. See
docs/ notes if curious.)
- MMI touchpad drag -> DPAD navigation
- Knob rotary, knob press, back / softkey buttons - already work via stock DSI, no patch needed
- Lane guidance on maneuver renderer (CMD_LANE_GUIDANCE protocol, arrow glyphs with status colors + dashed separators, see
docs/plan_lane_guidance_renderer.md)
CarPlay second screen on the instrument cluster cannot be enabled on MHI2Q - and no wireless dongle fixes it. The blocker is on the HU side (Cinemo iAP2 SDK + libairplay 210.81), not on the phone or USB transport. Full reverse-engineering write-up + diagrams in Why AltScreen is impossible below.
| Component | Type | Purpose | Output |
|---|---|---|---|
c_hook/ |
C (ARM32 QNX) | iAP2 hooks, route guidance + async cover-art bridge, TCP bus client (connects to Java :19810) | libcarplay_hook.so |
c_render/ |
C (ARM32 QNX / macOS) | Custom 3D maneuver renderer (EGL/GLES2), TCP command-driven | maneuver_render |
java_patch/ |
Java patch JAR | Route guidance rendering, BAP bridge, cover art forwarder, MMI touchpad / D-pad -> CarPlay input | carplay_hook.jar |
dio_manager.json |
System config patch | Enables iAP2 route guidance message exchange with iOS | Manual edit on device |
smartphone_integrator.json |
System config patch | Loads libcarplay_hook.so via LD_PRELOAD in dio_manager process |
Manual edit on device |
flowchart LR
iPhone[("📱 iPhone<br/>iAP2 / CarPlay")]
subgraph mmi["MMI input"]
direction TB
touchpad["Touchpad"]
dpad_btn["Knob / softkeys / back<br/>(stock - passes through)"]
end
subgraph cfg["System config (manual edits)"]
direction TB
cfg_int["smartphone_integrator.json<br/>(LD_PRELOAD declaration)"]
cfg_dio["dio_manager.json<br/>(0x5200..0x5204 registration)"]
end
subgraph HU["🖥️ Audi MHI2Q Head Unit (QNX 6.5, Qualcomm)"]
direction TB
subgraph dio["dio_manager process (Cinemo iAP2 SDK)"]
direction TB
subgraph hook["libcarplay_hook.so - LD_PRELOAD'd"]
direction TB
iap2["iAP2 transport hooks<br/>read() / recv() intercept<br/><i>(iAP2 thread)</i>"]
idpatch["Identify patcher<br/>+0x001E EAGroupComponent<br/>+ msg-IDs runtime patch"]
rgd["RGD parser<br/>0x5200..0x5204<br/>+ 0x52xx unknown warn"]
cover["Cover-art bridge<br/>chunked JPEG reassembly"]
worker["Async decode worker<br/>stb_image -> 256x256 PNG<br/>(pthread, coalesced queue)"]
bus[("TCP bus client<br/>connects to Java :19810<br/><i>connector + writer + reader<br/>+ heartbeat threads</i>")]
hb["Heartbeat thread<br/>EVT_PONG every 1 s<br/>(liveness signal for Java)"]
cksum["iAP2 cksum<br/>(NEG, hard-coded)<br/>+ sanity log"]
end
end
subgraph hmi["HMI process (lsd.jxe + class-replacements)"]
direction TB
subgraph jar["carplay_hook.jar"]
direction TB
carplayhook["CarPlayHook<br/>(lifecycle coordinator,<br/>retry thread)"]
bapbridge["BAPBridge<br/>(maneuver state machine,<br/>BAPActionBlink thread)"]
cursor["CursorController<br/>(touchpad Δx/Δy -> DPAD,<br/>speed-adaptive threshold)"]
dsihook["TerminalModeDSIKeyEvents<br/>Controller (class-replaced)"]
covjava["AppConnectorTerminalMode<br/>(class-replaced -<br/>cover art -> BAP picture mgr)"]
bussrv["TCP bus server<br/>listens on :19810<br/>SO_TIMEOUT 5 s = dead<br/>(accept + reader + writer threads)"]
rendsrv["RendererServer<br/>listens on :19800<br/>SO_TIMEOUT 5 s = dead<br/>(non-blocking accept thread)"]
tmevent["TerminalModeBapCombi$<br/>EventListener (class-replaced)"]
end
end
renderer["maneuver_render<br/>(separate ARM ELF process)<br/>EGL/GLES2 3D scene<br/>+ context-74 watchdog<br/>+ EVT_HEARTBEAT every 1 s"]
subgraph filesys["File system / tmpfs"]
direction TB
fs_cov[("/var/app/icab/tmp/37/<br/>coverart.png<br/>(tmpfs)")]
fs_log_hook[("/tmp/carplay_hook.log<br/>(tmpfs, reset on reboot)")]
fs_log_ren[("/tmp/maneuver_render.log<br/>(tmpfs, reset on reboot)")]
end
end
subgraph VC["🚗 Virtual Cockpit (cluster, NVIDIA Tegra 2 SoC)"]
direction TB
most_rx["MOST RX +<br/>TVMRCapture"]
kanzi["Kanzi scene composite<br/>(stock cluster firmware)"]
hud_widgets["HUD widgets<br/>maneuver arrow,<br/>distance + bargraph,<br/>lane indicator,<br/>text overlays"]
map_area["Native map area<br/>(receives MOST video<br/>+ our 3D overlay)"]
most_rx --> kanzi
kanzi --> hud_widgets
kanzi --> map_area
end
cfg_int -.->|"declares LD_PRELOAD"| dio
cfg_dio -.->|"registers RGD msg IDs<br/>in Cinemo SDK"| dio
iPhone <-->|"USB iAP2 (MOST150)"| iap2
iap2 --> idpatch
iap2 --> rgd
iap2 --> cover
iap2 -.->|"verify on first stock frame"| cksum
cover --> worker
rgd -->|"EVT_RGD_UPDATE"| bus
worker -->|"EVT_COVERART"| bus
hb -->|"EVT_PONG (1 Hz)"| bus
worker --> fs_cov
hook -.->|"writes"| fs_log_hook
bus -->|"connect TCP :19810"| bussrv
bussrv -->|"deliver events"| tmevent
tmevent --> bapbridge
tmevent --> covjava
bapbridge -->|"TCP :19800<br/>CMD_MANEUVER (48B)"| rendsrv
rendsrv -->|"connect TCP :19800"| renderer
renderer -->|"EVT_HEARTBEAT (1 Hz)"| rendsrv
bapbridge -->|"BAP LSG 50<br/>FctID 18, 19, 21..24, 46"| hud_widgets
covjava -->|"BAP picture mgr<br/>responseCoverArt()"| hud_widgets
fs_cov -.->|"read PNG"| covjava
touchpad -->|"DSI updateTouchEvents"| dsihook
dsihook --> cursor
cursor -->|"DSI postDpad<br/>KEY_DPAD_*"| iap2
dpad_btn -.->|"DSI postButtonEvent<br/>(stock path)"| iap2
renderer -->|"displayable 200<br/>(EGL surface)"| most_encoder
most_encoder -->|"H.264 over MOST150"| most_rx
renderer -.->|"writes"| fs_log_ren
most_encoder([HU video encoder<br/>H.264])
style hook fill:#fff4e6,stroke:#cc7700
style jar fill:#e6f3ff,stroke:#0066cc
style renderer fill:#e6ffe6,stroke:#008800
style filesys fill:#f5f5f5,stroke:#999,stroke-dasharray: 3 3
style cfg fill:#fff,stroke:#888,stroke-dasharray: 4 4
style mmi fill:#fff,stroke:#888
style VC fill:#f8f8ff,stroke:#666
The diagram shows a single CarPlay session at steady state. Cinemo
SDK is implicit - every iAP2 byte from iPhone first passes through
our read() / recv() hooks, then continues into the stock SDK code
underneath. We don't bypass the stock path; we intercept and (for
RGD / Identify / cover art) inject side effects.
Important: dio_manager is not spawned at boot. It is launched
on demand by the always-running smartphone_integrator process when
a phone is detected on USB. That's why our libcarplay_hook.so
constructor (LD_PRELOAD) only fires when the phone is plugged in,
not at QNX boot. The HMI process (lsd.jxe) does start at boot, so
carplay_hook.jar class-replacements load early - the Java bus
server accept()s as soon as the C hook connects.
Topology (lifetime hierarchy):
- Java HMI (long-lived, alive from boot) = TCP server on :19810 for the bus, opens :19800 for the renderer at session start.
- C hook (short-lived, per phone connect) = TCP client, connects to Java's :19810. Idempotent reconnect on Java HMI restart.
- Renderer (short-lived, per route) = TCP client, connects to Java's :19800. Idempotent reconnect on Java side restart.
Liveness detection (heartbeat-based):
- C hook sends
EVT_PONGevery 1 s; Java hasSO_TIMEOUT=5 son the accepted socket. Five seconds of silence → reader exits → bus re-accepts a fresh hook connection. Catches force-killeddio_managerwhose TCP state may linger. - Renderer sends
EVT_HEARTBEAT(cmd=0x80) every 1 s; same 5 s timeout pattern inRendererServer. Hung renderer detected within 5 s, then 3 consecutive send failures (~1.8 s) triggerslay -f -Q+ respawn.
%%{init: {'sequence': {'mirrorActors': false}}}%%
sequenceDiagram
participant qnx as QNX init
participant si as smartphone_integrator<br/>(boot resident)
participant dio as dio_manager<br/>(spawned on phone connect)
participant hook as libcarplay_hook.so<br/>(LD_PRELOAD'd)
participant lsd as lsd.jxe (HMI)
participant jar as carplay_hook.jar
participant rend as maneuver_render
participant ios as 📱 iPhone
Note over qnx,jar: --- Boot phase ---
qnx->>si: spawn smartphone_integrator
qnx->>lsd: spawn HMI app
lsd->>jar: load class-replacements<br/>(CarPlayHook, BAPBridge,<br/>CursorController, DSI hook,<br/>AppConnectorTerminalMode)
jar->>jar: open TCP bus server :19810<br/>accept() blocks waiting for hook
si->>si: idle, watch for USB phone
Note over qnx,ios: ... idle until iPhone plugged in ...
ios->>si: USB enumerate (Apple device detected)
si->>dio: spawn dio_manager<br/>(env from smartphone_integrator.json,<br/>incl. LD_PRELOAD=libcarplay_hook.so)
Note right of dio: LD_PRELOAD picks up<br/>libcarplay_hook.so before<br/>main() runs
dio->>hook: __attribute__((constructor))<br/>fires
hook->>hook: spawn worker threads<br/>(async cover-art, writer)
hook->>jar: connect TCP :19810 (instant)
Note over hook,jar: Java accept() unblocks -<br/>bus link established
Note over hook,ios: --- iAP2 handshake ---
dio->>ios: iAP2 Identify start
dio->>hook: read()/recv() with outgoing<br/>Identify frame
hook->>hook: Identify patcher injects<br/>+0x001E EAGroupComponent
hook->>ios: patched Identify forwarded<br/>(via stock SDK)
ios->>hook: Identify accepted (0x1D02)
ios->>hook: Auth complete (0xAA05)
Note over hook,jar: --- CarPlay session live ---
hook->>jar: EVT_RGD_UPDATE / EVT_COVERART
jar->>jar: open TCP :19800 server<br/>(BAPBridge listens for renderer)
jar->>rend: spawn maneuver_render<br/>(on first maneuver)
rend->>rend: load flag_atlas.rgba,<br/>create EGL context
rend->>jar: connect TCP :19800 (instant)
jar->>rend: CMD_MANEUVER packets
jar->>jar: BAP traffic to VC starts
Note over si,ios: --- Disconnect ---
ios->>si: USB unplug
si->>dio: SIGTERM / cleanup
Note right of dio: dio_manager exits<br/>hook destructor fires<br/>renderer killed via slay
| Thread | Process | Role |
|---|---|---|
| iAP2 main | dio_manager | runs Cinemo SDK, calls read()/recv() - our hooks intercept on this thread |
| Cover-art worker | dio_manager | pthread - picks complete JPEGs off the 1-slot queue, decodes, writes PNG, emits EVT_COVERART |
| Bus connector | dio_manager | TCP client - connect() to Java :19810 with retry, reconnect on disconnect |
| Bus writer | dio_manager | drains outbound event queue |
| Bus reader | dio_manager | parses inbound frames, dispatches to handlers, exits on EOF/error |
| Bus heartbeat | dio_manager | sends EVT_PONG every 1 s while connected (Java liveness signal) |
| HMI EDT | HMI process | UI loop - calls setMMIDisplayStatus, picture mgr, etc. |
CarplayBus-IO |
HMI process | accept loop on :19810 (one-at-a-time, force-replace stale) |
CarplayBus-Read |
HMI process | per-conn reader, SO_TIMEOUT=5 s triggers re-accept on hook silence |
BAPActionBlink |
HMI process | daemon - 600 ms ticks for bargraph blink animation |
CarPlayHook-Retry |
HMI process | retries tryInit() if OSGi services aren't ready yet |
RendererServer-Accept |
HMI process | accept loop on :19800, replaces socket on each new renderer connect |
RendererServer-Read |
HMI process | per-conn reader for renderer heartbeats, SO_TIMEOUT=5 s |
| Renderer main | maneuver_render | EGL/GLES2 draw loop, TCP client to Java :19800; sends EVT_HEARTBEAT every 1 s |
| Renderer watchdog | maneuver_render | re-declares context 74 to reclaim displayable 200 from stock RG widget |
| Path | Type | Lifetime | Purpose |
|---|---|---|---|
/mnt/app/root/hooks/libcarplay_hook.so |
persistent | survives reboot | C hook binary |
/mnt/app/root/hooks/maneuver_render |
persistent | survives reboot | Renderer ARM ELF |
/mnt/app/root/hooks/flag_atlas.rgba |
persistent | survives reboot | Renderer flag sprite atlas |
/mnt/app/eso/hmi/lsd/jars/carplay_hook.jar |
persistent | survives reboot | Java patch JAR |
/mnt/system/etc/eso/production/smartphone_integrator.json |
persistent | survives reboot | declares LD_PRELOAD |
/mnt/system/etc/eso/production/dio_manager.json |
persistent | survives reboot | registers RGD msg IDs |
/var/app/icab/tmp/37/coverart.png |
tmpfs | until reboot | current album art (regenerated each session) |
/tmp/carplay_hook.log |
tmpfs | until reboot | C hook + Java event log |
/tmp/maneuver_render.log |
tmpfs | until reboot | renderer stderr |
Branch closed 2026-04-23 after full static reverse of iOS 26.1, MHI3 and MHI2Q. AltScreen (CarPlay second screen on the instrument cluster) cannot be enabled on MHI2Q wired CarPlay - and no wireless dongle fixes this, because every gate that breaks the negotiation is on the head-unit side, not on the phone or USB transport.
For iOS to enable AltScreen, the head unit's iAP2 + AirPlay stack must declare two capabilities to the iPhone:
-
Themed-assets capability - a flag in the head unit's iAP2 "introduction card" (the Identification message) saying "I can render Apple's themed CarPlay UI elements on a second display." In the wire format this is
Param 21, sub-parameter 17. iOS reads it via_parseIdentificationParams_3()and onYESsets internal capability bit 21 in_eaAccessoryCapabilities, which CarKit later checks viasetSupportsThemeAssets:YESto exposealtScreenin its proposed feature array. -
Two-screen layout - the head unit's
libairplaymust answer the AirPlay/infoquery with a dict that contains two screens (main + alt), each with its own view areas. The library API for this isMainScreenDictCreate+AltScreenDictCreate+ScreenDictSetViewAreas, plus advertising AirPlay stream type 111 and feature bit 26 (0x04000000) so iOS opens an AltScreen video stream.
MHI2Q fails both:
| Layer | What MHI2Q ships | Why it fails |
|---|---|---|
| iAP2 SDK (Cinemo, pre-2020) | Only knows two flags: SUPPORTS_IAP2_CONNECTION, SUPPORTS_CARPLAY. No notion of Param 21 or the themed-assets sub-parameter. |
iAP2 introduction never carries the themed-assets flag -> iOS leaves capability bit 21 cleared -> CarKit never adds altScreen to its proposed features |
| libairplay (210.81) | CopyDisplaysInfo() returns a flat single-screen dict. No MainScreenDictCreate / AltScreenDictCreate, no ScreenDictSetViewAreas, no stream type 111 in advertised types. |
Even if the iAP2 layer somehow passed, AirPlay /info would not advertise altScreen, and there's no two-screen geometry to send |
A wireless adapter (Carlinkit / U2W / etc.) emulates a wired
carplay accessory on the iPhone end of the link - it speaks
the same iAP2 to the same dio_manager on MHI2Q, hits the same
Cinemo SDK, gets stuck at the same first failure. The dongle cannot
rewrite the HU's iAP2 SDK or libairplay version. No dongle,
present or future, will unlock AltScreen on this generation of HU.
| Term | What it means |
|---|---|
Param 21 + sub17 |
iAP2 Identification field declaring "I support themed CarPlay UI" |
_eaAccessoryCapabilities bit 21 |
iOS-internal capability bitmask, bit 21 = themed-assets supported |
setSupportsThemeAssets:YES |
iOS CarKit method that exposes altScreen once bit 21 is set |
MainScreenDictCreate / AltScreenDictCreate |
libairplay helpers building the two-screen /info response |
ScreenDictSetViewAreas |
libairplay helper attaching view geometry to a screen dict |
| Stream type 111 | AirPlay isochronous stream type number reserved for AltScreen video |
Feature bit 26 (0x04000000) |
AirPlay feature flag in /info declaring AltScreen capability |
CopyDisplaysInfo |
libairplay function building the AirPlay /info displays section |
| Cinemo SDK | Pre-2020 third-party iAP2 stack used by MHI2Q |
Single sequence diagram showing the actual function calls and message exchanges during AltScreen negotiation. Both head-unit variants share the same iOS code path; the differences are in two specific HU components (Cinemo iAP2 SDK and libairplay).
Color coding inside the diagram:
- Green
rect= MHI3-grade (modern SDK + libairplay 450.x) - passes - Red
rect= MHI2Q (Cinemo pre-2020 + libairplay 210.81) - fails - No tint = shared protocol layer that runs identically on both
flowchart TB
iOS([USB enumerate -> iAP2 link layer up -> Identification msg 0xEA00<br/>iOS-side gates 0,1,2,4 pre-pass on iPhone 12+])
iOS --> P0
subgraph P0 [Phase 0 - Discovery: Bonjour TXT srcvers]
direction LR
p0_mhi["<b>MHI3</b> srcvers=450.14.2<br/>(analytics only - NOT gated)"]:::ok
p0_mu["<b>MHI2Q</b> srcvers=210.81<br/>(analytics only - NOT gated)"]:::neut
p0_mhi ~~~ p0_mu
end
P0 --> P1
subgraph P1 [Phase 1 - iAP2 Identification 0xEA00: HU advertises themed-assets via subparam 17]
direction LR
p1_mhi["<b>MHI3 PASSES (Gate 3)</b><br/><br/>libesoiap2.so::iap2:: <br/>CIAP2ControlSessionModuleIdentMsgComposer:: <br/>identificationInformation @ 0x4F9A0<br/><br/>Loc2 @ 0x519C4 - Param 21<br/>(WirelessCarplayTransportComponent):<br/>subparam_alloc(buf, 0) /* sub_4A400 */<br/>buf[0] = void_marker_vtable /* off_A64E8 */<br/>subparam_emit(buf, 17) /* sub_48370 */<br/><br/>Wire bytes: 00 04 00 11<br/>(len=4, id=0x11, no payload)<br/><br/>iOS CoreAccessories<br/>_parseIdentificationParams_3<br/>case 17: iAP2MsgIsDataVoid -> v414=1<br/>output[131] = 1<br/><br/>iOS CoreAccessories<br/>iap2_identification_isIdentifiedForThemeAssets<br/>returns *(struct + 131) & 1 = 1<br/><br/>iOS ACCExternalAccessory<br/>caps |= 0x200000 (bit 21)<br/><br/>iOS CarKit/CRVehicleAccessoryManager<br/>[v setSupportsThemeAssets:(caps >> 21) & 1]<br/>= YES<br/><br/>iOS CarKit.mm CRCarPlayFeaturesAsAirPlayFeatures:<br/>bit 0 -> CFArray += @"altScreen""]:::ok
p1_mu["<b>MHI2Q FAILS (Gate 3)</b><br/><br/>No libesoiap2.so present in firmware.<br/>iAP2 logic embedded in dio_manager<br/>(via Cinemo SDK + libNmeSDK.so).<br/><br/>Cinemo capability API exposes ONLY:<br/>- SUPPORTS_IAP2_CONNECTION<br/>- SUPPORTS_CARPLAY<br/>No CMessageParameterWirelessCarplay-<br/>TransportComponent class.<br/>No subparam 10..18 support.<br/>No void_marker_vtable system.<br/><br/>Identify message has no Param 21<br/>structured TLV - just two bools.<br/><br/>iOS CoreAccessories<br/>_parseIdentificationParams_3<br/>case 17 path NEVER hit<br/>output[131] = 0<br/><br/>iOS CoreAccessories<br/>iap2_identification_isIdentifiedForThemeAssets<br/>returns 0<br/><br/>iOS ACCExternalAccessory<br/>caps bit 21 stays 0<br/><br/>iOS CarKit/CRVehicleAccessoryManager<br/>setSupportsThemeAssets: NO<br/><br/>iOS CarKit.mm CRCarPlayFeaturesAsAirPlayFeatures:<br/>@"altScreen" NEVER added to CFArray"]:::bad
p1_mhi ~~~ p1_mu
end
P1 --> P2
subgraph P2 [Phase 2 - AirPlay RTSP SETUP: HU echoes enabledFeatures + handles AltScreen stream type]
direction LR
p2_mhi["<b>MHI3 PASSES (Gates 5+6)</b><br/><br/>libairplay 450.14.2:<br/><br/>AirPlayCopyServerInfo @ 0x4F900<br/>builds /info CFDict with:<br/>- displays array (Main + Alt)<br/> via AirPlayAltScreenDictCreate @ 0x5F860<br/> + AirPlayInfoArrayAddScreen @ 0x5FEC0<br/>- AirPlayScreenDictSetViewAreas @ 0x5F990<br/> per-screen geometry<br/>- features |= bit 26 (0x04000000)<br/>- streamTypes contains 111<br/><br/>iOS sends RTSP SETUP with proposed<br/>features incl @"altScreen".<br/><br/>libairplay SessionSetup builds response<br/>CFDict with key @"enabledFeatures":<br/>CFArray = [@"altScreen", @"viewAreas",<br/>@"uiContext", @"cornerMasks",<br/>@"focusTransfer"]<br/><br/>(rodata @ 0x115478 for key,<br/>0x1153D0..0x115418 for values)<br/><br/>iOS AirPlaySender/Activate<br/>CFArrayContainsValue(arr, @"altScreen")<br/>-> derived[63] = 1 (altScreen enabled)<br/><br/>Stream type=111 dispatched to AltScreen<br/>handler -> ScreenStreamProcessData ->><br/>compositor pipeline -> cluster display."]:::ok
p2_mu["<b>MHI2Q FAILS (Gates 5+6)</b><br/><br/>libairplay 210.81 (predates Ferrite):<br/><br/>0 string matches for:<br/>- @"altScreen" / @"enabledFeatures"<br/>- @"viewAreas" / @"cornerMasks"<br/>- @"focusTransfer" / @"uiContext"<br/><br/>0 symbol matches for:<br/>- AirPlayAltScreenDictCreate<br/>- AirPlayInfoArrayAddScreen<br/>- AirPlayScreenDictSetViewAreas<br/>- AirPlayReceiverSessionHasFeatureAltScreen<br/><br/>/info CFDict contains only single<br/>(main) display, no enabledFeatures key<br/>in SETUP response.<br/><br/>iOS AirPlaySender/Activate<br/>CFArrayContainsValue(arr, @"altScreen")<br/>= false (key absent)<br/>derived[63] = 0<br/><br/>libairplay AirPlayReceiverSessionSetup<br/>@ 0x272B8 stream-type switch:<br/>case 100/101: handle audio<br/>case 110: mainscreen_setup /* sub_26F9C */<br/>default: LogInfo("Unsupported stream<br/>type: %d") goto skip<br/><br/>type=111 falls into default ->><br/>"Unsupported stream type: 111" logged.<br/>Even if iOS opened it, refused here."]:::bad
p2_mhi ~~~ p2_mu
end
P2 --> P3
subgraph P3 [Phase 3 - End state]
direction LR
p3_mhi["<b>MHI3 - altScreen ACTIVE</b><br/><br/>iOS opens RTSP RECORD<br/>type=111 AltScreen stream.<br/>Frames flow ScreenStreamProcessData<br/>-> H.264 decode -> Kanzi composite<br/>-> cluster TFT display.<br/><br/>themed CarPlay UI rendered on<br/>instrument cluster's second screen."]:::ok
p3_mu["<b>MHI2Q - altScreen NEVER</b><br/><br/>Phase 1 already broke - altScreen<br/>missing from CarKit feature array<br/>so iOS never proposes it in SETUP.<br/><br/>Phase 2 also broken independently -<br/>even if Phase 1 magically passed,<br/>SETUP response lacks enabledFeatures<br/>and stream 111 handler.<br/><br/>Two independent failures.<br/>No dongle bypasses HU-side stack."]:::bad
p3_mhi ~~~ p3_mu
end
classDef ok fill:#efe,stroke:#0a0,color:#040
classDef bad fill:#fee,stroke:#900,color:#600
classDef neut fill:#eef,stroke:#447,color:#224
style P0 fill:#e8e8e8,stroke:#444,color:#000
style P1 fill:#e8e8e8,stroke:#444,color:#000
style P2 fill:#e8e8e8,stroke:#444,color:#000
style P3 fill:#e8e8e8,stroke:#444,color:#000
Static-analysis sources (offsets/strings cross-referenced):
- MHI3 —
MHI3_ER_AU_P4364_8W0906961DR/libesoiap2.so(629 KB ARM64) +_swup_carplay_aa/extracted_elf_opt/libairplay.so(450.14.2)- MHI2Q —
dio_manager.i64+libNmeSDK.so+ MHI2Q appimglibairplay.so(210.81)- iOS 26.1 —
CoreAccessories.framework,CarKit.framework,AirPlayReceiver.framework,AirPlaySender.framework,carkitdFull file/function map:
docs/altscreen_gate_analysis.md.
The diagram makes the two independent failure points visually explicit:
- Phase 1 fails on MHI2Q because Cinemo's pre-2020 iAP2 SDK
doesn't emit Param 21 / sub17, so iOS never marks the accessory
as themed-assets-capable, and CarKit's feature array never grows
the
"altScreen"string. - Phase 2 fails independently on MHI2Q because libairplay 210.81
has no
MainScreenDictCreate/AltScreenDictCreate/ScreenDictSetViewAreashelpers; even if Phase 1 magically passed, the/inforesponse still lacks the two-screen shape, feature bit 26, and stream type 111 that iOS requires.
A wireless dongle (Carlinkit / U2W) replaces only the iPhone-side
iAP2 endpoint. The HU's Cinemo SDK and libairplay 210.81 stay the
same - both red phases still fail. No dongle, present or future,
will turn the red rects green on this generation of HU.
| Layer | iOS expects | MHI2Q (broken) | MHI3-grade (works) |
|---|---|---|---|
| Gates 1-3 (iAP2 Identification) - Cinemo SDK | Param 21 + subparam 17 | pre-2020 SDK, no Param 21 | SDK >= 2020, emits Param 21 |
| Gates 4-6 (AirPlay /info) - libairplay | Alt/Main dict helpers, stream 111, feature bit 26 | 210.81, flat dict only | 450.14.2, full helpers |
| Wireless dongle viable? | n/a | NO - same stack still fails | yes - wireless == wired here |
-
QNX SDP 6.5 - needed for cross-compiling C hook and renderer to ARM32 QNX. Easiest way is a QNX VM with the toolchain accessible over SSH. QNX VM image: https://archive.org/details/qnxsdp-65.7z (or any other QNX SDP 6.5 VM image available online). Set up SSH on the VM and configure the IP in the build scripts.
-
GLFW 3 (macOS only) - needed for local renderer development/testing. Install via
brew install glfw. -
lsd.jar - original HU classes, needed to compile the Java patch against. Extract from your firmware dump:
- Get
/mnt/app/eso/hmi/lsd/lsd.jxefrom your firmware - Convert with jxe2jar (
../jxe2jar) - Output lands in
jxe2jar/out/lsd.jar(default path used bybuild_java.sh)
- Get
Cross-compiles on the QNX VM via SSH. Set QNX_VM IP in compile_hook.sh.
./compile_hook.shOutput: ./libcarplay_hook.so
Build flags:
| Flag | Default | Description |
|---|---|---|
LOG |
1 |
Enable/disable all logging |
LOG_RGD_PACKET_RAW |
0 |
Full raw RGD packet dump (requires LOG=1) |
# default (logging on)
./compile_hook.sh
# silent build
LOG=0 ./compile_hook.sh
# verbose packet logging
LOG=1 LOG_RGD_PACKET_RAW=1 ./compile_hook.shFor QNX (deployment): Cross-compiles on the QNX VM via SSH. Set QNX_VM IP in compile_render_qnx.sh.
./compile_render_qnx.shOutput: ./maneuver_render
To build with the debug grid overlay:
./compile_render_qnx.sh gridFor macOS (local development): Builds natively with GLFW + OpenGL 2.1. Requires brew install glfw.
make -C c_renderOutput: c_render/c_render (renderer) + c_render/test_harness (test client)
The macOS build also compiles a test harness - see Testing the renderer locally below.
Requires lsd.jar (see Prerequisites). Check paths in build_java.sh.
./build_java.shOutput: ./carplay_hook.jar
All classes are compiled with -source 1.2 -target 1.2 for MHI2Q JVM compatibility. The bundled JDK from jxe2jar is used automatically.
https://github.com/luka-dev/mib2q-carplay-rgi/raw/main/docs/test_manuver_render.mov
The macOS build includes a test harness (c_render/test_harness) that sends TCP commands to the renderer, letting you cycle through all maneuver types and verify animations without a real device.
Run both in parallel:
cd c_render
./c_render & ./test_harnessThe renderer window opens and the harness connects to 127.0.0.1:19800.
Harness controls:
| Key | Action |
|---|---|
| Left / Right | Cycle through maneuver presets (turns, roundabouts, U-turns, merges, arrivals, etc.) |
| R | Send a random maneuver with random angle, junction streets, and bargraph |
| P | Toggle perspective / orthographic view |
| D | Toggle debug grid overlay |
| Space | Save screenshot (PPM) |
| S | Toggle sidescreen / popup viewport mode |
| Q / Esc | Quit |
The harness auto-cycles through a built-in preset list that covers all icon types with varying directions, exit angles, junction configurations, and bargraph levels. Each arrow key press sends a CMD_MANEUVER packet triggering a push transition.
./compile_hook.sh- buildlibcarplay_hook.so./compile_render_qnx.sh- buildmaneuver_render./build_java.sh- buildcarplay_hook.jarmkdir -p /mnt/app/root/hookson device- Copy
libcarplay_hook.soto/mnt/app/root/hooks/,chmod 755 - Copy
maneuver_renderto/mnt/app/root/hooks/,chmod 755 - Copy
c_render/resources/flag_atlas.rgbato/mnt/app/root/hooks/ - Copy
carplay_hook.jarto/mnt/app/eso/hmi/lsd/jars/ - Set
LD_PRELOADinsmartphone_integrator.json - Add
0x5200/01/02/03/04IDs indio_manager.json - Reboot (wait 30s after file changes)
The hooks directory does not exist by default. Create it on the device:
mkdir -p /mnt/app/root/hooks
chmod 755 /mnt/app/root/hookscp libcarplay_hook.so /mnt/app/root/hooks/
chmod 755 /mnt/app/root/hooks/libcarplay_hook.soRuntime log (resets on each reboot): /tmp/carplay_hook.log
File: /mnt/system/etc/eso/production/smartphone_integrator.json
Add LD_PRELOAD under $.children.carplay.envs:
"carplay": {
"exec": "dio_manager",
"envs": [
"LD_LIBRARY_PATH=/mnt/app/root/lib-target:/eso/lib:/mnt/app/usr/lib:/mnt/app/armle/lib:/mnt/app/armle/lib/dll:/mnt/app/armle/usr/lib",
"IPL_CONFIG_DIR_DIO_MANAGER=/etc/eso/production",
"LD_PRELOAD=/mnt/app/root/hooks/libcarplay_hook.so"
]
}File: /mnt/system/etc/eso/production/dio_manager.json
Add these iAP2 message IDs to the Cinemo SDK's Identify registration:
MessagesSentByAccessory - add:
0x5200=StartRouteGuidanceUpdates0x5203=StopRouteGuidanceUpdates
MessagesReceivedFromDevice - add:
0x5201=RouteGuidanceUpdate0x5202=RouteGuidanceManeuverUpdate0x5204=LaneGuidanceInformation
Why this AND the C hook patches the Identify message: Cinemo's
SDK has the message-ID list compiled into its MessagesSentByAccessory
/ MessagesReceivedFromDevice arrays - the JSON edit registers the
RGD message IDs so the SDK actually pumps them. The C hook
(rgd_identify_patcher in c_hook/routeguidance/rgd_hook.c)
separately patches the EAGroupComponent declaration in the
outgoing Identify message at runtime - adds component 0x001E so iOS
recognises the accessory as a navigation component and starts sending
RG payloads. Both are required: without the JSON edit iOS sends RG
but the SDK drops it; without the hook patch iOS doesn't even start
sending. The runtime patch shows up in the log as
Identify patched: 535 -> 621 bytes.
Example (before):
"MessagesSentByAccessory": [
"0x5000", "0x5002", "0xAE00", "0xAE02", "0xAE03",
"0x4154", "0x4156", "0x4157", "0x4159",
"0xFFFB", "0x4C00", "0x4C02", "0x4C03", "0x4C05"
],
"MessagesReceivedFromDevice": [
"0x4E09", "0x4E0A", "0x4E0C", "0x5001", "0xAE01",
"0x4155", "0x4158", "0xFFFA", "0xFFFC", "0x4C01", "0x4C04"
]After:
"MessagesSentByAccessory": [
"0x5000", "0x5002", "0xAE00", "0xAE02", "0xAE03",
"0x4154", "0x4156", "0x4157", "0x4159",
"0xFFFB", "0x4C00", "0x4C02", "0x4C03", "0x4C05",
"0x5200", "0x5203"
],
"MessagesReceivedFromDevice": [
"0x4E09", "0x4E0A", "0x4E0C", "0x5001", "0xAE01",
"0x4155", "0x4158", "0xFFFA", "0xFFFC", "0x4C01", "0x4C04",
"0x5201", "0x5202", "0x5204"
]cp carplay_hook.jar /mnt/app/eso/hmi/lsd/jars/cp maneuver_render /mnt/app/root/hooks/
cp c_render/resources/flag_atlas.rgba /mnt/app/root/hooks/
chmod 755 /mnt/app/root/hooks/maneuver_renderThe flag atlas (flag_atlas.rgba, source path c_render/resources/flag_atlas.rgba) must end up reachable from the renderer's working directory. The renderer searches in this order: resources/flag_atlas.rgba, flag_atlas.rgba, <binary_dir>/resources/flag_atlas.rgba, <binary_dir>/flag_atlas.rgba. Placing it next to the binary (as above) hits the second/fourth path.
Launched automatically by the Java bridge (BAPBridge) when CarPlay route guidance starts. Log: /tmp/maneuver_render.log.
Reboot the infotainment system. Give it 30+ seconds after file changes before rebooting - otherwise changes may not be flushed to disk.
Maneuver icon testing. The iAP2-to-BAP maneuver mapping covers all 54 CarPlay maneuver types, but has only been tested on a limited set of real-world routes. Edge cases like complex interchanges and multi-lane roundabouts need more road testing. If you see a wrong or missing icon on the HUD, a log from /tmp/carplay_hook.log of the exact moment + explanation of what's wrong would help. Note: the log resets on each device reboot, so grab it before restarting.
The hook automatically logs unrecognised iAP2 RGD-family messages as
[HOOK] Unknown 0x52xx msgid=0xNNNN dir=IN len=N followed by a hex
dump - these are the most useful starting point if iOS sends a
maneuver type we don't handle yet.
Thanks for the previous work and knowledge that helped to figure this out.