DocumentationComponentsVMS

How the VMS works

The VMS supervision tree, the component pattern every driver follows, the managers that pick the gear and arbitrate control levels, and how metrics and actions reach the dashboard.

The VMS is one OTP application, VmsCore, that supervises the framework's own processes and whatever your vehicle's composer returns from children/0. This page walks through that tree, the recipe every component follows, the two managers, and the path from a component to the dashboard. VMS covers what the VMS is and where it runs.

#Supervision tree

text
VmsCore.Application                          (framework)
├── VmsCore.Repo               SQLite: throttle calibration and other persisted data
├── Ecto.Migrator              applies pending migrations on boot (skipped when RELEASE_NAME is set)
├── VmsCore.Metrics            the last value of every bus message, for the dashboard and API
├── VmsCore.NetworkInterfaces  CAN interface statistics (errors, bus state)
├── OvcsBus.Cluster            connects this BEAM to the vehicle's other firmwares
└── composer children          (your package, selected by VEHICLE)
    ├── VmsCore.Status         VMS heartbeat (0x1A0), ready-to-drive, controller reset (0x1AA)
    ├── Components             hardware drivers (inverter, BMS, brakes, body, …)
    ├── Managers               control level, gear
    └── Generic controllers    Arduino I/O drivers

The supervisor is one_for_one: a driver that crashes restarts alone. OvcsBus's Phoenix.PubSub runs in its own OTP application, :ovcs_bus, so every BEAM that depends on the library reaches it by name.

VmsCore.Status is in your composer's children rather than the framework's because its sources are yours: it relays the :vms_status and :ready_to_drive of the components you name into the vms_status frame the generic controllers watch. At boot it also resets the generic controllers once, since a VMS reboot always outlasts their heartbeat timeout.

#The component pattern

Every hardware driver is a GenServer with the same recipe:

  1. Subscribes to CAN frames with Cantastic.Receiver.subscribe/3, receiving {:handle_frame, %Cantastic.Frame{}}.
  2. Subscribes to internal messages with OvcsBus.subscribe/1, receiving %OvcsBus.Message{}.
  3. Runs a periodic loop, typically every 10 ms, to emit CAN frames and broadcast its metrics.
  4. Exposes actions through trigger_action/2, so a dashboard button can call into it.

Components never import each other. A component that needs the vehicle speed is told, in the init configuration the composer gives it, which module publishes it, and matches on source:

elixir
alias OvcsBus, as: Bus

# Broadcasting a metric
Bus.broadcast("messages", %Bus.Message{
  name: :speed,
  value: Decimal.new("45.2"),
  source: VmsCore.Components.Volkswagen.Polo9N.ABS
})

# Receiving it in another component
def handle_info(%Bus.Message{name: :speed, value: speed, source: source}, state)
    when source == state.speed_source do
  {:noreply, %{state | speed: speed}}
end

The composer is where the wiring lives: swap the ABS module for another speed source and only the composer changes. A message reaches the subscribers of its own node only, so a slow link to another board cannot stall a drivetrain component; The OVCS bus explains the bus and how its messages leave the VMS. Add a component writes one from scratch.

#Managers

Managers hold logic that spans several components. Both are framework modules your composer supervises and configures.

  • VmsCore.Managers.ControlLevel decides who commands the vehicle and routes each actuator's request from that commander. It has its own section below.
  • VmsCore.Managers.Gear turns the requested gear or direction into the selected gear, emits it on gear_status (0x600) and broadcasts :selected_gear. With the contact off it selects parking. Losing ready-to-drive while in drive or reverse drops to neutral. Parking, drive and reverse need the vehicle below 1 km/h and the throttle request below 0.05, and drive and reverse also need ready-to-drive. An unknown speed, a source gone quiet, never counts as stopped.

#Control levels

VmsCore.Managers.ControlLevel arbitrates between commanders. It reads two independent switches on the radio transmitter, because authority and autonomy are different questions:

ComponentValuesAnswers
OVCS.RadioControl.RequestedControlLevel:manual / :radio / :roswho has authority
OVCS.RadioControl.RequestedRosCommander:teleop / :autonomouswhich ROS node, when ROS does

:ros means the vehicle takes its commands from the ROS bridge, not that it drives itself: a human on a gamepad and a planner reach the VMS over the same topics and CAN frames. The commander switch says which of them has the wheel. Only :ros consults it, so the second switch is inert in :manual and :radio.

Both switches only request. The manager refuses unsafe moves: in motion, not ready to drive, or while a fault has forced a lower level. :ros is reachable only from :radio, so getting there takes two deliberate throws with the middle position in between. A refused request is logged once, with the reason.

The radio trigger (radio_braking_source) drops :ros to :radio, and so does an optional rotation_fault_source: a OVCS.RotationFusion whose sources disagree (:cross_check_fault) means the odometry, and every costmap cell placed with it, can no longer be trusted. Either one forces :radio until the switch comes back to :radio or below, and :ros is refused while it lasts. The OVCS Mini reference vehicle wires its motor rotation fusion there. The manual brake, or losing ready-to-drive, forces :manual from any level; the full list of what the framework enforces is in Safety mechanisms.

#Channel layout

Which transmitter channel each component reads is a vehicle decision. The radio control bridge copies the receiver's channels 1 to 8 onto 0x2A0 and 0x2A1 unchanged, and each composer names the channel every component reads. The two reference vehicles with radio control use:

PurposeOVCS MiniOVCS1
Steering11
Throttle (and radio_braking, the human takeover)22
Control level63
Direction (the reverse button, through Managers.Gear)74
ROS commander5not wired

Switch positions are 1000, 1500 and 2000 µs with a margin of 100. A position outside every margin falls back to the safe one: :manual for the level, :teleop for the commander, :forward for direction.

The Mini's link is ExpressLRS in MAVLink mode, which forces Hybrid switch mode. Channel 5 is the link's arm channel and only carries 1000 or 2000; channels 6 to 8 are 3-position and reach the bus. Hence the three-position level on 6 and the two-position commander on 5.

#Source maps

Each requested_*_sources map in a composer is keyed by level; the :ros entry is itself keyed by commander:

elixir
requested_throttle_sources: %{
  manual: OVCS.ThrottlePedal,
  radio: OVCS.RadioControl.Throttle,
  ros: %{teleop: OVCS.RosActuatorCommand.Throttle, autonomous: OVCS.RosVelocityCommand}
}

A hand's throttle usually goes through an OVCS.InputCurve first, and the map names the curve: dead zone and expo belong to the hand, one curve per hand, while the actuator only applies its caps. In the OVCS Mini reference vehicle:

elixir
{OVCS.InputCurve,
 %{
   process_name: Vms.RadioThrottleInputCurve,
   throttle_source: OVCS.RadioControl.Throttle,
   deadzone: @throttle_deadzone,
   expo: @throttle_expo
 }},
...
requested_throttle_sources: %{
  manual: nil,
  radio: Vms.RadioThrottleInputCurve,
  ros: %{teleop: Vms.TeleopThrottleInputCurve, autonomous: OVCS.RosVelocityCommand}
},
radio_braking_source: OVCS.RadioControl.Throttle

A velocity is a physical quantity and is named directly, as is the takeover: radio_braking reads the raw trigger. A hand wired to an actuator without a curve gets no dead zone, and a trigger that drifts at rest creeps the vehicle.

A missing key resolves to nil: nothing commands that actuator. That is the safe direction, and it is how a vehicle without a planner is expressed: ros: %{teleop: ...} leaves the autonomous position commanding nothing. Such a vehicle also omits requested_ros_commander_source, which pins the commander to :teleop.

#Driving on the host bench

On the host there is no radio receiver and no controller. The level starts at default_control_level (:manual in the OVCS Mini reference vehicle, where every source is nil); nothing emits 0x2A0/0x2A1, and nothing emits the pulse counter frame 0x709. Without that frame the speed is unknown, and the manager refuses every mode change with :speed_unknown. A bench session synthesises both.

First the speed, as a stream in its own terminal: the frame watcher needs several on-time frames to declare 0x709 alive and drops it as soon as they stop. A count and frequency of zero is a stationary vehicle:

sh
cangen vcan0 -I 709 -L 4 -D 00000000 -g 10

Then the switches, in the Mini's channel layout. 0x2A0 carries channels 1 to 4 as little-endian uint16, two bytes each; 0x2A1 carries 5 to 8. 1500 is DC05, 2000 is D007, 1000 is E803:

sh
# Steering and throttle centred (channels 1 and 2)
cansend vcan0 2A0#DC05DC0500000000

# Level -> :radio (channel 6 = 1500; channel 5 = 1000 keeps :teleop)
cansend vcan0 2A1#E803DC0500000000

# Level -> :ros (channel 6 = 2000). Two steps, in this order.
cansend vcan0 2A1#E803D00700000000

# Optional: hand it to the planner (channel 5 = 2000). Needs a standstill.
cansend vcan0 2A1#D007D00700000000

Channels 7 and 8 read as 0 here, outside every margin, so they take the safe fallback. ready_to_drive is hardcoded true in the Mini (OvcsMini.Vms); where it comes from contactors or an inverter, it has to be true as well. For your vehicle, substitute the channels your composer names. Testing with CAN covers cansend, cangen and replaying captures.

#Metrics and actions

The dashboard never talks to a component directly.

  • Metrics. VmsCore.Metrics subscribes to every message on the VMS's bus and keeps the last value per source and name, with the unit its publisher gave. The API's metrics channel streams the module and key pairs a client subscribes to, so every value a component broadcasts is already a metric: a dashboard row only names it.
  • Actions. A dashboard button posts to /api/actions with a module, an action name and parameters. The API calls that module's trigger_action/2, which must return :ok; the throttle pedal's "calibrate_boundaries" and a generic controller's adoption are actions. See How an action reaches your code.
  • Network interfaces. VmsCore.NetworkInterfaces reports each CAN interface Cantastic is configured with, and its ip link statistics with the detailed error counters, for the network-interfaces channel.

#Where next

This page is docs/vms/how_it_works.md in the ovcs repository; edit it there.