ControllerServer
Minimal HTTP + WebSocket (RFC 6455) server for streaming controller input from a browser page, without SceneGraph. Adapted and trimmed from https://github.com/markwpearce/roku-gamepad - serves GET only (static files under pkg:/source/controller-web), no upload/directory listing.
Properties
port(integer)
Constructor
new ControllerServer(port: integer): ControllerServerParameters
port(integer)
Instance Methods
listen(messagePort: roMessagePort): boolean
Starts listening. Returns true if the socket bound/listened successfully (m.tcpListen.eOK()), false otherwise - the caller should surface a false return rather than silently proceeding with a server that never accepts connections.
Parameters
messagePort(roMessagePort)
Returns
boolean
drainEvents(): dynamic
Returns and clears the queue of connection lifecycle/message events collected since the last call.
Returns
dynamic
poll(): void
Drives accept/read directly by polling each tracked socket's own isReadable() rather than relying on the message port's socket-event notification. On at least one real device (2024 Roku Ultra), the roMessagePort passed to setMessagePort()/notifyReadable() never actually delivers a roSocketEvent for either the listening socket or an accepted connection, even though isReadable() itself correctly reflects a pending connection/readable data when polled directly - confirmed by instrumenting both paths against a real TCP client hung waiting on the listener. notifyReadable(true) is still called (it's what keeps isReadable() accurate), just not depended on to notify via the message port. Call this once per frame.
Returns
void
close(): void
Closes every open connection and the listening socket itself, freeing the port.
Returns
void
sendMessage(id: string, message: string): void
Sends one WebSocket text frame to a connection - e.g. Game sending {playerIndex, labels} once a handshake completes. No-op for an unknown/closed connection id.
Parameters
id(string)message(string)
Returns
void
closeConnection(id: string): void
Closes and forgets one tracked connection, pushing an onClose event if it had completed the websocket handshake. Safe to call for an already-closed/unknown id (no-op).
ControllerServerEvent's connectionId
Parameters
id(string) — the connection id, as passed to a
Returns
void
httpHeaderTerminatorFound(buffer: roByteArray): boolean
arrived, meaning a full set of request headers is present and parseHttpRequest() can be called regardless of total buffer size so far. Engine-internal; not private only so the spec can exercise it directly (needs no socket).
Parameters
buffer(roByteArray) — a connection's accumulated httpBuffer
Returns
boolean
httpBufferExceedsMax(bufferCount: integer): boolean
CONTROLLER_HTTP_BUFFER_SIZE and should be rejected. Only meaningful (and only ever checked by onHttpReadable) when httpHeaderTerminatorFound() is still false - see its doc comment for why the terminator check must come first. Engine-internal; not private only so the spec can exercise it directly.
Parameters
bufferCount(integer) — a connection's accumulated httpBuffer size
Returns
boolean
parseHttpRequest(
buffer: roByteArray,
bufferSize: integer,
): BGE.Controller.HttpRequest
Parses a raw request buffer into {command, path, protocol, headers}, or invalid if the request line is malformed (fewer than the three required tokens) - the caller closes the connection in that case. Engine-internal; not private only so the spec can exercise it.
Note: bufferSize is trusted as-is (no clamp to CONTROLLER_HTTP_BUFFER_SIZE) - an earlier clamp here silently corrupted any request larger than that cap on real hardware: buffer[bufferSize] = 0 wrote a null byte mid-buffer (at the clamped position, not the real end), and roByteArray.ToAsciiString() truncates at the first null on a real device (confirmed via on-device testing - brs-cli's ToAsciiString() does NOT truncate this way, so this was invisible to the headless test suite), silently cutting off everything after it, including the actual header terminator. onHttpReadable() is the only caller and already decides whether a buffer this large should even be parsed (see httpHeaderTerminatorFound()/httpBufferExceedsMax()) - by the time this function runs, the size is already accepted.
Parameters
buffer(roByteArray)bufferSize(integer)
Returns
BGE.Controller.HttpRequest
isWebSocketUpgradeRequest(
request: BGE.Controller.HttpRequest,
): boolean
True only for a request carrying all three RFC 6455 handshake headers. Engine-internal; not private only so the spec can exercise it.
Parameters
request(BGE.Controller.HttpRequest) — a parseHttpRequest() result
Returns
boolean
wsBufferExceedsMax(bufferCount: integer): boolean
CONTROLLER_WS_BUFFER_MAX_SIZE (see its own doc comment) and should be rejected. Engine-internal; not private only so the spec can exercise it directly (needs no socket).
Parameters
bufferCount(integer)
Returns
boolean