Basic logs (#35)
This commit is contained in:
parent
25d09a106f
commit
83b768cb04
File diff suppressed because it is too large
Load Diff
|
@ -39,7 +39,7 @@
|
||||||
"antd": "^4.14.1",
|
"antd": "^4.14.1",
|
||||||
"electron-squirrel-startup": "^1.0.0",
|
"electron-squirrel-startup": "^1.0.0",
|
||||||
"js-yaml": "^4.0.0 ",
|
"js-yaml": "^4.0.0 ",
|
||||||
"mlg-converter": "^0.1.5",
|
"mlg-converter": "^0.3.0",
|
||||||
"parsimmon": "^1.16.0",
|
"parsimmon": "^1.16.0",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
24
src/App.tsx
24
src/App.tsx
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
useLocation,
|
useLocation,
|
||||||
Switch,
|
Switch,
|
||||||
|
@ -21,7 +21,8 @@ import 'react-perfect-scrollbar/dist/css/styles.css';
|
||||||
import './App.less';
|
import './App.less';
|
||||||
import { Routes } from './routes';
|
import { Routes } from './routes';
|
||||||
import { Config as ConfigType } from './types/config';
|
import { Config as ConfigType } from './types/config';
|
||||||
import { storageGet } from './utils/storage';
|
import Log from './components/Log';
|
||||||
|
import useStorage from './hooks/useStorage';
|
||||||
|
|
||||||
const { Content } = Layout;
|
const { Content } = Layout;
|
||||||
|
|
||||||
|
@ -33,13 +34,15 @@ const mapStateToProps = (state: AppState) => ({
|
||||||
|
|
||||||
const App = ({ ui, config }: { ui: UIState, config: ConfigType }) => {
|
const App = ({ ui, config }: { ui: UIState, config: ConfigType }) => {
|
||||||
const margin = ui.sidebarCollapsed ? 80 : 250;
|
const margin = ui.sidebarCollapsed ? 80 : 250;
|
||||||
|
// const [lastDialogPath, setLastDialogPath] = useState<string|null>();
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
const { storageGetSync } = useStorage();
|
||||||
|
const lastDialogPath = storageGetSync('lastDialog');
|
||||||
const dialogMatchedPath: DialogMatchedPathType = useMemo(() => matchPath(pathname, {
|
const dialogMatchedPath: DialogMatchedPathType = useMemo(() => matchPath(pathname, {
|
||||||
path: Routes.DIALOG,
|
path: Routes.DIALOG,
|
||||||
exact: true,
|
exact: true,
|
||||||
}) || { url: '', params: { category: '', dialog: '' } }, [pathname]);
|
}) || { url: '', params: { category: '', dialog: '' } }, [pathname]);
|
||||||
|
|
||||||
const lastDialogPath = storageGet('lastDialog');
|
|
||||||
const firstDialogPath = useMemo(() => {
|
const firstDialogPath = useMemo(() => {
|
||||||
if (!config.menus) {
|
if (!config.menus) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -52,11 +55,15 @@ const App = ({ ui, config }: { ui: UIState, config: ConfigType }) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadAll();
|
loadAll();
|
||||||
// window.addEventListener('beforeunload', beforeUnload);
|
// storageGet('lastDialog')
|
||||||
|
// .then((path) => setLastDialogPath(path));
|
||||||
|
|
||||||
|
// window.addEventListener('beforeunload', beforeUnload);
|
||||||
// return () => {
|
// return () => {
|
||||||
// window.removeEventListener('beforeunload', beforeUnload);
|
// window.removeEventListener('beforeunload', beforeUnload);
|
||||||
// };
|
// };
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -84,6 +91,15 @@ const App = ({ ui, config }: { ui: UIState, config: ConfigType }) => {
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path={Routes.LOG}>
|
||||||
|
<Layout style={{ marginLeft: margin }}>
|
||||||
|
<Layout className="app-content">
|
||||||
|
<Content>
|
||||||
|
<Log />
|
||||||
|
</Content>
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
</Route>
|
||||||
<Route>
|
<Route>
|
||||||
<Result
|
<Result
|
||||||
status="warning"
|
status="warning"
|
||||||
|
|
|
@ -33,7 +33,7 @@ import { findOnPage } from '../utils/config/find';
|
||||||
import { parseXy, parseZ } from '../utils/tune/table';
|
import { parseXy, parseZ } from '../utils/tune/table';
|
||||||
import Map from './Dialog/Map';
|
import Map from './Dialog/Map';
|
||||||
import { evaluateExpression, isExpression } from '../utils/tune/expression';
|
import { evaluateExpression, isExpression } from '../utils/tune/expression';
|
||||||
import { storageSet } from '../utils/storage';
|
import useStorage from '../hooks/useStorage';
|
||||||
|
|
||||||
interface DialogsAndCurves {
|
interface DialogsAndCurves {
|
||||||
[name: string]: DialogType | CurveType | TableType,
|
[name: string]: DialogType | CurveType | TableType,
|
||||||
|
@ -95,10 +95,10 @@ const Dialog = ({
|
||||||
burnButton: any
|
burnButton: any
|
||||||
}) => {
|
}) => {
|
||||||
const isDataReady = Object.keys(tune.constants).length && Object.keys(config.constants).length;
|
const isDataReady = Object.keys(tune.constants).length && Object.keys(config.constants).length;
|
||||||
|
const { storageSet } = useStorage();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
storageSet('lastDialog', url);
|
storageSet('lastDialog', url);
|
||||||
}, [url]);
|
}, [storageSet, url]);
|
||||||
|
|
||||||
const renderHelp = (link?: string) => (link &&
|
const renderHelp = (link?: string) => (link &&
|
||||||
<Popover
|
<Popover
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
import {
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import {
|
||||||
|
Spin,
|
||||||
|
Layout,
|
||||||
|
Tabs,
|
||||||
|
Checkbox,
|
||||||
|
Row,
|
||||||
|
Skeleton,
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
FileTextOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
DashboardOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { CheckboxValueType } from 'antd/lib/checkbox/Group';
|
||||||
|
import { Parser } from 'mlg-converter';
|
||||||
|
import { Field, Result as ParserResult } from 'mlg-converter/dist/types';
|
||||||
|
import PerfectScrollbar from 'react-perfect-scrollbar';
|
||||||
|
import { loadLogs } from '../utils/api';
|
||||||
|
import Canvas, { LogEntry } from './Log/Canvas';
|
||||||
|
|
||||||
|
// const { SubMenu } = Menu;
|
||||||
|
const { TabPane } = Tabs;
|
||||||
|
const { Content } = Layout;
|
||||||
|
|
||||||
|
const Log = () => {
|
||||||
|
const { Sider } = Layout;
|
||||||
|
const sidebarWidth = 250;
|
||||||
|
const siderProps = {
|
||||||
|
width: sidebarWidth,
|
||||||
|
collapsible: true,
|
||||||
|
breakpoint: 'xl',
|
||||||
|
// collapsed: ui.sidebarCollapsed,
|
||||||
|
// onCollapse: (collapsed: boolean) => store.dispatch({ type: 'ui/sidebarCollapsed', payload: collapsed }),
|
||||||
|
} as any;
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [logs, setLogs] = useState<ParserResult>();
|
||||||
|
const [fields, setFields] = useState<Field[]>([]);
|
||||||
|
const [selectedFields, setSelectedFields] = useState<CheckboxValueType[]>([
|
||||||
|
'RPM',
|
||||||
|
'TPS',
|
||||||
|
'AFR Target',
|
||||||
|
'AFR',
|
||||||
|
'MAP',
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadLogs()
|
||||||
|
.then((data) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
const parsed = new Parser(data).parse();
|
||||||
|
setLogs(parsed);
|
||||||
|
setIsLoading(false);
|
||||||
|
setFields(parsed.fields);
|
||||||
|
|
||||||
|
console.log(parsed);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Sider {...siderProps} className="app-sidebar">
|
||||||
|
{isLoading ?
|
||||||
|
<Skeleton />
|
||||||
|
:
|
||||||
|
<Tabs defaultActiveKey="fields" style={{ marginLeft: 20 }}>
|
||||||
|
<TabPane tab={<EditOutlined />} key="fields">
|
||||||
|
<PerfectScrollbar options={{ suppressScrollX: true }}>
|
||||||
|
<Checkbox.Group onChange={setSelectedFields} value={selectedFields}>
|
||||||
|
{fields.map((field) => (
|
||||||
|
<Row key={field.name}>
|
||||||
|
<Checkbox key={field.name} value={field.name}>{field.name}</Checkbox>
|
||||||
|
</Row>
|
||||||
|
))}
|
||||||
|
</Checkbox.Group>
|
||||||
|
</PerfectScrollbar>
|
||||||
|
</TabPane>
|
||||||
|
<TabPane tab={<FileTextOutlined />} key="files">
|
||||||
|
<PerfectScrollbar options={{ suppressScrollX: true }}>
|
||||||
|
Files
|
||||||
|
</PerfectScrollbar>
|
||||||
|
</TabPane>
|
||||||
|
<TabPane tab={<DashboardOutlined />} key="gauges">
|
||||||
|
Gauges
|
||||||
|
</TabPane>
|
||||||
|
</Tabs>
|
||||||
|
}
|
||||||
|
</Sider>
|
||||||
|
<Layout style={{ width: '100%', textAlign: 'center', marginTop: 50 }}>
|
||||||
|
<Content>
|
||||||
|
{isLoading ?
|
||||||
|
<Spin size="large" />
|
||||||
|
:
|
||||||
|
<Canvas data={logs!.records as LogEntry[]} />
|
||||||
|
}
|
||||||
|
</Content>
|
||||||
|
</Layout>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Log;
|
|
@ -0,0 +1,158 @@
|
||||||
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
MouseEvent,
|
||||||
|
WheelEvent,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
export interface LogEntry {
|
||||||
|
[id: string]: number
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Colors {
|
||||||
|
RED = '#f32450',
|
||||||
|
CYAN = '#8dd3c7',
|
||||||
|
YELLOW = '#ffff00',
|
||||||
|
PURPLE = '#bebada',
|
||||||
|
GREEN = '#77de3c',
|
||||||
|
BLUE = '#2fe3ff',
|
||||||
|
GREY = '#334455',
|
||||||
|
WHITE = '#fff',
|
||||||
|
}
|
||||||
|
|
||||||
|
const Canvas = ({ data }: { data: LogEntry[] }) => {
|
||||||
|
const [zoom, setZoom] = useState(1);
|
||||||
|
const [pan, setPan] = useState(0);
|
||||||
|
const [isMouseDown, setIsMouseDown] = useState(false);
|
||||||
|
const [indicatorPos, setIndicatorPos] = useState(0);
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>();
|
||||||
|
|
||||||
|
const plot = useCallback(() => {
|
||||||
|
const canvas = canvasRef.current!;
|
||||||
|
const leftBoundary = 0;
|
||||||
|
const ctx = canvas.getContext('2d')!;
|
||||||
|
const lastEntry = data[data.length - 1];
|
||||||
|
const maxTime = lastEntry.Time / (zoom < 1 ? 1 : zoom);
|
||||||
|
const xScale = canvas.width / maxTime;
|
||||||
|
const firstEntry = data[0];
|
||||||
|
|
||||||
|
let start = pan;
|
||||||
|
|
||||||
|
if (pan > leftBoundary) {
|
||||||
|
start = leftBoundary;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log(start, zoom, maxTime, xScale);
|
||||||
|
|
||||||
|
// if (pan < rightBoundary) {
|
||||||
|
// start = rightBoundary;
|
||||||
|
// }
|
||||||
|
|
||||||
|
const plotEntry = (field: string, yScale: number, color: string) => {
|
||||||
|
ctx.strokeStyle = color;
|
||||||
|
ctx.beginPath();
|
||||||
|
|
||||||
|
// initial value
|
||||||
|
ctx.moveTo(start + firstEntry.Time, canvas.height - (firstEntry[field] * yScale));
|
||||||
|
|
||||||
|
data.forEach((entry) => {
|
||||||
|
const time = entry.Time * xScale; // scale time to max width
|
||||||
|
const value = canvas.height - (entry[field] * yScale); // scale the value
|
||||||
|
|
||||||
|
ctx.lineTo(start + time, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.stroke();
|
||||||
|
};
|
||||||
|
|
||||||
|
const plotIndicator = () => {
|
||||||
|
ctx.setLineDash([5]);
|
||||||
|
ctx.strokeStyle = Colors.WHITE;
|
||||||
|
ctx.beginPath();
|
||||||
|
|
||||||
|
ctx.moveTo(indicatorPos, 0);
|
||||||
|
ctx.lineTo(indicatorPos, canvas.height);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.lineWidth = Math.max(1.25, canvas.height / 400);
|
||||||
|
|
||||||
|
plotIndicator();
|
||||||
|
plotEntry('RPM', 0.16, Colors.RED);
|
||||||
|
plotEntry('TPS', 20, Colors.BLUE);
|
||||||
|
plotEntry('AFR Target', 4, Colors.YELLOW);
|
||||||
|
plotEntry('AFR', 4, Colors.GREEN);
|
||||||
|
plotEntry('MAP', 5, Colors.GREY);
|
||||||
|
}, [data, zoom, pan, indicatorPos]);
|
||||||
|
|
||||||
|
const onWheel = (e: WheelEvent) => {
|
||||||
|
const canvas = canvasRef.current!;
|
||||||
|
const leftBoundary = 0;
|
||||||
|
|
||||||
|
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
|
||||||
|
setZoom((current) => current < 1 ? 1 : current - e.deltaY / 100);
|
||||||
|
}
|
||||||
|
if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
|
||||||
|
setPan((current) => {
|
||||||
|
if (current > leftBoundary) {
|
||||||
|
return leftBoundary;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (current < rightBoundary) {
|
||||||
|
// return rightBoundary;
|
||||||
|
// }
|
||||||
|
|
||||||
|
return current - e.deltaX;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseMove = (e: MouseEvent) => {
|
||||||
|
const leftBoundary = 0;
|
||||||
|
setIndicatorPos(e.nativeEvent.offsetX);
|
||||||
|
|
||||||
|
if (isMouseDown) {
|
||||||
|
setPan((current) => {
|
||||||
|
if (current > leftBoundary) {
|
||||||
|
return leftBoundary;
|
||||||
|
}
|
||||||
|
|
||||||
|
return current + e.movementX;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseDown = (e: MouseEvent) => {
|
||||||
|
setIsMouseDown(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseUp = (e: MouseEvent) => {
|
||||||
|
setIsMouseDown(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
plot();
|
||||||
|
}, [plot]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef as any}
|
||||||
|
id="plot"
|
||||||
|
width="1200px"
|
||||||
|
height="600px"
|
||||||
|
style={{
|
||||||
|
border: 'solid #222',
|
||||||
|
}}
|
||||||
|
onWheel={onWheel as any}
|
||||||
|
onMouseMove={onMouseMove}
|
||||||
|
onMouseDown={onMouseDown}
|
||||||
|
onMouseUp={onMouseUp}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Canvas;
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import Storage from '../utils/storage';
|
||||||
|
|
||||||
|
const useStorage = () => {
|
||||||
|
const storage = useMemo(() => new Storage(), []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
storageGet: (key: string) => storage.get(key),
|
||||||
|
storageGetSync: (key: string) => storage.getSync(key),
|
||||||
|
storageSet: (key: string, value: string) => storage.set(key, value),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useStorage;
|
|
@ -6,18 +6,17 @@ import stdDialogs from '../data/standardDialogs';
|
||||||
import help from '../data/help';
|
import help from '../data/help';
|
||||||
import { divider } from '../data/constants';
|
import { divider } from '../data/constants';
|
||||||
|
|
||||||
export const loadAll = () => {
|
export const loadAll = async () => {
|
||||||
const started = new Date();
|
const started = new Date();
|
||||||
// const version = 202012;
|
// const version = 202012;
|
||||||
const version = 202103;
|
const version = 202103;
|
||||||
|
|
||||||
fetch(`./tunes/${version}.json`)
|
const json: ConfigType = await fetch(`./tunes/${version}.json`)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json());
|
||||||
.then((json: ConfigType) => {
|
|
||||||
|
const tune = await fetch(`./tunes/${version}.msq`)
|
||||||
|
.then((response) => response.text());
|
||||||
|
|
||||||
fetch(`./tunes/${version}.msq`)
|
|
||||||
.then((response) => response.text())
|
|
||||||
.then((tune) => {
|
|
||||||
const xml = (new DOMParser()).parseFromString(tune, 'text/xml');
|
const xml = (new DOMParser()).parseFromString(tune, 'text/xml');
|
||||||
const xmlPages = xml.getElementsByTagName('page');
|
const xmlPages = xml.getElementsByTagName('page');
|
||||||
const constants: any = {};
|
const constants: any = {};
|
||||||
|
@ -75,8 +74,6 @@ export const loadAll = () => {
|
||||||
type: 'status',
|
type: 'status',
|
||||||
payload: loadingTimeInfo,
|
payload: loadingTimeInfo,
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const test = () => 'test';
|
export const loadLogs = () => fetch('./logs/middle.mlg').then((response) => response.arrayBuffer());
|
||||||
|
|
|
@ -1,3 +1,24 @@
|
||||||
export const storageGet = (key: string) => window.localStorage.getItem(key);
|
import BrowserStorage from './storage/browserStorage';
|
||||||
|
import { StorageInterface } from './storageInterface';
|
||||||
|
|
||||||
export const storageSet = (key: string, value: string) => window.localStorage.setItem(key, value);
|
class Storage {
|
||||||
|
private storage: StorageInterface;
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
this.storage = new BrowserStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get(key: string) {
|
||||||
|
return this.storage.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSync(key: string) {
|
||||||
|
return this.storage.getSync(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public set(key: string, value: string) {
|
||||||
|
return this.storage.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Storage;
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { StorageInterface } from '../storageInterface';
|
||||||
|
|
||||||
|
class BrowserStorage implements StorageInterface {
|
||||||
|
private storage: Storage;
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
this.storage = window.localStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async get(key: string): Promise<string | null> {
|
||||||
|
await Promise.resolve();
|
||||||
|
return this.storage.getItem(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSync(key: string): string | null {
|
||||||
|
return this.storage.getItem(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async set(key: string, value: string): Promise<void> {
|
||||||
|
await Promise.resolve();
|
||||||
|
this.storage.setItem(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async isAvailable(): Promise<boolean> {
|
||||||
|
return !!this.storage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BrowserStorage;
|
|
@ -0,0 +1,6 @@
|
||||||
|
export interface StorageInterface {
|
||||||
|
get(key: string): Promise<string | null>;
|
||||||
|
getSync(key: string): string | null;
|
||||||
|
set(key: string, value: string): Promise<void>;
|
||||||
|
isAvailable(): Promise<boolean>;
|
||||||
|
}
|
Loading…
Reference in New Issue