Visual Pinball
Visual Pinball Driver (gRPC)
The gRPC driver bridges Cade to Visual Pinball X (VPX) and other gRPC-capable clients. External simulators send switch events into Cade and receive coil, light, flipper, and autofire commands back over a bidirectional event stream, so you can run a full Cade configuration against a simulated table without any physical hardware.
How Events Flow
- Inbound (simulator to Cade): switch activations, ball-device captures, and other input events are sent from the client. Cade treats them exactly like events from a physical driver.
- Outbound (Cade to simulator): coil, light, flipper, and autofire commands are streamed back to every connected client, so the simulator mirrors what Cade would drive on real hardware.
Because inbound events flow through the same path as physical hardware events, scoring and game logic behave identically whether you are running on hardware or against VPX.
Hybrid Control Model
Flippers and autofire rules use a hybrid control model:
- VPX handles physics locally for zero-latency response. The client processes flipper button presses and autofire triggers (bumpers, slingshots) directly within the simulator.
- Cade controls the lifecycle. Cade owns when a device is live — flippers and autofire rules come up at ball start and go down on tilt — and receives the resulting flipper and autofire events for scoring and state tracking. Autofire enable and disable are streamed to the client as commands; flipper enablement is tracked within Cade and reflected in scoring and game state.
This split keeps the simulator responsive while keeping game-state management centralised in Cade.
Configuration
The gRPC driver is configured in a platform block within your Cade configuration file.
platform "grpc" "vpx" {
port = 50051
enable_gateway = true
enable_reflection = true
enable_tls = false
enable_cors = true
}Properties
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
port | int | No | 50051 | gRPC server listen port (1-65535) that clients such as VPX connect to |
enable_gateway | bool | No | true | Enable the gRPC-gateway REST proxy |
enable_reflection | bool | No | true | Enable gRPC server reflection for tooling |
enable_tls | bool | No | false | Enable TLS for the gRPC server |
cert_file | string | No | "" | Path to the TLS certificate file (required when TLS is enabled) |
key_file | string | No | "" | Path to the TLS private key file (required when TLS is enabled) |
enable_cors | bool | No | true | Enable CORS headers on the gateway (defaults to allowing all origins) |
platform_address | string | No | "" | Advanced. When set, Cade connects outward to a separate platform service at this address instead of only accepting inbound client connections. Leave empty for the standard VPX bridge. |
connect_timeout_sec | int | No | 10 | Advanced. Timeout, in seconds, for the initial outward connection when platform_address is set. |
Supported Capabilities
The gRPC driver supports the core device capabilities you would find on a physical platform:
- Switches. Switch events are received from the client. No pre-configuration is required — switches are registered dynamically as events arrive.
- Coils. Pulse (with pulse duration and power level), hold, and release commands are streamed to the client.
- Lights. Set, off, flash, and fade commands are streamed to the client, including batch updates for atomic delivery within a single simulator frame. Fades are sent as a target brightness and interpolated by the client.
- Flippers. Cade tracks flipper enablement as part of game state — for scoring, tilt, and ball lifecycle — while the client handles flipper physics locally.
- Autofire rules. Cade streams enable and disable commands to the client, which handles bumper and slingshot physics locally.
- REST gateway. When
enable_gatewayis on, the driver exposes the same operations over HTTP for tooling and dashboards.
Servos are not supported over the gRPC bridge. A servo command issued against a gRPC platform is logged and discarded rather than forwarded to the client.
Platform Capabilities
| Capability | Value |
|---|---|
| Transport | gRPC |
| Max switches | 1024 |
| Max coils | 256 |
| Max lights | 512 |
| Max servos | 0 (unsupported) |
| RGB lights | Yes |
| PWM coils | No |
| Firmware flippers | No (hybrid model – client-local physics) |
| Firmware autofire | No (hybrid model – client-local physics) |
| Address format | numeric |
Ball Device Commands
Cade distinguishes two ball-device operations so score and ball-in-play counters stay consistent:
eject_ball— creates a new ball and kicks it. A held ball is not required. Angle and strength are carried on the command.kick_ball— fires an already-held ball without creating a new one and without incrementing the ball-in-play counter. Angle and strength for kicks are configured once per kicker and applied by the client.destroy_ball— removes a ball from the simulated playfield, used when a ball drains. This command is meaningful only to a simulator; on physical hardware it does nothing.
If the client receives a kick_ball for a kicker with no ball held, it logs a warning and ignores the command.
Getting Started
Cade includes example clients and a sample VPX table configuration in the source repository:
examples/grpc_event_client/– A reference gRPC event client with implementations in Go and Python.examples/vpinball_example_table/– A sample VPX table with a Cade configuration file.
To connect a gRPC client:
- Configure the gRPC driver in your Cade configuration file with the desired port.
- Start Cade. The gRPC server begins listening on the configured port.
- Connect your client to the gRPC server and establish a bidirectional event stream.
- The client sends switch events to Cade and receives device commands back.
Two files, two jobs
A VPX setup splits across two files, and it helps to keep the distinction clear:
<table>.cade— the machine definition. Devices, events, audio clips, and scoring rules: what the table is. This travels with the table and is the same whether you run it against VPX or real hardware..cade.hcl— the runtime configuration. Theplatform "grpc"block, the web server, and logging: how to run it here. This is the file that changes when you move between a simulator and a machine.
A minimal runtime configuration for VPX looks like this:
platform "grpc" "vpx_bridge" {
port = 50051
enable_gateway = true
enable_reflection = true
}
web {
enabled = true
port = 8080
health { enabled = true }
debug { enabled = true }
}
logging {
level = "info"
format = "text"
}Start it from the directory holding those files:
cade console
The console picks up the platform "grpc" block automatically and starts
listening on the configured port; VPX connects to it. Override the port at the
command line when you need to:
cade console --grpc-port 50052
Coming from a VPX table script
If you are converting an existing table, most of the VBScript has a declarative equivalent in Cade rather than a line-by-line translation:
| VPX concept | Cade equivalent |
|---|---|
Sub Object_Hit() | An event in a global_events block |
Sub Object_Timer | A timer-based event with duration / interval |
PlaySound with AudioPan / AudioFade | An audio_clip block with spatial = true |
Dim variable and assignment | A variable "int" or variable "bool" block |
.State = 1 / .State = 0 | Light state driven by event descriptions |
.isDropped = 0 (target reset) | The reset_delay device setting |
| Score values | score "event" blocks with point values |
| A manual balls-in-play counter | Handled by Cade’s ball devices — drain and launch are explicit devices |
Positional audio is the clearest example of the shift. Where the VPX script
computes stereo pan, front-back fade, volume from ball speed, and pitch by hand,
Cade takes spatial = true and derives all of it from the device’s coordinates.
REST Gateway
When enable_gateway is true (the default), the same event operations are
available over HTTP on the web server’s port, which is useful for debugging a
bridge without writing a gRPC client:
| Method | Path | Purpose |
|---|---|---|
POST | /api/v1/events | Send a single event |
POST | /api/v1/events/batch | Send a batch of events |
POST | /api/v1/events/device/{device_key} | Send an event for a specific device |
GET | /api/v1/events/stream | Stream events |
GET | /api/v1/events/sse | Stream events as Server-Sent Events; repeat ?category= to filter |
GET | /api/v1/events/ws | Bidirectional event stream over a WebSocket |
GET | /api/v1/events/health | Health check |
A quick reachability check:
curl http://localhost:8080/api/v1/events/health
curl -N "http://localhost:8080/api/v1/events/sse?category=system"
With enable_reflection on, grpcurl also works directly against the gRPC port:
grpcurl -plaintext localhost:50051 list
Reflection and permissive CORS are conveniences for local development. On a
machine exposed to a network, turn enable_reflection off and enable TLS.
The VPX table script
When you bridge a VPX table to Cade, the table’s VBScript stays deliberately small. All game logic — scoring, light effects, ball management, drain handling, multiball — lives in your Cade configuration. The bridge also takes care of the table mechanics you would normally script by hand:
- Flippers are registered as autofire devices. VPX fires the flipper coils locally for zero-latency response, and Cade controls the enable/disable lifecycle and records the flipper events for scoring. You do not wire flipper keys in the script.
- Bumpers and slingshots are autofire devices too — same model, no script.
- Drain and ball serve are driven by Cade. The drain switch streams to Cade, which sends
destroy_ballandeject_ballcommands back to remove the drained ball and launch the next one. There is no manual balls-in-play counter and noDrain_Hithandler.
The one thing the bridge cannot infer is the player’s manual plunger launch, so that is the only glue the table author writes. This is the complete table script:
Option Explicit
Dim EnableRetractPlunger
EnableRetractPlunger = False ' True: retract a button/key plunger at a linear speed — pull
' back, hold at maximum for one second, then return to rest
Sub Table1_KeyDown(ByVal keycode)
If keycode = PlungerKey Then
If EnableRetractPlunger Then
Plunger.PullBackandRetract
Else
Plunger.PullBack
End If
End If
End Sub
Sub Table1_KeyUp(ByVal keycode)
If keycode = PlungerKey Then
Plunger.Fire
End If
End SubKeyDown pulls the plunger back and KeyUp releases it with Plunger.Fire — VPX runs the plunger physics locally. The EnableRetractPlunger toggle chooses how the pull behaves: leave it False for the analog PullBack (the plunger tracks how long the key is held), or set it True for PullBackandRetract, a fixed linear pull-and-return that feels better with a button or key plunger. Everything else (bumpers, slingshots, targets, GI, scoring, ball lifecycle) flows over the gRPC bridge with no corresponding Sub in the script.
Troubleshooting
| Symptom | Likely cause | What to try |
|---|---|---|
| The client reports the connection was refused | Cade is not running, or it is listening on a different port than the client is dialing | Start Cade first and confirm it logs that the gRPC server is listening on the expected port. Make sure the client’s server address (default localhost:50051) matches, or override the port with --grpc-port. |
| The client cannot complete a TLS handshake | enable_tls is set on one side but not the other, or the certificate and key paths are wrong | Match the TLS setting on both ends. When enable_tls = true, point cert_file and key_file at valid files and configure the client to use TLS as well. For local development, leave enable_tls = false. |
| A browser-based client is blocked by a CORS error | The gateway is rejecting the browser’s origin | Confirm enable_gateway and enable_cors are both true (both default on). The gateway then serves the same operations over HTTP with permissive CORS headers. |
| No events appear to flow between the client and Cade | The client connected but is not exchanging events, or you want to confirm the server independently | Use grpcurl against the configured port, or call the REST gateway’s health endpoint, to verify the server is reachable before debugging the client. |
A kick_ball command seems to do nothing | The kicker had no ball held when the command arrived | This is expected: the client logs a warning and ignores a kick_ball for an empty kicker. Use an eject to create and launch a new ball instead. |