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 CAN and CARLA integration where the two disagree.

The placeholder frame contract in CAN output is unchanged: IDs 0x100 / 0x101 / 0x102, classic 8-byte DLC, existing scaling.

graph TB Encoder[common encoder<br/>0x100 0x101 0x102] Batch[complete 3-frame batch] Encoder --> Batch Batch --> POSIX[freertos-posix<br/>nonblocking PF_CAN] Batch --> FVP[zephyr-fvp TAP<br/>one UDP batch datagram] Batch --> S32Z[zephyr-s32z<br/>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<br/>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:

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, CAN and CARLA integration, and 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.

  1. 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.

  2. Stub Autoware follower Open AD Kit mounts its stub control.launch.xml into the control service, same pattern as the planning-simulator demo.

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

  4. Host CAN-CARLA bridge Same demo/can_carla_bridge/ as PR 1. Pass --ego-role ego_vehicle matches Open AD Kit. VehicleAckermannControl mapping unchanged.

  5. 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.

  6. 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.

  1. 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.

  1. Zephyr FVP TAP integration

Run FVP with hostbridge TAP, gateway, vcan0, same decoder, FVP watchdog timeout. No CARLA.

  1. 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.

  1. 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.

  2. 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.