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",
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"js-yaml": "^4.0.0 ",
|
||||
"mlg-converter": "^0.1.5",
|
||||
"mlg-converter": "^0.3.0",
|
||||
"parsimmon": "^1.16.0",
|
||||
"react": "^17.0.1",
|
||||
"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 {
|
||||
useLocation,
|
||||
Switch,
|
||||
|
@ -21,7 +21,8 @@ import 'react-perfect-scrollbar/dist/css/styles.css';
|
|||
import './App.less';
|
||||
import { Routes } from './routes';
|
||||
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;
|
||||
|
||||
|
@ -33,13 +34,15 @@ const mapStateToProps = (state: AppState) => ({
|
|||
|
||||
const App = ({ ui, config }: { ui: UIState, config: ConfigType }) => {
|
||||
const margin = ui.sidebarCollapsed ? 80 : 250;
|
||||
// const [lastDialogPath, setLastDialogPath] = useState<string|null>();
|
||||
const { pathname } = useLocation();
|
||||
const { storageGetSync } = useStorage();
|
||||
const lastDialogPath = storageGetSync('lastDialog');
|
||||
const dialogMatchedPath: DialogMatchedPathType = useMemo(() => matchPath(pathname, {
|
||||
path: Routes.DIALOG,
|
||||
exact: true,
|
||||
}) || { url: '', params: { category: '', dialog: '' } }, [pathname]);
|
||||
|
||||
const lastDialogPath = storageGet('lastDialog');
|
||||
const firstDialogPath = useMemo(() => {
|
||||
if (!config.menus) {
|
||||
return null;
|
||||
|
@ -52,11 +55,15 @@ const App = ({ ui, config }: { ui: UIState, config: ConfigType }) => {
|
|||
|
||||
useEffect(() => {
|
||||
loadAll();
|
||||
// window.addEventListener('beforeunload', beforeUnload);
|
||||
// storageGet('lastDialog')
|
||||
// .then((path) => setLastDialogPath(path));
|
||||
|
||||
// window.addEventListener('beforeunload', beforeUnload);
|
||||
// return () => {
|
||||
// window.removeEventListener('beforeunload', beforeUnload);
|
||||
// };
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
@ -84,6 +91,15 @@ const App = ({ ui, config }: { ui: UIState, config: ConfigType }) => {
|
|||
</Layout>
|
||||
</Layout>
|
||||
</Route>
|
||||
<Route path={Routes.LOG}>
|
||||
<Layout style={{ marginLeft: margin }}>
|
||||
<Layout className="app-content">
|
||||
<Content>
|
||||
<Log />
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Route>
|
||||
<Route>
|
||||
<Result
|
||||
status="warning"
|
||||
|
|
|
@ -33,7 +33,7 @@ import { findOnPage } from '../utils/config/find';
|
|||
import { parseXy, parseZ } from '../utils/tune/table';
|
||||
import Map from './Dialog/Map';
|
||||
import { evaluateExpression, isExpression } from '../utils/tune/expression';
|
||||
import { storageSet } from '../utils/storage';
|
||||
import useStorage from '../hooks/useStorage';
|
||||
|
||||
interface DialogsAndCurves {
|
||||
[name: string]: DialogType | CurveType | TableType,
|
||||
|
@ -95,10 +95,10 @@ const Dialog = ({
|
|||
burnButton: any
|
||||
}) => {
|
||||
const isDataReady = Object.keys(tune.constants).length && Object.keys(config.constants).length;
|
||||
|
||||
const { storageSet } = useStorage();
|
||||
useEffect(() => {
|
||||
storageSet('lastDialog', url);
|
||||
}, [url]);
|
||||
}, [storageSet, url]);
|
||||
|
||||
const renderHelp = (link?: string) => (link &&
|
||||
<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;
|
117
src/utils/api.ts
117
src/utils/api.ts
|
@ -6,77 +6,74 @@ import stdDialogs from '../data/standardDialogs';
|
|||
import help from '../data/help';
|
||||
import { divider } from '../data/constants';
|
||||
|
||||
export const loadAll = () => {
|
||||
export const loadAll = async () => {
|
||||
const started = new Date();
|
||||
// const version = 202012;
|
||||
const version = 202103;
|
||||
|
||||
fetch(`./tunes/${version}.json`)
|
||||
.then((response) => response.json())
|
||||
.then((json: ConfigType) => {
|
||||
const json: ConfigType = await fetch(`./tunes/${version}.json`)
|
||||
.then((response) => response.json());
|
||||
|
||||
fetch(`./tunes/${version}.msq`)
|
||||
.then((response) => response.text())
|
||||
.then((tune) => {
|
||||
const xml = (new DOMParser()).parseFromString(tune, 'text/xml');
|
||||
const xmlPages = xml.getElementsByTagName('page');
|
||||
const constants: any = {};
|
||||
const tune = await fetch(`./tunes/${version}.msq`)
|
||||
.then((response) => response.text());
|
||||
|
||||
Object.keys(xmlPages).forEach((key: any) => {
|
||||
const page = xmlPages[key];
|
||||
const pageElements = page.children;
|
||||
const xml = (new DOMParser()).parseFromString(tune, 'text/xml');
|
||||
const xmlPages = xml.getElementsByTagName('page');
|
||||
const constants: any = {};
|
||||
|
||||
Object.keys(pageElements).forEach((item: any) => {
|
||||
const element = pageElements[item];
|
||||
Object.keys(xmlPages).forEach((key: any) => {
|
||||
const page = xmlPages[key];
|
||||
const pageElements = page.children;
|
||||
|
||||
if (element.tagName === 'constant') {
|
||||
const attributes: any = {};
|
||||
Object.keys(pageElements).forEach((item: any) => {
|
||||
const element = pageElements[item];
|
||||
|
||||
Object.keys(element.attributes).forEach((attr: any) => {
|
||||
attributes[element.attributes[attr].name] = element.attributes[attr].value;
|
||||
});
|
||||
if (element.tagName === 'constant') {
|
||||
const attributes: any = {};
|
||||
|
||||
const val = element.textContent?.replace(/"/g, '').toString();
|
||||
|
||||
constants[attributes.name] = {
|
||||
value: Number.isNaN(Number(val)) ? val : Number(val),
|
||||
// digits: Number.isNaN(Number(attributes.digits)) ? attributes.digits : Number(attributes.digits),
|
||||
// cols: Number.isNaN(Number(attributes.cols)) ? attributes.cols : Number(attributes.cols),
|
||||
// rows: Number.isNaN(Number(attributes.rows)) ? attributes.rows : Number(attributes.rows),
|
||||
units: attributes.units ?? null,
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!Object.keys(constants).length) {
|
||||
console.error('Invalid tune');
|
||||
}
|
||||
|
||||
const config = json;
|
||||
|
||||
// override / merge standard dialogs, constants and help
|
||||
config.dialogs = {
|
||||
...config.dialogs,
|
||||
...stdDialogs,
|
||||
};
|
||||
config.help = {
|
||||
...config.help,
|
||||
...help,
|
||||
};
|
||||
config.constants.pages[0].data.divider = divider;
|
||||
|
||||
const loadingTimeInfo = `Tune loaded in ${(new Date().getTime() - started.getTime())}ms`;
|
||||
console.log(loadingTimeInfo);
|
||||
|
||||
store.dispatch({ type: 'config/load', payload: config });
|
||||
store.dispatch({ type: 'tune/load', payload: { constants } });
|
||||
store.dispatch({
|
||||
type: 'status',
|
||||
payload: loadingTimeInfo,
|
||||
});
|
||||
Object.keys(element.attributes).forEach((attr: any) => {
|
||||
attributes[element.attributes[attr].name] = element.attributes[attr].value;
|
||||
});
|
||||
|
||||
const val = element.textContent?.replace(/"/g, '').toString();
|
||||
|
||||
constants[attributes.name] = {
|
||||
value: Number.isNaN(Number(val)) ? val : Number(val),
|
||||
// digits: Number.isNaN(Number(attributes.digits)) ? attributes.digits : Number(attributes.digits),
|
||||
// cols: Number.isNaN(Number(attributes.cols)) ? attributes.cols : Number(attributes.cols),
|
||||
// rows: Number.isNaN(Number(attributes.rows)) ? attributes.rows : Number(attributes.rows),
|
||||
units: attributes.units ?? null,
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!Object.keys(constants).length) {
|
||||
console.error('Invalid tune');
|
||||
}
|
||||
|
||||
const config = json;
|
||||
|
||||
// override / merge standard dialogs, constants and help
|
||||
config.dialogs = {
|
||||
...config.dialogs,
|
||||
...stdDialogs,
|
||||
};
|
||||
config.help = {
|
||||
...config.help,
|
||||
...help,
|
||||
};
|
||||
config.constants.pages[0].data.divider = divider;
|
||||
|
||||
const loadingTimeInfo = `Tune loaded in ${(new Date().getTime() - started.getTime())}ms`;
|
||||
console.log(loadingTimeInfo);
|
||||
|
||||
store.dispatch({ type: 'config/load', payload: config });
|
||||
store.dispatch({ type: 'tune/load', payload: { constants } });
|
||||
store.dispatch({
|
||||
type: 'status',
|
||||
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