| // Copyright 2021 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. |
| |
| /* eslint-env browser */ |
| import { |
| Button, |
| TextField, |
| makeStyles, |
| Typography, |
| createTheme, |
| ThemeProvider, |
| } from '@material-ui/core'; |
| import {ToggleButtonGroup, ToggleButton} from '@material-ui/lab'; |
| import {WebSerialTransport} from '../transport/web_serial_transport'; |
| import {Decoder, Frame, Encoder} from '@pigweed/pw_hdlc'; |
| import {SerialLog} from './serial_log'; |
| import {Log} from './log'; |
| import * as React from 'react'; |
| import {useState, useRef} from 'react'; |
| import {Channel, Client, UnaryMethodStub} from '@pigweed/pw_rpc'; |
| import {Status} from '@pigweed/pw_status'; |
| import {ProtoCollection} from 'web_proto_collection/generated/ts_proto_collection'; |
| |
| const RPC_ADDRESS = 82; |
| |
| const darkTheme = createTheme({ |
| palette: { |
| type: 'dark', |
| primary: { |
| main: '#42a5f5', |
| }, |
| secondary: { |
| main: 'rgb(232, 21, 165)', |
| }, |
| }, |
| }); |
| |
| const useStyles = makeStyles(() => ({ |
| root: { |
| display: 'flex', |
| 'flex-direction': 'column', |
| 'align-items': 'flex-start', |
| color: 'rgba(255, 255, 255, 0.8);', |
| overflow: 'hidden', |
| width: '1000px', |
| }, |
| connect: { |
| 'margin-top': '16px', |
| 'margin-bottom': '20px', |
| }, |
| rpc: { |
| margin: '10px', |
| }, |
| })); |
| |
| export function App() { |
| const [connected, setConnected] = useState<boolean>(false); |
| const [frames, setFrames] = useState<Frame[]>([]); |
| const [logLines, setLogLines] = useState<string[]>([]); |
| const [echoText, setEchoText] = useState<string>('Hello World'); |
| const [logViewer, setLogViewer] = useState<string>('log'); |
| |
| const classes = useStyles(); |
| |
| const transportRef = useRef(new WebSerialTransport()); |
| const decoderRef = useRef(new Decoder()); |
| const encoderRef = useRef(new Encoder()); |
| const protoCollectionRef = useRef(new ProtoCollection()); |
| const channelsRef = useRef([ |
| new Channel(1, (bytes: Uint8Array) => { |
| sendPacket(transportRef.current!, bytes); |
| }), |
| ]); |
| const clientRef = useRef<Client>( |
| Client.fromProtoSet(channelsRef.current!, protoCollectionRef.current!) |
| ); |
| const echoService = clientRef |
| .current!.channel()! |
| .methodStub('pw.rpc.EchoService.Echo') as UnaryMethodStub; |
| |
| function onConnected() { |
| setConnected(true); |
| transportRef.current!.chunks.subscribe((item: Uint8Array) => { |
| const decoded = decoderRef.current!.process(item); |
| for (const frame of decoded) { |
| setFrames(old => [...old, frame]); |
| if (frame.address === RPC_ADDRESS) { |
| const status = clientRef.current!.processPacket(frame.data); |
| } |
| if (frame.address === 1) { |
| const decodedLine = new TextDecoder().decode(frame.data); |
| const date = new Date(); |
| const logLine = `[${date.toLocaleTimeString()}] ${decodedLine}`; |
| setLogLines(old => [...old, logLine]); |
| } |
| } |
| }); |
| } |
| |
| function sendPacket( |
| transport: WebSerialTransport, |
| packetBytes: Uint8Array |
| ): void { |
| const hdlcBytes = encoderRef.current.uiFrame(RPC_ADDRESS, packetBytes); |
| transport.sendChunk(hdlcBytes); |
| } |
| |
| function echo(text: string) { |
| const request = new echoService.method.responseType(); |
| request.setMsg(text); |
| echoService |
| .call(request) |
| .then(([status, response]) => { |
| console.log(response.toObject()); |
| }) |
| .catch(() => {}); |
| } |
| |
| return ( |
| <div className={classes.root}> |
| <ThemeProvider theme={darkTheme}> |
| <Typography variant="h3">Pigweb Demo</Typography> |
| <Button |
| className={classes.connect} |
| disabled={connected} |
| variant="contained" |
| color="primary" |
| onClick={() => { |
| transportRef.current |
| .connect() |
| .then(onConnected) |
| .catch(error => { |
| setConnected(false); |
| console.log(error); |
| }); |
| }} |
| > |
| {connected ? 'Connected' : 'Connect'} |
| </Button> |
| <ToggleButtonGroup |
| value={logViewer} |
| onChange={(event, selected) => { |
| setLogViewer(selected); |
| }} |
| exclusive |
| > |
| <ToggleButton value="log">Log Viewer</ToggleButton> |
| <ToggleButton value="serial">Serial Debug</ToggleButton> |
| </ToggleButtonGroup> |
| {logViewer === 'log' ? ( |
| <Log lines={logLines} frames={frames} /> |
| ) : ( |
| <SerialLog frames={frames} /> |
| )} |
| <span className={classes.rpc}> |
| <TextField |
| id="echo-text" |
| label="Echo Text" |
| disabled={!connected} |
| value={echoText} |
| onChange={event => { |
| setEchoText(event.target.value); |
| }} |
| ></TextField> |
| <Button |
| disabled={!connected} |
| variant="contained" |
| color="primary" |
| onClick={() => { |
| echo(echoText); |
| }} |
| > |
| Send Echo RPC |
| </Button> |
| </span> |
| </ThemeProvider> |
| </div> |
| ); |
| } |