Module 1: The Process Manager
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
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
webcamerad's condition function isdriverview, and it's currentlyenabled=Falsein the inventory you captured. Will passingstarted=Truetowebcamerad.should_run(...)change whether it appears in youronroadlist? Why or why not.- If you set the
BLOCKenvironment variable tocontrolsdbefore startingmanager.py, which of the three gates in theifstatement above would stop it from running, even while the car isstartedandiscaris true? - 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
- No. The list comprehensions filter on
p.enabled and p.should_run(...), and Python'sandshort-circuits: sincewebcamerad.enabledisFalseon this machine (it's gated on theUSE_WEBCAMenvironment variable at process-registration time, a static per-hardware choice made once whenprocess_config.pyis imported),should_runis never even called.enabledand "currently satisfies its condition function" are independent gates, and a process failing the first one is invisible to the second regardless ofstarted. - The
not_rungate — the middle one.enabledonly reflects hardware/mode support andBLOCKdoesn't change that;should_runwould still evaluate toTrueforcontrolsdwhile onroad. But"controlsd" in not_runbecomesTrue, andp.name not in not_runfails, soensure_runningskips it regardless of whatshould_runwould have said. uiis a clean example: its condition isalways_run, unconditionallyTrue, 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.