npm / kopytko-roku-device v1.3.3

Roku Device Toolkit

Everything needed to discover, control, profile, and debug a Roku device — one standalone, editor-agnostic npm package.

Subsystems

One package, every device protocol. All response shapes are verified against real hardware.

SSDP discovery

UDP 1900
SsdpClient

Active M-SEARCH scans + passive NOTIFY alive/byebye monitoring with per-IP debounce.

ECP client

HTTP 8060 / 80
EcpClient

Device info, apps, icons, deep-link launch/input, remote-control keys (keypress/keydown/keyup + Lit_ text entry), exit-app, active app, media-player state, registry, app state, chanperf, sgnodes (all/roots/by-id), app-ui (rendered UI tree), object counts, r2d2-bitmaps, graphics-frame-rate, TV channel queries, rendezvous + beacon tracking, dev-password validation (digest auth).

Discovery orchestration

DeviceManager

Merges SSDP, ECP health checks, persistence, and network-change events into one device list. Storage and network signals are injected interfaces.

SceneGraph debug console

TCP 8080
DebugConsoleClient

Idle-framed request/response console (chanperf, sgnodes, r2d2_bitmaps, free) with exponential-backoff auto-reconnect.

Interactive debug consoles

TCP 8085 / 8080
ConsoleStream, completeCommand

Raw bidirectional streaming for terminal use — every byte forwarded verbatim, prompts and unsolicited output included — plus a per-port command catalog with completion, subcommands and destructive-command detection.

Diagnostics parsers + collectors

8060 / 8080
PollingCollector + 10 collectors

Typed event model (mem-cpu, node-counts, textures, rendezvous, beacons, …) with parsers verified against real hardware.

Remote debug protocol

TCP 8081
ProtocolClient, DebugCommands, IOClient

Binary socket-based debugger: threads, stack traces, variables, breakpoints, step control, app output via the dynamic IO port.

Perfetto streaming

WS 8060
PerfettoWebSocketClient, enablePerfettoTracing

Streams raw Perfetto TracePacket protobuf from ws://device:8060/perfetto-session with a quiet-window stop.

RALE TrackerTask client

TCP 49152–65535
RaleTrackerClient, FrameDecoder

Live SceneGraph editing through an injected RALE TrackerTask: ECP-input activation, [start]/[end]-framed JSON commands (selectNode, setField, getNodeTree, …) with uuid correlation.

Web-admin automation

HTTP 80
InstallerClient

Digest-auth automation of the developer web-admin page: install/delete/rekey/package a channel, screenshot, profiling data, OS update check, reboot.

Installation

npm install kopytko-roku-device

Requires Node.js ≥ 24. The only runtime dependency is ws (Perfetto streaming).

Device discovery

DeviceManager orchestrates SSDP, ECP health checks, and persistence. Storage and network-change signals are injected interfaces, so it runs anywhere — VS Code, a CLI, or CI.

import { SsdpClient, EcpClient, DeviceManager } from 'kopytko-roku-device';

// DeviceStorage + NetworkWatcher are host-provided interfaces:
// the VS Code extension backs them with Memento global state and a
// focus-aware network poller; a CLI can use a JSON file + no-op watcher.
const manager = new DeviceManager(
  new SsdpClient(),
  new EcpClient(),
  myStorage,        // implements DeviceStorage
  myNetworkWatcher, // implements NetworkWatcher
);

manager.on('devices-changed', () => {
  for (const device of manager.getDevices()) {
    console.log(device.friendlyName, device.ip, device.state);
  }
});

await manager.initialize(); // starts SSDP + initial M-SEARCH scan

ECP queries

Typed wrappers over Roku's External Control Protocol on port 8060, plus dev-password validation via digest auth on port 80.

import { EcpClient, parseRegistryXml } from 'kopytko-roku-device';

const ecp = new EcpClient();

const info = await ecp.queryDeviceInfo('192.168.1.20');
console.log(info['model-name'], info['software-version']);

const apps = await ecp.queryApps('192.168.1.20');
const dev = apps.find((a) => a.id === 'dev'); // sideloaded channel

// Registry of the sideloaded channel (all channels sharing the dev key)
const registryXml = await ecp.queryRegistry('192.168.1.20', 'dev');
const registry = parseRegistryXml(registryXml);
console.log(registry.plugins); // e.g. "158987,268970,dev"

// Deep linking — relaunch with params, or roInput to the running channel
await ecp.launchApp('192.168.1.20', '12', { contentId: 'movie-123', mediaType: 'movie' });
await ecp.sendInput('192.168.1.20', { contentId: 'movie-123', mediaType: 'movie' });

// Channel icon — raw bytes + content type (binary-safe)
const { data, contentType } = await ecp.queryAppIcon('192.168.1.20', '12');

Remote control & playback state

Simulate the remote (/keypress, /keydown, /keyup), type on the on-screen keyboard with ordered Lit_ keypresses, and query the foreground app and media-player state — the primitives under the extension's Device Manager remote and RASP script runner.

import { EcpClient, EcpKeys, textToLitKeys } from 'kopytko-roku-device';

const ecp = new EcpClient();

// Press-and-release a named key (POST /keypress/{key})
await ecp.keypress('192.168.1.20', EcpKeys.Home);

// Hold a key — the device keeps it pressed until the matching keyup
await ecp.keydown('192.168.1.20', EcpKeys.Right);
await ecp.keyup('192.168.1.20', EcpKeys.Right);

// Type on the on-screen keyboard: one strictly sequential keypress per
// Unicode code point, Lit_-encoded ('€' → 'Lit_%E2%82%AC')
await ecp.sendText('192.168.1.20', 'my search term');
textToLitKeys('r€'); // → ['Lit_r', 'Lit_%E2%82%AC']

// Playback state — foreground app + media player (state, codecs, DRM)
const active = await ecp.queryActiveApp('192.168.1.20');
const player = await ecp.queryMediaPlayer('192.168.1.20');
if (player.state === 'play') console.log(player.format?.video, player.format?.drm);

Runtime metrics

Polling collectors emit a typed event stream (mem-cpu, node-counts, textures, rendezvous, fw-beacon, …) — the data source of the extension's Kopytko Diagnostics panel.

import {
  DebugConsoleClient,
  ChanperfCollector,
  EcpObjectCountsCollector,
} from 'kopytko-roku-device';

// SceneGraph debug console (port 8080) — idle-framed, auto-reconnecting
const console8080 = new DebugConsoleClient({ host: '192.168.1.20' });

// Poll per-channel CPU + memory every second
const chanperf = new ChanperfCollector(console8080, 1000);
chanperf.on('sample', (s) => {
  // { type: 'mem-cpu', wall, memKiB, anonKiB, cpuPct, ... }
  console.log(s);
});
chanperf.start();

// Collectors never throw into callers: a failed poll skips that
// interval and the collector self-heals when the device answers again.

BrightScript debugger protocol

A client for Roku's socket-based debug protocol — the transport under the extension's DAP debugger.

import { ProtocolClient, DebugCommands, IOClient } from 'kopytko-roku-device';

// Binary BrightScript remote debug protocol (port 8081)
const client = new ProtocolClient('192.168.1.20');
client.on('update', (update) => console.log(update)); // stops, compile errors, IO port
await client.connect(); // 8-byte magic handshake + version negotiation

const commands = new DebugCommands(client);
const threads = await commands.threads();
const stack = await commands.stacktrace(threads.primary);
await commands.addBreakpoints([{ filePath: 'pkg:/source/main.brs', lineNumber: 42 }]);

Web-admin automation

Drives the developer web-admin page (http://<device-ip>/) the same way a browser would — install/delete/rekey/package a channel, capture a screenshot, download profiling data, or check for updates and reboot. Surfaced in the extension through the Device Manager's Device view.

import { InstallerClient } from 'kopytko-roku-device';

const installer = new InstallerClient();
const password = 'my-dev-password'; // username is always 'rokudev'

// Installer tab
await installer.installChannel('192.168.1.20', password, './build/archive.zip');

// Packager tab — installs, signs, and downloads the .pkg
await installer.packageChannel(
  '192.168.1.20', password, './build/archive.zip',
  'MyApp/1.0', 'my-signing-password', './out/signed.pkg',
);

// Utilities tab
await installer.takeScreenshot('192.168.1.20', password, './out/screenshot.jpg');
await installer.downloadProfilingData('192.168.1.20', password, './out/channel.bsprof');

// Update tab
await installer.checkForUpdate('192.168.1.20', password);

RALE TrackerTask client

Talks to an injected Roku Advanced Layout Editor TrackerTask to edit SceneGraph nodes on a running app: an ECP input activates the task, which opens a TCP server on the device; commands are [start]{json}[end]-framed JSON with uuid-correlated, chunked responses. Powers the extension's SceneGraph Tree Edit mode.

import { EcpClient, RaleTrackerClient } from 'kopytko-roku-device';

// Requires Roku's RALE TrackerTask injected and started in the (dev) channel.
const rale = new RaleTrackerClient({ host: '192.168.1.20', ecp: new EcpClient() });
const { raleVersion } = await rale.connect(); // ECP activate -> TCP connect -> init
await rale.hideSelectorView();                // no red selector rectangle on the TV

// setField writes to the *selected* node — always selectNode first.
const { node } = await rale.selectNode([{ child: 0 }, { child: 2 }]);
if (node.item.subtype === 'Label') {
  await rale.setField('text', 'Hello from RALE');
  await rale.setField('visible', true);
}

rale.close(); // the task returns to awaiting activation

Terminal CLI

kopytko-roku exposes every ECP and web-admin operation above from the shell — no VS Code required. Config resolves from flags → --config file → ROKU_HOST/ROKU_PASSWORD env vars; it deliberately never reads .vscode/settings.json or .kopytkorc. See the full CLI reference for every subcommand and flag.

npm install -g kopytko-roku-device
kopytko-roku --help

# ECP
kopytko-roku ecp device-info --host 192.168.1.20
kopytko-roku ecp keypress --host 192.168.1.20 --key Home
kopytko-roku ecp launch --host 192.168.1.20 --app dev --param contentId=42
kopytko-roku ecp app-ui --host 192.168.1.20

# Developer web-admin
kopytko-roku installer screenshot --host 192.168.1.20 --password secret --out shot.jpg

# Discovery
kopytko-roku discover --json

Device behaviour notes

  • GET /query/sgrendezvous and GET /query/fwbeacons drain the device-side event queue — two simultaneous pollers each miss half the events.
  • chanperf / sgnodes / the port-8080 console report nothing while the channel is backgrounded — a Roku OS limitation, not an error. Rendezvous, beacons, and app-state keep working.
  • Port-8080 responses are idle-framed: > appears inside XML output, so it can't be used as a terminator.
  • The port-8085 log stream accepts only one consumer at a time — beacons are collected via ECP instead.
  • /plugin_install returns HTTP 200 for both success and failure — the real result is a success/error/info message embedded as JSON in the response page, which InstallerClient parses and throws on.