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): ControllerServer

Parameters

  • 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