Base work for tooth logs (#325)

This commit is contained in:
Piotr Rogowski 2021-12-19 21:37:31 +01:00 committed by GitHub
parent 0f1d35093c
commit 117ab6615d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 407 additions and 191 deletions

32
package-lock.json generated
View File

@ -24,7 +24,9 @@
"react-scripts": "^4.0.3",
"react-table-drag-select": "^0.3.1",
"recharts": "^2.1.8",
"timechart": "^1.0.0-beta.4"
"timechart": "^1.0.0-beta.4",
"uplot": "^1.6.18",
"uplot-react": "^1.1.1"
},
"devDependencies": {
"@craco/craco": "^6.4.3",
@ -21479,6 +21481,23 @@
"yarn": "*"
}
},
"node_modules/uplot": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.18.tgz",
"integrity": "sha512-x7+bFfIZ8rMjOmDGhUlJCkYWiZX617xQWNfT94JUhidliRtzMHKIX0xUiN92TZ/7il6xMf9oLwbhsz7nbqF1YQ=="
},
"node_modules/uplot-react": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/uplot-react/-/uplot-react-1.1.1.tgz",
"integrity": "sha512-zCvwyZVm4nfYDi+KjaK0FppqftGzga/x+u0h2baRWj1vXMB9/hfJ1qb9gXAdXMfp17C9Rk57HoZDE9MewNWLfg==",
"engines": {
"node": ">=8.10"
},
"peerDependencies": {
"react": ">=16.8.6",
"uplot": "^1.6.7"
}
},
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
@ -40124,6 +40143,17 @@
"resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
"integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg=="
},
"uplot": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.18.tgz",
"integrity": "sha512-x7+bFfIZ8rMjOmDGhUlJCkYWiZX617xQWNfT94JUhidliRtzMHKIX0xUiN92TZ/7il6xMf9oLwbhsz7nbqF1YQ=="
},
"uplot-react": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/uplot-react/-/uplot-react-1.1.1.tgz",
"integrity": "sha512-zCvwyZVm4nfYDi+KjaK0FppqftGzga/x+u0h2baRWj1vXMB9/hfJ1qb9gXAdXMfp17C9Rk57HoZDE9MewNWLfg==",
"requires": {}
},
"uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",

View File

@ -45,7 +45,9 @@
"react-scripts": "^4.0.3",
"react-table-drag-select": "^0.3.1",
"recharts": "^2.1.8",
"timechart": "^1.0.0-beta.4"
"timechart": "^1.0.0-beta.4",
"uplot": "^1.6.18",
"uplot-react": "^1.1.1"
},
"devDependencies": {
"@craco/craco": "^6.4.3",

View File

@ -123,6 +123,7 @@ Flag,Flag,Flag,Flag,ms,ms,ms,ms
0.0,0.0,0.0,1.0,564200.94,564200.94,35.864,564200.94
0.0,1.0,1.0,1.0,564250.3,564250.3,49.376,564250.3
1.0,1.0,0.0,1.0,564258.0,564258.0,7.728,564258.0
MARK 000
1.0,0.0,1.0,1.0,564286.5,564286.5,28.484,564286.5
0.0,0.0,0.0,1.0,564294.0,564294.0,7.468,564294.0
1.0,0.0,0.0,1.0,564351.06,564351.06,57.056,564351.06
@ -382,4 +383,4 @@ Flag,Flag,Flag,Flag,ms,ms,ms,ms
1.0,0.0,0.0,1.0,568452.75,568452.75,57.408,568452.75
0.0,0.0,0.0,1.0,568488.8,568488.8,36.024,568488.8
0.0,1.0,1.0,1.0,568538.44,568538.44,49.624,568538.44
MARK 000
MARK 001

1 #Firmware: Speeduino 2021.09-dev
123 0.0,0.0,0.0,1.0,564200.94,564200.94,35.864,564200.94
124 0.0,1.0,1.0,1.0,564250.3,564250.3,49.376,564250.3
125 1.0,1.0,0.0,1.0,564258.0,564258.0,7.728,564258.0
126 MARK 000
127 1.0,0.0,1.0,1.0,564286.5,564286.5,28.484,564286.5
128 0.0,0.0,0.0,1.0,564294.0,564294.0,7.468,564294.0
129 1.0,0.0,0.0,1.0,564351.06,564351.06,57.056,564351.06
383 1.0,0.0,0.0,1.0,568452.75,568452.75,57.408,568452.75
384 0.0,0.0,0.0,1.0,568488.8,568488.8,36.024,568488.8
385 0.0,1.0,1.0,1.0,568538.44,568538.44,49.624,568538.44
386 MARK 000 MARK 001

Binary file not shown.

View File

@ -0,0 +1,29 @@
import {
Popover,
Space,
Typography,
} from 'antd';
import { QuestionCircleOutlined } from '@ant-design/icons';
const { Text, Title } = Typography;
const CanvasHelp = () => (
<div style={{ marginTop: -20, marginBottom: 10, textAlign: 'left', marginLeft: 20 }}>
<Popover
placement="bottom"
content={
<Space direction="vertical">
<Title level={5}>Navigation</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>
);
export default CanvasHelp;

View File

@ -13,6 +13,7 @@ import {
Steps,
Space,
Divider,
Typography,
} from 'antd';
import {
FileTextOutlined,
@ -28,15 +29,23 @@ import {
Config,
Logs,
} from '@speedy-tuner/types';
import { loadCompositeLogs } from '../utils/api';
import {
loadCompositeLogs,
loadToothLogs,
} from '../utils/api';
import store from '../store';
import { formatBytes } from '../utils/number';
import CompositeCanvas from './TriggerLog/CompositeCanvas';
import { isNumber } from '../utils/tune/expression';
import TriggerLogsParser, {
CompositeLogEntry,
ToothLogEntry,
} from '../utils/logs/TriggerLogsParser';
import ToothCanvas from './TriggerLog/ToothCanvas';
const { TabPane } = Tabs;
const { Content } = Layout;
const { Step } = Steps;
const edgeUnknown = 'Unknown';
const mapStateToProps = (state: AppState) => ({
@ -46,19 +55,6 @@ const mapStateToProps = (state: AppState) => ({
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;
@ -83,6 +79,7 @@ const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loa
},
};
const [logs, setLogs] = useState<CompositeLogEntry[]>();
const [toothLogs, setToothLogs] = useState<ToothLogEntry[]>();
useEffect(() => {
const controller = new AbortController();
@ -90,79 +87,24 @@ const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loa
const loadData = async () => {
try {
const raw = await loadCompositeLogs((percent, total, edge) => {
const compositeRaw = 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[] = [];
const toothRaw = await loadToothLogs(undefined, signal);
setFileSize(formatBytes(compositeRaw.byteLength));
setStep(1);
// TODO: extract this, make a parser class
string.split('\n').forEach((line, index) => {
const trimmed = line.trim();
const parser = new TriggerLogsParser();
const resultComposite = parser.parse(pako.inflate(new Uint8Array(compositeRaw))).getCompositeLogs();
const resultTooth = parser.parse(pako.inflate(new Uint8Array(toothRaw))).getToothLogs();
// skip comments
if (trimmed.startsWith('#')) {
return;
}
setLogs(resultComposite);
setToothLogs(resultTooth);
// 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);
@ -191,7 +133,8 @@ const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loa
<Tabs defaultActiveKey="files" style={{ marginLeft: 20 }}>
<TabPane tab={<FileTextOutlined />} key="files">
<PerfectScrollbar options={{ suppressScrollX: true }}>
composite.csv
<Typography.Paragraph>tooth.csv</Typography.Paragraph>
<Typography.Paragraph>composite.csv</Typography.Paragraph>
</PerfectScrollbar>
</TabPane>
</Tabs>
@ -200,13 +143,22 @@ const Diagnose = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loa
<Layout style={{ width: '100%', textAlign: 'center', marginTop: 50 }}>
<Content>
<div ref={contentRef} style={{ width: '100%', marginRight: margin }}>
{logs
{toothLogs && logs
?
<CompositeCanvas
data={logs!}
width={canvasWidth}
height={canvasWidth * 0.4}
/>
(
<>
<ToothCanvas
data={toothLogs!}
width={canvasWidth}
height={canvasWidth * 0.3}
/>
<CompositeCanvas
data={logs!}
width={canvasWidth}
height={canvasWidth * 0.3}
/>
</>
)
:
<Space
direction="vertical"

View File

@ -13,6 +13,7 @@ import {
ResponsiveContainer,
Label,
} from 'recharts';
import { Colors } from '../../utils/colors';
import LandscapeNotice from './LandscapeNotice';
import Table from './Table';
@ -54,8 +55,6 @@ const Curve = ({
const [data, setData] = useState(mapData([yData, xData]));
const { sm } = useBreakpoint();
const margin = 15;
const mainColor = '#ccc';
const tooltipBg = '#2E3338';
const animationDuration = 500;
if (!sm) {
@ -85,7 +84,7 @@ const Curve = ({
<Label
value={`${xLabel} (${xUnits})`}
position="bottom"
style={{ fill: mainColor }}
style={{ fill: Colors.TEXT }}
/>
</XAxis>
<YAxis domain={['auto', 'auto']}>
@ -93,14 +92,14 @@ const Curve = ({
value={`${yLabel} (${yUnits})`}
position="left"
angle={-90}
style={{ fill: mainColor }}
style={{ fill: Colors.TEXT }}
/>
</YAxis>
<Tooltip
labelFormatter={(value) => `${xLabel} : ${value} ${xUnits}`}
formatter={(value: number) => [`${value} ${yUnits}`, yLabel]}
contentStyle={{
backgroundColor: tooltipBg,
backgroundColor: Colors.MAIN,
border: 0,
boxShadow: '0 3px 6px -4px rgb(0 0 0 / 12%), 0 6px 16px 0 rgb(0 0 0 / 8%), 0 9px 28px 8px rgb(0 0 0 / 5%)',
borderRadius: 5,
@ -111,7 +110,7 @@ const Curve = ({
strokeWidth={3}
type="linear"
dataKey="y"
stroke="#1e88ea"
stroke={Colors.ACCENT}
animationDuration={animationDuration}
/>
</LineChart>

View File

@ -5,31 +5,13 @@ import {
useRef,
} from 'react';
import { Logs } from '@speedy-tuner/types';
import {
Popover,
Space,
Typography,
Grid,
} from 'antd';
import { QuestionCircleOutlined } from '@ant-design/icons';
import { Grid } from 'antd';
import TimeChart from 'timechart';
import { EventsPlugin } from 'timechart/dist/lib/plugins_extra/events';
import { colorHsl } from '../../utils/number';
import LandscapeNotice from '../Dialog/LandscapeNotice';
import CanvasHelp from '../CanvasHelp';
// 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 {
@ -155,22 +137,7 @@ const LogCanvas = ({ data, width, height, selectedFields }: Props) => {
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>
<CanvasHelp />
<div
ref={canvasRef}
style={{ width, height }}

View File

@ -2,54 +2,19 @@ import {
useEffect,
useRef,
} from 'react';
import {
Popover,
Space,
Typography,
Grid,
} from 'antd';
import { QuestionCircleOutlined } from '@ant-design/icons';
import { Grid } from 'antd';
import TimeChart from 'timechart';
import { EventsPlugin } from 'timechart/dist/lib/plugins_extra/events';
import LandscapeNotice from '../Dialog/LandscapeNotice';
import {
CompositeLogEntry,
EntryType,
} from '../../utils/logs/TriggerLogsParser';
import CanvasHelp from '../CanvasHelp';
import { Colors } from '../../utils/colors';
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;
@ -73,14 +38,14 @@ const CompositeCanvas = ({ data, width, height }: Props) => {
const sync: DataPoint[] = [];
data.forEach((entry, index) => {
if (entry.type === 'marker') {
if (entry.type === EntryType.MARKER) {
markers.push({
x: index,
name: '',
});
}
if (entry.type === 'trigger') {
if (entry.type === EntryType.TRIGGER) {
const prevSecondary = data[index - 1] ? data[index - 1].secondaryLevel : 0;
const currentSecondary = (entry.secondaryLevel + 3) * 2; // apply scale
@ -166,22 +131,7 @@ const CompositeCanvas = ({ data, width, height }: Props) => {
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>
<CanvasHelp />
<div
ref={canvasRef}
style={{ width, height }}

View File

@ -0,0 +1,102 @@
import {
useEffect,
useState,
} from 'react';
import { Grid } from 'antd';
import UplotReact from 'uplot-react';
import uPlot from 'uplot';
import LandscapeNotice from '../Dialog/LandscapeNotice';
import {
ToothLogEntry,
EntryType,
} from '../../utils/logs/TriggerLogsParser';
import CanvasHelp from '../CanvasHelp';
import 'uplot/dist/uPlot.min.css';
import { Colors } from '../../utils/colors';
const { useBreakpoint } = Grid;
const { bars } = uPlot.paths;
interface Props {
data: ToothLogEntry[];
width: number;
height: number;
};
const ToothCanvas = ({ data, width, height }: Props) => {
const { sm } = useBreakpoint();
const [options, setOptions] = useState<uPlot.Options>();
const [plotData, setPlotData] = useState<uPlot.AlignedData>();
useEffect(() => {
const xData: number[] = [];
const yData: (number | null)[] = [];
data.forEach((entry: ToothLogEntry, index) => {
if (entry.type === EntryType.TRIGGER) {
yData.push(entry.toothTime);
xData.push(index);
}
});
setPlotData([
xData,
yData,
]);
setOptions({
title: 'Tooth logs',
width,
height,
scales: {
x: { time: false },
},
series: [
{
label: 'Event',
},
{
label: 'Tooth time',
points: { show: false },
stroke: Colors.ACCENT,
fill: Colors.ACCENT,
scale: 'toothTime',
value: (self, rawValue) => `${rawValue.toLocaleString()}μs`,
paths: bars!({ size: [0.6, 100] }),
},
],
axes: [
{
stroke: Colors.TEXT,
grid: { stroke: Colors.MAIN_LIGHT },
},
{
scale: 'toothTime',
label: '',
stroke: Colors.TEXT,
grid: { stroke: Colors.MAIN_LIGHT },
},
],
cursor: {
drag: { y: false },
},
});
}, [data, width, height, sm]);
if (!sm) {
return <LandscapeNotice />;
}
return (
<>
<CanvasHelp />
<UplotReact
options={options!}
data={plotData!}
/>
</>
);
};
export default ToothCanvas;

View File

@ -90,8 +90,15 @@ export const loadLogs = (onProgress?: onProgressType, signal?: AbortSignal) =>
export const loadCompositeLogs = (onProgress?: onProgressType, signal?: AbortSignal) =>
fetchWithProgress(
'https://d29mjpbgm6k6md.cloudfront.net/trigger-logs/composite_1.csv.gz',
'https://d29mjpbgm6k6md.cloudfront.net/trigger-logs/composite_1_2.csv.gz',
// 'https://d29mjpbgm6k6md.cloudfront.net/trigger-logs/2.csv.gz',
onProgress,
signal,
).then((response) => response);
export const loadToothLogs = (onProgress?: onProgressType, signal?: AbortSignal) =>
fetchWithProgress(
'https://d29mjpbgm6k6md.cloudfront.net/trigger-logs/tooth_3.csv.gz',
onProgress,
signal,
).then((response) => response);

17
src/utils/colors.ts Normal file
View File

@ -0,0 +1,17 @@
// eslint-disable-next-line import/prefer-default-export
export enum Colors {
RED = '#f32450',
CYAN = '#8dd3c7',
YELLOW = '#ffff00',
PURPLE = '#bebada',
GREEN = '#77de3c',
BLUE = '#2fe3ff',
GREY = '#334455',
// dark theme
ACCENT = '#1e88ea',
TEXT = '#ddd',
MAIN = '#222629',
MAIN_DARK = '#191C1E',
MAIN_LIGHT = '#2E3338',
}

View File

@ -0,0 +1,160 @@
import { isNumber } from '../tune/expression';
export enum EntryType {
TRIGGER = 'trigger',
MARKER = 'marker',
}
export interface CompositeLogEntry {
type: EntryType;
primaryLevel: number;
secondaryLevel: number;
trigger: number;
sync: number;
refTime: number;
maxTime: number;
toothTime: number;
time: number;
}
export interface ToothLogEntry {
type: EntryType;
toothTime: number;
time: number;
}
class TriggerLogsParser {
COMMENT_PREFIX = '#';
MARKER_PREFIX = 'MARK';
isTooth: boolean = false;
isComposite: boolean = false;
resultComposite: CompositeLogEntry[] = [];
resultTooth: ToothLogEntry[] = [];
parse(buffer: ArrayBuffer): TriggerLogsParser {
const raw = (new TextDecoder()).decode(buffer);
this.parseCompositeLogs(raw);
this.parseToothLogs(raw);
return this;
}
getCompositeLogs(): CompositeLogEntry[] {
return this.resultComposite;
}
getToothLogs(): ToothLogEntry[] {
return this.resultTooth;
}
private parseToothLogs(raw: string): void {
this.resultTooth = [];
raw.split('\n').forEach((line) => {
const trimmed = line.trim();
if (trimmed.startsWith(this.COMMENT_PREFIX)) {
return;
}
if (trimmed.startsWith(this.MARKER_PREFIX)) {
const previous = this.resultTooth[this.resultTooth.length - 1] || {
toothTime: 0,
time: 0,
};
this.resultTooth.push({
type: EntryType.MARKER,
toothTime: previous.toothTime,
time: previous.time,
});
return;
}
const split = trimmed.split(',');
if (!isNumber(split[0])) {
return;
}
const time = Number(split[1]);
if (!time) {
return;
}
this.resultTooth.push({
type: EntryType.TRIGGER,
toothTime: Number(split[0]),
time,
});
});
}
private parseCompositeLogs(raw: string): void {
this.resultComposite = [];
raw.split('\n').forEach((line) => {
const trimmed = line.trim();
if (trimmed.startsWith(this.COMMENT_PREFIX)) {
return;
}
if (trimmed.startsWith(this.MARKER_PREFIX)) {
const previous = this.resultComposite[this.resultComposite.length - 1] || {
primaryLevel: 0,
secondaryLevel: 0,
trigger: 0,
sync: 0,
refTime: 0,
maxTime: 0,
toothTime: 0,
time: 0,
};
this.resultComposite.push({
type: EntryType.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,
});
return;
}
const split = trimmed.split(',');
if (!isNumber(split[0])) {
return;
}
const time = Number(split[7]);
if (!time) {
return;
}
this.resultComposite.push({
type: EntryType.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,
});
});
}
}
export default TriggerLogsParser;