Visual Pinball

driver vpx 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

PropertyTypeRequiredDefaultDescription
portintNo50051gRPC server listen port (1-65535) that clients such as VPX connect to
enable_gatewayboolNotrueEnable the gRPC-gateway REST proxy
enable_reflectionboolNotrueEnable gRPC server reflection for tooling
enable_tlsboolNofalseEnable TLS for the gRPC server
cert_filestringNo""Path to the TLS certificate file (required when TLS is enabled)
key_filestringNo""Path to the TLS private key file (required when TLS is enabled)
enable_corsboolNotrueEnable CORS headers on the gateway (defaults to allowing all origins)
platform_addressstringNo""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_secintNo10Advanced. 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_gateway is 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

CapabilityValue
TransportgRPC
Max switches1024
Max coils256
Max lights512
Max servos0 (unsupported)
RGB lightsYes
PWM coilsNo
Firmware flippersNo (hybrid model – client-local physics)
Firmware autofireNo (hybrid model – client-local physics)
Address formatnumeric

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:

  1. Configure the gRPC driver in your Cade configuration file with the desired port.
  2. Start Cade. The gRPC server begins listening on the configured port.
  3. Connect your client to the gRPC server and establish a bidirectional event stream.
  4. 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. The platform "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 conceptCade equivalent
Sub Object_Hit()An event in a global_events block
Sub Object_TimerA timer-based event with duration / interval
PlaySound with AudioPan / AudioFadeAn audio_clip block with spatial = true
Dim variable and assignmentA variable "int" or variable "bool" block
.State = 1 / .State = 0Light state driven by event descriptions
.isDropped = 0 (target reset)The reset_delay device setting
Score valuesscore "event" blocks with point values
A manual balls-in-play counterHandled 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:

MethodPathPurpose
POST/api/v1/eventsSend a single event
POST/api/v1/events/batchSend a batch of events
POST/api/v1/events/device/{device_key}Send an event for a specific device
GET/api/v1/events/streamStream events
GET/api/v1/events/sseStream events as Server-Sent Events; repeat ?category= to filter
GET/api/v1/events/wsBidirectional event stream over a WebSocket
GET/api/v1/events/healthHealth 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_ball and eject_ball commands back to remove the drained ball and launch the next one. There is no manual balls-in-play counter and no Drain_Hit handler.

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 Sub

KeyDown 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

SymptomLikely causeWhat to try
The client reports the connection was refusedCade is not running, or it is listening on a different port than the client is dialingStart 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 handshakeenable_tls is set on one side but not the other, or the certificate and key paths are wrongMatch 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 errorThe gateway is rejecting the browser’s originConfirm 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 CadeThe client connected but is not exchanging events, or you want to confirm the server independentlyUse 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 nothingThe kicker had no ball held when the command arrivedThis 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.