Driven Devices
Driven Devices
Coils, motors, servos, drop targets, and stand-up targets share more than they differ. They all have the same identity needs (a stable id, grouping tags, a description, replacement part number, service notes), the same runtime surface (namespaced events, state bindings, an optional audio block), and they often want to react to inputs without consulting the scoring engine. Cade’s driven-device layer formalizes this shared ground so these devices can be declared, referenced, and scripted consistently.
This page walks through the four things the layer gives you:
- A uniform identity block on every
device { } - Richer switch subtypes for mechanical targets (
drop,standup) - A symbolic
positions {}block on servo motors - A declarative
behavior {}sub-block that lowers to hardware reflexes
Shared Identity
Every device block — regardless of category — accepts the same identity fields:
| Field | Purpose |
|---|---|
id | Hardware address used by the platform driver |
tags | Grouping labels for bulk references (tag.drop_bank.hit, etc.) |
description | Human-readable summary — appears in the TUI and in error messages |
location | Optional playfield coordinates and named area |
part_number | Replacement part for service |
notes | Free-form service notes surfaced in the TUI device panel |
enabled | When false, the declaration stays but inputs and actions are ignored |
Because every driven device carries the same identity, you can bulk-address them with tags, filter them in the TUI by location, or temporarily disable a broken assembly with enabled = false without having to delete or comment out config.
Mechanical Target Subtypes
Drop targets and stand-up targets are both “the ball hit a small piece of metal” from the electrical perspective — they’re both switches — but they behave very differently in a game. Drop targets fall when hit and need a coil to stand back up; stand-ups stay upright indefinitely. The driven-device layer splits them into explicit subtypes so the config expresses the intent directly.
switch "drop" — Falling Targets
A drop target declares its own reset behavior as a first-class sub-block:
device "switch" "drop" "drop_center" {
id = 0x51
reset {
coil = device.drop_reset_coil
pulse_ms = 30
debounce_ms = 50
}
}
Behind that declaration is an implicit state machine with four states: up → hit → resetting → up. The runtime emits events at every transition:
device.drop_center.hit— the ball just knocked it downdevice.drop_center.state_changed— any state transitiondevice.drop_center.rested— the reset pulse has fired, debounce has elapsed, the target is confirmed standing
The rested event is the one most scoring rules care about, because firing a reset coil and actually having the target back upright are two different things — a debounce window is needed before awarding bank-complete bonuses or starting new shot sequences.
switch "standup" — Upright Targets
Stand-ups are hit-only. No reset coil, no state machine:
device "switch" "standup" "standup_3" {
id = 0x63
tags = ["standup_bank"]
}
They emit device.<name>.hit. Arrays of stand-ups typically share a tag so a single handler can score the whole bank:
score "event" "standup_hit" {
when = tag.standup_bank.hit
points = 500
}
Servo Positions
Cade’s core principle is that .cade files stay driver-agnostic — nothing in a table config should have to know whether the hardware is a FAST board, a Visual Pinball bridge, or a gRPC test harness. Servos challenge that principle because “move to 90 degrees” is a pulse-width setting on one driver and a step count on another.
The positions {} block solves this by letting the config assign symbolic names to abstract scalar positions:
device "motor" "servo" "left_diverter" {
id = 0x80
positions {
home = 0
up = 90
down = -45
}
}
Game logic references positions by name:
move_servo {
device = device.left_diverter
position = "up"
}
Each driver is responsible for mapping the scalar value to its own physical representation. If you later swap a FAST servo for a Visual Pinball stub, the positions stay put — only the driver’s mapping changes.
Servos emit a motion-aware event family: moved (motion started), moving (continuous while in motion), reached (arrived at target), and state_changed (any position transition). The event’s state field carries the position name.
Behaviors: Config-Compile-Time Reflexes
Some reactions need to happen immediately: a pop bumper auto-firing its coil, a drop target resetting after a bank is cleared, a servo parking when a mode ends. You could write scoring rules for all of these, but two concerns show up:
- Latency: the scoring engine runs at 50–100 Hz. Round-tripping a pop-bumper hit through a scoring rule adds unnecessary delay on a hot path.
- Clarity: reflexive reactions aren’t really scoring — they’re device behavior. Mixing them into the scoring rules makes the score block harder to read.
The behavior {} sub-block declares these reactions alongside the device that owns them:
device "switch" "bumper" "pop_bumper_1" {
id = 0x40
behavior "auto_fire_coil" {
when = device.pop_bumper_1.active
action = pulse_coil { device = device.pop_bumper_1_coil, pulse_ms = 20 }
cooldown_ms = 100
}
}
At config-load time the compiler recognizes this shape and lowers it to an autofire-style rule the platform driver can run locally. Once lowered, the pop bumper fires its coil without involving the scoring engine at all — the reaction runs at hardware speed, and the scoring engine is free to work through the rest of the frame.
This lowering happens once, at config compile time, not per-tick. The runtime cost of adding more behaviors is the cost of adding more driver-local rules — the scoring hot path is untouched. The performance budgets Cade guarantees elsewhere (50–100 Hz event tick, 30 Hz light tick, <200 ns runtime binding resolve, <1 ms scoring) are preserved because lowered behaviors never enter those loops.
Not every behavior is eligible for hardware lowering. Some reference runtime variables or scoring state that the platform driver cannot see at compile time. In that case, the compiler falls back to running the behavior through the same handler pipeline that powers scoring actions — the behavior still fires, just at event-tick latency instead of hardware speed. Hardware-lowered behaviors are the fast path; software-fallback behaviors are the safety net, so a behavior {} block is never silently ignored.
Action Verbs
behavior {} blocks and event handlers both produce side effects through the same driver-agnostic action verbs:
| Verb | What it does |
|---|---|
pulse_coil | Fires a coil for a bounded duration |
move_servo | Commands a servo to a named positions {} entry |
set_state | Transitions a stateful device (e.g., drop-target FSM) |
trigger_behavior | Fires a named behavior block on demand |
See the Device API reference for the full verb syntax.
Relationship to Lights
Lights share the identity layer — every device "light" "..." gets the same id/tags/description/location/part_number/notes/enabled surface. They do not share the behavior {} sub-block; light behavior is already handled by the dedicated light-show compiler, which has its own declarative surface optimized for the 30 Hz light tick. Think of the driven-device layer and the light-show compiler as siblings: both lower declarative game-config into driver-local reflexes, each tuned for its own hot path.
When to Reach for What
| You want to… | Reach for… |
|---|---|
| Award points when the ball hits something | A score "event" { … } rule |
| React to a hit with a coil pulse that must be immediate | A behavior { action = pulse_coil { … } } |
| Move a servo to a known position from game logic | move_servo { device = …, position = "…" } |
| Bank-complete bonuses that wait for the drop to re-rest | A scoring rule on device.<name>.rested |
| Bulk-score a whole target bank | A shared tag plus when = tag.<tag>.hit |
| Temporarily remove a broken device from play | Set enabled = false on the device |
Related
- Device API reference — full schema for identity, subtypes,
positions,behavior, and action verbs - Event Type reference — event naming, traits, and flow blocks
- Runtime Bindings — expression namespace used by
when,guard, and action-verb arguments