@matter/nodejs-shell 0.14.0-alpha.0-20250524-51a7e1721 → 0.14.0-alpha.0-20250525-d6ada0d45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/cjs/app.js +40 -4
- package/dist/cjs/app.js.map +1 -1
- package/dist/cjs/shell/Shell.js +12 -8
- package/dist/cjs/shell/Shell.js.map +1 -1
- package/dist/cjs/shell/cmd_subscribe.js +6 -6
- package/dist/cjs/shell/cmd_subscribe.js.map +2 -2
- package/dist/cjs/web_plumbing.js +170 -0
- package/dist/cjs/web_plumbing.js.map +6 -0
- package/package.json +13 -11
- package/src/app.ts +42 -5
- package/src/shell/Shell.ts +15 -9
- package/src/shell/cmd_subscribe.ts +4 -4
- package/src/shell/webassets/WebShell.png +0 -0
- package/src/shell/webassets/favicon.png +0 -0
- package/src/shell/webassets/index.html +428 -0
- package/src/web_plumbing.ts +173 -0
package/README.md
CHANGED
|
@@ -203,7 +203,13 @@ $ ls .matter-shell-1
|
|
|
203
203
|
$ more .matter-shell-1/Node.ip
|
|
204
204
|
"fe80::148d:9bd8:5006:243%en0"
|
|
205
205
|
```
|
|
206
|
+
# Running over websockets
|
|
206
207
|
|
|
208
|
+
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.
|
|
209
|
+
|
|
210
|
+
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.
|
|
211
|
+
|
|
212
|
+

|
|
207
213
|
```
|
|
208
214
|
█
|
|
209
215
|
█
|
package/dist/cjs/app.js
CHANGED
|
@@ -40,13 +40,16 @@ var import_protocol = require("#protocol");
|
|
|
40
40
|
var import_yargs = __toESM(require("yargs/yargs"));
|
|
41
41
|
var import_MatterNode = require("./MatterNode.js");
|
|
42
42
|
var import_Shell = require("./shell/Shell");
|
|
43
|
+
var import_web_plumbing = require("./web_plumbing.js");
|
|
43
44
|
/**
|
|
44
45
|
* @license
|
|
45
46
|
* Copyright 2022-2025 Matter.js Authors
|
|
46
47
|
* SPDX-License-Identifier: Apache-2.0
|
|
47
48
|
*/
|
|
48
49
|
const PROMPT = "matter> ";
|
|
50
|
+
const DEFAULT_WEBSOCKET_PORT = 3e3;
|
|
49
51
|
const logger = import_general.Logger.get("Shell");
|
|
52
|
+
let theShell;
|
|
50
53
|
if (process.stdin?.isTTY) import_general.Logger.format = import_general.LogFormat.ANSI;
|
|
51
54
|
let theNode;
|
|
52
55
|
function setLogLevel(identifier, level) {
|
|
@@ -105,12 +108,38 @@ async function main() {
|
|
|
105
108
|
description: "Logfile to use to log to. By Default debug loglevel is logged to the file. The provided value will be persisted for future runs.",
|
|
106
109
|
type: "string",
|
|
107
110
|
default: void 0
|
|
111
|
+
},
|
|
112
|
+
webSocketInterface: {
|
|
113
|
+
description: "Enable WebSocket interface",
|
|
114
|
+
type: "boolean",
|
|
115
|
+
default: false
|
|
116
|
+
},
|
|
117
|
+
webSocketPort: {
|
|
118
|
+
description: "WebSocket and HTTP server port",
|
|
119
|
+
type: "number",
|
|
120
|
+
default: DEFAULT_WEBSOCKET_PORT
|
|
121
|
+
},
|
|
122
|
+
webServer: {
|
|
123
|
+
description: "Enable Web server when using WebSocket interface",
|
|
124
|
+
type: "boolean",
|
|
125
|
+
default: false
|
|
108
126
|
}
|
|
109
127
|
});
|
|
110
128
|
},
|
|
111
129
|
async (argv) => {
|
|
112
130
|
if (argv.help) return;
|
|
113
|
-
const {
|
|
131
|
+
const {
|
|
132
|
+
nodeNum,
|
|
133
|
+
ble,
|
|
134
|
+
bleHciId,
|
|
135
|
+
nodeType,
|
|
136
|
+
factoryReset,
|
|
137
|
+
netInterface,
|
|
138
|
+
logfile,
|
|
139
|
+
webSocketInterface,
|
|
140
|
+
webSocketPort,
|
|
141
|
+
webServer
|
|
142
|
+
} = argv;
|
|
114
143
|
theNode = new import_MatterNode.MatterNode(nodeNum, netInterface);
|
|
115
144
|
await theNode.initialize(factoryReset);
|
|
116
145
|
if (logfile !== void 0) {
|
|
@@ -127,7 +156,12 @@ async function main() {
|
|
|
127
156
|
}
|
|
128
157
|
}
|
|
129
158
|
setLogLevel("default", await theNode.Store.get("LogLevel", "info"));
|
|
130
|
-
|
|
159
|
+
if (webSocketInterface) {
|
|
160
|
+
import_general.Logger.format = import_general.LogFormat.PLAIN;
|
|
161
|
+
(0, import_web_plumbing.initializeWebPlumbing)(theNode, nodeNum, webSocketPort, webServer);
|
|
162
|
+
} else {
|
|
163
|
+
theShell = new import_Shell.Shell(theNode, nodeNum, PROMPT, process.stdin, process.stdout);
|
|
164
|
+
}
|
|
131
165
|
if (bleHciId !== void 0) {
|
|
132
166
|
await theNode.Store.set("BleHciId", bleHciId);
|
|
133
167
|
}
|
|
@@ -141,9 +175,11 @@ async function main() {
|
|
|
141
175
|
);
|
|
142
176
|
}
|
|
143
177
|
console.log(`Started Node #${nodeNum} (Type: ${nodeType}) ${ble ? "with" : "without"} BLE`);
|
|
144
|
-
|
|
178
|
+
if (!webSocketInterface) {
|
|
179
|
+
theShell.start(theNode.storageLocation);
|
|
180
|
+
}
|
|
145
181
|
}
|
|
146
|
-
).version(false).scriptName("shell");
|
|
182
|
+
).version(false).scriptName("shell").strict();
|
|
147
183
|
await yargsInstance.wrap(yargsInstance.terminalWidth()).parseAsync();
|
|
148
184
|
}
|
|
149
185
|
process.on("message", function(message) {
|
package/dist/cjs/app.js.map
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/app.ts"],
|
|
4
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOA,qBAAoF;AACpF,oBAAiC;AACjC,wBAA0B;AAC1B,sBAAoB;AACpB,mBAAkB;AAClB,wBAA2B;AAC3B,mBAAsB;
|
|
4
|
+
"mappings": "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",
|
|
5
5
|
"names": ["yargs"]
|
|
6
6
|
}
|
package/dist/cjs/shell/Shell.js
CHANGED
|
@@ -69,10 +69,12 @@ class Shell {
|
|
|
69
69
|
/**
|
|
70
70
|
* Construct a new Shell object.
|
|
71
71
|
*/
|
|
72
|
-
constructor(theNode, nodeNum, prompt) {
|
|
72
|
+
constructor(theNode, nodeNum, prompt, input, output) {
|
|
73
73
|
this.theNode = theNode;
|
|
74
74
|
this.nodeNum = nodeNum;
|
|
75
75
|
this.prompt = prompt;
|
|
76
|
+
this.input = input;
|
|
77
|
+
this.output = output;
|
|
76
78
|
}
|
|
77
79
|
readline;
|
|
78
80
|
writeStream;
|
|
@@ -103,9 +105,9 @@ class Shell {
|
|
|
103
105
|
}
|
|
104
106
|
}
|
|
105
107
|
this.readline = import_node_readline.default.createInterface({
|
|
106
|
-
input:
|
|
107
|
-
output:
|
|
108
|
-
terminal:
|
|
108
|
+
input: this.input,
|
|
109
|
+
output: this.output,
|
|
110
|
+
terminal: this.input === process.stdin && this.output === process.stdout,
|
|
109
111
|
prompt: this.prompt,
|
|
110
112
|
history: history.reverse(),
|
|
111
113
|
historySize: MAX_HISTORY_SIZE
|
|
@@ -125,11 +127,13 @@ class Shell {
|
|
|
125
127
|
process.stderr.write(`Error happened during history file write: ${e}
|
|
126
128
|
`);
|
|
127
129
|
}
|
|
128
|
-
(
|
|
129
|
-
process.
|
|
130
|
+
if (this.input === process.stdin && this.output === process.stdout) {
|
|
131
|
+
(0, import_app.exit)().then(() => process.exit(0)).catch((e) => {
|
|
132
|
+
process.stderr.write(`Close error: ${e}
|
|
130
133
|
`);
|
|
131
|
-
|
|
132
|
-
|
|
134
|
+
process.exit(1);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
133
137
|
});
|
|
134
138
|
this.readline.prompt();
|
|
135
139
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/shell/Shell.ts"],
|
|
4
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAMA,qBAA4B;AAC5B,qBAAgD;AAChD,2BAAqB;
|
|
4
|
+
"mappings": "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",
|
|
5
5
|
"names": ["readline", "yargs", "cmdCommission", "cmdConfig", "cmdSession", "cmdNodes", "cmdSubscribe", "cmdIdentify", "cmdDiscover", "cmdAttributes", "cmdEvents", "cmdCommands", "cmdTlv"]
|
|
6
6
|
}
|
|
@@ -39,16 +39,16 @@ function commands(theNode) {
|
|
|
39
39
|
});
|
|
40
40
|
},
|
|
41
41
|
handler: async (argv) => {
|
|
42
|
-
const { nodeId } = argv;
|
|
43
|
-
const node = (await theNode.connectAndGetNodes(
|
|
42
|
+
const { nodeId: subscribeNodeId } = argv;
|
|
43
|
+
const node = (await theNode.connectAndGetNodes(subscribeNodeId))[0];
|
|
44
44
|
await node.subscribeAllAttributesAndEvents({
|
|
45
|
-
attributeChangedCallback: ({ path: { nodeId
|
|
46
|
-
`${
|
|
45
|
+
attributeChangedCallback: ({ path: { nodeId, clusterId, endpointId, attributeName }, value }) => console.log(
|
|
46
|
+
`${subscribeNodeId}: Attribute ${nodeId}/${endpointId}/${clusterId}/${attributeName} changed to ${import_general.Diagnostic.json(
|
|
47
47
|
value
|
|
48
48
|
)}`
|
|
49
49
|
),
|
|
50
|
-
eventTriggeredCallback: ({ path: { nodeId
|
|
51
|
-
`${
|
|
50
|
+
eventTriggeredCallback: ({ path: { nodeId, clusterId, endpointId, eventName }, events }) => console.log(
|
|
51
|
+
`${subscribeNodeId} Event ${nodeId}/${endpointId}/${clusterId}/${eventName} triggered with ${import_general.Diagnostic.json(
|
|
52
52
|
events
|
|
53
53
|
)}`
|
|
54
54
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/shell/cmd_subscribe.ts"],
|
|
4
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAMA,qBAA2B;AAN3B;AAAA;AAAA;AAAA;AAAA;AAUe,SAAR,SAA0B,SAAqB;AAClD,SAAO;AAAA,IACH,SAAS;AAAA,IACT,UAAU;AAAA,IACV,SAAS,CAAC,UAAgB;AACtB,aAAO,MAAM,WAAW,WAAW;AAAA,QAC/B,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,MACV,CAAC;AAAA,IACL;AAAA,IAEA,SAAS,OAAO,SAAc;AAC1B,YAAM,EAAE,
|
|
5
|
-
"names": [
|
|
4
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAMA,qBAA2B;AAN3B;AAAA;AAAA;AAAA;AAAA;AAUe,SAAR,SAA0B,SAAqB;AAClD,SAAO;AAAA,IACH,SAAS;AAAA,IACT,UAAU;AAAA,IACV,SAAS,CAAC,UAAgB;AACtB,aAAO,MAAM,WAAW,WAAW;AAAA,QAC/B,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,MACV,CAAC;AAAA,IACL;AAAA,IAEA,SAAS,OAAO,SAAc;AAC1B,YAAM,EAAE,QAAQ,gBAAgB,IAAI;AACpC,YAAM,QAAQ,MAAM,QAAQ,mBAAmB,eAAe,GAAG,CAAC;AAElE,YAAM,KAAK,gCAAgC;AAAA,QACvC,0BAA0B,CAAC,EAAE,MAAM,EAAE,QAAQ,WAAW,YAAY,cAAc,GAAG,MAAM,MACvF,QAAQ;AAAA,UACJ,GAAG,eAAe,eAAe,MAAM,IAAI,UAAU,IAAI,SAAS,IAAI,aAAa,eAAe,0BAAW;AAAA,YACzG;AAAA,UACJ,CAAC;AAAA,QACL;AAAA,QACJ,wBAAwB,CAAC,EAAE,MAAM,EAAE,QAAQ,WAAW,YAAY,UAAU,GAAG,OAAO,MAClF,QAAQ;AAAA,UACJ,GAAG,eAAe,UAAU,MAAM,IAAI,UAAU,IAAI,SAAS,IAAI,SAAS,mBAAmB,0BAAW;AAAA,YACpG;AAAA,UACJ,CAAC;AAAA,QACL;AAAA,MACR,CAAC;AAAA,IACL;AAAA,EACJ;AACJ;",
|
|
5
|
+
"names": []
|
|
6
6
|
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var web_plumbing_exports = {};
|
|
30
|
+
__export(web_plumbing_exports, {
|
|
31
|
+
initializeWebPlumbing: () => initializeWebPlumbing
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(web_plumbing_exports);
|
|
34
|
+
var import_general = require("@matter/general");
|
|
35
|
+
var import_node_stream = require("node:stream");
|
|
36
|
+
var import_ws = __toESM(require("ws"));
|
|
37
|
+
var import_Shell = require("./shell/Shell");
|
|
38
|
+
var import_fs = __toESM(require("fs"));
|
|
39
|
+
var import_node_http = __toESM(require("node:http"));
|
|
40
|
+
var import_path = __toESM(require("path"));
|
|
41
|
+
/**
|
|
42
|
+
* @license
|
|
43
|
+
* Copyright 2022-2025 Matter.js Authors
|
|
44
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
45
|
+
*/
|
|
46
|
+
let client;
|
|
47
|
+
let server;
|
|
48
|
+
let wss;
|
|
49
|
+
const socketLogger = "websocket";
|
|
50
|
+
function initializeWebPlumbing(theNode, nodeNum, webSocketPort, webServer) {
|
|
51
|
+
if (webServer) {
|
|
52
|
+
const root = import_path.default.resolve(__dirname) ?? "./";
|
|
53
|
+
server = import_node_http.default.createServer((req, res) => {
|
|
54
|
+
const url = req.url ?? "/";
|
|
55
|
+
const safePath = import_path.default.normalize(
|
|
56
|
+
import_path.default.join(root, decodeURIComponent(url === "/" ? "/index.html" : url))
|
|
57
|
+
);
|
|
58
|
+
if (!safePath.startsWith(root)) {
|
|
59
|
+
res.writeHead(403).end("Forbidden");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
import_fs.default.readFile(safePath, (err, data) => {
|
|
63
|
+
if (err) return res.writeHead(404).end("Not Found");
|
|
64
|
+
res.writeHead(200).end(data);
|
|
65
|
+
});
|
|
66
|
+
}).listen(webSocketPort);
|
|
67
|
+
wss = new import_ws.WebSocketServer({ server });
|
|
68
|
+
} else wss = new import_ws.WebSocketServer({ port: webSocketPort });
|
|
69
|
+
console.info(`WebSocket server running on ws://localhost:${webSocketPort}`);
|
|
70
|
+
console.log = // console.debug = // too much traffic - kills the websocket
|
|
71
|
+
console.info = console.warn = console.error = (...args) => {
|
|
72
|
+
if (client && client.readyState === import_ws.default.OPEN) {
|
|
73
|
+
client.send(args.map((arg) => typeof arg === "object" ? JSON.stringify(arg) : arg).join(" "));
|
|
74
|
+
} else
|
|
75
|
+
process.stdout.write(
|
|
76
|
+
args.map((arg) => typeof arg === "object" ? JSON.stringify(arg) : arg).join(" ") + "\n"
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
wss.on("connection", (ws) => {
|
|
80
|
+
if (client && client.readyState === import_ws.default.OPEN) {
|
|
81
|
+
ws.send("ERROR: Shell in use by another client");
|
|
82
|
+
ws.close();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
client = ws;
|
|
86
|
+
createWebSocketLogger(ws).then((logger) => {
|
|
87
|
+
import_general.Logger.removeLogger("Shell");
|
|
88
|
+
import_general.Logger.addLogger(socketLogger, logger);
|
|
89
|
+
}).catch((err) => {
|
|
90
|
+
if (!(err instanceof import_general.NotImplementedError)) {
|
|
91
|
+
console.error("Failed to add WebSocket logger: " + err);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
const shell = new import_Shell.Shell(theNode, nodeNum, "", createReadableStream(ws), createWritableStream(ws));
|
|
95
|
+
shell.start(theNode.storageLocation);
|
|
96
|
+
ws.on("close", () => {
|
|
97
|
+
process.stdout.write("Client disconnected\n");
|
|
98
|
+
try {
|
|
99
|
+
if (import_general.Logger.getLoggerForIdentifier(socketLogger) !== void 0) {
|
|
100
|
+
import_general.Logger.removeLogger(socketLogger);
|
|
101
|
+
}
|
|
102
|
+
} catch (err) {
|
|
103
|
+
}
|
|
104
|
+
client = ws;
|
|
105
|
+
});
|
|
106
|
+
ws.on("error", (err) => {
|
|
107
|
+
process.stderr.write(`WebSocket error: ${err.message}
|
|
108
|
+
`);
|
|
109
|
+
try {
|
|
110
|
+
if (import_general.Logger.getLoggerForIdentifier(socketLogger) !== void 0) {
|
|
111
|
+
import_general.Logger.removeLogger(socketLogger);
|
|
112
|
+
}
|
|
113
|
+
} catch (err2) {
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
async function createWebSocketLogger(socket) {
|
|
118
|
+
if (socket.readyState === import_ws.default.CONNECTING) {
|
|
119
|
+
await new Promise((resolve, reject) => {
|
|
120
|
+
socket.onopen = () => resolve();
|
|
121
|
+
socket.onerror = (err) => reject(new Error(`WebSocket error: ${err.type}`));
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return (__level, formattedLog) => {
|
|
125
|
+
if (socket.readyState === import_ws.default.OPEN) {
|
|
126
|
+
socket.send(formattedLog);
|
|
127
|
+
} else {
|
|
128
|
+
process.stderr.write(`WebSocket logger not open, log dropped: ${formattedLog}
|
|
129
|
+
`);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function createReadableStream(ws) {
|
|
135
|
+
const readable = new import_node_stream.Readable({ read() {
|
|
136
|
+
} });
|
|
137
|
+
ws.on("message", (data) => {
|
|
138
|
+
const chunk = Buffer.isBuffer(data) ? data : Buffer.from(data.toString());
|
|
139
|
+
readable.push(chunk);
|
|
140
|
+
});
|
|
141
|
+
ws.on("close", () => {
|
|
142
|
+
readable.push(null);
|
|
143
|
+
});
|
|
144
|
+
ws.on("error", (err) => {
|
|
145
|
+
readable.emit("error", err);
|
|
146
|
+
readable.push(null);
|
|
147
|
+
});
|
|
148
|
+
return readable;
|
|
149
|
+
}
|
|
150
|
+
function createWritableStream(ws) {
|
|
151
|
+
const writable = new import_node_stream.Writable({
|
|
152
|
+
write(chunk, _encoding, callback) {
|
|
153
|
+
if (ws.readyState === import_ws.default.OPEN) {
|
|
154
|
+
ws.send(chunk, callback);
|
|
155
|
+
} else {
|
|
156
|
+
if (chunk.length > 0) process.stderr.write(`ERROR: WebSocket is not open. Failed to send "${chunk}"
|
|
157
|
+
`);
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
final(callback) {
|
|
161
|
+
if (ws.readyState === import_ws.default.OPEN) {
|
|
162
|
+
ws.close();
|
|
163
|
+
}
|
|
164
|
+
callback();
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
ws.on("error", (err) => writable.emit("WebSocket Write Error: ", err));
|
|
168
|
+
return writable;
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=web_plumbing.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/web_plumbing.ts"],
|
|
4
|
+
"mappings": "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",
|
|
5
|
+
"names": ["path", "http", "fs", "WebSocket", "err"]
|
|
6
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matter/nodejs-shell",
|
|
3
|
-
"version": "0.14.0-alpha.0-
|
|
3
|
+
"version": "0.14.0-alpha.0-20250525-d6ada0d45",
|
|
4
4
|
"description": "Shell app for Matter controller",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"iot",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"clean": "matter-build clean",
|
|
27
27
|
"build": "matter-build",
|
|
28
28
|
"build-clean": "matter-build --clean",
|
|
29
|
-
"shell": "matter-run src/app.ts"
|
|
29
|
+
"shell": "matter-run src/app.ts",
|
|
30
|
+
"bundle-shell": "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"
|
|
30
31
|
},
|
|
31
32
|
"bin": {
|
|
32
33
|
"matter-shell": "dist/cjs/app.js"
|
|
@@ -41,15 +42,16 @@
|
|
|
41
42
|
"#types": "@matter/types"
|
|
42
43
|
},
|
|
43
44
|
"dependencies": {
|
|
44
|
-
"@matter/general": "0.14.0-alpha.0-
|
|
45
|
-
"@matter/model": "0.14.0-alpha.0-
|
|
46
|
-
"@matter/node": "0.14.0-alpha.0-
|
|
47
|
-
"@matter/nodejs": "0.14.0-alpha.0-
|
|
48
|
-
"@matter/nodejs-ble": "0.14.0-alpha.0-
|
|
49
|
-
"@matter/protocol": "0.14.0-alpha.0-
|
|
50
|
-
"@matter/tools": "0.14.0-alpha.0-
|
|
51
|
-
"@matter/types": "0.14.0-alpha.0-
|
|
52
|
-
"@project-chip/matter.js": "0.14.0-alpha.0-
|
|
45
|
+
"@matter/general": "0.14.0-alpha.0-20250525-d6ada0d45",
|
|
46
|
+
"@matter/model": "0.14.0-alpha.0-20250525-d6ada0d45",
|
|
47
|
+
"@matter/node": "0.14.0-alpha.0-20250525-d6ada0d45",
|
|
48
|
+
"@matter/nodejs": "0.14.0-alpha.0-20250525-d6ada0d45",
|
|
49
|
+
"@matter/nodejs-ble": "0.14.0-alpha.0-20250525-d6ada0d45",
|
|
50
|
+
"@matter/protocol": "0.14.0-alpha.0-20250525-d6ada0d45",
|
|
51
|
+
"@matter/tools": "0.14.0-alpha.0-20250525-d6ada0d45",
|
|
52
|
+
"@matter/types": "0.14.0-alpha.0-20250525-d6ada0d45",
|
|
53
|
+
"@project-chip/matter.js": "0.14.0-alpha.0-20250525-d6ada0d45",
|
|
54
|
+
"ws": "^8.18.2",
|
|
53
55
|
"yargs": "^17.7.2"
|
|
54
56
|
},
|
|
55
57
|
"engines": {
|
package/src/app.ts
CHANGED
|
@@ -12,9 +12,13 @@ import { Ble } from "#protocol";
|
|
|
12
12
|
import yargs from "yargs/yargs";
|
|
13
13
|
import { MatterNode } from "./MatterNode.js";
|
|
14
14
|
import { Shell } from "./shell/Shell";
|
|
15
|
+
import { initializeWebPlumbing } from "./web_plumbing.js";
|
|
15
16
|
|
|
16
17
|
const PROMPT = "matter> ";
|
|
18
|
+
const DEFAULT_WEBSOCKET_PORT = 3000;
|
|
17
19
|
const logger = Logger.get("Shell");
|
|
20
|
+
let theShell: Shell;
|
|
21
|
+
|
|
18
22
|
if (process.stdin?.isTTY) Logger.format = LogFormat.ANSI;
|
|
19
23
|
|
|
20
24
|
let theNode: MatterNode;
|
|
@@ -86,12 +90,38 @@ async function main() {
|
|
|
86
90
|
type: "string",
|
|
87
91
|
default: undefined,
|
|
88
92
|
},
|
|
93
|
+
webSocketInterface: {
|
|
94
|
+
description: "Enable WebSocket interface",
|
|
95
|
+
type: "boolean",
|
|
96
|
+
default: false,
|
|
97
|
+
},
|
|
98
|
+
webSocketPort: {
|
|
99
|
+
description: "WebSocket and HTTP server port",
|
|
100
|
+
type: "number",
|
|
101
|
+
default: DEFAULT_WEBSOCKET_PORT,
|
|
102
|
+
},
|
|
103
|
+
webServer: {
|
|
104
|
+
description: "Enable Web server when using WebSocket interface",
|
|
105
|
+
type: "boolean",
|
|
106
|
+
default: false,
|
|
107
|
+
},
|
|
89
108
|
});
|
|
90
109
|
},
|
|
91
110
|
async argv => {
|
|
92
111
|
if (argv.help) return;
|
|
93
112
|
|
|
94
|
-
const {
|
|
113
|
+
const {
|
|
114
|
+
nodeNum,
|
|
115
|
+
ble,
|
|
116
|
+
bleHciId,
|
|
117
|
+
nodeType,
|
|
118
|
+
factoryReset,
|
|
119
|
+
netInterface,
|
|
120
|
+
logfile,
|
|
121
|
+
webSocketInterface,
|
|
122
|
+
webSocketPort,
|
|
123
|
+
webServer,
|
|
124
|
+
} = argv;
|
|
95
125
|
|
|
96
126
|
theNode = new MatterNode(nodeNum, netInterface);
|
|
97
127
|
await theNode.initialize(factoryReset);
|
|
@@ -111,8 +141,12 @@ async function main() {
|
|
|
111
141
|
}
|
|
112
142
|
setLogLevel("default", await theNode.Store.get<string>("LogLevel", "info"));
|
|
113
143
|
|
|
114
|
-
|
|
115
|
-
|
|
144
|
+
if (webSocketInterface) {
|
|
145
|
+
Logger.format = LogFormat.PLAIN;
|
|
146
|
+
initializeWebPlumbing(theNode, nodeNum, webSocketPort, webServer); // set up but wait for connect to create Shell
|
|
147
|
+
} else {
|
|
148
|
+
theShell = new Shell(theNode, nodeNum, PROMPT, process.stdin, process.stdout);
|
|
149
|
+
}
|
|
116
150
|
if (bleHciId !== undefined) {
|
|
117
151
|
await theNode.Store.set("BleHciId", bleHciId);
|
|
118
152
|
}
|
|
@@ -130,11 +164,14 @@ async function main() {
|
|
|
130
164
|
}
|
|
131
165
|
|
|
132
166
|
console.log(`Started Node #${nodeNum} (Type: ${nodeType}) ${ble ? "with" : "without"} BLE`);
|
|
133
|
-
|
|
167
|
+
if (!webSocketInterface) {
|
|
168
|
+
theShell.start(theNode.storageLocation);
|
|
169
|
+
}
|
|
134
170
|
},
|
|
135
171
|
)
|
|
136
172
|
.version(false)
|
|
137
|
-
.scriptName("shell")
|
|
173
|
+
.scriptName("shell")
|
|
174
|
+
.strict();
|
|
138
175
|
await yargsInstance.wrap(yargsInstance.terminalWidth()).parseAsync();
|
|
139
176
|
}
|
|
140
177
|
|
package/src/shell/Shell.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { MatterError } from "#general";
|
|
8
8
|
import { createWriteStream, readFileSync } from "node:fs";
|
|
9
9
|
import readline from "node:readline";
|
|
10
|
+
import { Readable, Writable } from "node:stream";
|
|
10
11
|
import yargs from "yargs/yargs";
|
|
11
12
|
import { MatterNode } from "../MatterNode.js";
|
|
12
13
|
import { exit } from "../app";
|
|
@@ -51,6 +52,8 @@ export class Shell {
|
|
|
51
52
|
public theNode: MatterNode,
|
|
52
53
|
public nodeNum: number,
|
|
53
54
|
public prompt: string,
|
|
55
|
+
public input: Readable,
|
|
56
|
+
public output: Writable,
|
|
54
57
|
) {}
|
|
55
58
|
|
|
56
59
|
start(storageBase?: string) {
|
|
@@ -80,9 +83,9 @@ export class Shell {
|
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
85
|
this.readline = readline.createInterface({
|
|
83
|
-
input:
|
|
84
|
-
output:
|
|
85
|
-
terminal:
|
|
86
|
+
input: this.input,
|
|
87
|
+
output: this.output,
|
|
88
|
+
terminal: this.input === process.stdin && this.output === process.stdout,
|
|
86
89
|
prompt: this.prompt,
|
|
87
90
|
history: history.reverse(),
|
|
88
91
|
historySize: MAX_HISTORY_SIZE,
|
|
@@ -103,12 +106,15 @@ export class Shell {
|
|
|
103
106
|
} catch (e) {
|
|
104
107
|
process.stderr.write(`Error happened during history file write: ${e}\n`);
|
|
105
108
|
}
|
|
106
|
-
exit
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
// only exit if we are running in a terminal
|
|
110
|
+
if (this.input === process.stdin && this.output === process.stdout) {
|
|
111
|
+
exit()
|
|
112
|
+
.then(() => process.exit(0))
|
|
113
|
+
.catch(e => {
|
|
114
|
+
process.stderr.write(`Close error: ${e}\n`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
112
118
|
});
|
|
113
119
|
|
|
114
120
|
this.readline.prompt();
|
|
@@ -21,19 +21,19 @@ export default function commands(theNode: MatterNode) {
|
|
|
21
21
|
},
|
|
22
22
|
|
|
23
23
|
handler: async (argv: any) => {
|
|
24
|
-
const { nodeId } = argv;
|
|
25
|
-
const node = (await theNode.connectAndGetNodes(
|
|
24
|
+
const { nodeId: subscribeNodeId } = argv;
|
|
25
|
+
const node = (await theNode.connectAndGetNodes(subscribeNodeId))[0];
|
|
26
26
|
|
|
27
27
|
await node.subscribeAllAttributesAndEvents({
|
|
28
28
|
attributeChangedCallback: ({ path: { nodeId, clusterId, endpointId, attributeName }, value }) =>
|
|
29
29
|
console.log(
|
|
30
|
-
`${
|
|
30
|
+
`${subscribeNodeId}: Attribute ${nodeId}/${endpointId}/${clusterId}/${attributeName} changed to ${Diagnostic.json(
|
|
31
31
|
value,
|
|
32
32
|
)}`,
|
|
33
33
|
),
|
|
34
34
|
eventTriggeredCallback: ({ path: { nodeId, clusterId, endpointId, eventName }, events }) =>
|
|
35
35
|
console.log(
|
|
36
|
-
`${
|
|
36
|
+
`${subscribeNodeId} Event ${nodeId}/${endpointId}/${clusterId}/${eventName} triggered with ${Diagnostic.json(
|
|
37
37
|
events,
|
|
38
38
|
)}`,
|
|
39
39
|
),
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<!--
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2022-2025 Matter.js Authors
|
|
5
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
6
|
+
|
|
7
|
+
-->
|
|
8
|
+
<html lang="en">
|
|
9
|
+
|
|
10
|
+
<head>
|
|
11
|
+
<meta charset="UTF-8">
|
|
12
|
+
<link rel="icon" href="/favicon.png" type="image/png" />
|
|
13
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
14
|
+
<title>Matter Web Shell</title>
|
|
15
|
+
<style>
|
|
16
|
+
body {
|
|
17
|
+
font-family: Arial, sans-serif;
|
|
18
|
+
background-color: #f0f2f5;
|
|
19
|
+
margin: 0;
|
|
20
|
+
padding: 20px;
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
gap: 20px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.container {
|
|
27
|
+
max-width: 1200px;
|
|
28
|
+
margin: 0 auto;
|
|
29
|
+
display: grid;
|
|
30
|
+
grid-template-columns: 1fr 3fr;
|
|
31
|
+
gap: 20px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.container>div {
|
|
35
|
+
background-color: #afa8a879;
|
|
36
|
+
border-radius: 8px;
|
|
37
|
+
padding: 5px;
|
|
38
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
39
|
+
box-sizing: border-box;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.node-container {
|
|
43
|
+
background-color: #aba8af79;
|
|
44
|
+
border-radius: 8px;
|
|
45
|
+
padding: 2px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.sidebar {
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
gap: 20px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.tiles {
|
|
55
|
+
display: grid;
|
|
56
|
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
57
|
+
gap: 15px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.tile {
|
|
61
|
+
background-color: white;
|
|
62
|
+
border-radius: 8px;
|
|
63
|
+
padding: 15px;
|
|
64
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
65
|
+
cursor: default;
|
|
66
|
+
transition: transform 0.2s;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.input-section {
|
|
70
|
+
background-color: white;
|
|
71
|
+
border-radius: 8px;
|
|
72
|
+
padding: 15px;
|
|
73
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.console {
|
|
77
|
+
background-color: #4e4e4e;
|
|
78
|
+
color: #fff;
|
|
79
|
+
border-radius: 8px;
|
|
80
|
+
padding: 15px;
|
|
81
|
+
height: 200px;
|
|
82
|
+
overflow-y: auto;
|
|
83
|
+
overflow-x: hidden !important;
|
|
84
|
+
font-family: 'Courier New', monospace;
|
|
85
|
+
font-size: 14px;
|
|
86
|
+
box-sizing: border-box;
|
|
87
|
+
width: 100%;
|
|
88
|
+
max-width: 100%;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.console div {
|
|
92
|
+
overflow-wrap: break-word !important;
|
|
93
|
+
white-space: normal !important;
|
|
94
|
+
word-break: break-all;
|
|
95
|
+
max-width: 100%;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.console .sent {
|
|
99
|
+
color: #0f0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.console .received {
|
|
103
|
+
color: #fff;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.console .status {
|
|
107
|
+
color: #ff0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.console .error {
|
|
111
|
+
color: #f00;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.connecting-node-status {
|
|
115
|
+
color: rgb(245, 54, 54);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.connected-node-status {
|
|
119
|
+
color: rgb(31, 218, 62);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.on {
|
|
123
|
+
background-color: lightgoldenrodyellow;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.off {
|
|
127
|
+
background-color: darkgrey;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
input {
|
|
131
|
+
width: 100%;
|
|
132
|
+
padding: 8px;
|
|
133
|
+
margin-top: 5px;
|
|
134
|
+
border: 1px solid #ccc;
|
|
135
|
+
border-radius: 4px;
|
|
136
|
+
box-sizing: border-box;
|
|
137
|
+
width: calc(100% - 10px);
|
|
138
|
+
margin-left: 5px;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
h2 {
|
|
142
|
+
margin: 0 0 10px 0;
|
|
143
|
+
font-size: 18px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
button {
|
|
147
|
+
padding: 8px 12px;
|
|
148
|
+
border: 1px solid #ccc;
|
|
149
|
+
border-radius: 4px;
|
|
150
|
+
background-color: #ffffff;
|
|
151
|
+
color: #333;
|
|
152
|
+
font-family: Arial, sans-serif;
|
|
153
|
+
font-size: 14px;
|
|
154
|
+
cursor: pointer;
|
|
155
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
156
|
+
transition: background-color 0.2s, transform 0.2s;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
button:hover {
|
|
160
|
+
background-color: #e0e0e0;
|
|
161
|
+
transform: scale(1.02);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
button:active {
|
|
165
|
+
background-color: #d0d0d0;
|
|
166
|
+
transform: scale(0.98);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.button-container {
|
|
170
|
+
display: flex;
|
|
171
|
+
gap: 10px;
|
|
172
|
+
align-items: center;
|
|
173
|
+
}
|
|
174
|
+
</style>
|
|
175
|
+
</head>
|
|
176
|
+
|
|
177
|
+
<body>
|
|
178
|
+
<div class="container">
|
|
179
|
+
<div class="sidebar">
|
|
180
|
+
<div class="input-section">
|
|
181
|
+
<h2>Execute Shell Command</h2>
|
|
182
|
+
<input type="text" id="matterCommand" placeholder="Enter matter.js command">
|
|
183
|
+
</div>
|
|
184
|
+
<div class="input-section">
|
|
185
|
+
<h2>Pair Device</h2>
|
|
186
|
+
<input type="text" id="pairingCode" placeholder="Enter pairing code">
|
|
187
|
+
</div>
|
|
188
|
+
<div class="input-section">
|
|
189
|
+
<h2>Console</h2>
|
|
190
|
+
<div class="button-container">
|
|
191
|
+
<input type="button" id="toggleconsole" value="Toggle"
|
|
192
|
+
onclick="document.getElementById('console').style.display = document.getElementById('console').style.display === 'none' ? 'block' : 'none';">
|
|
193
|
+
<input type="button" id="clearconsole" value="Clear" onclick="initConsole(true);">
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
<div>
|
|
198
|
+
<h2>matter.js Web Shell Example</h2>
|
|
199
|
+
<div class="tiles" id="tiles"></div>
|
|
200
|
+
<br>
|
|
201
|
+
<div class="console" id="console"></div>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<script>
|
|
206
|
+
const DEFAULT_PORT = 3000; // Default port for WebSocket connection
|
|
207
|
+
const tiles = document.getElementById('tiles');
|
|
208
|
+
const consoleDiv = document.getElementById('console');
|
|
209
|
+
const matterCommand = document.getElementById('matterCommand');
|
|
210
|
+
const pairingCode = document.getElementById('pairingCode');
|
|
211
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
212
|
+
const host = window.location.hostname || 'localhost';
|
|
213
|
+
const defaultPort = window.location.port || DEFAULT_PORT;
|
|
214
|
+
const wsUrl = `${protocol}//${host}:${defaultPort}`;
|
|
215
|
+
|
|
216
|
+
let ws = null;
|
|
217
|
+
let reconnectAttempts = 0;
|
|
218
|
+
|
|
219
|
+
const ON_OFF = 6;
|
|
220
|
+
const ILLUMINANCE = 1024;
|
|
221
|
+
const TEMPERATURE = 1026;
|
|
222
|
+
const PRESSURE = 1027;
|
|
223
|
+
const HUMIDITY = 1029;
|
|
224
|
+
const VOLTAGE = 144; // etc.
|
|
225
|
+
|
|
226
|
+
const valueFormat = {
|
|
227
|
+
[VOLTAGE]: (value, units = 'v') => { return (value / 1000).toFixed(1) + units; },
|
|
228
|
+
[ILLUMINANCE]: (value, units = 'lux') => { return (value / 100).toFixed(1) + units; },
|
|
229
|
+
[TEMPERATURE]: (value, units = 'F') => { return (units === "F" ? (value / 100 * 9 / 5 + 32) : (value / 100)).toFixed(1) + '°' + units; },
|
|
230
|
+
[PRESSURE]: (value, units = 'mb') => { return (value / 10000).toFixed(1) + units; },
|
|
231
|
+
[HUMIDITY]: (value, units = '%') => { return (value / 100).toFixed(1) + units; },
|
|
232
|
+
}
|
|
233
|
+
function initConsole(clear) {
|
|
234
|
+
if (clear) consoleDiv.innerHTML = ''; // Clear the console
|
|
235
|
+
const dummy = document.createElement('div'); // Inject invisible wide line to reserve width
|
|
236
|
+
dummy.style.visibility = 'hidden';
|
|
237
|
+
dummy.style.height = '0';
|
|
238
|
+
dummy.textContent = 'X'.repeat(120);
|
|
239
|
+
consoleDiv.appendChild(dummy);
|
|
240
|
+
}
|
|
241
|
+
function logMessage(message, type) {
|
|
242
|
+
const MAX_MESSAGES = 1000; // Maximum number of messages to keep in the console
|
|
243
|
+
const fragment = document.createDocumentFragment();
|
|
244
|
+
const lines = message.split('\n');
|
|
245
|
+
|
|
246
|
+
lines.forEach(line => {
|
|
247
|
+
const msg = document.createElement('div');
|
|
248
|
+
msg.textContent = line;
|
|
249
|
+
msg.className = type;
|
|
250
|
+
fragment.appendChild(msg);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
consoleDiv.appendChild(fragment);
|
|
254
|
+
|
|
255
|
+
while (consoleDiv.children.length > MAX_MESSAGES) consoleDiv.removeChild(consoleDiv.firstChild);
|
|
256
|
+
|
|
257
|
+
// Only scroll if the user was already at the bottom
|
|
258
|
+
const shouldScroll = consoleDiv.scrollTop + consoleDiv.clientHeight >= consoleDiv.scrollHeight - consoleDiv.lastChild.offsetHeight - 5; // Adjusted for new content
|
|
259
|
+
if (shouldScroll) consoleDiv.scrollTop = consoleDiv.scrollHeight;
|
|
260
|
+
}
|
|
261
|
+
function setupWebSocket() {
|
|
262
|
+
ws = new WebSocket(wsUrl);
|
|
263
|
+
initConsole(false);
|
|
264
|
+
|
|
265
|
+
ws.onopen = () => {
|
|
266
|
+
sendCommand('config loglevel set info'); // info level required for some msgs to be passed
|
|
267
|
+
sendCommand('nodes connect');
|
|
268
|
+
reconnectAttempts = 0;
|
|
269
|
+
}
|
|
270
|
+
ws.onclose = () => {
|
|
271
|
+
logMessage('WebSocket connection closed. Attempting to reconnect...', 'error');
|
|
272
|
+
scheduleReconnect();
|
|
273
|
+
};
|
|
274
|
+
ws.onerror = () => {
|
|
275
|
+
logMessage('WebSocket error...', 'error');
|
|
276
|
+
};
|
|
277
|
+
ws.onmessage = (event) => {
|
|
278
|
+
if (typeof event.data === 'string') {
|
|
279
|
+
const message = event.data;
|
|
280
|
+
let trimmed = message.slice(0, 5000); // don't let message to log get too long
|
|
281
|
+
if (trimmed.length != message.length) trimmed += ' ...';
|
|
282
|
+
logMessage(`Received: "${trimmed}"`, trimmed.toLowerCase().includes('error') ? 'error' : 'received');
|
|
283
|
+
|
|
284
|
+
// result of nodes log command
|
|
285
|
+
let matches = message.match(/INFO\s+PairedNode [^0-9]*(\d+)\D*/);
|
|
286
|
+
if (matches) { // could save this message of extensive node data for later use
|
|
287
|
+
let currentNode = matches[1];
|
|
288
|
+
let currentEndpoint = null;
|
|
289
|
+
let currentEndpointType = "";
|
|
290
|
+
const lines = message.split('\n');
|
|
291
|
+
lines.forEach(line => {
|
|
292
|
+
let matches = line.match(/ MA-[\w*]+ endpoint#: (\d+) type: MA-([\w*]+) \(([^)]+)\)/);
|
|
293
|
+
if (matches) { // could start collecting upcoming messages that define this endpoint
|
|
294
|
+
currentEndpoint = matches[1];
|
|
295
|
+
currentEndpointType = matches[2];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// device type list for the this node and endpoint
|
|
299
|
+
matches = line.match(/deviceTypeList id: 0x0 val: ((\{.*?\})(?:\s*\{.*?\})*)/);
|
|
300
|
+
if (matches) logMessage(`Device Type List: ${currentNode}/${currentEndpoint} ${matches[1]}`, 'status');
|
|
301
|
+
|
|
302
|
+
// could also check and handle others, "moveToLevel", "movetohueandsaturation", etc.
|
|
303
|
+
matches = line.match(/ (toggle$|on$|off$|measuredValue |voltage )/);
|
|
304
|
+
if (matches) {
|
|
305
|
+
if (!document.getElementById(`attr-${currentNode}/${currentEndpoint}/${matches[1]}`)) {
|
|
306
|
+
let element;
|
|
307
|
+
if (["on", "off", "toggle"].includes(matches[1])) {
|
|
308
|
+
element = document.createElement('button');
|
|
309
|
+
element.textContent = matches[1];
|
|
310
|
+
const cmd = `commands onoff ${matches[1]} ${currentNode} ${currentEndpoint}`;
|
|
311
|
+
element.onclick = () => sendCommand(cmd);
|
|
312
|
+
if (matches[1] === "on") sendCommand(`attributes onoff read onoff ${currentNode} ${currentEndpoint}`); // get initial state
|
|
313
|
+
} else element = document.createElement('span');
|
|
314
|
+
|
|
315
|
+
element.id = `attr-${currentNode}/${currentEndpoint}/${matches[1].trim()}`;
|
|
316
|
+
if (!document.getElementById(`${currentEndpoint}-container-${currentNode}`)) {
|
|
317
|
+
let endpointContainer = document.createElement('div');
|
|
318
|
+
endpointContainer.id = `${currentEndpoint}-container-${currentNode}`;
|
|
319
|
+
endpointContainer.className = 'node-container';
|
|
320
|
+
const small = document.createElement('small');
|
|
321
|
+
small.textContent = currentEndpointType.replace('MA-', '');
|
|
322
|
+
const br = document.createElement('br');
|
|
323
|
+
endpointContainer.innerHTML = '';
|
|
324
|
+
endpointContainer.appendChild(small);
|
|
325
|
+
endpointContainer.appendChild(br);
|
|
326
|
+
document.getElementById(`container-${currentNode}`).appendChild(endpointContainer);
|
|
327
|
+
}
|
|
328
|
+
document.getElementById(`container-${currentNode}`)?.appendChild(element);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
matches = message.match(/stateInformationCallback Node (\d+) (\S+)/);
|
|
335
|
+
if (matches) {
|
|
336
|
+
updateTile(matches[1], matches[2]);
|
|
337
|
+
if (matches[2] === "connected") {
|
|
338
|
+
sendCommand(`subscribe ${matches[1]}`);
|
|
339
|
+
sendCommand(`nodes log ${matches[1]}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
matches = message.match(/(\d+): Attribute (\w+|\d+)\/(\d+)\/(\d+)\/(\w+) changed to (.+)/); // attribute changed
|
|
344
|
+
if (matches) {
|
|
345
|
+
if (matches[4] == ON_OFF) styleContainer(matches[1], matches[3], matches[6]);
|
|
346
|
+
else if ([ILLUMINANCE, TEMPERATURE, PRESSURE, HUMIDITY, VOLTAGE].includes(Number(matches[4]))) {
|
|
347
|
+
const formatter = valueFormat[matches[4]];
|
|
348
|
+
const value = typeof formatter === 'function' ? formatter(matches[6]) : matches[6];
|
|
349
|
+
const element = document.getElementById(`attr-${matches[1]}/${matches[3]}/${matches[5]}`);
|
|
350
|
+
if (element) {
|
|
351
|
+
const small = document.createElement("small");
|
|
352
|
+
small.textContent = value;
|
|
353
|
+
element.innerHTML = "";
|
|
354
|
+
element.appendChild(small);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
matches = message.match(/Attribute value for (\w+) (\d+)\/(\d+)\/(\d+)\/(\w+): (.+)/); // from "read attribute"
|
|
360
|
+
if (matches)
|
|
361
|
+
if (matches[1] === "onOff") styleContainer(matches[2], matches[3], matches[6]);
|
|
362
|
+
// else if (matches[1] === etc. process as needed
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function styleContainer(nodeId, endpoint, state) {
|
|
367
|
+
const element = document.getElementById(`${endpoint}-container-${nodeId}`);
|
|
368
|
+
element.classList.remove("on", "off");
|
|
369
|
+
element.classList.add(state === "true" ? "on" : "off");
|
|
370
|
+
}
|
|
371
|
+
function updateTile(nodeId, status) {
|
|
372
|
+
let tile = document.getElementById(`tile-${nodeId}`);
|
|
373
|
+
if (!tile) {
|
|
374
|
+
let container = document.createElement('div');
|
|
375
|
+
container.id = `container-${nodeId}`;
|
|
376
|
+
container.className = 'node-container';
|
|
377
|
+
tile = document.createElement('div');
|
|
378
|
+
tile.className = 'tile';
|
|
379
|
+
tile.id = `tile-${nodeId}`;
|
|
380
|
+
container.appendChild(tile);
|
|
381
|
+
tiles.appendChild(container);
|
|
382
|
+
}
|
|
383
|
+
const small = document.createElement("small");
|
|
384
|
+
small.appendChild(document.createTextNode("Node " + nodeId));
|
|
385
|
+
small.appendChild(document.createElement("br"));
|
|
386
|
+
const span = document.createElement("span");
|
|
387
|
+
span.className = status === "connected" ? "connected-node-status" : "connecting-node-status";
|
|
388
|
+
span.textContent = status;
|
|
389
|
+
small.appendChild(span);
|
|
390
|
+
tile.innerHTML = "";
|
|
391
|
+
tile.appendChild(small);
|
|
392
|
+
}
|
|
393
|
+
matterCommand.onkeypress = (e) => {
|
|
394
|
+
if (e.key === 'Enter') {
|
|
395
|
+
if (matterCommand.value === 'clear') initConsole(true);
|
|
396
|
+
else sendCommand(matterCommand.value ? matterCommand.value : "help");
|
|
397
|
+
matterCommand.value = '';
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
pairingCode.onkeypress = (e) => {
|
|
401
|
+
if (e.key === 'Enter') {
|
|
402
|
+
sendCommand(pairingCode.value ? `commission pair --pairing-code ${pairingCode.value}` : "help");
|
|
403
|
+
pairingCode.value = '';
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
function scheduleReconnect() {
|
|
407
|
+
const RECONNECT_DELAY = 1000; // Initial delay in ms
|
|
408
|
+
const MAX_RECONNECT_DELAY = 30000; // Max delay in ms
|
|
409
|
+
if (ws && ws.readyState !== WebSocket.CLOSED) return;
|
|
410
|
+
const delay = Math.min(RECONNECT_DELAY * Math.pow(2, reconnectAttempts), MAX_RECONNECT_DELAY);
|
|
411
|
+
logMessage(`Attempting to reconnect in ${delay / 1000} seconds...\n`, 'error');
|
|
412
|
+
setTimeout(() => {
|
|
413
|
+
reconnectAttempts++;
|
|
414
|
+
setupWebSocket();
|
|
415
|
+
}, delay);
|
|
416
|
+
}
|
|
417
|
+
function sendCommand(cmd, outputType = 'sent') {
|
|
418
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
419
|
+
ws.send(cmd + '\n');
|
|
420
|
+
logMessage(`Sent: ${cmd}`, 'sent');
|
|
421
|
+
} else logMessage(`WebSocket not connected. Command "${cmd}" skipped.\n`, 'error');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
document.addEventListener('DOMContentLoaded', () => { setupWebSocket(); });
|
|
425
|
+
</script>
|
|
426
|
+
</body>
|
|
427
|
+
|
|
428
|
+
</html>
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2022-2025 Matter.js Authors
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Logger, LogLevel, NotImplementedError } from "@matter/general";
|
|
8
|
+
import { Readable, Writable } from "node:stream";
|
|
9
|
+
import WebSocket, { Data, WebSocketServer } from "ws";
|
|
10
|
+
import { MatterNode } from "./MatterNode.js";
|
|
11
|
+
import { Shell } from "./shell/Shell";
|
|
12
|
+
|
|
13
|
+
import fs from "fs";
|
|
14
|
+
import http, { Server } from "node:http";
|
|
15
|
+
import path from "path";
|
|
16
|
+
|
|
17
|
+
// Store active WebSocket
|
|
18
|
+
let client: WebSocket;
|
|
19
|
+
let server: Server;
|
|
20
|
+
let wss: WebSocketServer;
|
|
21
|
+
const socketLogger = "websocket";
|
|
22
|
+
|
|
23
|
+
export function initializeWebPlumbing(
|
|
24
|
+
theNode: MatterNode,
|
|
25
|
+
nodeNum: number,
|
|
26
|
+
webSocketPort: number,
|
|
27
|
+
webServer: boolean,
|
|
28
|
+
): void {
|
|
29
|
+
if (webServer) {
|
|
30
|
+
const root: string = path.resolve(__dirname) ?? "./";
|
|
31
|
+
|
|
32
|
+
server = http
|
|
33
|
+
.createServer((req, res) => {
|
|
34
|
+
const url = req.url ?? "/";
|
|
35
|
+
const safePath: string = path.normalize(
|
|
36
|
+
path.join(root, decodeURIComponent(url === "/" ? "/index.html" : url)),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Check that the resolved path is within the root directory
|
|
40
|
+
if (!safePath.startsWith(root)) {
|
|
41
|
+
res.writeHead(403).end("Forbidden");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fs.readFile(safePath, (err, data) => {
|
|
46
|
+
if (err) return res.writeHead(404).end("Not Found");
|
|
47
|
+
res.writeHead(200).end(data);
|
|
48
|
+
});
|
|
49
|
+
})
|
|
50
|
+
.listen(webSocketPort);
|
|
51
|
+
wss = new WebSocketServer({ server });
|
|
52
|
+
} else wss = new WebSocketServer({ port: webSocketPort });
|
|
53
|
+
|
|
54
|
+
console.info(`WebSocket server running on ws://localhost:${webSocketPort}`);
|
|
55
|
+
|
|
56
|
+
console.log =
|
|
57
|
+
// console.debug = // too much traffic - kills the websocket
|
|
58
|
+
console.info =
|
|
59
|
+
console.warn =
|
|
60
|
+
console.error =
|
|
61
|
+
(...args: any[]) => {
|
|
62
|
+
if (client && client.readyState === WebSocket.OPEN) {
|
|
63
|
+
client.send(args.map(arg => (typeof arg === "object" ? JSON.stringify(arg) : arg)).join(" "));
|
|
64
|
+
} else
|
|
65
|
+
process.stdout.write(
|
|
66
|
+
args.map(arg => (typeof arg === "object" ? JSON.stringify(arg) : arg)).join(" ") + "\n",
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
wss.on("connection", (ws: WebSocket) => {
|
|
71
|
+
if (client && client.readyState === WebSocket.OPEN) {
|
|
72
|
+
ws.send("ERROR: Shell in use by another client");
|
|
73
|
+
ws.close();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
client = ws; // Track the client
|
|
78
|
+
|
|
79
|
+
createWebSocketLogger(ws)
|
|
80
|
+
.then(logger => {
|
|
81
|
+
Logger.removeLogger("Shell");
|
|
82
|
+
Logger.addLogger(socketLogger, logger);
|
|
83
|
+
})
|
|
84
|
+
.catch(err => {
|
|
85
|
+
if (!(err instanceof NotImplementedError)) {
|
|
86
|
+
console.error("Failed to add WebSocket logger: " + err);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const shell = new Shell(theNode, nodeNum, "", createReadableStream(ws), createWritableStream(ws));
|
|
91
|
+
shell.start(theNode.storageLocation);
|
|
92
|
+
|
|
93
|
+
ws.on("close", () => {
|
|
94
|
+
process.stdout.write("Client disconnected\n");
|
|
95
|
+
try {
|
|
96
|
+
if (Logger.getLoggerForIdentifier(socketLogger) !== undefined) {
|
|
97
|
+
Logger.removeLogger(socketLogger);
|
|
98
|
+
}
|
|
99
|
+
} catch (err) {
|
|
100
|
+
// Intentionally left empty
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
client = ws;
|
|
104
|
+
});
|
|
105
|
+
ws.on("error", err => {
|
|
106
|
+
process.stderr.write(`WebSocket error: ${err.message}\n`);
|
|
107
|
+
try {
|
|
108
|
+
if (Logger.getLoggerForIdentifier(socketLogger) !== undefined) {
|
|
109
|
+
Logger.removeLogger(socketLogger);
|
|
110
|
+
}
|
|
111
|
+
} catch (err) {
|
|
112
|
+
// Intentionally left empty
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
async function createWebSocketLogger(socket: WebSocket): Promise<(level: LogLevel, formattedLog: string) => void> {
|
|
118
|
+
if (socket.readyState === WebSocket.CONNECTING) {
|
|
119
|
+
await new Promise<void>((resolve, reject) => {
|
|
120
|
+
socket.onopen = () => resolve();
|
|
121
|
+
socket.onerror = err => reject(new Error(`WebSocket error: ${err.type}`));
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return (__level: LogLevel, formattedLog: string) => {
|
|
126
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
127
|
+
socket.send(formattedLog);
|
|
128
|
+
} else {
|
|
129
|
+
process.stderr.write(`WebSocket logger not open, log dropped: ${formattedLog}\n`);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function createReadableStream(ws: WebSocket): Readable {
|
|
135
|
+
const readable = new Readable({ read() {} });
|
|
136
|
+
|
|
137
|
+
ws.on("message", (data: Data) => {
|
|
138
|
+
const chunk = Buffer.isBuffer(data) ? data : Buffer.from(data.toString());
|
|
139
|
+
|
|
140
|
+
// add the data to our readable stream that the readLine instance is reading from
|
|
141
|
+
readable.push(chunk);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
ws.on("close", () => {
|
|
145
|
+
readable.push(null);
|
|
146
|
+
});
|
|
147
|
+
ws.on("error", err => {
|
|
148
|
+
readable.emit("error", err);
|
|
149
|
+
readable.push(null);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return readable;
|
|
153
|
+
}
|
|
154
|
+
function createWritableStream(ws: WebSocket): Writable {
|
|
155
|
+
const writable = new Writable({
|
|
156
|
+
write(chunk: Buffer, _encoding: string, callback: (error?: Error | null) => void) {
|
|
157
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
158
|
+
ws.send(chunk, callback);
|
|
159
|
+
} else {
|
|
160
|
+
if (chunk.length > 0) process.stderr.write(`ERROR: WebSocket is not open. Failed to send "${chunk}"\n`);
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
final(callback: (error?: Error | null) => void) {
|
|
164
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
165
|
+
ws.close();
|
|
166
|
+
}
|
|
167
|
+
callback();
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
ws.on("error", err => writable.emit("WebSocket Write Error: ", err));
|
|
172
|
+
return writable;
|
|
173
|
+
}
|