1// Copyright 2021 The Pigweed Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); you may not 4// use this file except in compliance with the License. You may obtain a copy of 5// the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12// License for the specific language governing permissions and limitations under 13// the License. 14 15/* eslint-env browser */ 16import { 17 Button, 18 TextField, 19 makeStyles, 20 Typography, 21 createTheme, 22 ThemeProvider, 23} from '@material-ui/core'; 24import {ToggleButtonGroup, ToggleButton} from '@material-ui/lab'; 25import {WebSerialTransport} from '../transport/web_serial_transport'; 26import {Decoder, Frame, Encoder} from '@pigweed/pw_hdlc'; 27import {SerialLog} from './serial_log'; 28import {Log} from './log'; 29import * as React from 'react'; 30import {useState, useRef} from 'react'; 31import {Channel, Client, UnaryMethodStub} from '@pigweed/pw_rpc'; 32import {Status} from '@pigweed/pw_status'; 33import {ProtoCollection} from 'web_proto_collection/generated/ts_proto_collection'; 34 35const RPC_ADDRESS = 82; 36 37const darkTheme = createTheme({ 38 palette: { 39 type: 'dark', 40 primary: { 41 main: '#42a5f5', 42 }, 43 secondary: { 44 main: 'rgb(232, 21, 165)', 45 }, 46 }, 47}); 48 49const useStyles = makeStyles(() => ({ 50 root: { 51 display: 'flex', 52 'flex-direction': 'column', 53 'align-items': 'flex-start', 54 color: 'rgba(255, 255, 255, 0.8);', 55 overflow: 'hidden', 56 width: '1000px', 57 }, 58 connect: { 59 'margin-top': '16px', 60 'margin-bottom': '20px', 61 }, 62 rpc: { 63 margin: '10px', 64 }, 65})); 66 67export function App() { 68 const [connected, setConnected] = useState<boolean>(false); 69 const [frames, setFrames] = useState<Frame[]>([]); 70 const [logLines, setLogLines] = useState<string[]>([]); 71 const [echoText, setEchoText] = useState<string>('Hello World'); 72 const [logViewer, setLogViewer] = useState<string>('log'); 73 74 const classes = useStyles(); 75 76 const transportRef = useRef(new WebSerialTransport()); 77 const decoderRef = useRef(new Decoder()); 78 const encoderRef = useRef(new Encoder()); 79 const protoCollectionRef = useRef(new ProtoCollection()); 80 const channelsRef = useRef([ 81 new Channel(1, (bytes: Uint8Array) => { 82 sendPacket(transportRef.current!, bytes); 83 }), 84 ]); 85 const clientRef = useRef<Client>( 86 Client.fromProtoSet(channelsRef.current!, protoCollectionRef.current!) 87 ); 88 const echoService = clientRef 89 .current!.channel()! 90 .methodStub('pw.rpc.EchoService.Echo') as UnaryMethodStub; 91 92 function onConnected() { 93 setConnected(true); 94 transportRef.current!.chunks.subscribe((item: Uint8Array) => { 95 const decoded = decoderRef.current!.process(item); 96 for (const frame of decoded) { 97 setFrames(old => [...old, frame]); 98 if (frame.address === RPC_ADDRESS) { 99 const status = clientRef.current!.processPacket(frame.data); 100 } 101 if (frame.address === 1) { 102 const decodedLine = new TextDecoder().decode(frame.data); 103 const date = new Date(); 104 const logLine = `[${date.toLocaleTimeString()}] ${decodedLine}`; 105 setLogLines(old => [...old, logLine]); 106 } 107 } 108 }); 109 } 110 111 function sendPacket( 112 transport: WebSerialTransport, 113 packetBytes: Uint8Array 114 ): void { 115 const hdlcBytes = encoderRef.current.uiFrame(RPC_ADDRESS, packetBytes); 116 transport.sendChunk(hdlcBytes); 117 } 118 119 function echo(text: string) { 120 const request = new echoService.method.responseType(); 121 request.setMsg(text); 122 echoService 123 .call(request) 124 .then(([status, response]) => { 125 console.log(response.toObject()); 126 }) 127 .catch(() => {}); 128 } 129 130 return ( 131 <div className={classes.root}> 132 <ThemeProvider theme={darkTheme}> 133 <Typography variant="h3">Pigweb Demo</Typography> 134 <Button 135 className={classes.connect} 136 disabled={connected} 137 variant="contained" 138 color="primary" 139 onClick={() => { 140 transportRef.current 141 .connect() 142 .then(onConnected) 143 .catch(error => { 144 setConnected(false); 145 console.log(error); 146 }); 147 }} 148 > 149 {connected ? 'Connected' : 'Connect'} 150 </Button> 151 <ToggleButtonGroup 152 value={logViewer} 153 onChange={(event, selected) => { 154 setLogViewer(selected); 155 }} 156 exclusive 157 > 158 <ToggleButton value="log">Log Viewer</ToggleButton> 159 <ToggleButton value="serial">Serial Debug</ToggleButton> 160 </ToggleButtonGroup> 161 {logViewer === 'log' ? ( 162 <Log lines={logLines} /> 163 ) : ( 164 <SerialLog frames={frames} /> 165 )} 166 <span className={classes.rpc}> 167 <TextField 168 id="echo-text" 169 label="Echo Text" 170 disabled={!connected} 171 value={echoText} 172 onChange={event => { 173 setEchoText(event.target.value); 174 }} 175 ></TextField> 176 <Button 177 disabled={!connected} 178 variant="contained" 179 color="primary" 180 onClick={() => { 181 echo(echoText); 182 }} 183 > 184 Send Echo RPC 185 </Button> 186 </span> 187 </ThemeProvider> 188 </div> 189 ); 190} 191