diff --git a/package-lock.json b/package-lock.json
index a598d52..176b24a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6941,9 +6941,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001387",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001387.tgz",
- "integrity": "sha512-fKDH0F1KOJvR+mWSOvhj8lVRr/Q/mc5u5nabU2vi1/sgvlSqEsE8dOq0Hy/BqVbDkCYQPRRHB1WRjW6PGB/7PA==",
+ "version": "1.0.30001458",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001458.tgz",
+ "integrity": "sha512-lQ1VlUUq5q9ro9X+5gOEyH7i3vm+AYVT1WDCVB69XOZ17KZRhnZ9J0Sqz7wTHQaLBJccNCHq8/Ww5LlOIZbB0w==",
"funding": [
{
"type": "opencollective",
@@ -22881,9 +22881,9 @@
}
},
"caniuse-lite": {
- "version": "1.0.30001387",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001387.tgz",
- "integrity": "sha512-fKDH0F1KOJvR+mWSOvhj8lVRr/Q/mc5u5nabU2vi1/sgvlSqEsE8dOq0Hy/BqVbDkCYQPRRHB1WRjW6PGB/7PA=="
+ "version": "1.0.30001458",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001458.tgz",
+ "integrity": "sha512-lQ1VlUUq5q9ro9X+5gOEyH7i3vm+AYVT1WDCVB69XOZ17KZRhnZ9J0Sqz7wTHQaLBJccNCHq8/Ww5LlOIZbB0w=="
},
"case-sensitive-paths-webpack-plugin": {
"version": "2.4.0",
diff --git a/src/components/App/__snapshots__/App.test.js.snap b/src/components/App/__snapshots__/App.test.js.snap
index 8878484..d1a399a 100644
--- a/src/components/App/__snapshots__/App.test.js.snap
+++ b/src/components/App/__snapshots__/App.test.js.snap
@@ -9851,7 +9851,7 @@ exports[`App Route /vehicle-status authenticated 1`] = `
- ECUs
+ T.Rex logs
- Remote Commands
+ ECUs
+
+ Remote Commands
+
+
+
+
- ECUs
+ T.Rex logs
- Remote Commands
+ ECUs
+
+ Remote Commands
+
+
+
+
diff --git a/src/components/Cars/Status/index.jsx b/src/components/Cars/Status/index.jsx
index 1ea797f..94a49c6 100644
--- a/src/components/Cars/Status/index.jsx
+++ b/src/components/Cars/Status/index.jsx
@@ -17,6 +17,7 @@ import DigitalTwinTab from "./DigitalTwinTab";
import ECUsTab from "./ECUsTab";
import FleetsTab from "./FleetsTab";
import RemoteCommandsTab from "./RemoteCommandsTab";
+import TRexLogsTab from "./TRexLogsTab"
const tabHashes = ["details", "updates", "filters"];
@@ -42,6 +43,10 @@ const TabViews = [
label: "CAN Signals",
component: CANSignalsTab,
},
+ {
+ label: "T.Rex logs",
+ component: TRexLogsTab,
+ },
{
label: "ECUs",
component: ECUsTab,
diff --git a/src/components/Controls/TRexLogs/index.jsx b/src/components/Controls/TRexLogs/index.jsx
new file mode 100644
index 0000000..29e78d3
--- /dev/null
+++ b/src/components/Controls/TRexLogs/index.jsx
@@ -0,0 +1,336 @@
+import React, { useEffect, useState } from "react";
+
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableFooter,
+ TablePagination,
+ TableRow
+} from "@material-ui/core";
+import LinearProgress from '@mui/material/LinearProgress';
+import Checkbox from '@mui/material/Checkbox';
+import FormGroup from '@mui/material/FormGroup';
+import FormControlLabel from '@mui/material/FormControlLabel';
+import FormControl from '@mui/material/FormControl';
+import FormLabel from '@mui/material/FormLabel';
+
+import {
+ KeyboardDatePicker, MuiPickersUtilsProvider
+} from '@material-ui/pickers';
+import clsx from "clsx";
+import api from "../../../services/vehiclesAPI";
+
+import DateFnsUtils from '@date-io/date-fns';
+import { TableHead } from "@mui/material";
+import { logger } from "../../../services/monitoring";
+import { useStatusContext } from "../../Contexts/StatusContext";
+
+const tableColumns = [
+ {
+ id: "level",
+ label: "Level",
+ width: "5%",
+ },
+ {
+ id: "trex_timestamp",
+ label: "T.Rex Timestamp",
+ width: "10%",
+ },
+ {
+ id: "cloud_timestamp",
+ label: "Cloud Timestamp",
+ width: "10%",
+ },
+ {
+ id: "line_number",
+ label: "Line Number",
+ width: "5%",
+ },
+ {
+ id: "filename",
+ label: "Filename",
+ width: "10%",
+ },
+ {
+ id: "msg",
+ label: "Message",
+ width: "60%",
+ },
+];
+
+const logLevelCheckBoxes = [
+ {
+ "id": "trace",
+ "label": "Trace"
+ },
+ {
+ "id": "debug",
+ "label": "Debug"
+ },
+ {
+ "id": "info",
+ "label": "Info"
+ },
+ {
+ "id": "warning",
+ "label": "Warning"
+ },
+ {
+ "id": "error",
+ "label": "Error"
+ },
+ {
+ "id": "critical",
+ "label": "Critical"
+ },
+]
+
+const transformLogs = (logs) =>
+ logs.map((log) => {
+ const { level, timestamp, received_timestamp, line_number, filename, msg } = log;
+ //const trex_time = new Date(timestamp)
+ //const cloud_time = new Date(received_timestamp)
+ return {
+ level: level,
+ trex_timestamp: timestamp,
+ cloud_timestamp: received_timestamp,
+ line_number: line_number,
+ filename: filename,
+ msg: msg
+ };
+ })
+ .flat()
+
+const fromatDateForRequest = (date) => {
+ const getYear = date.toLocaleString("default", { year: "numeric" });
+ const getMonth = date.toLocaleString("default", { month: "2-digit" });
+ const getDay = date.toLocaleString("default", { day: "2-digit" });
+ return getYear + "-" + getMonth + "-" + getDay
+};
+
+//read at least 50000 bytes per one API request
+const ONE_READ_SIZE = 50000
+const DEFAULT_PAGE_SIZE = 25
+
+const TRexLogsTable = ({ vin, token, classes }) => {
+ const [allLogsFetched, setAllLogsFetched] = useState(false)
+ const [blobSize, setBlobSize] = useState(0)
+ const [currentOffset, setCurrentOffset] = useState(0)
+ const [currectLogLevels, setCurrentLogLevels] = useState({
+ trace: true,
+ debug: true,
+ info: true,
+ warning: true,
+ error: true,
+ critical: true
+ })
+ const [logs, setLogs] = useState([]);
+ const [pageIndex, setPageIndex] = useState(0);
+ const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
+ const [selectedDate, setSelectedDate] = useState(new Date());
+ const [total, setTotal] = useState(0);
+ const { setMessage } = useStatusContext();
+
+ let controller = new AbortController()
+ const readBlob = async (offset, count) => {
+ console.log(`reading from offset: ${offset}`)
+ return await api.getTRexLogs(vin, fromatDateForRequest(selectedDate), offset, count, "UP", token, controller)
+ }
+ const getDesiredSize = () => {
+ return pageSize * pageIndex + pageSize
+ }
+ const getReadPercentage = () => {
+ return (currentOffset * 100 / blobSize).toFixed(2);
+ }
+ const getFilteredLogs = (logs) => {
+ return logs.filter(log => currectLogLevels[log.level] === true)
+ }
+ const fetchAllLogs = async () => {
+ let fetched = []
+ let offset = currentOffset
+ let readSize = ONE_READ_SIZE
+ for (; ;) {
+ const result = await readBlob(offset, readSize)
+ if (result.error) {
+ fetched.error = result.error
+ break
+ }
+ console.log(`ret.RealOffset ${result.RealOffset}\nret.bytesRead ${result.bytesRead}\ndesired offset ${offset}\ndesired read size ${readSize}\nblobsize: ${result.blobSize}`)
+ setBlobSize(result.blobSize)
+ readSize *= 2
+ console.log(`new read size: ${readSize}`)
+ offset = result.RealOffset + result.bytesRead
+ setCurrentOffset(offset)
+ fetched = transformLogs(result.data).concat(fetched)
+ setLogs(fetched)
+ if (offset >= result.blobSize) {
+ setMessage(`All log for ${fromatDateForRequest(selectedDate)} fetched`)
+ setAllLogsFetched(true)
+ break
+ }
+ }
+
+ if (fetched.length === 0) {
+ if (logs.length !== 0) {
+ setMessage(`No more T.Rex logs for ${fromatDateForRequest(selectedDate)}`)
+ return
+ }
+ setTotal(0)
+ const msg = `Cannot fetch logs for ${fromatDateForRequest(selectedDate)}`
+ setMessage(msg)
+ console.log(`${msg}, Cloud error:\n${fetched.error}`);
+ return
+ }
+ setCurrentOffset(offset)
+ return fetched
+ }
+
+ useEffect(() => {
+ (async () => {
+ try {
+ if (!vin || !token) return;
+ const desiredSize = getDesiredSize()
+ console.log(`desired size: ${desiredSize}, actual size: ${logs.length}`)
+ if (desiredSize < logs.length || allLogsFetched) {
+ console.log(`enough logs in cache`)
+ setTotal(getFilteredLogs(logs).length)
+ return
+ }
+ let fetched = await fetchAllLogs()
+ if (!fetched || fetched.length === 0) {
+ return
+ }
+ setTotal(getFilteredLogs(fetched).length);
+ } catch (e) {
+ setMessage(e.message);
+ logger.warn(e.stack);
+ }
+ })();
+ return () => controller?.abort()
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [vin, token, pageIndex, pageSize, selectedDate, currectLogLevels]);
+
+ const handleChangePageSize = (event) => {
+ setPageSize(parseInt(event.target.value, 10));
+ setTotal(getFilteredLogs(logs).length)
+ setPageIndex(0);
+ };
+
+ const handleNewDate = (newValue) => {
+ setPageIndex(0);
+ setCurrentOffset(0)
+ setLogs([])
+ setAllLogsFetched(false)
+ setBlobSize(0)
+ setSelectedDate(newValue)
+ };
+
+ const handleNewFilter = (event) => {
+ setPageIndex(0)
+ console.log(event)
+ setCurrentLogLevels({
+ ...currectLogLevels,
+ [event.target.defaultValue]: event.target.checked,
+ });
+ };
+ return (
+
+
+
+
+
+ Log levels
+
+ {logLevelCheckBoxes.map((box) => (
+ }
+ label={box.label}
+ labelPlacement="bottom"
+ onChange={handleNewFilter}
+ />
+ ))}
+
+
+
+
+
+
+
+
+
+
+ {
+ blobSize === 0 ? `No logs for ${fromatDateForRequest(selectedDate)}` :
+ `Read ${getReadPercentage()}% of logs`
+ }
+ {
+
+ }
+
+
+
+
+
+
+
+ {tableColumns.map((column) => (
+ {column.label}
+ ))}
+
+
+
+ {getFilteredLogs(logs).slice(-getDesiredSize(), (pageIndex === 0 ? undefined : -(pageSize * pageIndex))).map((log, i) => (
+
+ {log.level}
+ {log.trex_timestamp}
+ {log.cloud_timestamp}
+ {log.line_number}
+ {log.filename}
+ {log.msg}
+
+ ))}
+
+
+
+ setPageIndex(newValue)}
+ onRowsPerPageChange={handleChangePageSize}
+ />
+
+
+
+
+ );
+};
+
+export default TRexLogsTable;
diff --git a/src/components/DigitalTwin/index.js b/src/components/DigitalTwin/index.js
index d581c9b..24c9ee6 100644
--- a/src/components/DigitalTwin/index.js
+++ b/src/components/DigitalTwin/index.js
@@ -13,6 +13,14 @@ const openCloseState = (value) => (value ? "open" : "closed");
const mapOpenCloseState = (value) =>
keyValueTemplate(value[0], openCloseState(value[1]));
+const windowState = (value) => {
+ if (value[1] === 0 || value[1] > 100) {
+ return keyValueTemplate(value[0], `closed (${value[1]})`);
+ } else {
+ return keyValueTemplate(value[0], `${value[1]}% open`);
+ }
+}
+
const DigitalTwin = (props) => {
const classes = useStyles();
const { battery, doors, location, trex_version, ip, updated, windows, misc_windows, sunroof, dbc_version, door_locks } = props;
@@ -45,12 +53,7 @@ const DigitalTwin = (props) => {
Windows
{Object.entries(windows).map((value) => {
- if (value[1] === 0) {
- return keyValueTemplate(value[0], "closed");
- } else {
- const percentOpen = Math.min(value[1], 100);
- return keyValueTemplate(value[0], `${percentOpen}% open`);
- }
+ return windowState(value);
})}
)}
@@ -58,11 +61,7 @@ const DigitalTwin = (props) => {
Misc Windows
{Object.entries(misc_windows).map((value) => {
- if (value[1] === 0 || value[1] > 100) {
- return keyValueTemplate(value[0], `closed ${value[1]}%`);
- } else {
- return keyValueTemplate(value[0], `${value[1]}% open`);
- }
+ return windowState(value);
})}
)}
@@ -70,12 +69,7 @@ const DigitalTwin = (props) => {
Sunroof
{Object.entries(sunroof).map((value) => {
- if (value[1] === 0) {
- return keyValueTemplate(value[0], "closed");
- } else {
- const percentOpen = Math.min(value[1], 100);
- return keyValueTemplate(value[0], `${percentOpen}% open`);
- }
+ return windowState(value);
})}
)}
diff --git a/src/components/useStyles.jsx b/src/components/useStyles.jsx
index 3b3a752..1931093 100644
--- a/src/components/useStyles.jsx
+++ b/src/components/useStyles.jsx
@@ -285,6 +285,10 @@ const useStyles = makeStyles((theme) => ({
color: "blue",
textDecoration: "underline",
},
+ tableHeader: {
+ textDecorationStyle: "solid",
+ fontWeight:500,
+ }
}));
export default useStyles;
diff --git a/src/services/__mocks__/vehiclesAPI.js b/src/services/__mocks__/vehiclesAPI.js
index d18a88b..168b539 100644
--- a/src/services/__mocks__/vehiclesAPI.js
+++ b/src/services/__mocks__/vehiclesAPI.js
@@ -58,6 +58,38 @@ const signals = {data:[
},
]};
+const trexLogs = {
+ RealOffset: 0,
+ blobSize: 62072819,
+ bytesRead: 29874,
+ data: [
+ {
+ "level": "info",
+ "timestamp": "2023-Feb-15 23:44:36.934746",
+ "line_number": 948,
+ "filename": "ota_service.cpp",
+ "msg": "ota::cleanup() Deleting directory \"/fisker/ota/1534\"",
+ "received_timestamp": "2023-Feb-15 23:44:37.203102197"
+ },
+ {
+ "level": "debug",
+ "timestamp": "2023-Feb-15 23:44:36.970715",
+ "line_number": 992,
+ "filename": "ota_service.cpp",
+ "msg": "ota::end() manifest process aborted with error state",
+ "received_timestamp": "2023-Feb-15 23:44:37.203197597"
+ },
+ {
+ "level": "debug",
+ "timestamp": "2023-Feb-15 23:44:36.971745",
+ "line_number": 91,
+ "filename": "vom_impl.cpp",
+ "msg": "vom::send_ota_result_fb() Sending 0x4F9->tbox_ota_res_fb (==OTA_error)",
+ "received_timestamp": "2023-Feb-15 23:44:37.203249997"
+ }
+ ]
+}
+
const vehiclesAPI = {
addVehicle: async (vehicle) => {
data.push(vehicle);
@@ -117,6 +149,9 @@ const vehiclesAPI = {
getCANSignals: async (vin, vehicle) => {
return signals;
},
+ getTRexLogs: async (vin, date, offset, count, direction, token) => {
+ return trexLogs;
+ },
getVersionLog: async (vin) => ({
"data": [
{
diff --git a/src/services/vehiclesAPI.js b/src/services/vehiclesAPI.js
index 9d8d0db..8d38faa 100644
--- a/src/services/vehiclesAPI.js
+++ b/src/services/vehiclesAPI.js
@@ -173,6 +173,18 @@ const vehiclesAPI = {
.then(fetchRespHandler)
.catch(errorHandler),
+ getTRexLogs: async (vin, date, offset, count, direction, token, controller) =>
+ fetch(`${API_ENDPOINT}/vehicle/${vin}/trex-logs?date=${date}&offset=${offset}&count=${count}&direction=${direction}`, {
+ method: "GET",
+ headers: Object.assign(
+ { "Content-Type": "application/json" },
+ getAuthHeaderOptions(token)
+ ),
+ signal: controller.signal
+ })
+ .then(fetchRespHandler)
+ .catch(errorHandler),
+
getVersionLog: async ({vin, ...search}, token) => {
const u = addQueryParams(`${API_ENDPOINT}/vehicle/${vin}/version/logs`, search);
return fetch(u, {
diff --git a/src/utils/locations.js b/src/utils/locations.js
index a308375..f4fdaba 100644
--- a/src/utils/locations.js
+++ b/src/utils/locations.js
@@ -1,3 +1,5 @@
+const invalidLocation = 99999;
+
export const ValidateLocationData = (location) => {
if (Math.abs(location.latitude) > 90 || Math.abs(location.longitude) > 180) {
return false;
@@ -9,6 +11,7 @@ export const ValidateLocationData = (location) => {
}
export const ValidateLocationByParam = (parameter, value) => {
+ if (invalidLocation === value) return false;
switch (parameter) {
case "latitude":
return Math.abs(value) <= 90;