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(): Array.<BGE.Controller.ControllerServerEvent>
Returns and clears the queue of connection lifecycle/message events collected since the last call.
Returns
Array.<BGE.Controller.ControllerServerEvent>
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).
Parameters
id(string) — the connection id, as passed to a ControllerServerEvent's connectionId
Returns
void
httpHeaderTerminatorFound(buffer: roByteArray): boolean
Parameters
buffer(roByteArray) — a connection's accumulated httpBuffer
Returns
boolean
httpBufferExceedsMax(bufferCount: integer): boolean
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
Parameters
bufferCount(integer)
Returns
boolean