.. # Copyright (c) 2026, Arm Limited. # # SPDX-License-Identifier: Apache-2.0 ######################################## CAN and CARLA implementation plan ######################################## Corrected dual-runtime plan for classic CAN from the Safety Island into a CARLA ego. This page is the implementation source of truth for the stacked work in https://github.com/autowarefoundation/autoware-safety-island/issues/43. It supersedes the original open-loop sketch in :doc:`can_carla_integration` where the two disagree. The placeholder frame contract in :doc:`can_output` is unchanged: IDs ``0x100`` / ``0x101`` / ``0x102``, classic 8-byte DLC, existing scaling. .. mermaid:: graph TB Encoder[common encoder
0x100 0x101 0x102] Batch[complete 3-frame batch] Encoder --> Batch Batch --> POSIX[freertos-posix
nonblocking PF_CAN] Batch --> FVP[zephyr-fvp TAP
one UDP batch datagram] Batch --> S32Z[zephyr-s32z
native zephyr,canbus] POSIX --> VCAN[SocketCAN vcan0] FVP --> TAP[LAN91C111 tap0] TAP --> GW[host UDP-to-CAN gateway] GW --> VCAN S32Z --> HW[physical CAN bus] VCAN --> Decoder[shared assembler
commit sequence watchdog] Decoder --> CARLA[VehicleAckermannControl] ********************** Why this plan ********************** The original PR 1 design assumed ``freertos-posix`` plus ``vcan`` only, with the in-memory mock as the unset-interface fallback, and treated Zephyr FVP as unreachable. Cross-check against the current code and issue #43 found that that sketch cannot ship as written: - ``platform_can.h`` dispatches every FreeRTOS target through one header. Linux ``PF_CAN`` must never enter the S32Z2/X5H ``arm-none-eabi`` builds. - The mock always reports successful ``can_init()``. If it remains the runtime fallback, ``CAN_ONLY`` cannot fail-fast and commands disappear into memory. - ``freertos-posix`` currently defaults to ``DDS_AND_CAN`` only to exercise that mock. Once the mock is test-only, the unconfigured runtime default must be ``DDS_ONLY``. - Sequence lives only in ``0x102``. One UDP datagram per CAN frame lets loss or reordering pair stale ``0x100`` / ``0x101`` data with a later commit. - Existing CAN CI records mock frames. It does not create ``vcan0``, send SocketCAN, or run a decoder. Creating ``vcan0`` needs ``CAP_NET_ADMIN``; FVP TAP also needs ``/dev/net/tun``. That path is not privilege-free. - Autoware steering is CCW/left-positive. CARLA Ackermann steering is right-positive. Velocity and steering rate must not be discarded. - ``0x102`` is the only cycle marker. The decoder must require fresh ``0x100`` and ``0x101``, wrapping sequence (accept forward jumps, reject replays and stale values), and a receive-timeout safe stop. Zephyr FVP still has no host-reachable CAN IP. The existing TAP Ethernet path (``192.168.10.2`` on FVP, ``192.168.10.1`` on the host) is the only virtual way to reach the host. Native ``zephyr-s32z`` already speaks real CAN and stays on that path. ********************** Shared decisions ********************** - **Actuation is CAN.** The host talks to CARLA through the Python API, not by republishing ROS control commands. - **DDS inputs stay DDS.** Trajectory, odometry, steering, acceleration, and operation mode are unchanged. - **One sink.** Open-loop and closed-loop share the same ``vcan0`` decoder and CARLA process. Closed-loop does not add a second command path through ``autoware_carla_interface``. - **PR 1 (#49) local runtime is** ``freertos-posix`` **plus** ``vcan``. That matches issue #43 / PR #49 acceptance. - **PR 3 is Zephyr FVP TAP UDP** (open-loop and closed-loop). It reuses the same ``vcan0`` decoder. Native FVP CAN stays loopback-only. - **Native Zephyr S32Z is unchanged.** It keeps ``zephyr,canbus`` / ``&can0``. Hardware CAN into the host is out of this stack. - **Embedded FreeRTOS CAN is out of this stack.** ``freertos-s32z2`` and ``freertos-x5h`` stay ``DDS_ONLY``. Selecting ``CAN_ONLY`` or ``DDS_AND_CAN`` on those targets must fail at configure time. - **CI never launches CARLA.** GitHub Actions covers encode, transport, decode, and watchdog. The visual CARLA loop is a documented host demo. - **Mock is test-only.** It is compiled into ``--can-output-test``, not selected by an unset environment variable. ********************** Common transport contract ********************** Keep ``encode_control_command()`` and the three-frame layout. Change how frames leave the controller. ``ControlCommandCanOutput::send()`` today encodes, then calls ``platform::can_send()`` once per frame while holding a mutex, and increments ``sequence_`` before encoding can fail (``control_command_can_output.hpp``). Replace that with a batch send: 1. Encode the complete command. 2. Call ``platform::can_send_batch(frames, count)``. 3. Increment ``sequence_`` only after the batch succeeds. ``can_send_batch()`` is the platform boundary. Semantics: - Never send ``0x102`` unless ``0x100`` and ``0x101`` were handed to the transport. A later write failure may still leave the data frames visible on ``vcan0``; only the UDP datagram is all-or-nothing. - Sends are nonblocking, or bounded to a short deadline. Do not stall the controller timer callback on SocketCAN or UDP backpressure. - A failed batch leaves ``sequence_`` unchanged so the next successful command stays contiguous. Unit-test this: encode or mid-batch send fails, then the next successful command reuses the same sequence. Keep ``platform::can_send(frame)`` only where a backend truly is frame-at-a-time (native Zephyr CAN, the test mock). The POSIX SocketCAN and FVP UDP backends implement the batch entry point directly. ********************** Decoder and CARLA mapping ********************** The host's Python decoder in ``demo/can_carla_bridge/decoder.py`` is the only assembler. The Safety Island encoder remains C++; both runtimes inject classic frames onto ``vcan0`` and the bridge never sees UDP. Assembly ======== - ``0x102`` is the commit marker. Do not apply a command until a commit arrives. - Require a fresh ``0x100`` and a fresh ``0x101`` since the previous accepted commit. Duplicate IDs in the same cycle replace the pending frame; they do not count as a second cycle. - Sequence is the little-endian ``uint16`` at bytes 2-3 of ``0x102``. Accept the next expected value and forward jumps, wrapping modulo-65536 (including 65535 to 0). A forward jump resyncs the expected sequence and the watchdog anchor. Reject replays and stale sequences (wrapping signed delta ``< 0``). - On first command after start or after a watchdog safe-stop, accept the first valid complete cycle as the baseline. Do not require sequence 0. - After a rejected commit, drop pending ``0x100`` / ``0x101``. - Unknown CAN IDs are ignored and do not affect the cycle. - IDs ``0x100`` / ``0x101`` / ``0x102`` with DLC other than 8, extended ID, or non-classic flags are discarded: that ID is not fresh. They do not themselves trigger safe-stop. Watchdog ======== - Use host monotonic receive time, not the payload timestamp. ``timestamp_ms_modulo()`` is producer metadata. FreeRTOS POSIX uses host epoch time; Zephyr FVP defaults to time since boot. Those clocks are not comparable. - If no accepted commit arrives within the configured timeout, apply a safe stop and hold it until a new valid cycle is accepted. - POSIX default timeout is 0.5 s, matching controller ``timeout_thr_sec`` and covering a skipped 150 ms tick (~300 ms to the next frame). Do not use 200 ms: that is tighter than one missed period. - FVP is not real-time. The FVP demo and TAP integration job use a much larger configured timeout; they do not share the 0.5 s POSIX default. CARLA boundary ============== Autoware ``Lateral.idl`` is CCW/left-positive. CARLA Ackermann steering is right-positive. Use ``carla.VehicleAckermannControl``, not ``VehicleControl``. CARLA 0.9.16 ``AckermannController`` does not treat zeros as "hold": - ``Abs(steer_speed) < 0.001`` snaps steer to the target immediately. - ``Abs(acceleration) < 0.0001`` clips the speed PID with the vehicle's max accel/decel, not "no acceleration". - The controller takes ``Abs(steer_speed)``, so negating Autoware's rate does not reverse steering direction. Sign conversion is only ``steer = -steering_tire_angle``. Apply: - ``steer = -steering_tire_angle`` - ``steer_speed = abs(steering_tire_rotation_rate)`` when the rate flag is set. If unset, use a configured slew (not 0) so CARLA does not snap. - ``speed = velocity`` - ``acceleration`` when the acceleration flag is set. If unset, use a configured magnitude (not 0). - ``jerk = 0``: the current frames do not carry a scaled jerk field. Safe stop: ``speed = 0``, a configured braking acceleration (not 0), hold last steer, and a configured slew. Do not command ``steer = 0, steer_speed = 0`` (that snaps wheels to center). Constants remain bridge configuration, defaulted to the Autoware sample-vehicle ballpark. This is not an OEM vehicle model. ********************** FreeRTOS POSIX ********************** Backend isolation ================= ``platform_network.h`` already special-cases ``PLATFORM_FREERTOS_S32Z2`` and ``PLATFORM_FREERTOS_X5H`` before the generic FreeRTOS header. ``platform_can.h`` must do the same: - ``actuation_module/freertos/CMakeLists.txt`` defines ``PLATFORM_FREERTOS_POSIX=1`` in addition to ``PLATFORM_FREERTOS``. - ``platform_can.h`` includes a POSIX-only SocketCAN header when ``PLATFORM_FREERTOS_POSIX`` is set, a test mock when the CAN-output-test target is built, and keeps embedded targets off Linux ``PF_CAN``. - ``freertos-s32z2`` and ``freertos-x5h`` CMake must ``FATAL_ERROR`` if ``CONFIG_CONTROL_CMD_OUTPUT_MODE`` is not ``DDS_ONLY``. SocketCAN backend ================= - Runtime CAN output requires ``SAFETY_ISLAND_CAN_IFACE`` (for example ``vcan0``). Missing, empty, or unopenable interface: ``can_init()`` returns false. ``CAN_ONLY`` then exits in ``controller_node.cpp``; ``DDS_AND_CAN`` logs and continues on DDS. - Open ``PF_CAN`` / ``SOCK_RAW`` / ``CAN_RAW``, bind to the named interface, set ``O_NONBLOCK``. - ``can_send_batch()`` writes ``0x100``, ``0x101``, then ``0x102``. If a write fails before the commit, do not send ``0x102``. - Default ``CONFIG_CONTROL_CMD_OUTPUT_MODE`` for ``freertos-posix`` becomes ``DDS_ONLY``. Demo and integration builds pass ``--control-output CAN_ONLY`` or ``DDS_AND_CAN``. Mock backend ============ Compile the in-memory recorder only into ``BUILD_TEST=4`` (``--can-output-test``). Keep ``reset_recorded_can_frames()`` and friends behind that target so the existing encoder and recording assertions stay privilege-free. ********************** Zephyr FVP UDP tunnel ********************** Native FVP CAN remains ``zephyr,can-loopback`` and is still the ``--can-output-test`` path. It cannot reach the host. The CARLA path is a separate transport over the existing TAP network. Selection ========= Do not key both Zephyr backends on ``PLATFORM_ZEPHYR`` alone. ``zephyr_can.h`` requires ``chosen { zephyr,canbus }`` and must stay the S32Z / loopback-test backend. Today ``CONTROL_CMD_OUTPUT_CAN_ONLY`` and ``CONTROL_CMD_OUTPUT_DDS_AND_CAN`` both ``select CAN``. ``zephyr_can.h`` then ``#error``s unless ``chosen { zephyr,canbus }`` exists. FVP TAP has no canbus node unless the loopback overlay is applied, so a transport choice that "does not select CAN" is not enough: the output-mode ``select`` still pulls in the CAN stack. Change Kconfig: - Remove ``select CAN`` from the output-mode choice. - ``CONFIG_CONTROL_CMD_CAN_TRANSPORT_NATIVE`` (default) ``select CAN`` and is the current ``zephyr,canbus`` path. - ``CONFIG_CONTROL_CMD_CAN_TRANSPORT_UDP_TUNNEL`` selects ``NET_UDP`` / ``NET_IPV4``, does not ``select CAN``, and does not require ``zephyr,canbus``. - ``platform_can.h`` includes ``zephyr_can_udp_tunnel.h`` vs ``zephyr_can.h`` from the transport choice so the native header is not compiled on the TAP path. ``build.sh --platform zephyr-fvp --network tap --control-output CAN_ONLY`` (or ``DDS_AND_CAN``) applies a new conf fragment ``fvp_baser_aemv8r_smp_can_udp_tunnel.conf`` on top of ``fvp_baser_aemv8r_smp_tap_network.conf``. Reject the UDP tunnel without ``--network tap``. Extend ``--control-output`` to ``zephyr-fvp``; keep it invalid for ``zephyr-s32z`` in this stack. Wire format =========== One datagram is one command. Do not serialize ``CanFrame`` as a C++ object: native endianness, ``bool``, and padding are not a protocol. Fixed little-endian layout, exact length 48 bytes: .. list-table:: :header-rows: 1 :widths: 15 15 70 * - Offset - Size - Field * - 0 - 2 - magic bytes ``[0]=0x43, [1]=0x54`` (ASCII ``CT``). Do not encode this as little-endian ``uint16`` ``0x4354`` (that is ``TC`` on the wire). * - 2 - 1 - version ``1`` * - 3 - 1 - flags (bit 0 = classic batch; other bits reserved 0) * - 4 - 2 - batch sequence (same value encoded in ``0x102``) * - 6 - 2 - reserved 0 * - 8 - 13 - frame 0: ``uint32`` id, ``uint8`` dlc, 8 data bytes * - 21 - 13 - frame 1: same * - 34 - 13 - frame 2: same * - 47 - 1 - reserved 0 Frame order in the datagram is ``0x100``, ``0x101``, ``0x102``. The gateway rejects any other length, magic, version, flags, DLC, ID, or frame order, and does not inject a partial cycle. Sender ====== - New header ``zephyr_can_udp_tunnel.h``, selected by the transport choice. - ``can_init()`` creates a UDP ``net_context`` (or BSD socket) to ``CONFIG_CONTROL_CMD_CAN_TUNNEL_PEER`` default ``192.168.10.1`` and ``CONFIG_CONTROL_CMD_CAN_TUNNEL_PORT`` default ``5555``. - ``can_send_batch()`` packs the 48-byte datagram and sends once, nonblocking. Do not retry inside the controller callback. - ``main.cpp`` currently ignores ``configure_network()`` failures. For the UDP tunnel, a failed TAP/IP bring-up must exit before ``can_init()``. The TAP conf already sets ``CONFIG_NET_IFACE1_ADDR="192.168.10.2"``. Host gateway ============ ``demo/can_tunnel_bridge/`` binds UDP ``192.168.10.1:5555``, validates a complete datagram, and injects the three classic frames onto ``vcan0`` in order. If SocketCAN injection fails before ``0x102``, do not send the commit. This process is not the CARLA bridge; it only creates the same SocketCAN stream that ``freertos-posix`` writes natively. ********************** Host CARLA bridge ********************** ``demo/can_carla_bridge/`` stays on ``vcan0`` via ``python-can``. It does not know which runtime produced the frames. - Receive classic frames. - Run the assembler, sequence check, and watchdog above. - Map to ``VehicleAckermannControl`` and apply through the CARLA 0.9.16 client, same version as Open AD Kit. - Inputs to the Safety Island still come from the existing rosbag or a canned DDS publisher on domain 2. Open-loop does not start planning simulator or Open AD Kit. ********************** Work items ********************** Land in this order so each step is testable without CARLA. PR 1 (#49) — ``freertos-posix`` + ``vcan`` ========================================== 1. **Common batch API and sequence fix** ``can_send_batch()``, increment sequence only on success, keep per-frame ``can_send()`` as a backend helper. Test: failed encode or mid-batch send does not advance ``sequence_``. 2. **POSIX backend isolation** ``PLATFORM_FREERTOS_POSIX``, test-only mock, SocketCAN source that cannot compile into S32Z2/X5H, ``DDS_ONLY`` default, CMake ``FATAL_ERROR`` on embedded CAN modes. 3. **Host decoder** Use the Python decoder directly in the bridge and in the SocketCAN roundtrip test against the C++ sender. Cover startup, duplicates, missing/reordered frames, bad DLC, wrap ``65535 -> 0``, ignored unknown IDs, forward-jump accept, stale/replay reject, timeout safe-stop, and re-baseline after timeout. 4. **FreeRTOS ``vcan`` integration** Privileged test: create ``vcan0``, set ``SAFETY_ISLAND_CAN_IFACE``, send a real command, decode on the host. No CARLA. 5. **CARLA bridge (POSIX demo)** Ackermann mapping, 0.5 s watchdog, documented host demo for ``freertos-posix``. 6. **Docs** Keep this page, :doc:`can_carla_integration`, and :doc:`can_output` aligned. Closed-loop is PR 2. Zephyr FVP is PR 3. CAN-FD is opt-in (PR 4). PR 2 (#50) — Open AD Kit closed-loop ==================================== Autoware planning against CARLA, Safety Island as the controller, actuation still over classic CAN into the same ``vcan0`` bridge as PR 1. Autoware plus CARLA compose lives in Open AD Kit (``deployments/safety-island-carla-simulation/``, PR #146), including the domain-bridge. This repository keeps the SI binary, ``vcan0``, and ``demo/can_carla_bridge``. 7. **Sensors-only CARLA interface** ``autoware_carla_interface`` has no upstream sensor-only flag. ``SensorLoop`` calls ``ego_actor.apply_control()`` every tick. Overlay Open AD Kit's ``overlay/patch_sensors_only.py`` skips that call so the CAN bridge is the sole CARLA driver. Remap ``input_control_cmd`` / ``output_actuation_cmd`` off the live topics. 8. **Stub Autoware follower** Open AD Kit mounts its stub ``control.launch.xml`` into the ``control`` service, same pattern as the planning-simulator demo. 9. **SI ``CAN_ONLY`` and domain-bridge** Build ``freertos-posix --control-output CAN_ONLY``. Bridge the five DDS inputs domain 1 → 2. Do not rely on ``control_cmd`` 2 → 1. Closed-loop CycloneDDS pins both domains to ``lo`` (Open AD Kit default). 10. **Host CAN-CARLA bridge** Same ``demo/can_carla_bridge/`` as PR 1. Pass ``--ego-role ego_vehicle`` matches Open AD Kit. ``VehicleAckermannControl`` mapping unchanged. 11. **Pins and topic contract** Open AD Kit's deployment ``config.env`` pins CARLA and component images by digest. Record the Open AD Kit and Safety Island commits when you run the demo. Its privilege-free tests check the five-input topic matrix against ``bridge-config.yaml``. 12. **Docs** Closed-loop launch is two-repo. CI never starts CARLA or Open AD Kit. PR 3 (#53) — Zephyr FVP TAP tunnel ================================== Open-loop and closed-loop with ``zephyr-fvp --network tap``. Same ``vcan0`` decoder as PR 1 / PR 2. 13. **Zephyr UDP transport and gateway** Kconfig as above (output modes do not ``select CAN``), TAP conf fragment, ``build.sh`` flags, 48-byte pack/unpack unit tests, host gateway. 14. **Zephyr FVP TAP integration** Run FVP with hostbridge TAP, gateway, ``vcan0``, same decoder, FVP watchdog timeout. No CARLA. 15. **CARLA bridge (FVP demo)** Same bridge as PR 1, documented TAP + gateway host demo. PR 4 (#54) — CAN-FD (opt-in) ============================== One 24-byte ``0x103`` frame replaces the classic batch on the same ``vcan0`` path. Classic stays the default; Zephyr, the FVP tunnel, and S32Z hardware stay classic. FD is not a second command path. 16. **FD frame and encoder** ``CanFdFrame`` (64-byte capacity, byte ``length``, ``brs``) plus ``encode_control_command_fd()``, which packs the classic payloads without duplicating scaling. Test: frame id, length 24, BRS off, payload offsets. 17. **Opt-in selection and bridge decode** ``SAFETY_ISLAND_CAN_FORMAT=fd`` on POSIX SocketCAN (MTU below 72 fails init) and ``bridge.py --can-format fd``. The decoder gains ``feed_fd()`` and reuses the commit, replay, and watchdog rules. ********************** Tests and CI ********************** Privilege-free (existing job, no extra capabilities) ==================================================== - Encoder payload, mode helpers, non-finite rejection. - PR 4: FD encoder frame id, byte length, BRS off, and payload offsets. - Mock recorder on ``freertos-posix --can-output-test``. - Failed encode or mid-batch send does not advance ``sequence_``. - Zephyr FVP ``zephyr,can-loopback`` on ``--can-output-test``. - Python decoder state-machine unit tests, including wrap, ignored unknown IDs, forward-jump accept, stale/replay reject, and timeout. - PR 3: UDP datagram pack/unpack and rejection of malformed lengths. Privileged integration (explicit capabilities) ============================================== Do not fold this into the unprivileged container job. On CI, load ``vcan`` on the runner host and run the test with host networking; fail if ``vcan0`` cannot be created. Local runs may skip (exit 77) when the kernel has no ``vcan``. - PR 1: FreeRTOS POSIX ``ip link add vcan0 type vcan``, ``SAFETY_ISLAND_CAN_IFACE=vcan0``, encode → SocketCAN → decode. Watchdog: stop the producer and assert safe-stop (0.5 s). - PR 3: Zephyr FVP TAP ``tap0`` at ``192.168.10.1/24``, FVP at ``192.168.10.2``, gateway → ``vcan0`` → same decoder, with the FVP timeout. - PR 4: the same ``vcan0`` roundtrip also sends one CAN-FD ``0x103`` frame (``SAFETY_ISLAND_CAN_FORMAT=fd``) and decodes it. Never launch CARLA, a GPU job, or an Open AD Kit compose in GitHub Actions. The Open AD Kit companion PR tests the sensors-only overlay and five-input topic matrix; this repository tests ego ``role_name`` in the bridge. ********************** Known limits ********************** - Placeholder CAN IDs, not a production DBC. - Open-loop CARLA motion will not match rosbag odometry. - FVP CARLA path is CAN-over-UDP, not classic CAN end-to-end. No arbitration, error frames, or bit timing. Message semantics (ID, DLC, data) are preserved. - Jerk is flagged in ``0x102`` but not carried as a scaled field. - ``zephyr-s32z`` hardware CAN to a host USB-CAN adapter is possible and out of this stack. - ``freertos-s32z2`` / ``freertos-x5h`` CAN is out of this stack.