1// Copyright 2022 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 15import {useEffect, useRef, useState} from "react"; 16import {pw_tokenizer, Device} from "pigweedjs"; 17import {AutoSizer, Table, Column} from 'react-virtualized'; 18import {listenToDefaultLogService} from "../common/logService"; 19import 'react-virtualized/styles.css'; 20import styles from "../styles/log.module.css"; 21 22type Detokenizer = pw_tokenizer.Detokenizer; 23 24interface LogProps { 25 device: Device | undefined, 26 tokenDB: string | undefined 27} 28 29interface LogEntry { 30 msg: string, 31 timestamp: number, 32 humanTime: string, 33 module: string, 34 file: string 35} 36 37function parseLogMsg(msg: string): LogEntry { 38 const pairs = msg.split("■").slice(1).map(pair => pair.split("♦")); 39 40 // Not a valid message, print as-is. 41 if (pairs.length === 0) { 42 return { 43 msg, 44 module: "", 45 file: "", 46 timestamp: Date.now(), 47 humanTime: new Date(Date.now()).toLocaleTimeString("en-US") 48 } 49 } 50 51 let map: any = {}; 52 pairs.forEach(pair => map[pair[0]] = pair[1]) 53 return { 54 msg: map.msg, 55 module: map.module, 56 file: map.file, 57 timestamp: Date.now(), 58 humanTime: new Date(Date.now()).toLocaleTimeString("en-US") 59 } 60} 61 62const keyToDisplayName: {[key: string]: string} = { 63 "msg": "Message", 64 "humanTime": "Time", 65 "module": "Module", 66 "file": "File" 67} 68 69export default function Log({device, tokenDB}: LogProps) { 70 const [logs, setLogs] = useState<LogEntry[]>([]); 71 const [detokenizer, setDetokenizer] = useState<Detokenizer | null>(null); 72 const logTable = useRef<Table | null>(null); 73 const _headerRenderer = ({dataKey, sortBy, sortDirection}: any) => { 74 return ( 75 <div> 76 {keyToDisplayName[dataKey]} 77 </div> 78 ); 79 } 80 81 const processFrame = (frame: Uint8Array) => { 82 if (detokenizer) { 83 const detokenized = detokenizer.detokenizeUint8Array(frame); 84 setLogs(oldLogs => [...oldLogs, parseLogMsg(detokenized)]); 85 } 86 else { 87 const decoded = new TextDecoder().decode(frame); 88 setLogs(oldLogs => [...oldLogs, parseLogMsg(decoded)]); 89 } 90 setTimeout(() => { 91 logTable.current!.scrollToRow(logs.length - 1); 92 }, 100); 93 } 94 95 useEffect(() => { 96 if (device) { 97 let cleanupFn: () => void; 98 listenToDefaultLogService(device, processFrame).then((unsub) => cleanupFn = unsub); 99 return () => { 100 if (cleanupFn) cleanupFn(); 101 } 102 } 103 }, [device, detokenizer]); 104 105 useEffect(() => { 106 if (tokenDB && tokenDB.length > 0) { 107 const detokenizer = new pw_tokenizer.Detokenizer(tokenDB); 108 setDetokenizer(detokenizer); 109 } 110 }, [tokenDB]) 111 112 return ( 113 <> 114 {/* @ts-ignore */} 115 <AutoSizer> 116 {({height, width}) => ( 117 <> 118 {/* @ts-ignore */} 119 <Table 120 className={styles.logsContainer} 121 headerHeight={40} 122 height={height} 123 rowCount={logs.length} 124 rowGetter={({index}) => logs[index]} 125 rowHeight={30} 126 ref={logTable} 127 width={width} 128 > 129 {/* @ts-ignore */} 130 <Column 131 dataKey="humanTime" 132 width={190} 133 headerRenderer={_headerRenderer} 134 /> 135 {/* @ts-ignore */} 136 <Column 137 dataKey="msg" 138 flexGrow={1} 139 width={290} 140 headerRenderer={_headerRenderer} 141 /> 142 {/* @ts-ignore */} 143 <Column 144 dataKey="module" 145 width={190} 146 headerRenderer={_headerRenderer} 147 /> 148 {/* @ts-ignore */} 149 <Column 150 dataKey="file" 151 flexGrow={1} 152 width={190} 153 headerRenderer={_headerRenderer} 154 /> 155 </Table> 156 </> 157 )} 158 </AutoSizer> 159 </> 160 ) 161} 162