# How the Trevor WebSocket Protocol Enables Remote Device Control in Nallely

> Discover how the Trevor WebSocket protocol enables real-time remote device control for Nallely. Connect virtual neurons and update parameters instantly over WebSockets.

- Repository: [dr-schlange/nallely-midi](https://github.com/dr-schlange/nallely-midi)
- Tags: deep-dive
- Published: 2026-02-28

---

**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`](https://github.com/dr-schlange/nallely-midi/blob/main/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`](https://github.com/dr-schlange/nallely-midi/blob/main/trevor_bus.py) (lines 45-48) spawns the server using:

```python
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`](https://github.com/dr-schlange/nallely-midi/blob/main/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`](https://github.com/dr-schlange/nallely-midi/blob/main/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:

```python
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`](https://github.com/dr-schlange/nallely-midi/blob/main/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`](https://github.com/dr-schlange/nallely-midi/blob/main/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:

```python
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:

```python

# 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`](https://github.com/dr-schlange/nallely-midi/blob/main/trevor_bus.py)), while the client creates a `NallelyWebsocketBus` (lines 71-90 in [`remote_ws_connector.py`](https://github.com/dr-schlange/nallely-midi/blob/main/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`](https://github.com/dr-schlange/nallely-midi/blob/main/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`](https://github.com/dr-schlange/nallely-midi/blob/main/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`](https://github.com/dr-schlange/nallely-midi/blob/main/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`](https://github.com/dr-schlange/nallely-midi/blob/main/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`](https://github.com/dr-schlange/nallely-midi/blob/main/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`](https://github.com/dr-schlange/nallely-midi/blob/main/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`](https://github.com/dr-schlange/nallely-midi/blob/main/trevor_bus.py), lines 92-104).