How the Trevor WebSocket Protocol Enables Remote Device Control in Nallely

The Trevor WebSocket protocol is a lightweight, bidirectional communication system that allows external programs to register virtual "neurons" on a running Nallely session and exchange parameter updates in real time over WebSocket connections on port 6788.

The dr-schlange/nallely-midi repository implements this protocol through the Trevor subsystem, which bridges external Python processes with Nallely's internal MIDI routing graph. By leveraging compact binary frames with JSON fallbacks, the protocol enables low-latency remote control while maintaining seamless integration with the existing VirtualDevice architecture.

TrevorBus: The Server-Side WebSocket Implementation

The TrevorBus class in nallely/trevor/trevor_bus.py serves as the WebSocket server endpoint. It extends VirtualDevice to expose remote parameters as native Nallely ports that can participate in the link graph.

WebSocket Server Initialization

When a Nallely session starts via start_trevor(), the system instantiates a TrevorBus that creates a synchronous WebSocket server on the default port 6788. The initialization code in trevor_bus.py (lines 45-48) spawns the server using:

self.server = serve(self.handler, host=host, port=port)

This handler manages all incoming connections, routing them based on URL path segments to specific virtual device services.

Service Registration and Autoconfiguration

External clients connect to ws://<host>:6788/<service_name>/autoconfig, where the first path segment identifies the remote device. The server extracts this identifier using path.split("/")[1] as implemented in the handler method (lines 73-76).

Upon receiving an autoconfig request, the server expects a JSON payload describing the neuron's parameters—including name, min/max ranges, and optional streaming flags. The server stores these definitions in self.known_services and invokes self.configure_remote_device(service_name, parameters=parameters) to create corresponding VirtualParameter instances (lines 92-104 in trevor_bus.py).

Message Parsing and Routing

After autoconfiguration, clients transmit parameter updates using either binary frames or JSON messages:

  • Binary format: <len(name)> <name bytes> <float64 value> packed via struct.pack("!B{ln}s d") as defined in WebSocketBus.make_frame (nallely/websocket_bus.py, lines 57-64)
  • JSON format: {"on": "<param>", "value": <number>}

The server decodes these using WebSocketBus.parse_binary and parse_json (lines 75-86), then constructs virtual parameter names by prefixing the service identifier: f"{service_name}_{param_name}". Setting this attribute via setattr(self, parameter, value) triggers the VirtualDevice receive path, propagating the value through Nallely's link graph (lines 45-52).

Broadcasting to Connected Clients

The protocol supports multi-client scenarios through the WebSocketBus.receiving method. After processing an incoming message, the server forwards the binary frame to all other WebSocket clients subscribed to the same service:

for connected in self.connected[device]:
    connected.send(self.make_frame(...))

If binary packing fails, the system gracefully falls back to JSON transmission (lines 94-104 in websocket_bus.py). Cleanup occurs via WebSocketBus.unregister_service, which removes parameters, severs links, and closes sockets when clients disconnect or explicitly call /unregister.

NallelyWebsocketBus: The Client-Side Connector

The NallelyWebsocketBus class in nallely/distributed/remote_ws_connector.py provides the client-side implementation, allowing external Python processes to participate in the Trevor protocol.

Service Registration and Connection

Clients instantiate the bus and call register(kind, name, parameters, config), which constructs a NallelyService instance (lines 85-90). The service opens a WebSocket connection to ws://<address>/<name>/autoconfig in a background thread using ws_connect(self.url) (lines 84-87).

The client transmits the autoconfig payload as a JSON array:

ws.send(json.dumps({
    "kind": self.kind,
    "parameters": registration
}))

This handshake (lines 88-99) establishes the parameter schema on the server before data transmission begins.

Binary Frame Protocol

To minimize latency, the client sends parameter updates as compact binary frames. The NallelyService.send method invokes _build_frame(name, value) (lines 135-141), which packs the parameter name length, name bytes, and float64 value into a single binary buffer matching the server's expected format.

Bidirectional Message Flow

The client maintains a continuous receive loop in its _run method. Incoming binary frames are parsed via _parse_frame, while JSON messages use standard json.loads decoding (lines 105-118). Both formats yield a dictionary {"on": <param>, "value": <value>} that updates the local config dictionary and triggers any registered onmessage callbacks, enabling real-time synchronization between the Nallely session and the external process.

Complete Remote Control Example

The following example demonstrates starting a Trevor server and connecting an external neuron from a separate Python process:


# Server side - start Trevor session (runs WebSocket server on port 6788)

from nallely.trevor.trevor_bus import start_trevor

start_trevor(
    include_builtins=True,
    serve_ui=True,  # HTTP server on localhost:3000

)

# Client side - register external neuron and exchange values

from nallely.distributed.remote_ws_connector import NallelyWebsocketBus
import time

# Define exposed parameters with ranges

params = {
    "note": {"min": 0, "max": 127},
    "velocity": {"min": 0, "max": 127},
}
config = {"note": 0, "velocity": 0}

# Connect via WebSocket (defaults to localhost:6789 for UI proxy)

bus = NallelyWebsocketBus()

# Register "my_synth" neuron and block until autoconf completes

service = bus.register(
    kind="external",
    name="my_synth",
    parameters=params,
    config=config,
    block=True,
)

# Handle incoming updates from Nallely

def on_msg(msg):
    print(f"Received {msg['on']} = {msg['value']}")

service.onmessage = on_msg

# Send binary frame updates

service.send("note", 60)      # Middle C

service.send("velocity", 100)

time.sleep(1)

service.send("note", 0)       # Note-off

service.dispose()             # Graceful shutdown

In this flow, start_trevor instantiates the TrevorBus (lines 40-53 in trevor_bus.py), while the client creates a NallelyWebsocketBus (lines 71-90 in remote_ws_connector.py). The send method builds binary frames (lines 158-161) that the server parses via parse_binary (lines 75-71 in websocket_bus.py), updating the virtual parameter my_synth_note and broadcasting to all connected clients.

Summary

  • The TrevorBus in nallely/trevor/trevor_bus.py implements a WebSocket server on port 6788 that accepts autoconfiguration handshakes and binary/JSON parameter updates.
  • Remote devices register via URL paths like ws://host:6788/<service_name>/autoconfig, sending JSON schema definitions that create VirtualParameter instances through configure_remote_device.
  • The protocol uses a compact binary frame format (<len(name)> <name> <float64 value>) for efficient transmission, with JSON fallbacks for compatibility.
  • NallelyWebsocketBus in nallely/distributed/remote_ws_connector.py provides the client implementation, handling connection management, frame packing via _build_frame, and bidirectional message routing.
  • The system broadcasts updates to all subscribed clients, enabling multiple external processes to synchronize with the same Nallely session simultaneously.

Frequently Asked Questions

What port does the Trevor WebSocket protocol use by default?

The TrevorBus server listens on port 6788 by default, as specified in the serve(self.handler, host, port) call within TrevorBus.__init__ (nallely/trevor/trevor_bus.py, lines 45-48). The UI proxy typically runs on port 6789 for client connections, though the underlying WebSocket server remains on 6788.

How does the binary message format work in the Trevor protocol?

The binary format packs three elements: a single byte indicating the parameter name length, the UTF-8 encoded name bytes, and a 64-bit float value. The WebSocketBus.make_frame method constructs these frames using struct.pack("!B{ln}s d", len(name), name.encode(), value) (nallely/websocket_bus.py, lines 57-64). The client-side _build_frame method in NallelyService produces identical formatting for server compatibility.

Can multiple clients connect to the same TrevorBus service simultaneously?

Yes. The WebSocketBus.receiving method maintains a list of connected clients in self.connected[device] and forwards incoming binary frames to all other subscribers using connected.send(self.make_frame(...)) (lines 94-104 in websocket_bus.py). This enables multiple external processes to monitor or control the same virtual neuron parameters concurrently.

How does an external program register a new neuron with Nallely?

External programs use the NallelyWebsocketBus client library to call register(kind, name, parameters, config), which opens a WebSocket to ws://<address>/<name>/autoconfig and transmits a JSON payload containing the parameter definitions. The server's handler method detects the /autoconfig path ending, parses the JSON via json.loads, and invokes configure_remote_device to instantiate the corresponding virtual ports (trevor_bus.py, lines 92-104).

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →