Skip to content

nsengupta/sdv_simulation_1

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

27 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

SDV simulation (Iteration 1) β€” repository overview


πŸ“Œ Note: This is the baseline prototype. For the next evolution of the project, check out the Iteration 2 Repository.


A Rust workspace that prototypes a software-defined vehicle control path: telemetry and actuation on a shared CAN bus, a gateway that translates wire traffic into domain events, and a digital twin that maintains vehicle state, decides when to actuate, and closes the loop when the body ECU acknowledges (or fails).

This is an educational / demonstrator codebase, not a product stack.

Companion narrative: blog post Prototyping a Software Defined Vehicle β€” Stage 2 (Prototype-Software-Defined-Vehicle-2 in the author’s blog source) β€” walks from the earlier two-process model (gateway + emulator, in-process headlamp) to the bus-backed arrangement documented here.

This README is the repo landing page: what is implemented now, key decisions, limits, and how to run β€” for readers who already know SDV vocabulary (VSS-shaped signals, gateways, digital twins, body ECUs).


What exists in this version

The prototype answers a narrow but realistic question: can we treat the CAN bus as the single nervous system between simulated ECUs and a twin that both consumes telemetry and commands actuators with correlated feedback?

Three processes share Linux SocketCAN (vcan0 by default):

Process Role
emulator Stand-in for powertrain + ambient-light sensing: publishes engine RPM and ambient lux on CAN (~10 Hz).
gateway Owns ingress (read CAN), projection into twin vocabulary, the digital twin runtime, and egress (write headlamp CMD frames).
front_headlamp_actuator Stand-in body ECU: receives CMD, replies with ACK or NACK on the same bus (~150 ms later).

Inside the gateway, the twin is not a loose script: it is a VirtualCarActor (mailbox, single-threaded handling) driving an FSM plus an orthogonal lighting sub-state in VehicleContext. Outcomes are visible on stdout β€” a deliberate stand-in for a dashboard or cloud stream (state transitions, actuation intents, alerts, headlamp correlation).

Signals are modeled in a VSS-inspired Rust enum (EngineRpm, AmbientLux, and a decoded-but-unused VehicleSpeed slot for a future observed-speed ECU). Payload layout and headlamp wire kinds live in vehicle_device_bus so the gateway stays thin and the actuator binary stays independent.

Demo screenshots

Still frames from a live three-process run on vcan0 (emulator, gateway, front-headlamp actuator). Both show the same gateway stdout surface β€” FSM transitions, RPM/lux telemetry, and (when lighting rules fire) headlamp actuation β€” under different ambient-light conditions.

Daylight band (headlamp off) β€” ambient lux stays above the OFF threshold (LUX_OFF 860). The twin keeps LightingState::Off; the gateway log has no headlamp CMD or ACK/NACK lines.

Gateway run in daylight β€” headlamp off, no actuation traffic

Tunnel / dim band (headlamp on) β€” lux falls through the ON threshold (LUX_ON 840), the twin requests ON, the gateway sends πŸ“€πŸ”† CMD on CAN, and the actuator reply appears as βœ…πŸ’‘ (or βŒπŸ”† / ⏱️ on failure paths).

Gateway run in low lux β€” headlamp ON requested and ACK’d


Current Architecture

Telemetry and commands meet on one virtual CAN interface; the gateway is the only component that speaks both β€œwire” and β€œtwin.”

SDV prototype β€” emulator, gateway (digital twin), front-headlamp actuator, and vcan0

Architecture diagram (Stage 2 blog figure). Local copy: assets/SDV-Blog-Inputs-4.jpg.

Ingress path: CAN frame β†’ VssSignal / headlamp payload β†’ PhysicalCarVocabulary β†’ PhysicalToDigitalProjector β†’ DigitalTwinCarVocabulary β†’ actor β†’ fsm::step.

Egress path: DomainAction (e.g. request headlamp ON) β†’ DefaultActuationManager β†’ ActuationCommand β†’ gateway encodes CMD β†’ CAN β†’ actuator β†’ ACK/NACK β†’ same reader thread β†’ policy correlation β†’ twin ACK/NACK/incomplete events.


Key design decisions

Gateway as orchestrator, not as actuator. Earlier iterations simulated the headlamp inside the gateway over Tokio channels. Here the gateway writes CMD and reads ACK/NACK on CAN, matching how a real SDV stack would separate body ECUs from the service platform.

Twin in common, gateway as runtime shell. FSM rules (transition_map), the step boundary, kinematics, thresholds, and the actor live in common so tests and contracts stay deterministic without sockets. gateway_runtime only wires CAN, timers, and channels.

Layered vocabulary. Physical ingress uses Confirmed/Rejected for actuator outcomes; the FSM uses OnAck/OffAck and ActuationIncomplete (timeout vs negative ACK). That keeps wire semantics separate from twin semantics and mirrors gateway projection patterns you would keep in production.

Speed is derived, not sensed on the bus (yet). The emulator does not publish speed. One composite RPM is scaled to km/h in vehicle_kinematics (rpm Γ— 0.114, simple tire model) on every step. A VehicleSpeed frame (0x101) can be decoded but is rejected at projection until an observed-speed path exists β€” kinematic expectation vs ECU measurement stay explicit.

Operational warning uses OR + AND semantics. Enter ExtremeOperationWarning when derived speed > 160 km/h or when speed > 160 and RPM > 5500 (commuter β€œfast” vs β€œsustained stress”). Recovery needs a 5 s cooldown and both conditions cleared. Buzzer and log lines reflect which threshold fired.

Lighting is orthogonal context, not a top-level FSM state. Lux hysteresis (LUX_ON 840 / LUX_OFF 860, demo-tuned against ~815–885 lux jitter) drives LightingState (Off β†’ OnRequested β†’ On β†’ …) while primary states remain Off / Idle / Driving / ExtremeOperationWarning. Pending states suppress duplicate CMDs; 2 s ACK waits surface timeout recovery on TimerTick.

Device policy in vehicle_device_bus. Correlation (session/sequence) and β€œignore command frames on ingress” live with the codec so adding another actuated device follows the same checklist.

Observability is human-first. Structured logging is a TODO; demo runs rely on timestamped println/eprintln and a small emoji vocabulary in front_headlamp_log (πŸ“€ command, βœ… ACK, ❌ NACK, ⏱️ timeout).


Digital twin: actor + FSM

The twin runtime is VirtualCarActor, a ractor actor β€” not a shared mutable object that the gateway updates directly. The gateway (via VehicleController) sends DigitalTwinCarVocabulary messages to the actor’s mailbox; each message is handled one at a time in handle, which calls the pure fsm::step boundary and then runs side effects.

Why an actor:

Benefit In this repo
Encapsulated state DigitalTwinCar (FsmState + VehicleContext) lives only inside the actor; ingress and timers do not race on context.
Async, ordered handling CAN-derived events, TimerTick, power on/off, and GetStatus RPCs are all messages β€” same queue, deterministic serial processing.
Separation of concerns FSM code stays pure (step, transition_map); the actor owns I/O (actuation channel, logs, optional transition/diagnostic sinks).
Stable platform boundary Gateway speaks vocabulary + ActorRef, not FSM internals β€” closer to how a vehicle service would target a twin API.
Room to grow Child actors, supervision, or backpressure on actuation can attach to the mailbox model without rewriting the FSM.

Mailbox shape: Fsm(FsmEvent) for telemetry and control; GetStatus for snapshot queries (reply port). That keeps the FSM event set small while the runtime vocabulary can extend.

Vehicle states in this prototype

The car’s operational mode is a single primary FSM (FsmState). What you see in logs as Transitioned to … and in cloud sync as VehicleState maps from these four values:

State Meaning
Off Ignition off. Twin does not treat the vehicle as running; derived speed is forced to 0; lighting context is cleared to Off.
Idle Powered on (PowerOn with healthy context), engine at rest β€” not in the β€œdriving” band (RPM ≀ 1000 after the last update).
Driving Powered on and RPM > 1000 while derived speed is non-zero. Normal motion band for the demo.
ExtremeOperationWarning Stress band active: derived speed > 160 km/h and/or speed > 160 km/h with RPM > 5500. Buzzer on; recovers to Driving or Idle after a 5 s cooldown once thresholds clear (TimerTick).

Typical primary flow:

Off ──PowerOn──► Idle ◄──speed=0── Driving
                  β–²                    β”‚
                  β”‚                    β”‚ operational_warning_active
                  └──── speed=0 ────────
                                       β–Ό
                          ExtremeOperationWarning
                                       β”‚
                          (cooldown + thresholds clear)
                                       β–Ό
                                 Driving or Idle

Separate from the four states above, front-headlamp progress is tracked in LightingState inside VehicleContext (not extra top-level FSM states):

Lighting state Meaning
Off Headlamp treated as off; may request ON when lux ≀ LUX_ON_THRESHOLD (840).
OnRequested CMD sent (or queued); waiting for ACK or timeout.
On ACK received; may request OFF when lux β‰₯ LUX_OFF_THRESHOLD (860).
OffRequested OFF CMD in flight; waiting for ACK or timeout.

So at any instant the twin holds one primary mode plus one lighting sub-state β€” e.g. Driving + On while cruising with headlamps confirmed on.

Per-message processing (inside handle)

  1. Apply the event to context (RPM, lux, headlamp ACK/NACK/incomplete, power, timer).
  2. Refresh derived speed from RPM; force speed 0 when ignition is Off.
  3. Transition primary FSM via transition_map and collect FsmActions via output (buzzer, cloud sync, warnings).
  4. Evaluate lighting rules and emit RequestFrontHeadlampOn/Off when thresholds cross.
  5. Handle lighting timeouts on TimerTick.
  6. Execute domain actions (including forwarding actuation commands to the gateway’s CMD channel).

Primary drive logic (simplified): power on β†’ Idle; RPM above idle band β†’ Driving; zero derived speed β†’ Idle; operational thresholds β†’ ExtremeOperationWarning with recovery on heartbeat ticks.


Intentional shortcomings

Area Current choice Why it matters
VSS Local VssSignal enum, not COVESA catalog / databroker Fast iteration; mapping to real VSS paths is future work.
Kinematics Single RPM β†’ speed multiplier No four-wheel model, slip, gear, or observed-speed fusion.
Lux scale High values (~850), narrow hysteresis Tuned so headlamp ON/OFF cycles often in a demo run, not photometric night/day.
Interface vcan0 hardcoded in three binaries No CLI/env yet; fine for Ubuntu + SocketCAN labs.
Dashboard stdout only No Zenoh, MQTT, or HMI; PublishStateSync is a log stub.
Gateway scope One reader thread, one actuator device No multi-bus, no security, no routing tables.
Speed on CAN Decoded, not consumed Deliberate separation until ECU path is designed.

These are documented as non-goals for the milestone, not oversights.


Software map

Crate Responsibility
common VSS encode/decode, vocabularies, projection, FSM + step, VirtualCarActor, VehicleController, actuation manager, vehicle_constants, vehicle_kinematics, front_headlamp_log.
vehicle_device_bus Front-headlamp CAN codec, wire kinds, ingress policy.
emulator World models (RPM target tracking, lux jitter/tunnels) β†’ telemetry frames.
gateway main + gateway_runtime: install twin, CAN loop, timer tick, CMD TX.
front_headlamp_actuator Blocking actuator loop on CMD with configurable drop/NACK probabilities.

Canonical FSM table: crates/common/src/engine/op_strategy/transition_map.rs. Lighting contract tests: crates/common/src/test/lighting_step_contract.rs.


Wire protocol (reference)

Telemetry β€” 11-bit standard IDs, 2-byte big-endian:

Signal ID Notes
Vehicle speed 0x101 Decoded; not fed to twin
Engine RPM 0x102 Ingress β†’ UpdateRpm
Ambient lux 0x103 Ingress β†’ UpdateAmbientLux

Front headlamp β€” ID 0x204, kinds in vehicle_device_bus (CMD / ACK / NACK for ON and OFF paths).


Tests

cargo test -p common
cargo test -p gateway --lib
cargo test -p vehicle_device_bus
cargo test -p common --features proptest   # optional

Bus integration tests need vcan0 up: cargo test -p vehicle_device_bus --test front_headlamp_bus_e2e, cargo test -p gateway --test front_headlamp_e2e.


How to run (Linux)

Requirements: Linux with SocketCAN, Rust (workspace edition 2024).

sudo modprobe vcan
sudo ip link add dev vcan0 type vcan 2>/dev/null || true
sudo ip link set up vcan0

Three terminals:

cargo run -p emulator
cargo run -p front_headlamp_actuator
cargo run -p gateway

Optional gateway flags (combine as needed):

cargo run -p gateway -- --print-timer-tick          # TimerTick heartbeat on stdout
cargo run -p gateway -- --print-transitions         # FSM transition lines
cargo run -p gateway -- --trace-actuation-ingress   # ignored headlamp ingress (CMD echo, correlation); off by default

Actuator with demo env (same terminal B instead of plain cargo run β€” values must be in 0.0..=1.0):

FRONT_HEADLAMP_ACTUATOR_DROP_RESPONSE_PROB=0.15 \
FRONT_HEADLAMP_ACTUATOR_ACK_NACK_RESPONSE_PROB=0.5 \
cargo run -p front_headlamp_actuator
Variable Example Effect
FRONT_HEADLAMP_ACTUATOR_DROP_RESPONSE_PROB 0.15 ~15% of CMDs get no ACK/NACK on CAN (gateway may log ⏱️ timeout).
FRONT_HEADLAMP_ACTUATOR_ACK_NACK_RESPONSE_PROB 0.5 When the actuator does respond, P(ACK)=0.5 (default if unset: 0.7).

Default actuator (no env): cargo run -p front_headlamp_actuator β€” always responds after ~150 ms, ~70% ACK / ~30% NACK.

Teardown: Ctrl+C each process; sudo ip link del vcan0.

Change DEFAULT_CAN_INTERFACE in emulator, actuator, and gateway_runtime if not using vcan0.

What a successful run looks like: same as the demo screenshots above β€” emulator debug lines with RPM/lux; gateway transitions and πŸ“€πŸ”† / πŸ“€πŸŒ‘ commands; actuator received ON/OFF CMD; gateway [actuation-can-ingress …] with βœ…πŸ’‘ / βœ…πŸŒ‘ or βŒπŸ”† / βŒπŸŒ‘; occasional ⏱️ alerts if the actuator drops responses; buzzer lines if RPM/speed thresholds are exceeded.


Dependencies (summary)

socketcan, tokio (gateway), ractor (actor), anyhow, rand (emulator models).


Roadmap

Major milestones ahead from this baseline (not in priority order). Several are advanced in Iteration 2.

  • Per-zone twin decomposition β€” assemblies as concurrent actors, not one monolithic context
  • Observability & audit β€” serializable transition ledger, offline verification, correlation end-to-end
  • Actuation resilience β€” retry/backoff, dedup, degraded/unavailable peer handling
  • Observed-speed ECU β€” consume measured speed on CAN; compare/fuse with RPM-derived kinematics
  • Standards alignment β€” COVESA VSS / databroker; DBC-driven CAN IDs
  • Transport & scale-up β€” configurable CAN interface; additional zones/devices on the bus
  • Structured egress β€” beyond stdout (Zenoh, uProtocol, HMI/dashboard)
  • Richer emulation β€” ECU profiles and world models

Update this file when user-visible behaviour or repo layout changes; keep the blog as the narrative arc, this README as the current truth.

About

The first stage of my journey towards a realistic prototype of a SDV Digital Twin of a car

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors