Skip to content

Commit

Permalink
Refactored core library to use async\await (#453)
Browse files Browse the repository at this point in the history
* Made `core` library to use async/await and turned Port Messages asynchronous

* Implemented control keys using `postMessage`

* Fixed `AstPrinter`

* Improved performance and fixed issues

* Fixed code formatting

* Fixed linting issues

* Fixed `visitDim`

* Fixed `visitTryCatch`

* Turned `e2e` and `interpreter` unit tests async

* Turned `stdlib` unit tests async

* Turned `brsTypes` unit tests async

* Fixed lint issue

* Implemented `MediaEvents` for `audio` and `video` using PostMessage

* Implemented `InputEvent` using PostMessage

* Implemented `wav` events using Post Message

* Removing commented code

* Implemented `getPlaybackDuration` using PostMessage

* Implemented `SystemLog` event using PostMessage

* Implemented `CECStatusEvent` using PostMessage

* Implemented `MemoryInfoEvent` using PostMessage

* Implemented `DebugEvent` using PostMessage and fixed MicroDebugger

* Fixed code formatting

* Adapted CLI and ECP service to use PostMessage

* Implemented support for TTY key press handling on CLI

* Fixed MicroDebugger support for CLI

* Improved ECP app list handling

* Added a flag to enable TTY control simulation

* Upgraded dependencies

* Added more TTY key control mappings

* Updated CLI and Remote Control documentation

* Fixed WAV events and standardized common constants

* Fixed linting issue

* Simplified `sendKey()` on `control.ts`

* Improved `sendInput()` on `control.ts`

* Reduced complexity

* Reduced complexity of `subscribeControl` handler on API

* Documented MicroDebugger functions

* Reduced complexity
  • Loading branch information
lvcabral authored Jan 18, 2025
1 parent 86b6fb2 commit 039d1ae
Show file tree
Hide file tree
Showing 89 changed files with 3,983 additions and 3,131 deletions.
31 changes: 15 additions & 16 deletions docs/remote-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,25 @@ The `brs-engine` simulates the Roku remote control, offering customizable mappin

If you want to create your own control simulation, such as a touch screen control, you can use the [Engine API](./engine-api.md) methods `sendKeyDown`, `sendKeyUp`, and `sendKeyPress`.

The NodeJS `brs.ecp.js` library supports sending commands via the [Roku ECP API](https://developer.roku.com/docs/developer-program/dev-tools/external-control-api.md), which can also be used to simulate remote control for applications [running under the CLI](./run-as-cli.md).

For applications [running under the CLI](./run-as-cli.md) the NodeJS library `brs.ecp.js` supports control commands via the **Keyboard** (option `--tty`) or [Roku ECP API](https://developer.roku.com/docs/developer-program/dev-tools/external-control-api.md), which can be enabled as a server using the `--ecp` option.
## Keyboard and Game Pad Control Reference

The default mapping of the keyboard and game pads to Roku remote control is described below:

| Keyboard | Game Pad | Roku Control | Description |
|-------------|------------|--------------|-----------------------------------------------------------------------|
| Esc or Del | 1 | Back | Return to the previous screen, some apps will close at the main menu. |
| Home or Shift+Esc| 8 | Home | Close the currently loaded app. |
| Arrow Keys |Joys & D-Pad| D-Pad | Directional controls to navigate on menus and control games. |
| Backspace | 6 or 16 | Replay | Instant replay button. |
| Enter | 0 | OK | Select button. |
| Insert | 4 or 7 | Info | Information/Settings button |
| PageDown | 2 | Rewind | Reverse scan button. |
| PageUp | 3 | Fast Forward | Forward scan button. |
| End | 5 or 9 | Play/Pause | Play/Pause button. |
| Ctrl+A | 10 | A | A game button. |
| Ctrl+Z | 11 | B | B game button. |
| F10 | 17 | Volume Mute | Button to toggle the simulator audio mute on/off. |
| Browser Keys | TTY Keys | Game Pad | Roku Control | Description |
|---------------|-----------|------------|---------------|-----------------------------------------------------------------------|
| Esc/Del | Esc/Del | 1 | Back | Return to the previous screen, some apps will close at the main menu. |
| Home/Shift+Esc| Home/Ctrl+D| 8 | Home | Close the currently loaded app. |
| Arrow Keys | Arrow Keys| D-Pad or Joys| D-Pad | Directional controls to navigate on menus and control games. |
| Backspace | Backspace | 6 or 16 | Replay | Instant replay button. |
| Enter | Return | 0 | OK | Select button. |
| Insert | Insert | 4 or 7 | Info | Information/Settings button |
| PageDown | Comma/PgDown| 2 | Rewind | Reverse scan button. |
| PageUp | Period/PgUp | 3 | Fast Forward | Forward scan button. |
| End | Space/End | 5 or 9 | Play/Pause | Play/Pause button. |
| Ctrl+A | A | 10 | A | A game button. |
| Ctrl+Z | Z | 11 | B | B game button. |
| F10 | _n.a._ | 17 | Volume Mute | Button to toggle the simulator audio mute on/off. |

**Note:** There are mappings not listed above, specific for MacOS or Windows, please look at the file [`src/api/control.ts`](../src/api/control.ts) for details.

Expand Down
7 changes: 6 additions & 1 deletion docs/run-as-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Options:
-c, --colors <level> Define the console color level (0 to disable). (default: 3)
-d, --debug Open the micro debugger if the app crashes.
-e, --ecp Enable the ECP server for control simulation.
-t, --tty Enable the keyboard (TTY) for control simulation.
-p, --pack <password> The password to generate the encrypted package. (default: "")
-o, --out <directory> The directory to save the encrypted package file. (default: "./")
-r, --root <directory> The root directory from which `pkg:` paths will be resolved.
Expand Down Expand Up @@ -107,7 +108,11 @@ The `<columns>` defines the width in number of character columns, the height wil

### Controlling the App

The CLI runs the BrightScript Engine on a single thread, if you need to use control simulation, enable the option `--ecp` that will launch the ECP Server in port 8060 (same as a Roku device). With this option enabled, you can connect to your computer using any remote control app that uses ECP, including the [Roku Remote Tool](https://devtools.web.roku.com/#remote-tool), the [Roku GamePad Gateway](http://github.com/lvcabral/roku-gpg) or the Roku mobile apps. This option also enables an SSDP service to allow it to be discovered in your local network.
If you need to use remote control simulation, you have two options:

1. Enable the option `--tty` that will allow the usage of the keyboard to control the app or start the **Micro Debugger** with `Ctrl+C`. Check the [keyboard mapping](./remote-control.md) to learn all the keyboard shortcuts available.
1. Enable the option `--ecp` that will launch the ECP Server in port 8060 (same as a Roku device). With this option enabled, you can connect to your computer using any remote control app that uses ECP, including the [Roku Remote Tool](https://devtools.web.roku.com/#remote-tool), the [Roku GamePad Gateway](http://github.com/lvcabral/roku-gpg) or the Roku mobile apps. This option also enables an SSDP service to allow it to be discovered in your local network.


### Creating an encrypted App package file

Expand Down
131 changes: 74 additions & 57 deletions src/api/control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,8 @@
*
* Licensed under the MIT License. See LICENSE in the repository root for license information.
*--------------------------------------------------------------------------------------------*/
import { SubscribeCallback, saveDataBuffer } from "./util";
import {
DataType,
RemoteType,
DebugCommand,
keyBufferSize,
keyArraySpots,
platform,
BufferType,
} from "../core/common";
import { SubscribeCallback } from "./util";
import { RemoteType, platform, ControlEvent, InputEvent } from "../core/common";
/// #if BROWSER
import { deviceData } from "./package";
import gameControl, { GCGamepad, EventName } from "esm-gamecontroller.js";
Expand Down Expand Up @@ -57,12 +49,11 @@ const rokuKeys: Map<string, number> = new Map([

// Initialize Control Module
const controls = { keyboard: true, gamePads: true };
let sharedArray: Int32Array;
const keysMap: Map<string, string> = new Map();
let sendKeysEnabled = false;
let disableDebug: boolean = false;

export function initControlModule(array: Int32Array, options: any = {}) {
sharedArray = array;
export function initControlModule(options: any = {}) {
if (typeof options.disableDebug === "boolean") {
disableDebug = options.disableDebug;
}
Expand Down Expand Up @@ -123,67 +114,52 @@ export function enableSendKeys(enable: boolean) {

export function sendKey(key: string, mod: number, type: RemoteType = RemoteType.SIM, index = 0) {
key = key.toLowerCase();
if (["home", "volumemute", "poweroff"].includes(key)) {
if (mod === 0) {
notifyAll(key);
}
notifyAll("control", { key: key, mod: mod });
let handled = false;
const controlEvent: ControlEvent = {
key: -1,
mod: mod,
remote: `${RemoteType[type]}:${index}`,
};
if (["home", "volumemute", "poweroff"].includes(key) && mod === 0) {
notifyAll(key);
handled = true;
} else if (!sendKeysEnabled) {
return;
} else if (key === "break") {
if (!disableDebug) {
Atomics.store(sharedArray, DataType.DBG, DebugCommand.BREAK);
notifyAll("control", { key: key, mod: mod });
}
} else if (key === "break" && !disableDebug && mod === 0) {
notifyAll("break");
handled = true;
} else if (rokuKeys.has(key)) {
const code = rokuKeys.get(key);
if (typeof code !== "undefined") {
const next = getNext();
Atomics.store(sharedArray, DataType.RID + next, type + index);
Atomics.store(sharedArray, DataType.MOD + next, mod);
Atomics.store(sharedArray, DataType.KEY + next, code + mod);
notifyAll("control", { key: key, mod: mod });
}
} else if (key.slice(0, 4).toLowerCase() === "lit_") {
if (key.slice(4).length === 1 && key.charCodeAt(4) >= 32 && key.charCodeAt(4) < 255) {
const next = getNext();
Atomics.store(sharedArray, DataType.RID + next, type + index);
Atomics.store(sharedArray, DataType.MOD + next, mod);
Atomics.store(sharedArray, DataType.KEY + next, key.charCodeAt(4) + mod);
notifyAll("control", { key: key, mod: mod });
controlEvent.key = code + mod;
handled = true;
}
} else if (
key.toLowerCase().startsWith("lit_") &&
key.slice(4).length === 1 &&
key.charCodeAt(4) >= 32 &&
key.charCodeAt(4) < 255
) {
controlEvent.key = key.charCodeAt(4) + mod;
handled = true;
}
}

function getNext() {
for (let i = 0; i < keyBufferSize; i++) {
const next = i * keyArraySpots;
if (Atomics.load(sharedArray, DataType.KEY + next) < 0) {
return next;
}
if (controlEvent.key >= 0) {
notifyAll("post", controlEvent);
}
// buffer full
for (let i = 1; i < keyBufferSize; i++) {
const prev = (i - 1) * keyArraySpots;
const next = i * keyArraySpots;
Atomics.store(
sharedArray,
DataType.KEY + prev,
Atomics.load(sharedArray, DataType.KEY + next)
);
if (handled) {
notifyAll("control", { key: key, mod: mod });
}
return (keyBufferSize - 1) * keyArraySpots;
}

// Input API
export function sendInput(data: object) {
saveDataBuffer(sharedArray, JSON.stringify(data), BufferType.INPUT);
const inputEvent: InputEvent = { source_ip_addr: "", ...data };
notifyAll("post", inputEvent);
}

/// #if BROWSER

// Keyboard Mapping
const keysMap: Map<string, string> = new Map();
// Keyboard Mapping (Browser)
keysMap.set("ArrowUp", "up");
keysMap.set("ArrowDown", "down");
keysMap.set("ArrowLeft", "left");
Expand Down Expand Up @@ -333,4 +309,45 @@ function gamePadSubscribe(gamePad: GCGamepad, eventName: EventName, index: numbe
function gamePadOffHandler(id: number) {
console.info(`GamePad ${id} disconnected!`);
}
/// #else

// Keyboard Mapping (TTY)
keysMap.set("\x1B[A", "up");
keysMap.set("\x1B[B", "down");
keysMap.set("\x1B[D", "left");
keysMap.set("\x1B[C", "right");
keysMap.set("\r", "select");
keysMap.set("\x1B", "back");
keysMap.set("\x1B[3~", "back");
keysMap.set("\x1B[H", "home");
keysMap.set("\x1B[1~", "home");
keysMap.set("\x7F", "instantreplay");
keysMap.set("\x1B[F", "play");
keysMap.set("\x1B[4~", "play");
keysMap.set("\x1B[6~", "rev");
keysMap.set("\x1B[5~", "fwd");
keysMap.set("\x03", "break");
keysMap.set("\x04", "home");
keysMap.set("\x18", "exit");
keysMap.set("\b", "backspace");
keysMap.set(" ", "play");
keysMap.set(",", "rev");
keysMap.set(".", "fwd");
keysMap.set("*", "info");
keysMap.set("\x1B[2~", "info");
keysMap.set("a", "a");
keysMap.set("z", "b");

export function handleKeypressEvent(str: string, keyData: any) {
if (!controls.keyboard) {
return;
}
const key = keysMap.get(keyData.sequence);
if (key && key.toLowerCase() !== "ignore") {
setTimeout(function () {
sendKey(key, 100);
}, 300);
sendKey(key, 0);
}
}
/// #endif
2 changes: 1 addition & 1 deletion src/api/display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function initDisplayModule(mode: string, perfStats = false) {
if (event === "rect") {
videoRect = data;
return;
} else if (event === "bandwidth") {
} else if (["bandwidth", "post"].includes(event)) {
return;
}
videoState = event;
Expand Down
Loading

0 comments on commit 039d1ae

Please sign in to comment.