pw_web: Use device API in webconsole and add it to REPL context
Change-Id: Ibdb17ad5b91691a679d2ff474e93cf21eedb7248
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/108140
Pigweed-Auto-Submit: Asad Memon <asadmemon@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
Reviewed-by: Anthony DiGirolamo <tonymd@google.com>
diff --git a/pw_web/webconsole/common/device.ts b/pw_web/webconsole/common/device.ts
new file mode 100644
index 0000000..5ba735b
--- /dev/null
+++ b/pw_web/webconsole/common/device.ts
@@ -0,0 +1,30 @@
+// Copyright 2022 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import {Device} from "pigweedjs";
+import {createDefaultProtoCollection} from "./protos";
+
+/**
+ * Returns an instance of Device, ensures there is only one Device in
+ * current session.
+ *
+ * We do this to avoid multiple clients listening on single serial port.
+ */
+export default async function SingletonDevice(): Promise<Device> {
+ if ((window as any).device === undefined) {
+ const protoCollection = await createDefaultProtoCollection();
+ (window as any).device = new Device(protoCollection);
+ }
+ return (window as any).device;
+}
diff --git a/pw_web/webconsole/common/logService.ts b/pw_web/webconsole/common/logService.ts
new file mode 100644
index 0000000..2d49380
--- /dev/null
+++ b/pw_web/webconsole/common/logService.ts
@@ -0,0 +1,41 @@
+// Copyright 2022 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import {Device, pw_rpc} from "pigweedjs";
+type Client = pw_rpc.Client;
+
+function createDefaultRPCLogService(client: Client) {
+ const logService = client.channel()!
+ .methodStub('pw.log.Logs.Listen');
+
+ return logService;
+}
+
+export async function listenToDefaultLogService(
+ device: Device,
+ onFrame: (frame: Uint8Array) => void) {
+ const client = device.client;
+ // @ts-ignore
+ const logService: pw_rpc.ServerStreamingMethodStub = (createDefaultRPCLogService(client))!;
+ const request = new logService.method.responseType();
+ // @ts-ignore
+ const call = logService.invoke(request, (msg) => {
+ // @ts-ignore
+ msg.getEntriesList().forEach(entry => onFrame(entry.getMessage()));
+ });
+
+ return () => {
+ call.cancel();
+ };
+}
diff --git a/pw_web/webconsole/common/protos.ts b/pw_web/webconsole/common/protos.ts
new file mode 100644
index 0000000..3524ad5
--- /dev/null
+++ b/pw_web/webconsole/common/protos.ts
@@ -0,0 +1,20 @@
+// Copyright 2022 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+
+export async function createDefaultProtoCollection() {
+ // @ts-ignore
+ const ProtoCollection = await import("pigweedjs/protos/collection.umd");
+ return new ProtoCollection.ProtoCollection();
+}
diff --git a/pw_web/webconsole/common/utils.ts b/pw_web/webconsole/common/utils.ts
deleted file mode 100644
index 88dd583..0000000
--- a/pw_web/webconsole/common/utils.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright 2022 The Pigweed Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-
-import {pw_hdlc, pw_rpc, WebSerial} from "pigweedjs";
-type WebSerialTransport = WebSerial.WebSerialTransport
-type Client = pw_rpc.Client;
-const encoder = new pw_hdlc.Encoder();
-const decoder = new pw_hdlc.Decoder();
-const RPC_ADDRESS = 82;
-
-function sendPacket(
- transport: WebSerialTransport,
- packetBytes: Uint8Array
-) {
- const hdlcBytes = encoder.uiFrame(RPC_ADDRESS, packetBytes);
- transport.sendChunk(hdlcBytes);
-}
-
-async function createDefaultRPCLogClient(transport: WebSerialTransport): Promise<Client> {
- // @ts-ignore
- const ProtoCollection = await import("pigweedjs/protos/collection.umd");
- const protoCollection = new ProtoCollection.ProtoCollection();
- const channels = [
- new pw_rpc.Channel(1, (bytes: Uint8Array) => {
- sendPacket(transport, bytes);
- }),
- ];
- // @ts-ignore
- const client = pw_rpc.Client.fromProtoSet(channels, protoCollection);
- return client;
-}
-
-function createDefaultRPCLogService(client: Client) {
- const logService = client.channel()!
- .methodStub('pw.log.Logs.Listen');
-
- return logService;
-}
-
-export async function listenToDefaultLogService(
- transport: WebSerialTransport,
- onFrame: (frame: Uint8Array) => void) {
- const client = await createDefaultRPCLogClient(transport);
- const logService = (createDefaultRPCLogService(client))!;
- const request = new logService.method.responseType();
- // @ts-ignore
- logService.invoke(request, (msg) => {
- // @ts-ignore
- msg.getEntriesList().forEach(entry => onFrame(entry.getMessage()));
- });
-
- const subscription = transport.chunks.subscribe((item) => {
- const decoded = decoder.process(item);
- let elem = decoded.next();
- while (!elem.done) {
- const frame = elem.value;
- if (frame.address === RPC_ADDRESS) {
- client.processPacket(frame.data);
- }
- elem = decoded.next();
- }
- });
-
- return () => subscription.unsubscribe();
-}
diff --git a/pw_web/webconsole/components/connect.tsx b/pw_web/webconsole/components/connect.tsx
index 09f54aa..17961c3 100644
--- a/pw_web/webconsole/components/connect.tsx
+++ b/pw_web/webconsole/components/connect.tsx
@@ -14,21 +14,22 @@
import Button from '@mui/material/Button';
import {Alert} from '@mui/material';
-import {WebSerial} from "pigweedjs";
+import {Device, WebSerial} from "pigweedjs";
import {useState} from 'react';
+import DeviceFactory from "../common/device";
type WebSerialTransport = WebSerial.WebSerialTransport
interface LogProps {
- onConnection: (transport: WebSerialTransport) => void
+ onConnection: (device: Device) => void
}
export default function BtnConnect({onConnection}: LogProps) {
const [connected, setConnected] = useState(false);
if (connected) return (<Alert severity="success">Connected!</Alert>)
return (<Button onClick={async () => {
- const transport = new WebSerial.WebSerialTransport();
- await transport.connect();
+ const device = await DeviceFactory();
+ await device.connect();
setConnected(true);
- onConnection(transport);
+ onConnection(device);
}} variant="contained">Connect</Button>)
}
diff --git a/pw_web/webconsole/components/log.tsx b/pw_web/webconsole/components/log.tsx
index 59ca455..4ad1efe 100644
--- a/pw_web/webconsole/components/log.tsx
+++ b/pw_web/webconsole/components/log.tsx
@@ -13,17 +13,16 @@
// the License.
import {useEffect, useRef, useState} from "react";
-import {WebSerial, pw_tokenizer} from "pigweedjs";
+import {pw_tokenizer, Device} from "pigweedjs";
import {AutoSizer, Table, Column} from 'react-virtualized';
-import {listenToDefaultLogService} from "../common/utils";
+import {listenToDefaultLogService} from "../common/logService";
import 'react-virtualized/styles.css';
import styles from "../styles/log.module.css";
-type WebSerialTransport = WebSerial.WebSerialTransport
type Detokenizer = pw_tokenizer.Detokenizer;
interface LogProps {
- transport: WebSerialTransport | undefined,
+ device: Device | undefined,
tokenDB: string | undefined
}
@@ -67,7 +66,7 @@
"file": "File"
}
-export default function Log({transport, tokenDB}: LogProps) {
+export default function Log({device, tokenDB}: LogProps) {
const [logs, setLogs] = useState<LogEntry[]>([]);
const [detokenizer, setDetokenizer] = useState<Detokenizer | null>(null);
const logTable = useRef<Table | null>(null);
@@ -94,14 +93,14 @@
}
useEffect(() => {
- if (transport) {
+ if (device) {
let cleanupFn: () => void;
- listenToDefaultLogService(transport, processFrame).then((unsub) => cleanupFn = unsub);
+ listenToDefaultLogService(device, processFrame).then((unsub) => cleanupFn = unsub);
return () => {
if (cleanupFn) cleanupFn();
}
}
- }, [transport, detokenizer]);
+ }, [device, detokenizer]);
useEffect(() => {
if (tokenDB && tokenDB.length > 0) {
diff --git a/pw_web/webconsole/components/repl/index.tsx b/pw_web/webconsole/components/repl/index.tsx
index c77c5cd..a48e8cd 100644
--- a/pw_web/webconsole/components/repl/index.tsx
+++ b/pw_web/webconsole/components/repl/index.tsx
@@ -13,7 +13,7 @@
// the License.
import {useEffect, useState} from "react";
-import {WebSerial} from "pigweedjs";
+import {Device} from "pigweedjs";
import {EditorView} from "codemirror"
import {basicSetup} from "./basicSetup";
import {javascript, javascriptLanguage} from "@codemirror/lang-javascript"
@@ -26,11 +26,10 @@
import "xterm/css/xterm.css";
import styles from "../../styles/repl.module.css";
-type WebSerialTransport = WebSerial.WebSerialTransport
const isSSR = () => typeof window === 'undefined';
interface ReplProps {
- transport: WebSerialTransport | undefined
+ device: Device | undefined
}
const globalJavaScriptCompletions = javascriptLanguage.data.of({
@@ -74,13 +73,13 @@
historyStorage = new LocalStorageArray();
}
-export default function Repl({transport}: ReplProps) {
+export default function Repl({device}: ReplProps) {
const [terminal, setTerminal] = useState<any>(null);
const [codeEditor, setCodeEditor] = useState<EditorView | null>(null);
useEffect(() => {
let cleanupFns: {(): void; (): void;}[] = [];
- if (!terminal && !isSSR() && transport) {
+ if (!terminal && !isSSR() && device) {
const futureTerm = createTerminal(document.querySelector('#repl-log-container')!);
futureTerm.then(async (term) => {
cleanupFns.push(() => {
@@ -94,11 +93,11 @@
cleanupFns.forEach(fn => fn());
}
}
- else if (terminal && !transport) {
+ else if (terminal && !device) {
terminal.dispose();
setTerminal(null);
}
- }, [transport]);
+ }, [device]);
useEffect(() => {
if (!terminal) return;
diff --git a/pw_web/webconsole/package.json b/pw_web/webconsole/package.json
index b5ff44a..77cd836 100644
--- a/pw_web/webconsole/package.json
+++ b/pw_web/webconsole/package.json
@@ -18,7 +18,7 @@
"@mui/material": "^5.9.3",
"codemirror": "^6.0.1",
"next": "12.2.3",
- "pigweedjs": "../../",
+ "pigweedjs": "file:../../",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-virtualized": "^9.22.3",
diff --git a/pw_web/webconsole/pages/index.tsx b/pw_web/webconsole/pages/index.tsx
index 349efae..2086687 100644
--- a/pw_web/webconsole/pages/index.tsx
+++ b/pw_web/webconsole/pages/index.tsx
@@ -19,12 +19,12 @@
import Repl from "../components/repl";
import Connect from "../components/connect";
import BtnUploadDB from '../components/uploadDb';
-import {WebSerial} from "pigweedjs";
+import {WebSerial, Device} from "pigweedjs";
import {useState} from 'react';
type WebSerialTransport = WebSerial.WebSerialTransport
const Home: NextPage = () => {
- const [transport, setTransport] = useState<WebSerialTransport | undefined>(undefined);
+ const [device, setDevice] = useState<Device | undefined>(undefined);
const [tokenDB, setTokenDB] = useState("");
return (
<div className={styles.container}>
@@ -37,8 +37,8 @@
<main className={styles.main}>
<div className={styles.toolbar}>
<span className={styles.logo}><span>Pigweed</span> Web Console</span>
- <Connect onConnection={(transport) => {
- setTransport(transport);
+ <Connect onConnection={(device) => {
+ setDevice(device);
}} />
<BtnUploadDB onUpload={(db) => {
setTokenDB(db);
@@ -47,10 +47,10 @@
<div className={styles.grid}>
<div>
- <Log transport={transport} tokenDB={tokenDB}></Log>
+ <Log device={device} tokenDB={tokenDB}></Log>
</div>
<div>
- <Repl transport={transport}></Repl>
+ <Repl device={device}></Repl>
</div>
</div>
</main>