diff --git a/public/logs/trigger/2021-05-31_14.19.11-composite.csv b/public/logs/trigger/composite_1.csv similarity index 99% rename from public/logs/trigger/2021-05-31_14.19.11-composite.csv rename to public/logs/trigger/composite_1.csv index b6e9beb..db1536c 100644 --- a/public/logs/trigger/2021-05-31_14.19.11-composite.csv +++ b/public/logs/trigger/composite_1.csv @@ -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 diff --git a/public/logs/trigger/composite_1.csv.gz b/public/logs/trigger/composite_1.csv.gz new file mode 100644 index 0000000..bcf8144 Binary files /dev/null and b/public/logs/trigger/composite_1.csv.gz differ diff --git a/public/logs/trigger/other/2021-12-10_18.41.41.csv b/public/logs/trigger/other/1.csv similarity index 100% rename from public/logs/trigger/other/2021-12-10_18.41.41.csv rename to public/logs/trigger/other/1.csv diff --git a/public/logs/trigger/other/2021-12-10_18.46.57.csv b/public/logs/trigger/other/2.csv similarity index 100% rename from public/logs/trigger/other/2021-12-10_18.46.57.csv rename to public/logs/trigger/other/2.csv diff --git a/public/logs/trigger/other/2021-12-10_18.54.44.csv b/public/logs/trigger/other/3.csv similarity index 100% rename from public/logs/trigger/other/2021-12-10_18.54.44.csv rename to public/logs/trigger/other/3.csv diff --git a/public/logs/trigger/other/2021-12-10_19.02.50.csv b/public/logs/trigger/other/4.csv similarity index 100% rename from public/logs/trigger/other/2021-12-10_19.02.50.csv rename to public/logs/trigger/other/4.csv diff --git a/public/logs/trigger/2021-05-31_14.24.40-trigger.csv b/public/logs/trigger/trigger_1.csv similarity index 100% rename from public/logs/trigger/2021-05-31_14.24.40-trigger.csv rename to public/logs/trigger/trigger_1.csv diff --git a/src/App.tsx b/src/App.tsx index c1ab695..a7c72ca 100644 --- a/src/App.tsx +++ b/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 }) => { - + + + + + + + diff --git a/src/components/Diagnose.tsx b/src/components/Diagnose.tsx new file mode 100644 index 0000000..8c0382a --- /dev/null +++ b/src/components/Diagnose.tsx @@ -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(); + const [step, setStep] = useState(0); + const [edgeLocation, setEdgeLocation] = useState(edgeUnknown); + const [fetchError, setFetchError] = useState(); + const contentRef = useRef(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(); + + 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 ( + <> + + {!logs && !loadedLogs.length ? +
+ : + !ui.sidebarCollapsed && + + } key="files"> + + composite.csv + + + + } +
+ + +
+ {logs + ? + + : + + + + + + {edgeLocation} + + } + /> + + + + + } +
+
+
+ + ); +}; + +export default connect(mapStateToProps)(Diagnose); diff --git a/src/components/Log.tsx b/src/components/Log.tsx index e917b24..63a89df 100644 --- a/src/components/Log.tsx +++ b/src/components/Log.tsx @@ -211,7 +211,7 @@ const Log = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loadedLo } key="files"> - Files + some_tune.mlg @@ -225,7 +225,7 @@ const Log = ({ ui, config, loadedLogs }: { ui: UIState, config: Config, loadedLo : diff --git a/src/components/Log/LogCanvas.tsx b/src/components/Log/LogCanvas.tsx index 1ab7456..f983170 100644 --- a/src/components/Log/LogCanvas.tsx +++ b/src/components/Log/LogCanvas.tsx @@ -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 ; diff --git a/src/components/TriggerLog/CompositeCanvas.tsx b/src/components/TriggerLog/CompositeCanvas.tsx new file mode 100644 index 0000000..257f1e5 --- /dev/null +++ b/src/components/TriggerLog/CompositeCanvas.tsx @@ -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(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 ; + } + + return ( + <> +
+ + Navigation + Pinch to zoom + Drag to pan + Ctrl + wheel scroll to zoom X axis + Hold Shift to speed up zoom 5 times + + } + > + + +
+
+ + ); +}; + +export default CompositeCanvas; diff --git a/src/utils/api.ts b/src/utils/api.ts index 25bf237..cffd865 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -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);