Base work for Diagnose tab (#323)
This commit is contained in:
parent
7e2e98ce1e
commit
41e56101ae
|
@ -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
|
|
Binary file not shown.
18
src/App.tsx
18
src/App.tsx
|
@ -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>
|
||||
|
|
|
@ -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);
|
|
@ -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}
|
||||
/>
|
||||
:
|
||||
|
|
|
@ -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 />;
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue