Base work for Diagnose tab (#323)

This commit is contained in:
Piotr Rogowski 2021-12-16 23:13:18 +01:00 committed by GitHub
parent 7e2e98ce1e
commit 41e56101ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 468 additions and 16 deletions

View File

@ -1,4 +1,4 @@
#
#Firmware: Speeduino 2021.09-dev
PriLevel,SecLevel,Trigger,Sync,RefTime,MaxTime,ToothTime,Time
Flag,Flag,Flag,Flag,ms,ms,ms,ms
0.0,1.0,1.0,1.0,560535.4,560535.4,2274.308,560535.4
1 # #Firmware: Speeduino 2021.09-dev
2 PriLevel,SecLevel,Trigger,Sync,RefTime,MaxTime,ToothTime,Time
3 Flag,Flag,Flag,Flag,ms,ms,ms,ms
4 0.0,1.0,1.0,1.0,560535.4,560535.4,2274.308,560535.4

Binary file not shown.

View File

@ -7,10 +7,7 @@ import {
Redirect,
generatePath,
} from 'react-router-dom';
import {
Layout,
Result,
} from 'antd';
import { Layout } from 'antd';
import { connect } from 'react-redux';
import {
useEffect,
@ -32,6 +29,7 @@ import 'react-perfect-scrollbar/dist/css/styles.css';
import './App.less';
import { Routes } from './routes';
import Log from './components/Log';
import Diagnose from './components/Diagnose';
import useStorage from './hooks/useStorage';
import useConfig from './hooks/useConfig';
@ -113,11 +111,13 @@ const App = ({ ui, config }: { ui: UIState, config: ConfigType }) => {
</Layout>
</Route>
<Route>
<Result
status="warning"
title="There is nothing here"
style={{ marginTop: 50 }}
/>
<Layout style={{ marginLeft: margin }}>
<Layout className="app-content">
<Content>
<Diagnose />
</Content>
</Layout>
</Layout>
</Route>
</Switch>
</Layout>

250
src/components/Diagnose.tsx Normal file
View File

@ -0,0 +1,250 @@
/* eslint-disable import/no-webpack-loader-syntax */
import {
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import {
Layout,
Tabs,
Skeleton,
Progress,
Steps,
Space,
Divider,
} from 'antd';
import {
FileTextOutlined,
GlobalOutlined,
} from '@ant-design/icons';
import pako from 'pako';
import useBreakpoint from 'antd/lib/grid/hooks/useBreakpoint';
import { connect } from 'react-redux';
import PerfectScrollbar from 'react-perfect-scrollbar';
import {
AppState,
UIState,
Config,
Logs,
} from '@speedy-tuner/types';
import { loadCompositeLogs } from '../utils/api';
import store from '../store';
import { formatBytes } from '../utils/number';
import CompositeCanvas from './TriggerLog/CompositeCanvas';
import { isNumber } from '../utils/tune/expression';
const { TabPane } = Tabs;
const { Content } = Layout;
const { Step } = Steps;
const edgeUnknown = 'Unknown';
const mapStateToProps = (state: AppState) => ({
ui: state.ui,
status: state.status,
config: state.config,
loadedLogs: state.logs,
});
// TODO: extract this to types package
interface CompositeLogEntry {
type: 'trigger' | 'marker';
primaryLevel: number;
secondaryLevel: number;
trigger: number;
sync: number;
refTime: number;
maxTime: number;
toothTime: number;
time: number;
}
const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loadedLogs: Logs }) => {
const { lg } = useBreakpoint();
const { Sider } = Layout;
const [progress, setProgress] = useState(0);
const [fileSize, setFileSize] = useState<string>();
const [step, setStep] = useState(0);
const [edgeLocation, setEdgeLocation] = useState(edgeUnknown);
const [fetchError, setFetchError] = useState<Error>();
const contentRef = useRef<HTMLDivElement | null>(null);
const margin = 30;
const [canvasWidth, setCanvasWidth] = useState(0);
const sidebarWidth = 250;
const calculateCanvasWidth = useCallback(() => setCanvasWidth((contentRef.current?.clientWidth || 0) - margin), []);
const siderProps = {
width: sidebarWidth,
collapsible: true,
breakpoint: 'xl',
collapsed: ui.sidebarCollapsed,
onCollapse: (collapsed: boolean) => {
store.dispatch({ type: 'ui/sidebarCollapsed', payload: collapsed });
setTimeout(calculateCanvasWidth, 1);
},
};
const [logs, setLogs] = useState<CompositeLogEntry[]>();
useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
const loadData = async () => {
try {
const raw = await loadCompositeLogs((percent, total, edge) => {
setProgress(percent);
setFileSize(formatBytes(total));
setEdgeLocation(edge || edgeUnknown);
}, signal);
setFileSize(formatBytes(raw.byteLength));
const buff = pako.inflate(new Uint8Array(raw));
const string = (new TextDecoder()).decode(buff);
const result: CompositeLogEntry[] = [];
setStep(1);
// TODO: extract this, make a parser class
string.split('\n').forEach((line, index) => {
const trimmed = line.trim();
// skip comments
if (trimmed.startsWith('#')) {
return;
}
// markers
if (trimmed.startsWith('MARK')) {
const previous = result[result.length - 1] || {
primaryLevel: 0,
secondaryLevel: 0,
trigger: 0,
sync: 0,
refTime: 0,
maxTime: 0,
toothTime: 0,
time: 0,
};
result.push({
type: 'marker',
primaryLevel: previous.primaryLevel,
secondaryLevel: previous.secondaryLevel,
trigger: previous.trigger,
sync: previous.sync,
refTime: previous.refTime,
maxTime: previous.maxTime,
toothTime: previous.toothTime,
time: previous.time,
});
}
const split = trimmed.split(',');
if (!isNumber(split[0])) {
return;
}
const time = Number(split[7]);
if (!time) {
return;
}
result.push({
type: 'trigger',
primaryLevel: Number(split[0]),
secondaryLevel: Number(split[1]),
trigger: Number(split[2]),
sync: Number(split[3]),
refTime: Number(split[4]),
maxTime: Number(split[5]),
toothTime: Number(split[6]),
time,
});
});
setLogs(result);
setStep(2);
} catch (error) {
setFetchError(error as Error);
console.error(error);
}
};
loadData();
calculateCanvasWidth();
window.addEventListener('resize', calculateCanvasWidth);
return () => {
controller.abort();
window.removeEventListener('resize', calculateCanvasWidth);
};
}, [calculateCanvasWidth, loadedLogs]);
return (
<>
<Sider {...(siderProps as any)} className="app-sidebar">
{!logs && !loadedLogs.length ?
<div style={{ padding: 20 }}><Skeleton active /></div>
:
!ui.sidebarCollapsed &&
<Tabs defaultActiveKey="files" style={{ marginLeft: 20 }}>
<TabPane tab={<FileTextOutlined />} key="files">
<PerfectScrollbar options={{ suppressScrollX: true }}>
composite.csv
</PerfectScrollbar>
</TabPane>
</Tabs>
}
</Sider>
<Layout style={{ width: '100%', textAlign: 'center', marginTop: 50 }}>
<Content>
<div ref={contentRef} style={{ width: '100%', marginRight: margin }}>
{logs
?
<CompositeCanvas
data={logs!}
width={canvasWidth}
height={canvasWidth * 0.4}
/>
:
<Space
direction="vertical"
size="large"
style={{ width: '80%', maxWidth: 1000 }}
>
<Progress
type="circle"
percent={progress}
width={170}
/>
<Divider />
<Steps current={step} direction={lg ? 'horizontal' : 'vertical'}>
<Step
title="Downloading"
subTitle={fileSize}
description={
fetchError ? fetchError!.message : <Space>
<GlobalOutlined />{edgeLocation}
</Space>
}
/>
<Step
title="Decoding"
description="Parsing CSV"
/>
<Step
title="Rendering"
description="Putting pixels on your screen"
/>
</Steps>
</Space>
}
</div>
</Content>
</Layout>
</>
);
};
export default connect(mapStateToProps)(Diagnose);

View File

@ -211,7 +211,7 @@ const Log = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loadedLo
</TabPane>
<TabPane tab={<FileTextOutlined />} key="files">
<PerfectScrollbar options={{ suppressScrollX: true }}>
Files
some_tune.mlg
</PerfectScrollbar>
</TabPane>
</Tabs>
@ -225,7 +225,7 @@ const Log = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loadedLo
<LogCanvas
data={loadedLogs || (logs!.records as Logs)}
width={canvasWidth}
height={canvasWidth * 0.45}
height={canvasWidth * 0.4}
selectedFields={prepareSelectedFields}
/>
:

View File

@ -130,7 +130,7 @@ const LogCanvas = ({ data, width, height, selectedFields }: Props) => {
});
let chart: TimeChart;
if (canvasRef.current) {
if (canvasRef.current && sm) {
chart = new TimeChart(canvasRef.current, {
series,
lineWidth: 2,
@ -148,7 +148,7 @@ const LogCanvas = ({ data, width, height, selectedFields }: Props) => {
}
return () => chart && chart.dispose();
}, [data, fieldsToPlot, hsl, selectedFields, width, height]);
}, [data, fieldsToPlot, hsl, selectedFields, width, height, sm]);
if (!sm) {
return <LandscapeNotice />;

View File

@ -0,0 +1,195 @@
import {
useEffect,
useRef,
} from 'react';
import {
Popover,
Space,
Typography,
Grid,
} from 'antd';
import { QuestionCircleOutlined } from '@ant-design/icons';
import TimeChart from 'timechart';
import { EventsPlugin } from 'timechart/dist/lib/plugins_extra/events';
import LandscapeNotice from '../Dialog/LandscapeNotice';
enum Colors {
RED = '#f32450',
CYAN = '#8dd3c7',
YELLOW = '#ffff00',
PURPLE = '#bebada',
GREEN = '#77de3c',
BLUE = '#2fe3ff',
GREY = '#334455',
WHITE = '#fff',
BG = '#222629',
}
const { Text } = Typography;
const { useBreakpoint } = Grid;
export interface SelectedField {
name: string;
label: string;
units: string;
scale: string | number;
transform: string | number;
format: string;
};
// TODO: extract this to types package
interface CompositeLogEntry {
type: 'trigger' | 'marker';
primaryLevel: number;
secondaryLevel: number;
trigger: number;
sync: number;
refTime: number;
maxTime: number;
toothTime: number;
time: number;
}
interface Props {
data: CompositeLogEntry[];
width: number;
height: number;
};
interface DataPoint {
x: number;
y: number;
}
const CompositeCanvas = ({ data, width, height }: Props) => {
const { sm } = useBreakpoint();
const canvasRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
let chart: TimeChart;
const markers: { x: number, name: string }[] = [];
const primary: DataPoint[] = [];
const secondary: DataPoint[] = [];
const sync: DataPoint[] = [];
data.forEach((entry, index) => {
if (entry.type === 'marker') {
markers.push({
x: index,
name: '',
});
}
if (entry.type === 'trigger') {
const prevSecondary = data[index - 1] ? data[index - 1].secondaryLevel : 0;
const currentSecondary = (entry.secondaryLevel + 3) * 2; // apply scale
const prevPrimary = data[index - 1] ? data[index - 1].primaryLevel : 0;
const currentPrimary = (entry.primaryLevel + 1) * 2; // apply scale
const prevSync = data[index - 1] ? data[index - 1].sync : 0;
const currentSync = entry.sync;
// make it square
if (prevSecondary !== currentSecondary) {
secondary.push({
x: index - 1,
y: currentSecondary,
});
}
secondary.push({
x: index,
y: currentSecondary,
});
if (prevPrimary !== currentPrimary) {
primary.push({
x: index - 1,
y: currentPrimary,
});
}
primary.push({
x: index,
y: currentPrimary,
});
if (prevSync !== currentSync) {
sync.push({
x: index - 1,
y: currentSync,
});
}
sync.push({
x: index,
y: currentSync,
});
}
});
const series = [{
name: 'Primary',
color: Colors.BLUE,
data: primary,
}, {
name: 'Secondary',
color: Colors.GREEN,
data: secondary,
}, {
name: 'Sync',
color: Colors.RED,
data: sync,
}];
if (canvasRef.current && sm) {
chart = new TimeChart(canvasRef.current, {
series,
lineWidth: 2,
tooltip: true,
legend: false,
zoom: {
x: { autoRange: true },
y: { autoRange: true },
},
yRange: { min: -1, max: 9 },
tooltipXLabel: 'Event',
plugins: {
events: new EventsPlugin(markers),
},
});
}
return () => chart && chart.dispose();
}, [data, width, height, sm]);
if (!sm) {
return <LandscapeNotice />;
}
return (
<>
<div style={{ marginTop: -20, marginBottom: 10, textAlign: 'left', marginLeft: 20 }}>
<Popover
placement="bottom"
content={
<Space direction="vertical">
<Typography.Title level={5}>Navigation</Typography.Title>
<Text>Pinch to zoom</Text>
<Text>Drag to pan</Text>
<Text>Ctrl + wheel scroll to zoom X axis</Text>
<Text>Hold Shift to speed up zoom 5 times</Text>
</Space>
}
>
<QuestionCircleOutlined />
</Popover>
</div>
<div
ref={canvasRef}
style={{ width, height }}
className="log-canvas"
/>
</>
);
};
export default CompositeCanvas;

View File

@ -86,5 +86,12 @@ export const loadLogs = (onProgress?: onProgressType, signal?: AbortSignal) =>
// 'https://d29mjpbgm6k6md.cloudfront.net/logs/markers.mlg.gz',
onProgress,
signal,
)
.then((response) => response);
).then((response) => response);
export const loadCompositeLogs = (onProgress?: onProgressType, signal?: AbortSignal) =>
fetchWithProgress(
'https://d29mjpbgm6k6md.cloudfront.net/trigger-logs/composite_1.csv.gz',
// 'https://d29mjpbgm6k6md.cloudfront.net/trigger-logs/2.csv.gz',
onProgress,
signal,
).then((response) => response);