Skip to content

Matter Shell WebSocket #1990

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 24, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions packages/nodejs-shell/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,13 @@ $ ls .matter-shell-1
$ more .matter-shell-1/Node.ip
"fe80::148d:9bd8:5006:243%en0"
```
# Running over websockets

If the matter shell is started with the parameter --webSocketInterface all interaction with the shell will be done over a websocket instead of the local terminal. The parameter --webSocketPort NNNN can be used to change from the default port of 3000 to a user-specified port. If the parameter --webServer is added, the matter shell will also start an http server that will serve files from the same directory as the application itself utilizying the same port as the websocket. The functionality of the shell will be identical to the above description with the exception that the "exit" command will only close the websocket and not exit the matter shell application.

An example application that shows interaction from a web browser is included. The example shows how commands can be sent from html and javascript in the browser to the shell and how the results of the commands can be parsed to create a user interface.

![Web Interface](src/shell/webassets/WebShell.png "Example Application")
```
Expand Down
4 changes: 3 additions & 1 deletion packages/nodejs-shell/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"clean": "matter-build clean",
"build": "matter-build",
"build-clean": "matter-build --clean",
"shell": "matter-run src/app.ts"
"shell": "matter-run src/app.ts",
"bundle-device": "esbuild src/app.ts --bundle --platform=node --conditions=esbuild --external:@stoprocent/noble --external:@stoprocent/bluetooth-hci-socket --sourcemap --minify --keep-names --outfile=build/bundle/app.cjs"
},
"bin": {
"matter-shell": "dist/cjs/app.js"
Expand All @@ -41,6 +42,7 @@
"@matter/nodejs-ble": "*",
"@matter/tools": "*",
"@project-chip/matter.js": "*",
"ws": "^8.18.2",
"yargs": "^17.7.2"
},
"engines": {
Expand Down
47 changes: 42 additions & 5 deletions packages/nodejs-shell/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ import { Ble } from "@matter/protocol";
import yargs from "yargs/yargs";
import { MatterNode } from "./MatterNode.js";
import { Shell } from "./shell/Shell";
import { initializeWebPlumbing } from "./web_plumbing.js";

const PROMPT = "matter> ";
const DEFAULT_WEBSOCKET_PORT = 3000;
const logger = Logger.get("Shell");
let theShell: Shell;

if (process.stdin?.isTTY) Logger.format = LogFormat.ANSI;

let theNode: MatterNode;
Expand Down Expand Up @@ -86,12 +90,38 @@ async function main() {
type: "string",
default: undefined,
},
webSocketInterface: {
description: "Enable WebSocket interface",
type: "boolean",
default: false,
},
webSocketPort: {
description: "WebSocket and HTTP server port",
type: "number",
default: DEFAULT_WEBSOCKET_PORT,
},
webServer: {
description: "Enable Web server when using WebSocket interface",
type: "boolean",
default: false,
},
});
},
async argv => {
if (argv.help) return;

const { nodeNum, ble, bleHciId, nodeType, factoryReset, netInterface, logfile } = argv;
const {
nodeNum,
ble,
bleHciId,
nodeType,
factoryReset,
netInterface,
logfile,
webSocketInterface,
webSocketPort,
webServer,
} = argv;

theNode = new MatterNode(nodeNum, netInterface);
await theNode.initialize(factoryReset);
Expand All @@ -111,8 +141,12 @@ async function main() {
}
setLogLevel("default", await theNode.Store.get<string>("LogLevel", "info"));

const theShell = new Shell(theNode, nodeNum, PROMPT);

if (webSocketInterface) {
Logger.format = LogFormat.PLAIN;
initializeWebPlumbing(theNode, nodeNum, webSocketPort, webServer); // set up but wait for connect to create Shell
} else {
theShell = new Shell(theNode, nodeNum, PROMPT, process.stdin, process.stdout);
}
if (bleHciId !== undefined) {
await theNode.Store.set("BleHciId", bleHciId);
}
Expand All @@ -130,11 +164,14 @@ async function main() {
}

console.log(`Started Node #${nodeNum} (Type: ${nodeType}) ${ble ? "with" : "without"} BLE`);
theShell.start(theNode.storageLocation);
if (!webSocketInterface) {
theShell.start(theNode.storageLocation);
}
},
)
.version(false)
.scriptName("shell");
.scriptName("shell")
.strict();
await yargsInstance.wrap(yargsInstance.terminalWidth()).parseAsync();
}

Expand Down
24 changes: 15 additions & 9 deletions packages/nodejs-shell/src/shell/Shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { MatterError } from "@matter/general";
import { createWriteStream, readFileSync } from "node:fs";
import readline from "node:readline";
import { Readable, Writable } from "node:stream";
import yargs from "yargs/yargs";
import { MatterNode } from "../MatterNode.js";
import { exit } from "../app";
Expand Down Expand Up @@ -51,6 +52,8 @@ export class Shell {
public theNode: MatterNode,
public nodeNum: number,
public prompt: string,
public input: Readable,
public output: Writable,
) {}

start(storageBase?: string) {
Expand Down Expand Up @@ -80,9 +83,9 @@ export class Shell {
}
}
this.readline = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: true,
input: this.input,
output: this.output,
terminal: this.input === process.stdin && this.output === process.stdout,
prompt: this.prompt,
history: history.reverse(),
historySize: MAX_HISTORY_SIZE,
Expand All @@ -103,12 +106,15 @@ export class Shell {
} catch (e) {
process.stderr.write(`Error happened during history file write: ${e}\n`);
}
exit()
.then(() => process.exit(0))
.catch(e => {
process.stderr.write(`Close error: ${e}\n`);
process.exit(1);
});
// only exit if we are running in a terminal
if (this.input === process.stdin && this.output === process.stdout) {
exit()
.then(() => process.exit(0))
.catch(e => {
process.stderr.write(`Close error: ${e}\n`);
process.exit(1);
});
}
});

this.readline.prompt();
Expand Down
8 changes: 4 additions & 4 deletions packages/nodejs-shell/src/shell/cmd_subscribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,19 @@ export default function commands(theNode: MatterNode) {
},

handler: async (argv: any) => {
const { nodeId } = argv;
const node = (await theNode.connectAndGetNodes(nodeId))[0];
const { nodeId: subscribeNodeId } = argv;
const node = (await theNode.connectAndGetNodes(subscribeNodeId))[0];

await node.subscribeAllAttributesAndEvents({
attributeChangedCallback: ({ path: { nodeId, clusterId, endpointId, attributeName }, value }) =>
console.log(
`${nodeId}: Attribute ${nodeId}/${endpointId}/${clusterId}/${attributeName} changed to ${Diagnostic.json(
`${subscribeNodeId}: Attribute ${nodeId}/${endpointId}/${clusterId}/${attributeName} changed to ${Diagnostic.json(
value,
)}`,
),
eventTriggeredCallback: ({ path: { nodeId, clusterId, endpointId, eventName }, events }) =>
console.log(
`${nodeId} Event ${nodeId}/${endpointId}/${clusterId}/${eventName} triggered with ${Diagnostic.json(
`${subscribeNodeId} Event ${nodeId}/${endpointId}/${clusterId}/${eventName} triggered with ${Diagnostic.json(
events,
)}`,
),
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading