Read thumbnails from rlog files when video isn't working (#28)

* Add rlog thumbnails to cabana

* Fixing more linting errors
main
Chris Vickery 2019-10-09 11:32:17 -07:00 committed by GitHub
parent 1be5544df3
commit 5c83305260
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 667 additions and 461 deletions

View File

@ -5,7 +5,7 @@
"homepage": "https://community.comma.ai/cabana",
"dependencies": {
"@commaai/comma-api": "1.1.6",
"@commaai/log_reader": "^0.3.1",
"@commaai/log_reader": "^0.5.3",
"@commaai/my-comma-auth": "^1.1.0",
"@commaai/pandajs": "^0.3.4",
"@craco/craco": "^5.5.0",
@ -60,6 +60,7 @@
"socket.io-client": "^2.0.3",
"stream-selector": "^0.1.1",
"streamsaver": "^1.0.1",
"thyming": "^0.1.1",
"vega": "^5.3.4",
"vega-lite": "^3.0.0",
"vega-tooltip": "^0.4.0"
@ -96,11 +97,26 @@
"deploy": "npm run build && gh-pages -d build"
},
"lint-staged": {
"*.{js,jsx}": ["eslint --fix", "git add"],
"*.json": ["prettier --parser json --write", "git add"],
"*.{graphql,gql}": ["prettier --parser graphql --write", "git add"],
"*.{md,markdown}": ["prettier --parser markdown --write", "git add"],
"*.scss": ["prettier --parser postcss --write", "git add"]
"*.{js,jsx}": [
"eslint --fix",
"git add"
],
"*.json": [
"prettier --parser json --write",
"git add"
],
"*.{graphql,gql}": [
"prettier --parser graphql --write",
"git add"
],
"*.{md,markdown}": [
"prettier --parser markdown --write",
"git add"
],
"*.scss": [
"prettier --parser postcss --write",
"git add"
]
},
"jest": {
"moduleNameMapper": {
@ -111,7 +127,11 @@
}
},
"browserslist": {
"production": [">0.2%", "not dead", "not op_mini all"],
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",

View File

@ -40,21 +40,11 @@ const MessageParser = require('./workers/message-parser.worker.js');
const CanStreamerWorker = require('./workers/CanStreamerWorker.worker.js');
export default class CanExplorer extends Component {
static propTypes = {
dongleId: PropTypes.string,
name: PropTypes.string,
dbc: PropTypes.instanceOf(DBC),
dbcFilename: PropTypes.string,
githubAuthToken: PropTypes.string,
autoplay: PropTypes.bool,
max: PropTypes.number,
url: PropTypes.string
};
constructor(props) {
super(props);
this.state = {
messages: {},
thumbnails: [],
selectedMessages: [],
route: null,
canFrameOffset: 0,
@ -317,6 +307,45 @@ export default class CanExplorer extends Component {
});
}
mergeThumbnails(newThumbnails) {
const { thumbnails } = this.state;
if (!newThumbnails || !newThumbnails.length) {
return thumbnails;
}
if (!thumbnails.length) {
return newThumbnails;
}
let oldIndex = 0;
let newIndex = 0;
// is old immediately after new?
if (newThumbnails[0].monoTime > thumbnails[thumbnails.length - 1]) {
return thumbnails.concat(newThumbnails);
}
// is new immediately after old?
if (newThumbnails[newThumbnails.length - 1] < thumbnails[0]) {
return newThumbnails.concat(thumbnails);
}
let result = [];
while (oldIndex < thumbnails.length && newIndex < newThumbnails.length) {
if (thumbnails[oldIndex].monoTime < newThumbnails[newIndex].monoTime) {
result.push(thumbnails[oldIndex]);
oldIndex += 1;
} else {
result.push(newThumbnails[newIndex]);
newIndex += 1;
}
}
if (oldIndex < thumbnails.length) {
result = result.concat(thumbnails.slice(oldIndex));
} else if (newIndex < newThumbnails.length) {
result = result.concat(newThumbnails.slice(newIndex));
}
return result;
}
addAndRehydrateMessages(newMessages, options) {
// Adds new message entries to messages state
// and "rehydrates" ES6 classes (message frame)
@ -442,14 +471,13 @@ export default class CanExplorer extends Component {
dbcFilename,
route,
firstCanTime,
canFrameOffset,
maxByteStateChangeCount
canFrameOffset
} = this.state;
let { maxByteStateChangeCount } = this.state;
if (!prevMsgEntries) {
// we have previous messages loaded
const { messages } = this.state;
const canStartTime = firstCanTime - canFrameOffset;
prevMsgEntries = {};
Object.keys(messages).forEach((key) => {
const { entries } = messages[key];
@ -485,7 +513,8 @@ export default class CanExplorer extends Component {
return;
}
let { newMessages, maxByteStateChangeCount, isFinished } = e.data;
maxByteStateChangeCount = e.data.maxByteStateChangeCount;
const { newMessages, newThumbnails, isFinished } = e.data;
if (maxByteStateChangeCount > this.state.maxByteStateChangeCount) {
this.setState({ maxByteStateChangeCount });
} else {
@ -497,12 +526,14 @@ export default class CanExplorer extends Component {
maxByteStateChangeCount
);
const prevMsgEntries = {};
for (const key in newMessages) {
Object.keys(newMessages).forEach((key) => {
prevMsgEntries[key] = newMessages[key].entries[newMessages[key].entries.length - 1];
}
});
const thumbnails = this.mergeThumbnails(newThumbnails);
if (!isFinished) {
this.setState({ messages });
this.setState({ messages, thumbnails });
} else {
const loadingParts = this.state.loadingParts.filter((p) => p !== part);
const loadedParts = [part, ...this.state.loadedParts];
@ -510,6 +541,7 @@ export default class CanExplorer extends Component {
this.setState(
{
messages,
thumbnails,
partsLoaded: this.state.partsLoaded + 1,
loadingParts,
loadedParts
@ -943,6 +975,26 @@ export default class CanExplorer extends Component {
}
render() {
const {
route,
messages,
selectedMessages,
currentParts,
dbcFilename,
dbcLastSaved,
seekTime,
seekIndex,
shareUrl,
maxByteStateChangeCount,
live,
thumbnails,
selectedMessage,
canFrameOffset,
firstCanTime,
currentPart,
partsLoaded
} = this.state;
return (
<div
id="cabana"
@ -973,52 +1025,53 @@ export default class CanExplorer extends Component {
</div>
<div className="cabana-window">
<Meta
url={this.state.route ? this.state.route.url : null}
messages={this.state.messages}
selectedMessages={this.state.selectedMessages}
url={this.state.route ? route.url : null}
messages={messages}
selectedMessages={selectedMessages}
updateSelectedMessages={this.updateSelectedMessages}
showEditMessageModal={this.showEditMessageModal}
currentParts={this.state.currentParts}
currentParts={currentParts}
onMessageSelected={this.onMessageSelected}
onMessageUnselected={this.onMessageUnselected}
showLoadDbc={this.showLoadDbc}
showSaveDbc={this.showSaveDbc}
dbcFilename={this.state.dbcFilename}
dbcLastSaved={this.state.dbcLastSaved}
dbcFilename={dbcFilename}
dbcLastSaved={dbcLastSaved}
dongleId={this.props.dongleId}
name={this.props.name}
route={this.state.route}
seekTime={this.state.seekTime}
seekIndex={this.state.seekIndex}
shareUrl={this.state.shareUrl}
maxByteStateChangeCount={this.state.maxByteStateChangeCount}
route={route}
seekTime={seekTime}
seekIndex={seekIndex}
shareUrl={shareUrl}
maxByteStateChangeCount={maxByteStateChangeCount}
isDemo={this.props.isDemo}
live={this.state.live}
live={live}
saveLog={debounce(this.downloadLogAsCSV, 500)}
/>
{this.state.route || this.state.live ? (
{route || live ? (
<Explorer
url={this.state.route ? this.state.route.url : null}
live={this.state.live}
messages={this.state.messages}
selectedMessage={this.state.selectedMessage}
url={route ? route.url : null}
live={live}
messages={messages}
thumbnails={thumbnails}
selectedMessage={selectedMessage}
onConfirmedSignalChange={this.onConfirmedSignalChange}
onSeek={this.onSeek}
onUserSeek={this.onUserSeek}
canFrameOffset={this.state.canFrameOffset}
firstCanTime={this.state.firstCanTime}
seekTime={this.state.seekTime}
seekIndex={this.state.seekIndex}
currentParts={this.state.currentParts}
selectedPart={this.state.currentPart}
partsLoaded={this.state.partsLoaded}
canFrameOffset={canFrameOffset}
firstCanTime={firstCanTime}
seekTime={seekTime}
seekIndex={seekIndex}
currentParts={currentParts}
selectedPart={currentPart}
partsLoaded={partsLoaded}
autoplay={this.props.autoplay}
showEditMessageModal={this.showEditMessageModal}
onPartChange={this.onPartChange}
routeStartTime={
this.state.route ? this.state.route.start_time : Moment()
route ? route.start_time : Moment()
}
partsCount={this.state.route ? this.state.route.proclog : 0}
partsCount={route ? route.proclog : 0}
/>
) : null}
</div>
@ -1063,3 +1116,14 @@ export default class CanExplorer extends Component {
);
}
}
CanExplorer.propTypes = {
dongleId: PropTypes.string,
name: PropTypes.string,
dbc: PropTypes.instanceOf(DBC),
dbcFilename: PropTypes.string,
githubAuthToken: PropTypes.string,
autoplay: PropTypes.bool,
max: PropTypes.number,
url: PropTypes.string
};

View File

@ -0,0 +1,61 @@
/* eslint-env jest */
import {
signedShortToByteArray,
shortToByteArray,
longToByteArray,
signedLongToByteArray,
getThermalFlags,
// getHealthFlags,
// getFlags,
// getUbloxGnss,
// getEgoData,
// getCarStateControls,
// getWheelSpeeds,
// getThermalFreeSpace,
// getThermalData,
// getThermalCPU,
// getHealth
} from '../../workers/rlog-utils';
describe('byte array methods', () => {
test('signedShortToByteArray', () => {
expect(signedShortToByteArray(123)).toMatchObject([0, 123]);
expect(signedShortToByteArray(-123)).toMatchObject([255, 133]);
});
test('shortToByteArray', () => {
expect(shortToByteArray(123)).toMatchObject([0, 123]);
expect(shortToByteArray(-123)).toMatchObject([255, 133]);
});
test('longToByteArray', () => {
expect(longToByteArray(123)).toMatchObject([0, 0, 0, 123]);
expect(longToByteArray(-123)).toMatchObject([255, 255, 255, 133]);
});
test('signedLongToByteArray', () => {
expect(signedLongToByteArray(123)).toMatchObject([0, 0, 0, 123]);
expect(signedLongToByteArray(-123)).toMatchObject([255, 255, 255, 133]);
});
});
describe('flags', () => {
test('getThermalFlags', () => {
expect(getThermalFlags({
UsbOnline: false,
Started: false
})).toBe(0x00);
expect(getThermalFlags({
UsbOnline: true,
Started: false
})).toBe(0x01);
expect(getThermalFlags({
UsbOnline: false,
Started: true
})).toBe(0x02);
expect(getThermalFlags({
UsbOnline: true,
Started: true
})).toBe(0x03);
});
});

View File

@ -460,6 +460,8 @@ export default class Explorer extends Component {
? 'is-expanded'
: null;
const { thumbnails, messages } = this.props;
let graphSegment = this.state.segment;
if (!graphSegment.length && this.props.currentParts) {
graphSegment = [
@ -471,7 +473,7 @@ export default class Explorer extends Component {
return (
<div className="cabana-explorer">
<div className={cx('cabana-explorer-signals', signalsExpandedClass)}>
{this.props.messages[this.props.selectedMessage]
{messages[this.props.selectedMessage]
? this.renderExplorerSignals()
: this.renderSelectMessagePrompt()}
</div>
@ -485,7 +487,7 @@ export default class Explorer extends Component {
<div className="cabana-explorer-visuals-header g-row" />
<br />
<RouteVideoSync
message={this.props.messages[this.props.selectedMessage]}
message={messages[this.props.selectedMessage]}
segment={this.state.segment}
seekIndex={this.props.seekIndex}
userSeekIndex={this.state.userSeekIndex}
@ -500,6 +502,7 @@ export default class Explorer extends Component {
onPause={this.onPause}
userSeekTime={this.state.userSeekTime}
playSpeed={this.state.playSpeed}
thumbnails={thumbnails}
/>
</div>
) : null}
@ -515,7 +518,7 @@ export default class Explorer extends Component {
) : null}
<CanGraphList
plottedSignals={this.state.plottedSignals}
messages={this.props.messages}
messages={messages}
onGraphTimeClick={this.onGraphTimeClick}
seekTime={this.props.seekTime}
onSegmentChanged={this.onSegmentChanged}

View File

@ -44,21 +44,6 @@ const Styles = StyleSheet.create({
});
export default class RouteVideoSync extends Component {
static propTypes = {
userSeekIndex: PropTypes.number.isRequired,
segment: PropTypes.array.isRequired,
message: PropTypes.object,
canFrameOffset: PropTypes.number.isRequired,
url: PropTypes.string.isRequired,
playing: PropTypes.bool.isRequired,
onPlaySeek: PropTypes.func.isRequired,
onUserSeek: PropTypes.func.isRequired,
onPlay: PropTypes.func.isRequired,
onPause: PropTypes.func.isRequired,
userSeekTime: PropTypes.number.isRequired,
playSpeed: PropTypes.number.isRequired
};
constructor(props) {
super(props);
this.state = {
@ -73,49 +58,71 @@ export default class RouteVideoSync extends Component {
this.segmentProgress = this.segmentProgress.bind(this);
this.onVideoElementAvailable = this.onVideoElementAvailable.bind(this);
this.onUserSeek = this.onUserSeek.bind(this);
this.onPlaySeek = this.onPlaySeek.bind(this);
this.onHlsRestart = this.onHlsRestart.bind(this);
this.ratioTime = this.ratioTime.bind(this);
}
componentWillReceiveProps(nextProps) {
componentDidUpdate(nextProps) {
const {
userSeekIndex,
message,
canFrameOffset,
userSeekTime
} = this.props;
const { videoElement } = this.state;
if (
this.props.userSeekIndex !== nextProps.userSeekIndex
|| this.props.canFrameOffset !== nextProps.canFrameOffset
|| (this.props.message
userSeekIndex !== nextProps.userSeekIndex
|| canFrameOffset !== nextProps.canFrameOffset
|| (message
&& nextProps.message
&& this.props.message.entries.length !== nextProps.message.entries.length)
&& message.entries.length !== nextProps.message.entries.length)
) {
this.setState({ shouldRestartHls: true });
}
if (
nextProps.userSeekTime
&& this.props.userSeekTime !== nextProps.userSeekTime
&& userSeekTime !== nextProps.userSeekTime
) {
if (this.state.videoElement) {
this.state.videoElement.currentTime = nextProps.userSeekTime;
if (videoElement) {
videoElement.currentTime = nextProps.userSeekTime;
}
}
}
nearestFrameUrl() {
const { url } = this.props;
const sec = Math.round(this.props.userSeekTime);
if (isNaN(sec)) {
debugger;
}
return RouteApi(url).getJpegUrl(sec);
onVideoElementAvailable(videoElement) {
this.setState({ videoElement });
}
loadingOverlay() {
return (
<div className={css(Styles.loadingOverlay)}>
<img
className={css(Styles.loadingSpinner)}
src={`${process.env.PUBLIC_URL}/img/loading.svg`}
alt="Loading video"
/>
</div>
);
onUserSeek(ratio) {
/* ratio in [0,1] */
const { videoElement } = this.state;
const { onUserSeek } = this.props;
const seekTime = this.ratioTime(ratio);
const funcSeekToRatio = () => onUserSeek(seekTime);
if (Number.isNaN(videoElement.duration)) {
this.setState({ shouldRestartHls: true }, funcSeekToRatio);
return;
}
videoElement.currentTime = seekTime;
if (ratio === 0) {
this.setState({ shouldRestartHls: true }, funcSeekToRatio);
} else {
funcSeekToRatio();
}
}
onHlsRestart() {
this.setState({ shouldRestartHls: false });
}
onPlaySeek(offset) {
const { onPlaySeek } = this.props;
this.seekTime = offset;
onPlaySeek(offset);
}
onLoadStart() {
@ -132,6 +139,18 @@ export default class RouteVideoSync extends Component {
});
}
loadingOverlay() {
return (
<div className={css(Styles.loadingOverlay)}>
<img
className={css(Styles.loadingSpinner)}
src={`${process.env.PUBLIC_URL}/img/loading.svg`}
alt="Loading video"
/>
</div>
);
}
videoLength() {
if (this.props.segment.length) {
return this.props.segment[1] - this.props.segment[0];
@ -168,73 +187,73 @@ export default class RouteVideoSync extends Component {
return ratio * this.videoLength() + this.startTime();
}
onVideoElementAvailable(videoElement) {
this.setState({ videoElement });
}
onUserSeek(ratio) {
/* ratio in [0,1] */
const { videoElement } = this.state;
if (isNaN(videoElement.duration)) {
this.setState({ shouldRestartHls: true }, funcSeekToRatio);
return;
nearestFrameUrl() {
const { thumbnails } = this.props;
if (!this.seekTime) {
return '';
}
const seekTime = this.ratioTime(ratio);
videoElement.currentTime = seekTime;
const funcSeekToRatio = () => this.props.onUserSeek(seekTime);
if (ratio === 0) {
this.setState({ shouldRestartHls: true }, funcSeekToRatio);
} else {
funcSeekToRatio();
for (let i = 0, l = thumbnails.length; i < l; ++i) {
if (Math.abs(thumbnails[i].monoTime - this.seekTime) < 5) {
const data = btoa(String.fromCharCode(...thumbnails[i].data));
return `data:image/jpeg;base64,${data}`;
}
}
}
onHlsRestart() {
this.setState({ shouldRestartHls: false });
return '';
}
render() {
const {
isLoading,
shouldRestartHls,
shouldShowJpeg
} = this.state;
const {
userSeekTime,
url,
playSpeed,
playing,
onVideoClick,
segmentIndices
} = this.props;
return (
<div className="cabana-explorer-visuals-camera">
{this.state.isLoading ? this.loadingOverlay() : null}
{this.state.shouldShowJpeg ? (
{isLoading ? this.loadingOverlay() : null}
{shouldShowJpeg ? (
<img
src={this.nearestFrameUrl()}
className={css(Styles.img)}
alt={`Camera preview at t = ${Math.round(this.props.userSeekTime)}`}
alt={`Camera preview at t = ${Math.round(userSeekTime)}`}
/>
) : null}
<HLS
className={css(Styles.hls)}
source={VideoApi(
this.props.url,
url,
process.env.REACT_APP_VIDEO_CDN
).getRearCameraStreamIndexUrl()}
startTime={this.startTime()}
videoLength={this.videoLength()}
playbackSpeed={this.props.playSpeed}
playbackSpeed={playSpeed}
onVideoElementAvailable={this.onVideoElementAvailable}
playing={this.props.playing}
onClick={this.props.onVideoClick}
playing={playing}
onClick={onVideoClick}
onLoadStart={this.onLoadStart}
onLoadEnd={this.onLoadEnd}
onUserSeek={this.onUserSeek}
onPlaySeek={this.props.onPlaySeek}
onPlaySeek={this.onPlaySeek}
segmentProgress={this.segmentProgress}
shouldRestart={this.state.shouldRestartHls}
shouldRestart={shouldRestartHls}
onRestart={this.onHlsRestart}
/>
<RouteSeeker
className={css(Styles.seekBar)}
nearestFrameTime={this.props.userSeekTime}
nearestFrameTime={userSeekTime}
segmentProgress={this.segmentProgress}
startTime={this.startTime()}
videoLength={this.videoLength()}
segmentIndices={this.props.segmentIndices}
segmentIndices={segmentIndices}
onUserSeek={this.onUserSeek}
onPlaySeek={this.props.onPlaySeek}
onPlaySeek={this.onPlaySeek}
videoElement={this.state.videoElement}
onPlay={this.props.onPlay}
onPause={this.props.onPause}
@ -245,3 +264,21 @@ export default class RouteVideoSync extends Component {
);
}
}
RouteVideoSync.propTypes = {
userSeekIndex: PropTypes.number.isRequired,
segment: PropTypes.array.isRequired,
message: PropTypes.object,
thumbnails: PropTypes.array,
canFrameOffset: PropTypes.number.isRequired,
url: PropTypes.string.isRequired,
playing: PropTypes.bool.isRequired,
onPlaySeek: PropTypes.func.isRequired,
onUserSeek: PropTypes.func.isRequired,
onPlay: PropTypes.func.isRequired,
onPause: PropTypes.func.isRequired,
userSeekTime: PropTypes.number.isRequired,
playSpeed: PropTypes.number.isRequired,
onVideoClick: PropTypes.func,
segmentIndices: PropTypes.array,
};

View File

@ -1,5 +1,5 @@
/* eslint-env worker */
/* eslint-disable no-restricted-globals */
/* eslint-disable no-restricted-globals, no-param-reassign */
import LogStream from '@commaai/log_reader';
import { timeout } from 'thyming';
import { partial } from 'ap';
@ -8,47 +8,25 @@ import { getLogPart } from '../api/rlog';
import DbcUtils from '../utils/dbc';
import DBC from '../models/can/dbc';
import { addressForName } from '../models/can/logSignals';
import {
getFlags,
getUbloxGnss,
getEgoData,
getCarStateControls,
getWheelSpeeds,
getThermalFreeSpace,
getThermalData,
getThermalCPU,
getHealth
} from './rlog-utils';
const DEBOUNCE_DELAY = 100;
self.onmessage = handleMessage;
function handleMessage(msg) {
const options = msg.data;
if (options.action === 'terminate') {
close();
return;
}
options.dbc = new DBC(options.dbcText);
const entry = new CacheEntry(options);
}
function CacheEntry(options) {
options = options || {};
this.options = options;
const {
route, part, dbc, logUrls
} = options;
this.messages = {};
this.route = route;
this.part = part;
this.dbc = dbc;
this.logUrls = logUrls;
this.sendBatch = partial(sendBatch, this);
// load in the data!
loadData(this);
}
function sendBatch(entry) {
delete entry.batching;
const { messages } = entry;
const { messages, thumbnails } = entry;
entry.messages = {};
entry.thumbnails = [];
let { maxByteStateChangeCount } = entry.options;
const newMaxByteStateChangeCount = DbcUtils.findMaxByteStateChangeCount(
@ -67,8 +45,9 @@ function sendBatch(entry) {
self.postMessage({
newMessages: messages,
maxByteStateChangeCount,
isFinished: entry.ended
newThumbnails: thumbnails,
isFinished: entry.ended,
maxByteStateChangeCount
});
if (entry.ended) {
@ -77,6 +56,93 @@ function sendBatch(entry) {
}
}
function queueBatch(entry) {
if (!entry.batching) {
entry.batching = timeout(entry.sendBatch, DEBOUNCE_DELAY);
}
}
function getPrevMsgEntry(messages, prevMsgEntries, id) {
if (messages[id].entries.length) {
return messages[id].entries[messages[id].entries.length - 1];
}
return prevMsgEntries[id] || null;
}
function insertEventData(src, part, entry, logTime, getData) {
const id = `${src}:${part}`;
const address = addressForName(id);
if (!entry.messages[id]) {
entry.messages[id] = DbcUtils.createMessageSpec(
entry.dbc,
address,
id,
src
);
entry.messages[id].isLogEvent = true;
}
const prevMsgEntry = getPrevMsgEntry(
entry.messages,
entry.options.prevMsgEntries,
id
);
const { msgEntry, byteStateChangeCounts } = DbcUtils.parseMessage(
entry.dbc,
logTime,
address,
getData(),
entry.options.canStartTime,
prevMsgEntry
);
entry.messages[id].byteStateChangeCounts = byteStateChangeCounts.map(
(count, idx) => entry.messages[id].byteStateChangeCounts[idx] + count
);
entry.messages[id].entries.push(msgEntry);
}
function insertCanMessage(entry, logTime, msg) {
const src = msg.Src;
const address = Number(msg.Address);
const addressHexStr = address.toString(16);
const id = `${src}:${addressHexStr}`;
if (!entry.messages[id]) {
entry.messages[id] = DbcUtils.createMessageSpec(
entry.dbc,
address,
id,
src
);
entry.messages[id].isLogEvent = false;
}
const prevMsgEntry = getPrevMsgEntry(
entry.messages,
entry.options.prevMsgEntries,
id
);
const { msgEntry, byteStateChangeCounts } = DbcUtils.parseMessage(
entry.dbc,
logTime,
address,
msg.Dat,
entry.options.canStartTime,
prevMsgEntry
);
entry.messages[id].byteStateChangeCounts = byteStateChangeCounts.map(
(count, idx) => entry.messages[id].byteStateChangeCounts[idx] + count
);
entry.messages[id].entries.push(msgEntry);
// console.log(id);
}
async function loadData(entry) {
let url = null;
@ -85,9 +151,10 @@ async function loadData(entry) {
}
if (!url || url.indexOf('.7z') !== -1) {
return self.postMessage({
self.postMessage({
error: 'Invalid or missing log files'
});
return;
}
const res = await getLogPart(entry.logUrls[entry.part]);
const logReader = new LogStream(res);
@ -102,10 +169,6 @@ async function loadData(entry) {
});
});
const msgArr = [];
const startTime = Date.now();
const i = 0;
logReader((msg) => {
if (entry.ended) {
console.log('You can get msgs after end', msg);
@ -191,290 +254,49 @@ async function loadData(entry) {
monoTime,
partial(getThermalFreeSpace, msg.Thermal)
);
} else if ('Thumbnail' in msg) {
const monoTime = msg.LogMonoTime / 1000000000 - entry.options.canStartTime;
const data = new Uint8Array(msg.Thumbnail.Thumbnail);
entry.thumbnails.push({ data, monoTime });
} else {
// console.log(Object.keys(msg));
return;
}
queueBatch(entry);
});
}
function queueBatch(entry) {
if (!entry.batching) {
entry.batching = timeout(entry.sendBatch, DEBOUNCE_DELAY);
}
function CacheEntry(options) {
options = options || {};
this.options = options;
const {
route, part, dbc, logUrls
} = options;
this.messages = {};
this.thumbnails = [];
this.route = route;
this.part = part;
this.dbc = dbc;
this.logUrls = logUrls;
this.sendBatch = partial(sendBatch, this);
this.loadData = partial(loadData, this);
}
function insertEventData(src, part, entry, logTime, getData) {
const id = `${src}:${part}`;
const address = addressForName(id);
function handleMessage(msg) {
const options = msg.data;
if (!entry.messages[id]) {
entry.messages[id] = DbcUtils.createMessageSpec(
entry.dbc,
address,
id,
src
);
entry.messages[id].isLogEvent = true;
if (options.action === 'terminate') {
close();
return;
}
const prevMsgEntry = getPrevMsgEntry(
entry.messages,
entry.options.prevMsgEntries,
id
);
const { msgEntry, byteStateChangeCounts } = DbcUtils.parseMessage(
entry.dbc,
logTime,
address,
getData(),
entry.options.canStartTime,
prevMsgEntry
);
options.dbc = new DBC(options.dbcText);
entry.messages[id].byteStateChangeCounts = byteStateChangeCounts.map(
(count, idx) => entry.messages[id].byteStateChangeCounts[idx] + count
);
entry.messages[id].entries.push(msgEntry);
const entry = new CacheEntry(options);
// load in the data!
entry.loadData();
}
function getThermalFlags(state) {
let flags = 0x00;
if (state.UsbOnline) {
flags |= 0x01;
}
if (state.Started) {
flags |= 0x02;
}
return flags;
}
function getThermalFreeSpace(state) {
return longToByteArray(state.FreeSpace * 1000000000);
}
function getThermalData(state) {
return shortToByteArray(state.Mem)
.concat(shortToByteArray(state.Gpu))
.concat(shortToByteArray(state.FanSpeed))
.concat(state.BatteryPercent)
.concat(getThermalFlags(state));
}
function getThermalCPU(state) {
return shortToByteArray(state.Cpu0)
.concat(shortToByteArray(state.Cpu1))
.concat(shortToByteArray(state.Cpu2))
.concat(shortToByteArray(state.Cpu3));
}
function getHealth(state) {
return signedShortToByteArray(state.Voltage)
.concat(state.Current)
.concat(getHealthFlags(state));
}
function getHealthFlags(state) {
let flags = 0x00;
if (state.Started) {
flags |= 0x01;
}
if (state.ControlsAllowed) {
flags |= 0x02;
}
if (state.GasInterceptorDetected) {
flags |= 0x04;
}
if (state.StartedSignalDetected) {
flags |= 0x08;
}
return flags;
}
function getUbloxGnss(state) {
return signedLongToByteArray(state.RcvTow / 1000)
.concat(signedShortToByteArray(state.GpsWeek))
.concat([state.LeapSeconds])
.concat([state.NumMeas]);
}
function getEgoData(state) {
return signedShortToByteArray(state.VEgo * 1000)
.concat(signedShortToByteArray(state.AEgo * 1000))
.concat(signedShortToByteArray(state.VEgoRaw * 1000))
.concat(signedShortToByteArray(state.YawRate * 1000));
}
function getCarStateControls(state) {
return signedLongToByteArray(state.SteeringAngle * 1000)
.concat(signedShortToByteArray(state.Brake * 1000))
.concat(signedShortToByteArray(state.Gas * 1000));
}
function getWheelSpeeds(state) {
return signedShortToByteArray(state.WheelSpeeds.Fl * 100)
.concat(signedShortToByteArray(state.WheelSpeeds.Fr * 100))
.concat(signedShortToByteArray(state.WheelSpeeds.Rl * 100))
.concat(signedShortToByteArray(state.WheelSpeeds.Rr * 100));
}
function getFlags(state) {
let flags = 0x00;
const arr = [0, 0, 0];
if (state.LeftBlinker) {
flags |= 0x01;
}
if (state.RightBlinker) {
flags |= 0x02;
}
if (state.GenericToggle) {
flags |= 0x04;
}
if (state.DoorOpen) {
flags |= 0x08;
}
if (state.SeatbeltUnlatched) {
flags |= 0x10;
}
if (state.GasPressed) {
flags |= 0x20;
}
if (state.BrakeLights) {
flags |= 0x40;
}
if (state.SteeringPressed) {
flags |= 0x80;
}
arr[0] = flags;
flags = 0x00;
if (state.Standstill) {
flags |= 0x01;
}
if (state.CruiseState.Enabled) {
flags |= 0x02;
}
if (state.CruiseState.Available) {
flags |= 0x04;
}
if (state.CruiseState.Standstill) {
flags |= 0x08;
}
if (state.GearShifter) {
flags |= state.GearShifter << 4;
}
arr[1] = flags;
arr[2] = state.CruiseState.Speed;
return arr;
}
function insertCanMessage(entry, logTime, msg) {
const src = msg.Src;
const address = Number(msg.Address);
const busTime = msg.BusTime;
const addressHexStr = address.toString(16);
const id = `${src}:${addressHexStr}`;
if (!entry.messages[id]) {
entry.messages[id] = DbcUtils.createMessageSpec(
entry.dbc,
address,
id,
src
);
entry.messages[id].isLogEvent = false;
}
const prevMsgEntry = getPrevMsgEntry(
entry.messages,
entry.options.prevMsgEntries,
id
);
const { msgEntry, byteStateChangeCounts } = DbcUtils.parseMessage(
entry.dbc,
logTime,
address,
msg.Dat,
entry.options.canStartTime,
prevMsgEntry
);
entry.messages[id].byteStateChangeCounts = byteStateChangeCounts.map(
(count, idx) => entry.messages[id].byteStateChangeCounts[idx] + count
);
entry.messages[id].entries.push(msgEntry);
// console.log(id);
}
function getPrevMsgEntry(messages, prevMsgEntries, id) {
if (messages[id].entries.length) {
return messages[id].entries[messages[id].entries.length - 1];
}
return prevMsgEntries[id] || null;
}
function signedShortToByteArray(short) {
const byteArray = [0, 0];
const isNegative = short < 0;
if (isNegative) {
short += Math.pow(2, 8 * byteArray.length);
}
for (let index = byteArray.length - 1; index >= 0; --index) {
const byte = short & 0xff;
byteArray[index] = byte;
short >>= 8;
}
return byteArray;
}
function shortToByteArray(short) {
const byteArray = [0, 0];
for (let index = byteArray.length - 1; index >= 0; --index) {
const byte = short & 0xff;
byteArray[index] = byte;
short >>= 8;
}
return byteArray;
}
function longToByteArray(long) {
const byteArray = [0, 0, 0, 0];
for (let index = byteArray.length - 1; index >= 0; --index) {
const byte = long & 0xff;
byteArray[index] = byte;
long >>= 8;
}
return byteArray;
}
function signedLongToByteArray(long) {
const byteArray = [0, 0, 0, 0];
const isNegative = long < 0;
if (isNegative) {
long += Math.pow(2, 8 * byteArray.length);
}
for (let index = byteArray.length - 1; index >= 0; --index) {
const byte = long & 0xff;
byteArray[index] = byte;
long >>= 8;
}
return byteArray;
}
self.onmessage = handleMessage;

View File

@ -0,0 +1,194 @@
/* eslint-disable no-param-reassign, no-bitwise */
export function signedShortToByteArray(short) {
const byteArray = [0, 0];
const isNegative = short < 0;
if (isNegative) {
short += 2 ** (8 * byteArray.length);
}
for (let index = byteArray.length - 1; index >= 0; --index) {
const byte = short & 0xff;
byteArray[index] = byte;
short >>= 8;
}
return byteArray;
}
export function shortToByteArray(short) {
const byteArray = [0, 0];
for (let index = byteArray.length - 1; index >= 0; --index) {
const byte = short & 0xff;
byteArray[index] = byte;
short >>= 8;
}
return byteArray;
}
export function longToByteArray(long) {
const byteArray = [0, 0, 0, 0];
for (let index = byteArray.length - 1; index >= 0; --index) {
const byte = long & 0xff;
byteArray[index] = byte;
long >>= 8;
}
return byteArray;
}
export function signedLongToByteArray(long) {
const byteArray = [0, 0, 0, 0];
const isNegative = long < 0;
if (isNegative) {
long += 2 ** (8 * byteArray.length);
}
for (let index = byteArray.length - 1; index >= 0; --index) {
const byte = long & 0xff;
byteArray[index] = byte;
long >>= 8;
}
return byteArray;
}
export function getThermalFlags(state) {
let flags = 0x00;
if (state.UsbOnline) {
flags |= 0x01;
}
if (state.Started) {
flags |= 0x02;
}
return flags;
}
export function getHealthFlags(state) {
let flags = 0x00;
if (state.Started) {
flags |= 0x01;
}
if (state.ControlsAllowed) {
flags |= 0x02;
}
if (state.GasInterceptorDetected) {
flags |= 0x04;
}
if (state.StartedSignalDetected) {
flags |= 0x08;
}
return flags;
}
export function getFlags(state) {
let flags = 0x00;
const arr = [0, 0, 0];
if (state.LeftBlinker) {
flags |= 0x01;
}
if (state.RightBlinker) {
flags |= 0x02;
}
if (state.GenericToggle) {
flags |= 0x04;
}
if (state.DoorOpen) {
flags |= 0x08;
}
if (state.SeatbeltUnlatched) {
flags |= 0x10;
}
if (state.GasPressed) {
flags |= 0x20;
}
if (state.BrakeLights) {
flags |= 0x40;
}
if (state.SteeringPressed) {
flags |= 0x80;
}
arr[0] = flags;
flags = 0x00;
if (state.Standstill) {
flags |= 0x01;
}
if (state.CruiseState.Enabled) {
flags |= 0x02;
}
if (state.CruiseState.Available) {
flags |= 0x04;
}
if (state.CruiseState.Standstill) {
flags |= 0x08;
}
if (state.GearShifter) {
flags |= state.GearShifter << 4;
}
arr[1] = flags;
arr[2] = state.CruiseState.Speed;
return arr;
}
export function getUbloxGnss(state) {
return signedLongToByteArray(state.RcvTow / 1000)
.concat(signedShortToByteArray(state.GpsWeek))
.concat([state.LeapSeconds])
.concat([state.NumMeas]);
}
export function getEgoData(state) {
return signedShortToByteArray(state.VEgo * 1000)
.concat(signedShortToByteArray(state.AEgo * 1000))
.concat(signedShortToByteArray(state.VEgoRaw * 1000))
.concat(signedShortToByteArray(state.YawRate * 1000));
}
export function getCarStateControls(state) {
return signedLongToByteArray(state.SteeringAngle * 1000)
.concat(signedShortToByteArray(state.Brake * 1000))
.concat(signedShortToByteArray(state.Gas * 1000));
}
export function getWheelSpeeds(state) {
return signedShortToByteArray(state.WheelSpeeds.Fl * 100)
.concat(signedShortToByteArray(state.WheelSpeeds.Fr * 100))
.concat(signedShortToByteArray(state.WheelSpeeds.Rl * 100))
.concat(signedShortToByteArray(state.WheelSpeeds.Rr * 100));
}
export function getThermalFreeSpace(state) {
return longToByteArray(state.FreeSpace * 1000000000);
}
export function getThermalData(state) {
return shortToByteArray(state.Mem)
.concat(shortToByteArray(state.Gpu))
.concat(shortToByteArray(state.FanSpeed))
.concat(state.BatteryPercent)
.concat(getThermalFlags(state));
}
export function getThermalCPU(state) {
return shortToByteArray(state.Cpu0)
.concat(shortToByteArray(state.Cpu1))
.concat(shortToByteArray(state.Cpu2))
.concat(shortToByteArray(state.Cpu3));
}
export function getHealth(state) {
return signedShortToByteArray(state.Voltage)
.concat(state.Current)
.concat(getHealthFlags(state));
}

View File

@ -911,6 +911,13 @@
exec-sh "^0.3.2"
minimist "^1.2.0"
"@commaai/capnp-json@^0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@commaai/capnp-json/-/capnp-json-0.2.0.tgz#866bba39274b02f899803dc7968a7fe79fb02321"
integrity sha512-PXqEjvoLkb7VPaeMlFV7NspHAUMfTGhd6plQ72rilrjagf1os73fdItdlhk/q2XELx+l/Vq1xRSLJT1Os0DVqQ==
dependencies:
capnp-ts "0.2.4"
"@commaai/comma-api@1.1.6":
version "1.1.6"
resolved "https://registry.yarnpkg.com/@commaai/comma-api/-/comma-api-1.1.6.tgz#be486443a8d8843c74a811cdd406b85694b5d526"
@ -931,21 +938,21 @@
joi-browser "^13.4.0"
querystringify "^2.1.1"
"@commaai/log_reader@^0.3.1":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@commaai/log_reader/-/log_reader-0.3.1.tgz#0e2b47b621ac9b852ccf5137787e17952a34170c"
integrity sha512-WMV6BiSBOfPFOld3KKuuEHRSAPv95cpeDes4LWwy4cONYZHnnuVu91v5nYzQVbB3F6eQaCvkakmwLSzR2UQkow==
"@commaai/log_reader@^0.5.3":
version "0.5.3"
resolved "https://registry.yarnpkg.com/@commaai/log_reader/-/log_reader-0.5.3.tgz#c3dd2c6f3b911198c32bbebd34d0e9cc862e0672"
integrity sha512-/962u7s7uXPMGLz2AH+L57w+CSXCX1uZmh0OLSwGZXzyxO/dGvimaVspYe9N78Q0BYa1B5ctv986zliaAblrmg==
dependencies:
"@commaai/unbzip2-stream" "^2.0.0"
"@commaai/capnp-json" "^0.2.0"
JSONStream "^1.3.2"
ap "^0.2.0"
capnp-json "^0.1.2"
capnp-split "^0.1.1"
capnp-ts "^0.2.4"
commander "^2.15.1"
file-type "^7.6.0"
geval "^2.2.0"
stream-selector "^0.4.0"
wasm-bz2 "^0.0.2"
"@commaai/my-comma-auth@^1.1.0":
version "1.1.3"
@ -978,11 +985,6 @@
optionalDependencies:
usb "^1.3.1"
"@commaai/unbzip2-stream@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@commaai/unbzip2-stream/-/unbzip2-stream-2.0.0.tgz#93821dc5e72f04ec5eedef5ed4e6ff57412019e0"
integrity sha512-Dqzzmos7r2OsvpJ9YtID1n50GpVwB4dw7ft41XJZM2V7bJ0L68ygeK4WktaT1ZslgWz5NzqHEn0t9nWFuopixg==
"@craco/craco@^5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@craco/craco/-/craco-5.5.0.tgz#081b25522d866fbc14b80fe61517f2f10e3e4499"
@ -2830,11 +2832,6 @@ canvas@^1.6:
dependencies:
nan "^2.10.0"
capnp-json@^0.1.2:
version "0.1.3"
resolved "https://registry.yarnpkg.com/capnp-json/-/capnp-json-0.1.3.tgz#e305dd690b9fcf53e2e19278eb6a99d187ea8047"
integrity sha512-S1bRHkHECsFVjETOZKnxIeab00ozGb7wyS7HEkNhOLUo1/9oDvcURdttp7AbOPDUSIUWM9HoPKdnnH/SSHNBWA==
capnp-split@^0.1.1:
version "0.1.4"
resolved "https://registry.yarnpkg.com/capnp-split/-/capnp-split-0.1.4.tgz#c3f86890645cd203c5ce23ff1ccb4fe6642ea4a3"
@ -2842,7 +2839,7 @@ capnp-split@^0.1.1:
dependencies:
through2 "^2.0.3"
capnp-ts@^0.2.4:
capnp-ts@0.2.4, capnp-ts@^0.2.4:
version "0.2.4"
resolved "https://registry.yarnpkg.com/capnp-ts/-/capnp-ts-0.2.4.tgz#da729493311f384d65d480d9afe979ceab9f41bb"
integrity sha512-A9+Awl2WQDhg0fpEoyDpIF7RUQp27gpYBLRGV2zKA37a0IBEutdgIKDI7pO44C9AhzxvCfR6Ooj5W14D3TKaQA==
@ -13330,6 +13327,14 @@ walker@^1.0.7, walker@~1.0.5:
dependencies:
makeerror "1.0.x"
wasm-bz2@^0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/wasm-bz2/-/wasm-bz2-0.0.2.tgz#ff5c4e3ed660169ec14f00bd9f4650c43c142a13"
integrity sha512-OjWSwiIvz8yacBBqajiZTSVI9Yom1nHFjBuXJFTOwa9rkHIQFNmFaEG65kmnUlFMUNU6mxmGGKAwF8BGMesZ6g==
dependencies:
ap "^0.2.0"
weakmap-event "^2.0.7"
watchpack@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"