Module 1: The Process Manager

Goal. Find out how many separate programs make up a running sunnypilot, what decides which ones are alive at any moment, and where that decision is made.

Setup. A checkout of this repository with its .venv active (source .venv/bin/activate). No car, camera, or device needed for this module; every command below is read-only inspection.

Predict

sunnypilot is not one program. Before looking at any code: guess how many separate processes you think a running sunnypilot is made of, and then guess what fraction of those you'd expect to be alive while the device is just sitting on a desk, powered on, with no car plugged in at all.

Run

Every process sunnypilot can start is declared in one file, system/manager/process_config.py, as an entry in a Python list called procs. Each entry names the process, says where its code lives, and gives it a condition function: a small function of (started, params, CP) that returns True exactly when that process should be running. started means "the car's ignition is on and sunnypilot is engaged with a car", not just "the device is powered on."

procs = [
  DaemonProcess("manage_athenad", "system.athena.manage_athenad", "AthenadPid"),

  NativeProcess("loggerd", "system/loggerd", ["./loggerd"], logging),
  NativeProcess("encoderd", "system/loggerd", ["./encoderd"], only_onroad),
  NativeProcess("stream_encoderd", "system/loggerd", ["./encoderd", "--stream"], or_(and_(livestream, not_(iscar)), notcar)),
  PythonProcess("logmessaged", "system.logmessaged", always_run),

(system/manager/process_config.py lines 118-121, near the start of the list, shown to illustrate the shape: name, code location, condition function.) The list runs to 55 entries in this checkout, each following the same pattern; three more of interest for this workshop, quoted individually since they're spread across the file rather than adjacent to each other: PythonProcess("controlsd", "selfdrive.controls.controlsd", and_(not_joystick, iscar)) (line 140), PythonProcess("plannerd", "selfdrive.controls.plannerd", not_long_maneuver) (line 152), and PythonProcess("radard", "selfdrive.controls.radard", only_onroad) (line 155).

Import that list directly and ask it two questions: how many entries exist, and — using each entry's own condition function, called with started=False (offroad) and again with started=True (onroad) — how many would actually run in each case on this machine. The essence of it, simplified from the real script (full listing on the Source page):

offroad = [n for n, p in managed_processes.items() if p.enabled and p.should_run(False, params, CP)]
onroad  = [n for n, p in managed_processes.items() if p.enabled and p.should_run(True, params, CP)]

print(f'would run OFFROAD (started=False): {len(offroad)}')
print(f'would run ONROAD (started=True):  {len(onroad)}')

Observe

enabled on this machine: 43 / 55
would run OFFROAD (started=False): 14
would run ONROAD (started=True):  30

(cross-check: Log page)

count who
Registered total 55 every entry in procs
Enabled on this hardware 43 excludes 12 gated to comma's own hardware (TICI) or other platforms
Running offroad (desk, no car) 14 ui, camerad, soundd, pandad, hardwared, the uploader/registration daemons, ...
Running onroad (started=True) 30 adds controlsd, radard, plannerd, card, modeld, locationd, ...

enabled and "would run" are two different gates, and mixing them up is the easy mistake here: enabled is a static, per-hardware switch decided once (12 processes like sensord and qcomgpsd are simply irrelevant on a PC and permanently off); "would run" is a dynamic, per-moment decision every enabled process's condition function re-evaluates continuously. A process can be enabled and still not running most of the time.

Diagram: the onroad-only processes, by pipeline stage

Process layers, onroad

Every box names a real process from the inventory above; the grouping (sensing, perception, localization, planning, control, actuation) is this workshop's own, not a directory in the repo — it's the mental model the rest of this tour will keep reusing.

Explain

system/manager/manager.py is the one process that reads this list and acts on it. Its main loop is short:

while True:
    sm.update(1000)

    started = sm['deviceState'].started

(system/manager/manager.py lines 147-150.) A few onroad/offroad-transition bookkeeping lines sit between here and the loop's other end (clearing params on ignition changes, mostly), then the same call you already saw offroad-only at manager startup runs again every iteration, now with whatever started this iteration measured:

ensure_running(managed_processes.values(), started, params=params, CP=sm['carParams'], not_run=ignore)

(system/manager/manager.py line 168.) Every loop iteration it re-reads whether the car is started, then calls ensure_running, which walks the full process list and starts anything whose condition just became true, stops anything whose condition just became false, and restarts anything that crashed. The 14-versus-30 split you just measured is this loop's decision, re-made continuously, not a one-time boot sequence: unplug the car mid-session and the 18 processes that appear only in the onroad list (controlsd, card, modeld, radard, plannerd, selfdrived, and 12 others) get stopped within one loop iteration, without the device rebooting or manager.py itself restarting.

The condition functions read like a small policy language once you've seen a few: iscar = started and not CP.notCar, only_onroad = started, driverview = started or IsDriverViewEnabled. Composing them (and_(not_joystick, iscar) for controlsd) is how one list expresses "run onroad, unless a debug joystick mode is active" without a single if statement inside manager.py itself — the policy lives entirely in process_config.py, and manager.py never needs to know why a given process should or shouldn't be running.

Aside: why some imports say openpilot.selfdrive... and others just selfdrive...

Look again at the entry for controlsd: PythonProcess("controlsd", "selfdrive.controls.controlsd", ...). manager.py's launcher imports that string bare (importlib.import_module("selfdrive.controls.controlsd")), which works because selfdrive/ is a real directory at the repo root, importable as a top-level package. But open selfdrive/controls/controlsd.py itself and its own imports all start with openpilot., e.g. from openpilot.common.params import Params. That also works, but through a different path: this repo ships a directory literally named openpilot/ containing nothing but a symlink back to each real top-level directory (common -> ../common, selfdrive -> ../selfdrive/, and so on), so openpilot.selfdrive.controls.controlsd and selfdrive.controls.controlsd are two import names for the exact same file. Every file's internal imports consistently use the openpilot.-prefixed form so they resolve the same way no matter which of the two names was used to launch that file in the first place; only the small number of process_config.py entry points use the bare form.

One more knob: the not_run blocklist

manager_thread builds an ignore list before its first ensure_running call — some processes are held back for reasons that have nothing to do with started, like no registered device (manage_athenad, uploader) or an explicit env var:

ignore: list[str] = []
if params.get("DongleId") in (None, UNREGISTERED_DONGLE_ID):
    ignore += ["manage_athenad", "uploader"]
if os.getenv("NOBOARD") is not None:
    ignore.append("pandad")
ignore += [x for x in os.getenv("BLOCK", "").split(",") if len(x) > 0]

(system/manager/manager.py lines 131-136.) That list becomes ensure_running's not_run argument, checked here (system/manager/process.py line 252):

if p.enabled and p.name not in not_run and p.should_run(started, params, CP):

Three independent gates, in order: is this process even possible on this hardware (enabled), has it been explicitly blocked for this run (not_run), and does its own policy function say to run right now (should_run). All three have to pass.

Quiz

  1. webcamerad's condition function is driverview, and it's currently enabled=False in the inventory you captured. Will passing started=True to webcamerad.should_run(...) change whether it appears in your onroad list? Why or why not.
  2. If you set the BLOCK environment variable to controlsd before starting manager.py, which of the three gates in the if statement above would stop it from running, even while the car is started and iscar is true?
  3. Name one process from the "running offroad" list above whose job clearly doesn't require the car to be present, and explain what its condition function is actually gating on instead.

→ Answer Key — Module 1

Answer Key — Module 1
  1. No. The list comprehensions filter on p.enabled and p.should_run(...), and Python's and short-circuits: since webcamerad.enabled is False on this machine (it's gated on the USE_WEBCAM environment variable at process-registration time, a static per-hardware choice made once when process_config.py is imported), should_run is never even called. enabled and "currently satisfies its condition function" are independent gates, and a process failing the first one is invisible to the second regardless of started.
  2. The not_run gate — the middle one. enabled only reflects hardware/mode support and BLOCK doesn't change that; should_run would still evaluate to True for controlsd while onroad. But "controlsd" in not_run becomes True, and p.name not in not_run fails, so ensure_running skips it regardless of what should_run would have said.
  3. ui is a clean example: its condition is always_run, unconditionally True, because the on-device touchscreen UI is useful whether or not a car is attached (it shows setup screens, Wi-Fi setup, and driver-view camera preview offroad). Its condition function isn't gating on the car at all, it's gating on essentially nothing, by design.

↑ Back to Quiz 1

→ Module 2: cereal & msgq