Basic logs (#35)

This commit is contained in:
Piotr Rogowski 2021-03-27 13:00:52 +01:00 committed by GitHub
parent 25d09a106f
commit 83b768cb04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 4428 additions and 28985 deletions

32927
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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",

BIN
public/logs/long.mlg Normal file

Binary file not shown.

BIN
public/logs/middle.mlg Normal file

Binary file not shown.

BIN
public/logs/short.mlg Normal file

Binary file not shown.

View File

@ -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"

View File

@ -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

105
src/components/Log.tsx Normal file
View File

@ -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;

View File

@ -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;

14
src/hooks/useStorage.ts Normal file
View File

@ -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;

View File

@ -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());

View File

@ -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;

View File

@ -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;

View File

@ -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>;
}