merge ah/cabana-webusb
parent
be6382f3f5
commit
bd2ba52a35
|
@ -113,7 +113,8 @@ module.exports = {
|
|||
/\.(js|jsx)(\?.*)?$/,
|
||||
/\.css$/,
|
||||
/\.json$/,
|
||||
/\.svg$/
|
||||
/\.svg$/,
|
||||
/\.png$/,
|
||||
],
|
||||
loader: 'url',
|
||||
query: {
|
||||
|
@ -165,6 +166,11 @@ module.exports = {
|
|||
{ loader: 'worker-loader' },
|
||||
{ loader: 'babel-loader' }
|
||||
]
|
||||
},
|
||||
{
|
||||
// static image loader
|
||||
test: /\.png$/,
|
||||
loader: 'base64-inline-loader?name=[name].[ext]'
|
||||
}
|
||||
// ** STOP ** Are you adding a new loader?
|
||||
// Remember to add the new extension(s) to the "url" loader exclusion list.
|
||||
|
|
|
@ -111,7 +111,8 @@ module.exports = {
|
|||
/\.(js|jsx)$/,
|
||||
/\.css$/,
|
||||
/\.json$/,
|
||||
/\.svg$/
|
||||
/\.svg$/,
|
||||
/\.png$/
|
||||
],
|
||||
loader: 'url',
|
||||
query: {
|
||||
|
@ -169,6 +170,11 @@ module.exports = {
|
|||
{ loader: 'worker-loader' },
|
||||
{ loader: 'babel-loader' }
|
||||
]
|
||||
},
|
||||
{
|
||||
// static image loader
|
||||
test: /\.png$/,
|
||||
loader: 'base64-inline-loader?name=[name].[ext]'
|
||||
}
|
||||
// ** STOP ** Are you adding a new loader?
|
||||
// Remember to add the new extension(s) to the "url" loader exclusion list.
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
"homepage": "https://community.comma.ai/cabana",
|
||||
"dependencies": {
|
||||
"aphrodite": "^1.2.1",
|
||||
"base64-inline-loader": "^1.1.0",
|
||||
"classnames": "^2.2.5",
|
||||
"clipboard": "^1.7.1",
|
||||
"core-js": "^2.4.1",
|
||||
|
@ -28,6 +29,8 @@
|
|||
"react-measure": "^2.0.2",
|
||||
"react-test-renderer": "^15.6.1",
|
||||
"react-vega": "^3.0.0",
|
||||
"react-visibility-sensor": "^3.10.1",
|
||||
"simple-statistics": "^4.1.0",
|
||||
"socket.io-client": "^2.0.3",
|
||||
"vega": "git+ssh://git@github.com/commaai/vega.git#HEAD",
|
||||
"vega-tooltip": "^0.4.0"
|
||||
|
@ -79,7 +82,7 @@
|
|||
"start": "python simple-cors-http-server.py & python server.py & npm run sass & node scripts/start.js",
|
||||
"build": "node scripts/build.js",
|
||||
"test": "node scripts/test.js --env=jsdom",
|
||||
"sass": "sass --watch src/index.scss:src/index.css"
|
||||
"sass": "scss src/index.scss:src/index.css; sass --watch src/index.scss:src/index.css"
|
||||
},
|
||||
"esLintConfig": {
|
||||
"rules": {
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
|
||||
import React, { Component } from 'react';
|
||||
import Moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {USE_UNLOGGER, PART_SEGMENT_LENGTH} from './config';
|
||||
import {USE_UNLOGGER, PART_SEGMENT_LENGTH, STREAMING_WINDOW} from './config';
|
||||
import * as GithubAuth from './api/github-auth';
|
||||
import cx from 'classnames';
|
||||
|
||||
|
@ -11,19 +12,23 @@ import DBC from './models/can/dbc';
|
|||
import Meta from './components/meta';
|
||||
import Explorer from './components/explorer';
|
||||
import * as Routes from './api/routes';
|
||||
import OnboardingModal from './components/Modals/OnboardingModal';
|
||||
import SaveDbcModal from './components/SaveDbcModal';
|
||||
import LoadDbcModal from './components/LoadDbcModal';
|
||||
const CanFetcher = require('./workers/can-fetcher.worker.js');
|
||||
const MessageParser = require("./workers/message-parser.worker.js");
|
||||
const CanOffsetFinder = require('./workers/can-offset-finder.worker.js');
|
||||
const CanStreamerWorker = require('./workers/CanStreamerWorker.worker.js');
|
||||
import debounce from './utils/debounce';
|
||||
import EditMessageModal from './components/EditMessageModal';
|
||||
import LoadingBar from './components/LoadingBar';
|
||||
import {persistDbc} from './api/localstorage';
|
||||
import {persistDbc, fetchPersistedDbc} from './api/localstorage';
|
||||
import OpenDbc from './api/opendbc';
|
||||
import UnloggerClient from './api/unlogger';
|
||||
import PandaReader from './api/panda-reader';
|
||||
import * as ObjectUtils from './utils/object';
|
||||
import {hash} from './utils/string';
|
||||
import DbcUtils from './utils/dbc';
|
||||
|
||||
export default class CanExplorer extends Component {
|
||||
static propTypes = {
|
||||
|
@ -42,17 +47,20 @@ export default class CanExplorer extends Component {
|
|||
this.state = {
|
||||
messages: {},
|
||||
selectedMessages: [],
|
||||
route: {},
|
||||
route: null,
|
||||
canFrameOffset: -1,
|
||||
firstCanTime: 0,
|
||||
lastBusTime: null,
|
||||
selectedMessage: null,
|
||||
currentParts: [0,0],
|
||||
showOnboarding: false,
|
||||
showLoadDbc: false,
|
||||
showSaveDbc: false,
|
||||
showEditMessageModal: false,
|
||||
editMessageModalMessage: null,
|
||||
dbc: new DBC(),
|
||||
dbcFilename: 'New_DBC',
|
||||
dbc: (props.dbc ? props.dbc : new DBC()),
|
||||
dbcText: (props.dbc ? props.dbc.text() : (new DBC()).text()),
|
||||
dbcFilename: (props.dbcFilename ? props.dbcFilename : 'New_DBC'),
|
||||
dbcLastSaved: null,
|
||||
seekTime: 0,
|
||||
seekIndex: 0,
|
||||
|
@ -60,12 +68,18 @@ export default class CanExplorer extends Component {
|
|||
isLoading: true,
|
||||
partsLoaded: 0,
|
||||
spawnWorkerHash: null,
|
||||
attemptingPandaConnection: false,
|
||||
pandaNoDeviceSelected: false,
|
||||
};
|
||||
this.openDbcClient = new OpenDbc(props.githubAuthToken);
|
||||
if(USE_UNLOGGER) {
|
||||
this.unloggerClient = new UnloggerClient();
|
||||
}
|
||||
|
||||
this.pandaReader = new PandaReader();
|
||||
|
||||
this.showOnboarding = this.showOnboarding.bind(this);
|
||||
this.hideOnboarding = this.hideOnboarding.bind(this);
|
||||
this.showLoadDbc = this.showLoadDbc.bind(this);
|
||||
this.hideLoadDbc = this.hideLoadDbc.bind(this);
|
||||
this.showSaveDbc = this.showSaveDbc.bind(this);
|
||||
|
@ -83,27 +97,33 @@ export default class CanExplorer extends Component {
|
|||
this.onMessageUnselected = this.onMessageUnselected.bind(this);
|
||||
this.initCanData = this.initCanData.bind(this);
|
||||
this.updateSelectedMessages = this.updateSelectedMessages.bind(this);
|
||||
this.handlePandaConnect = this.handlePandaConnect.bind(this);
|
||||
this.processStreamedCanMessages = this.processStreamedCanMessages.bind(this);
|
||||
this.onStreamedCanMessagesProcessed = this.onStreamedCanMessagesProcessed.bind(this);
|
||||
this.showingModal = this.showingModal.bind(this);
|
||||
this.lastMessageEntriesById = this.lastMessageEntriesById.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const {dongleId, name} = this.props;
|
||||
Routes.fetchRoutes(dongleId).then((routes) => {
|
||||
if(routes && routes[name]) {
|
||||
const route = routes[name];
|
||||
const {dongleId, name, isDemo} = this.props;
|
||||
if(this.props.max && this.props.url) {
|
||||
const {max, url} = this.props;
|
||||
const route = {fullname: name, proclog: max, url: url};
|
||||
this.setState({route, currentParts: [0, Math.min(max - 1, PART_SEGMENT_LENGTH - 1)]}, this.initCanData);
|
||||
} else if(dongleId && name) {
|
||||
Routes.fetchRoutes(dongleId).then((routes) => {
|
||||
if(routes && routes[name]) {
|
||||
const route = routes[name];
|
||||
|
||||
const newState = {route, currentParts: [0, Math.min(route.proclog - 1, PART_SEGMENT_LENGTH - 1)]};
|
||||
if(this.props.dbc !== undefined) {
|
||||
newState.dbc = this.props.dbc;
|
||||
newState.dbcFilename = this.props.dbcFilename;
|
||||
const newState = {route, currentParts: [0, Math.min(route.proclog - 1, PART_SEGMENT_LENGTH - 1)]};
|
||||
this.setState(newState, this.initCanData);
|
||||
} else {
|
||||
this.showOnboarding();
|
||||
}
|
||||
this.setState(newState, this.initCanData);
|
||||
} else if(this.props.max && this.props.url) {
|
||||
const {max, url} = this.props;
|
||||
const route = {fullname: name, proclog: max, url: url};
|
||||
this.setState({route, currentParts: [0, Math.min(max - 1, PART_SEGMENT_LENGTH - 1)]}, this.initCanData);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.showOnboarding();
|
||||
}
|
||||
}
|
||||
|
||||
initCanData() {
|
||||
|
@ -123,20 +143,27 @@ export default class CanExplorer extends Component {
|
|||
}
|
||||
|
||||
onDbcSelected(dbcFilename, dbc) {
|
||||
const {route} = this.state;
|
||||
const {route, messages} = this.state;
|
||||
this.hideLoadDbc();
|
||||
persistDbc(route.fullname,
|
||||
{dbcFilename, dbc});
|
||||
this.setState({dbc,
|
||||
dbcFilename,
|
||||
partsLoaded: 0,
|
||||
selectedMessage: null,
|
||||
messages: {}}, () => {
|
||||
const {route} = this.state;
|
||||
if(route) {
|
||||
persistDbc(route.fullname,
|
||||
{dbcFilename, dbc});
|
||||
this.setState({dbc,
|
||||
dbcFilename,
|
||||
dbcText: dbc.text(),
|
||||
partsLoaded: 0,
|
||||
selectedMessage: null,
|
||||
messages: {}}, () => {
|
||||
const {route} = this.state;
|
||||
|
||||
// Pass DBC text to webworker b/c can't pass instance of es6 class
|
||||
this.spawnWorker(this.state.currentParts);
|
||||
});
|
||||
// Pass DBC text to webworker b/c can't pass instance of es6 class
|
||||
this.spawnWorker(this.state.currentParts);
|
||||
});
|
||||
} else {
|
||||
persistDbc('live', {dbcFilename, dbc});
|
||||
|
||||
this.setState({dbc, dbcFilename, dbcText: dbc.text(), messages: {}});
|
||||
}
|
||||
}
|
||||
|
||||
onDbcSaved(dbcFilename) {
|
||||
|
@ -145,6 +172,27 @@ export default class CanExplorer extends Component {
|
|||
this.hideSaveDbc();
|
||||
}
|
||||
|
||||
addAndRehydrateMessages(newMessages, options) {
|
||||
// Adds new message entries to messages state
|
||||
// and "rehydrates" ES6 classes (message frame)
|
||||
// lost from JSON serialization in webworker data cloning.
|
||||
if(options === undefined) options = {};
|
||||
|
||||
const messages = {...this.state.messages};
|
||||
for(var key in newMessages) {
|
||||
// add message
|
||||
if ((options.replace !== true) && key in messages) {
|
||||
messages[key].entries = messages[key].entries.concat(newMessages[key].entries);
|
||||
messages[key].byteStateChangeCounts = newMessages[key].byteStateChangeCounts;
|
||||
} else {
|
||||
messages[key] = newMessages[key];
|
||||
messages[key].frame = this.state.dbc.messages.get(messages[key].address);
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
spawnWorker(parts, options) {
|
||||
// options is object of {part, prevMsgEntries, spawnWorkerHash, prepend}
|
||||
if(!this.state.isLoading) {
|
||||
|
@ -168,7 +216,7 @@ export default class CanExplorer extends Component {
|
|||
this.setState({partsLoaded: 0});
|
||||
}
|
||||
|
||||
const {dbc, dbcFilename, route, firstCanTime, canFrameOffset} = this.state;
|
||||
const {dbc, dbcFilename, route, firstCanTime, canFrameOffset, maxByteStateChangeCount} = this.state;
|
||||
var worker = new CanFetcher();
|
||||
|
||||
worker.onmessage = (e) => {
|
||||
|
@ -177,34 +225,28 @@ export default class CanExplorer extends Component {
|
|||
return;
|
||||
}
|
||||
|
||||
const {messages} = this.state;
|
||||
if(this.state.dbcFilename != dbcFilename) {
|
||||
// DBC changed while this worker was running
|
||||
// -- don't update messages and halt recursion.
|
||||
return;
|
||||
}
|
||||
|
||||
const {newMessages, maxByteStateChangeCount} = e.data;
|
||||
let {newMessages, maxByteStateChangeCount} = e.data;
|
||||
if(maxByteStateChangeCount > this.state.maxByteStateChangeCount) {
|
||||
this.setState({maxByteStateChangeCount});
|
||||
} else {
|
||||
maxByteStateChangeCount = this.state.maxByteStateChangeCount;
|
||||
}
|
||||
|
||||
for(var key in newMessages) {
|
||||
if (key in messages) {
|
||||
messages[key].entries = messages[key].entries.concat(newMessages[key].entries);
|
||||
} else {
|
||||
messages[key] = newMessages[key];
|
||||
messages[key].signals = this.state.dbc.getSignals(messages[key].address);
|
||||
messages[key].frame = this.state.dbc.messages.get(messages[key].address);
|
||||
}
|
||||
}
|
||||
|
||||
const messages = this.addAndRehydrateMessages(newMessages, maxByteStateChangeCount);
|
||||
const prevMsgEntries = {};
|
||||
for(let key in newMessages) {
|
||||
const msg = newMessages[key];
|
||||
|
||||
prevMsgEntries[key] = msg.entries[msg.entries.length - 1];
|
||||
}
|
||||
|
||||
|
||||
this.setState({messages,
|
||||
partsLoaded: this.state.partsLoaded + 1}, () => {
|
||||
if(part < maxPart) {
|
||||
|
@ -219,18 +261,28 @@ export default class CanExplorer extends Component {
|
|||
base: route.url,
|
||||
num: part,
|
||||
canStartTime: firstCanTime - canFrameOffset,
|
||||
prevMsgEntries
|
||||
prevMsgEntries,
|
||||
maxByteStateChangeCount
|
||||
});
|
||||
}
|
||||
|
||||
showingModal() {
|
||||
const {
|
||||
showOnboarding,
|
||||
showLoadDbc,
|
||||
showSaveDbc,
|
||||
showAddSignal,
|
||||
showEditMessageModal,
|
||||
} = this.state;
|
||||
return showLoadDbc || showSaveDbc || showAddSignal || showEditMessageModal;
|
||||
return showOnboarding || showLoadDbc || showSaveDbc || showAddSignal || showEditMessageModal;
|
||||
}
|
||||
|
||||
showOnboarding() {
|
||||
this.setState({ showOnboarding: true });
|
||||
}
|
||||
|
||||
hideOnboarding() {
|
||||
this.setState({ showOnboarding: false });
|
||||
}
|
||||
|
||||
showLoadDbc() {
|
||||
|
@ -249,33 +301,46 @@ export default class CanExplorer extends Component {
|
|||
this.setState({showSaveDbc: false})
|
||||
}
|
||||
|
||||
onConfirmedSignalChange(message) {
|
||||
const signals = message.signals;
|
||||
const {dbc, dbcFilename, route} = this.state;
|
||||
|
||||
dbc.setSignals(message.address, message.signals);
|
||||
persistDbc(route.fullname,
|
||||
{dbcFilename, dbc});
|
||||
|
||||
this.setState({dbc, isLoading: true});
|
||||
reparseMessages(messages) {
|
||||
this.setState({isLoading: true});
|
||||
|
||||
const {dbc} = this.state;
|
||||
var worker = new MessageParser();
|
||||
worker.onmessage = (e) => {
|
||||
const newMessage = e.data;
|
||||
newMessage.signals = dbc.getSignals(newMessage.address);
|
||||
newMessage.frame = dbc.messages.get(newMessage.address);
|
||||
let messages = e.data;
|
||||
messages = this.addAndRehydrateMessages(messages, {replace: true});
|
||||
|
||||
const messages = {};
|
||||
Object.assign(messages, this.state.messages);
|
||||
messages[message.id] = newMessage;
|
||||
this.setState({messages, isLoading: false})
|
||||
}
|
||||
|
||||
worker.postMessage({message,
|
||||
worker.postMessage({messages,
|
||||
dbcText: dbc.text(),
|
||||
canStartTime: this.state.firstCanTime});
|
||||
}
|
||||
|
||||
onConfirmedSignalChange(message, signals) {
|
||||
const {dbc, dbcFilename, route} = this.state;
|
||||
|
||||
dbc.setSignals(message.address, {...signals});
|
||||
|
||||
if(route) {
|
||||
persistDbc(route.fullname,
|
||||
{dbcFilename, dbc});
|
||||
} else {
|
||||
persistDbc('live', {dbcFilename, dbc});
|
||||
}
|
||||
|
||||
const messages = {};
|
||||
const newMessage = {...message};
|
||||
const frame = dbc.messages.get(message.address);
|
||||
newMessage.frame = frame;
|
||||
|
||||
messages[message.id] = newMessage;
|
||||
|
||||
this.setState({dbc, dbcText: dbc.text()},
|
||||
() => this.reparseMessages(messages));
|
||||
}
|
||||
|
||||
partChangeDebounced = debounce(() => {
|
||||
const {currentParts} = this.state;
|
||||
this.spawnWorker(currentParts);
|
||||
|
@ -315,7 +380,8 @@ export default class CanExplorer extends Component {
|
|||
|
||||
this.setState({showEditMessageModal: true,
|
||||
editMessageModalMessage: msgKey,
|
||||
messages: this.state.messages});
|
||||
messages: this.state.messages,
|
||||
dbcText: dbc.text()});
|
||||
}
|
||||
|
||||
hideEditMessageModal() {
|
||||
|
@ -336,7 +402,7 @@ export default class CanExplorer extends Component {
|
|||
{dbcFilename, dbc});
|
||||
|
||||
messages[editMessageModalMessage] = message;
|
||||
this.setState({messages});
|
||||
this.setState({messages, dbc, dbcText: dbc.text()});
|
||||
this.hideEditMessageModal();
|
||||
}
|
||||
|
||||
|
@ -388,8 +454,9 @@ export default class CanExplorer extends Component {
|
|||
}
|
||||
|
||||
loginWithGithub() {
|
||||
const {route} = this.state;
|
||||
return (
|
||||
<a href={GithubAuth.authorizeUrl(this.state.route.fullname || '')}
|
||||
<a href={GithubAuth.authorizeUrl(route && route.fullname ? route.fullname : '')}
|
||||
className='button button--dark button--inline'>
|
||||
<i className='fa fa-github'></i>
|
||||
<span> Log in with Github</span>
|
||||
|
@ -397,6 +464,110 @@ export default class CanExplorer extends Component {
|
|||
)
|
||||
}
|
||||
|
||||
lastMessageEntriesById(obj, [msgId, message]) {
|
||||
obj[msgId] = message.entries[message.entries.length - 1];
|
||||
return obj;
|
||||
}
|
||||
|
||||
processStreamedCanMessages(newCanMessages) {
|
||||
const {dbcText} = this.state;
|
||||
const {firstCanTime, lastBusTime, messages, maxByteStateChangeCount} = this.state;
|
||||
// map msg id to arrays
|
||||
const prevMsgEntries = Object.entries(messages).reduce(this.lastMessageEntriesById, {});
|
||||
|
||||
const byteStateChangeCountsByMessage = Object.entries(messages).reduce(
|
||||
(obj, [msgId, msg]) => {
|
||||
obj[msgId] = msg.byteStateChangeCounts
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
this.canStreamerWorker.postMessage({ newCanMessages,
|
||||
prevMsgEntries,
|
||||
firstCanTime,
|
||||
dbcText,
|
||||
lastBusTime,
|
||||
byteStateChangeCountsByMessage,
|
||||
maxByteStateChangeCount });
|
||||
}
|
||||
|
||||
firstEntryIndexInsideStreamingWindow(entries) {
|
||||
const lastEntryTime = entries[entries.length - 1].relTime;
|
||||
const windowFloor = lastEntryTime - STREAMING_WINDOW;
|
||||
|
||||
for(let i = 0; i < entries.length; i++) {
|
||||
if(entries[i].relTime > windowFloor) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
enforceStreamingMessageWindow(messages) {
|
||||
let messageIds = Object.keys(messages);
|
||||
for(let i = 0; i < messageIds.length; i++) {
|
||||
const messageId = messageIds[i];
|
||||
const message = messages[messageId];
|
||||
if(message.entries.length < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lastEntryTime = message.entries[message.entries.length - 1].relTime;
|
||||
const entrySpan = lastEntryTime - message.entries[0].relTime;
|
||||
if(entrySpan > STREAMING_WINDOW) {
|
||||
const newEntryFloor = this.firstEntryIndexInsideStreamingWindow(message.entries);
|
||||
message.entries = message.entries.slice(newEntryFloor);
|
||||
messages[messageId] = message;
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
_onStreamedCanMessagesProcessed(data) {
|
||||
let {newMessages, seekTime, lastBusTime, firstCanTime, byteStateChangeCountsByMessage, maxByteStateChangeCount} = data;
|
||||
|
||||
if(maxByteStateChangeCount < this.state.maxByteStateChangeCount) {
|
||||
maxByteStateChangeCount = this.state.maxByteStateChangeCount;
|
||||
}
|
||||
|
||||
let messages = this.addAndRehydrateMessages(newMessages);
|
||||
messages = this.enforceStreamingMessageWindow(messages);
|
||||
let {seekIndex, selectedMessages} = this.state;
|
||||
if(selectedMessages.length > 0) {
|
||||
seekIndex = messages[selectedMessages[0]].entries.length - 1;
|
||||
}
|
||||
this.setState({messages, seekTime, seekIndex, lastBusTime, firstCanTime, maxByteStateChangeCount});
|
||||
}
|
||||
|
||||
onStreamedCanMessagesProcessed(e) {
|
||||
this._onStreamedCanMessagesProcessed(e.data);
|
||||
}
|
||||
|
||||
handlePandaConnect(e) {
|
||||
this.setState({ attemptingPandaConnection: true, live: true });
|
||||
|
||||
const persistedDbc = fetchPersistedDbc('live');
|
||||
if(persistedDbc) {
|
||||
const {dbc, dbcText} = persistedDbc;
|
||||
this.setState({dbc, dbcText});
|
||||
}
|
||||
this.canStreamerWorker = new CanStreamerWorker();
|
||||
this.canStreamerWorker.onmessage = this.onStreamedCanMessagesProcessed;
|
||||
this.pandaReader.setOnMessagesReceivedCallback(this.processStreamedCanMessages);
|
||||
this.pandaReader.connect().then(() => {
|
||||
this.pandaReader.readLoop();
|
||||
this.setState({ attemptingPandaConnection: false });
|
||||
this.setState({ showOnboarding: false });
|
||||
this.setState({ showLoadDbc: true });
|
||||
}).catch((err) => {
|
||||
if (err.code === PandaReader.ERROR_NO_DEVICE_SELECTED) {
|
||||
this.setState({ attemptingPandaConnection: false });
|
||||
}
|
||||
else { console.log(err) }
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id='cabana' className={ cx({ 'is-showing-modal': this.showingModal() }) }>
|
||||
|
@ -414,7 +585,7 @@ export default class CanExplorer extends Component {
|
|||
</div>
|
||||
</div>
|
||||
<div className='cabana-window'>
|
||||
<Meta url={this.state.route.url}
|
||||
<Meta url={this.state.route ? this.state.route.url : null}
|
||||
messages={this.state.messages}
|
||||
selectedMessages={this.state.selectedMessages}
|
||||
updateSelectedMessages={this.updateSelectedMessages}
|
||||
|
@ -430,12 +601,15 @@ export default class CanExplorer extends Component {
|
|||
name={this.props.name}
|
||||
route={this.state.route}
|
||||
seekTime={this.state.seekTime}
|
||||
seekIndex={this.state.seekIndex}
|
||||
maxByteStateChangeCount={this.state.maxByteStateChangeCount}
|
||||
isDemo={this.props.isDemo}
|
||||
live={this.state.live}
|
||||
/>
|
||||
{this.state.route.url ?
|
||||
{this.state.route || this.state.live ?
|
||||
<Explorer
|
||||
url={this.state.route.url}
|
||||
url={this.state.route ? this.state.route.url : null}
|
||||
live={this.state.live}
|
||||
messages={this.state.messages}
|
||||
selectedMessage={this.state.selectedMessage}
|
||||
onConfirmedSignalChange={this.onConfirmedSignalChange}
|
||||
|
@ -451,11 +625,17 @@ export default class CanExplorer extends Component {
|
|||
showEditMessageModal={this.showEditMessageModal}
|
||||
onPartChange={this.onPartChange}
|
||||
route={this.state.route}
|
||||
partsCount={this.state.route.proclog || 0}
|
||||
partsCount={this.state.route ? this.state.route.proclog : 0}
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
|
||||
{ this.state.showOnboarding ?
|
||||
<OnboardingModal
|
||||
handlePandaConnect={ this.handlePandaConnect }
|
||||
attemptingPandaConnection= { this.state.attemptingPandaConnection }
|
||||
/> : null }
|
||||
|
||||
{this.state.showLoadDbc ?
|
||||
<LoadDbcModal
|
||||
onDbcSelected={this.onDbcSelected}
|
||||
|
@ -481,6 +661,7 @@ export default class CanExplorer extends Component {
|
|||
handleSave={this.onMessageFrameEdited}
|
||||
message={this.state.messages[this.state.editMessageModalMessage]}
|
||||
/> : null}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import Panda from '../../api/panda';
|
||||
|
||||
function arrayBufferFromHex(hex) {
|
||||
const buffer = Buffer.from(hex, 'hex');
|
||||
const arrayBuffer = new ArrayBuffer(buffer.length);
|
||||
const view = new Uint8Array(arrayBuffer);
|
||||
for(let i = 0; i < buffer.length; i++) {
|
||||
view[i] = buffer[i];
|
||||
}
|
||||
return arrayBuffer;
|
||||
}
|
||||
|
||||
test('parseCanBuffer correctly parses a message', () => {
|
||||
const panda = new Panda();
|
||||
// 16 byte buffer
|
||||
|
||||
const arrayBuffer = arrayBufferFromHex('abababababababababababababababab');
|
||||
|
||||
const messages = panda.parseCanBuffer(arrayBuffer);
|
||||
expect(messages.length).toEqual(1)
|
||||
expect(messages[0]).toEqual([1373, 43947, 'abababababababab', 10]);
|
||||
});
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
global.__JEST__ = 1
|
||||
|
||||
import DbcUtils from '../../utils/dbc';
|
||||
import DBC from '../../models/can/dbc';
|
||||
import Signal from '../../models/can/signal';
|
||||
|
||||
// want to mock pandareader and test processStreamedCanMessages
|
||||
const SAMPLE_MESSAGE = [0x10, 0, Buffer.from('abababababababab', 'hex'), 1];
|
||||
const SAMPLE_MESSAGE_ID = '1:10';
|
||||
|
||||
function expectSampleMessageFieldsPreserved(messages, frame) {
|
||||
const [address, busTime, data, source] = SAMPLE_MESSAGE;
|
||||
expect(messages[SAMPLE_MESSAGE_ID].address).toEqual(address);
|
||||
expect(messages[SAMPLE_MESSAGE_ID].id).toEqual(SAMPLE_MESSAGE_ID);
|
||||
expect(messages[SAMPLE_MESSAGE_ID].bus).toEqual(source);
|
||||
expect(messages[SAMPLE_MESSAGE_ID].frame).toEqual(frame);
|
||||
expect(messages[SAMPLE_MESSAGE_ID].byteStateChangeCounts).toEqual(Array(8).fill(0));
|
||||
}
|
||||
|
||||
function addMessages(messages, message, dbc, n) {
|
||||
const firstCanTime = 0;
|
||||
message = [...message];
|
||||
let nextMessage = () => {message[1] = message[1] + 1; return message};
|
||||
|
||||
for(let i = 0; i < n; i++) {
|
||||
DbcUtils.addCanMessage(nextMessage(), dbc, firstCanTime, messages);
|
||||
}
|
||||
}
|
||||
test('addCanMessage should add raw can message with empty dbc', () => {
|
||||
const messages = {};
|
||||
addMessages(messages, SAMPLE_MESSAGE, new DBC(), 1);
|
||||
|
||||
expect(messages[SAMPLE_MESSAGE_ID].entries.length).toEqual(1);
|
||||
expectSampleMessageFieldsPreserved(messages);
|
||||
});
|
||||
|
||||
test('addCanMessage should add multiple raw can messages with empty dbc', () => {
|
||||
const messages = {};
|
||||
addMessages(messages, SAMPLE_MESSAGE, new DBC(), 3);
|
||||
|
||||
expect(messages[SAMPLE_MESSAGE_ID].entries.length).toEqual(3);
|
||||
expectSampleMessageFieldsPreserved(messages);
|
||||
});
|
||||
|
||||
test('addCanMessage should add parsed can message with dbc containing message spec', () => {
|
||||
const messages = {};
|
||||
// create dbc with message spec and signal for sample_message
|
||||
const dbc = new DBC();
|
||||
dbc.createFrame(SAMPLE_MESSAGE[0]);
|
||||
const signal = new Signal({name: 'NEW_SIGNAL', startBit: 0, size: 8});
|
||||
dbc.addSignal(SAMPLE_MESSAGE[0], signal);
|
||||
|
||||
// add 1 sample_message
|
||||
addMessages(messages, SAMPLE_MESSAGE, dbc, 1);
|
||||
|
||||
// verify message and parsed signal added
|
||||
const sampleMessages = messages[SAMPLE_MESSAGE_ID];
|
||||
expect(sampleMessages.entries.length).toEqual(1);
|
||||
expect(sampleMessages.entries[0].signals[signal.name]).toEqual(0xab);
|
||||
expectSampleMessageFieldsPreserved(messages, dbc.messages.get(SAMPLE_MESSAGE[0]));
|
||||
});
|
||||
|
|
@ -6,7 +6,7 @@ export function fetchPersistedDbc(routeName) {
|
|||
const {dbcFilename, dbcText} = JSON.parse(maybeDbc);
|
||||
const dbc = new DBC(dbcText);
|
||||
|
||||
return {dbc, dbcFilename};
|
||||
return {dbc, dbcText, dbcFilename};
|
||||
} else return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
import Panda from './panda';
|
||||
|
||||
export default class PandaReader {
|
||||
|
||||
static ERROR_NO_DEVICE_SELECTED = 8;
|
||||
|
||||
constructor() {
|
||||
this.panda = new Panda();
|
||||
this.isReading = false;
|
||||
this.onMessagesReceived = () => {};
|
||||
this.callbackQueue = [];
|
||||
this.callbackQueueTimer = null;
|
||||
|
||||
this.readLoop = this.readLoop.bind(this);
|
||||
this.flushCallbackQueue = this.flushCallbackQueue.bind(this);
|
||||
this._flushCallbackQueue = this._flushCallbackQueue.bind(this);
|
||||
}
|
||||
|
||||
connect() {
|
||||
return this.panda.connect();
|
||||
}
|
||||
|
||||
setOnMessagesReceivedCallback(callback) {
|
||||
this.onMessagesReceived = callback;
|
||||
}
|
||||
|
||||
stopReadLoop() {
|
||||
this.isReading = false;
|
||||
window.cancelAnimationFrame(this.callbackQueueTimer);
|
||||
}
|
||||
|
||||
_flushCallbackQueue() {
|
||||
const messages = this.callbackQueue.reduce((arr, messages) => arr.concat(messages), [])
|
||||
this.onMessagesReceived(messages);
|
||||
|
||||
this.callbackQueue = [];
|
||||
}
|
||||
|
||||
flushCallbackQueue() {
|
||||
if(this.callbackQueue.length > 0) {
|
||||
this._flushCallbackQueue();
|
||||
}
|
||||
|
||||
this.callbackQueueTimer = window.requestAnimationFrame(this.flushCallbackQueue);
|
||||
}
|
||||
|
||||
readLoop() {
|
||||
if(!this.isReading) {
|
||||
this.isReading = true;
|
||||
// this.flushCallbackQueueTimer = wi
|
||||
this.callbackQueueTimer = window.requestAnimationFrame(this.flushCallbackQueue, 30);
|
||||
}
|
||||
|
||||
this.panda.canRecv().then(messages => {
|
||||
if(this.isReading && messages.canMessages.length > 0) {
|
||||
this.callbackQueue.push(messages);
|
||||
}
|
||||
this.readLoop();
|
||||
}, error => {
|
||||
if(this.isReading) {
|
||||
console.log('canRecv error', error);
|
||||
this.readLoop();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
import CloudLog from '../logging/CloudLog';
|
||||
require('core-js/fn/string/pad-end');
|
||||
|
||||
const PANDA_VENDOR_ID = 0xbbaa;
|
||||
const PANDA_PRODUCT_ID = 0xddcc;
|
||||
const PANDA_ENDPOINT_IN = 1;
|
||||
const PANDA_BUS_SPEED = 500000.0;
|
||||
|
||||
const CAN_EXTENDED = 4;
|
||||
const BUFFER_SIZE = 0x10 * 256;
|
||||
|
||||
export default class Panda {
|
||||
constructor() {
|
||||
this.device = null;
|
||||
}
|
||||
|
||||
connect() {
|
||||
// Must be called via a mouse click handler, per Chrome restrictions.
|
||||
return navigator.usb.requestDevice({ filters: [{ vendorId: PANDA_VENDOR_ID, productId: PANDA_PRODUCT_ID }] })
|
||||
.then(device => {
|
||||
this.device = device;
|
||||
return device.open();
|
||||
})
|
||||
.then(() => this.device.selectConfiguration(1))
|
||||
.then(() => this.device.claimInterface(0));
|
||||
}
|
||||
|
||||
async health() {
|
||||
const controlParams = {requestType: 'vendor',
|
||||
recipient: 'device',
|
||||
request: 0xd2,
|
||||
value: 0,
|
||||
index: 0};
|
||||
try {
|
||||
const result = await this.device.controlTransferIn(controlParams, 13);
|
||||
} catch(err) {
|
||||
CloudLog.error({event: 'Panda.health failed', 'error': err});
|
||||
}
|
||||
}
|
||||
|
||||
parseCanBuffer(buffer) {
|
||||
const messages = [];
|
||||
|
||||
for(let i = 0; i < buffer.byteLength; i+=0x10) {
|
||||
const dat = buffer.slice(i, i + 0x10);
|
||||
|
||||
const datView = Buffer.from(dat);
|
||||
const f1 = datView.readInt32LE(0), f2 = datView.readInt32LE(4);
|
||||
|
||||
let address;
|
||||
if(f2 & CAN_EXTENDED) {
|
||||
address = f1 >>> 3;
|
||||
} else {
|
||||
address = f1 >>> 21;
|
||||
}
|
||||
const busTime = (f2 >>> 16);
|
||||
const data = new Buffer(dat.slice(8, 8 + (f2 & 0xF)));
|
||||
const source = ((f2 >> 4) & 0xF) & 0xFF;
|
||||
|
||||
messages.push([address, busTime, data.toString('hex').padEnd(16, '0'), source]);
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
async canRecv() {
|
||||
let result = null, receiptTime = null;
|
||||
while(result === null) {
|
||||
try {
|
||||
result = await this.device.transferIn(1, BUFFER_SIZE);
|
||||
receiptTime = performance.now() / 1000;
|
||||
} catch(err) {
|
||||
console.warn('can_recv failed, retrying');
|
||||
}
|
||||
}
|
||||
|
||||
return {time: receiptTime, canMessages: this.parseCanBuffer(result.data.buffer)};
|
||||
}
|
||||
}
|
|
@ -31,8 +31,8 @@ export default class AddSignals extends Component {
|
|||
super(props);
|
||||
|
||||
let signals = {};
|
||||
if(props.message && props.message.signals) {
|
||||
Object.assign(signals, props.message.signals);
|
||||
if(props.message && props.message.frame && props.message.frame.signals) {
|
||||
signals = this.copySignals(props.message.frame.signals);
|
||||
}
|
||||
|
||||
this.state = {
|
||||
|
@ -56,6 +56,20 @@ export default class AddSignals extends Component {
|
|||
this.resetDragState = this.resetDragState.bind(this);
|
||||
}
|
||||
|
||||
copySignals(signals) {
|
||||
return Object.entries(signals).reduce((signalsCopy, [signalName, signal]) => {
|
||||
signalsCopy[signalName] = Object.assign(Object.create(signal), signal);
|
||||
return signalsCopy;
|
||||
}, {});
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return nextProps.message.hexData !== this.props.message.hexData
|
||||
|| nextProps.messageIndex !== this.props.messageIndex
|
||||
|| JSON.stringify(nextProps.plottedSignals) !== JSON.stringify(this.props.plottedSignals)
|
||||
|| JSON.stringify(this.state) !== JSON.stringify(nextState);
|
||||
}
|
||||
|
||||
signalColorStyle(signal) {
|
||||
const colors = signal.colors();
|
||||
|
||||
|
@ -94,13 +108,11 @@ export default class AddSignals extends Component {
|
|||
}
|
||||
|
||||
componentWillReceiveProps({message}) {
|
||||
const isNewMessage = message.address != this.props.message.address;
|
||||
|
||||
const isNewMessage = message.address !== this.props.message.address;
|
||||
if(isNewMessage) {
|
||||
const signalStyles = this.updateSignalStyles(message.signals);
|
||||
const signals = {};
|
||||
Object.assign(signals, message.signals);
|
||||
this.setState({signals}, this.updateSignalStyles);
|
||||
const signals = (message.frame ? message.frame.signals : {});
|
||||
const signalStyles = this.updateSignalStyles(signals);
|
||||
this.setState({signals: this.copySignals(signals)}, this.updateSignalStyles);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -446,12 +458,8 @@ export default class AddSignals extends Component {
|
|||
|
||||
propagateUpSignalChange() {
|
||||
const {signals} = this.state;
|
||||
const newMessage = {};
|
||||
|
||||
Object.assign(newMessage, this.props.message);
|
||||
newMessage.signals = signals;
|
||||
|
||||
this.props.onConfirmedSignalChange(newMessage);
|
||||
this.props.onConfirmedSignalChange(this.props.message, this.copySignals(signals));
|
||||
}
|
||||
|
||||
onSignalPlotChange(shouldPlot, signalName) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, {Component} from 'react';
|
||||
import Measure from 'react-measure';
|
||||
import Vega from 'react-vega';
|
||||
import * as vega from 'vega';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
|
||||
|
@ -42,6 +43,7 @@ export default class CanGraph extends Component {
|
|||
shiftX: 0,
|
||||
shiftY: 0,
|
||||
bounds: null,
|
||||
initialData: props.data,
|
||||
};
|
||||
this.onNewView = this.onNewView.bind(this);
|
||||
this.onSignalClickTime = this.onSignalClickTime.bind(this);
|
||||
|
@ -93,7 +95,7 @@ export default class CanGraph extends Component {
|
|||
segmentChanged = true;
|
||||
}
|
||||
|
||||
if(nextProps.currentTime !== this.props.currentTime) {
|
||||
if(!nextProps.live && nextProps.currentTime !== this.props.currentTime) {
|
||||
this.view.signal('videoTime', nextProps.currentTime);
|
||||
segmentChanged = true;
|
||||
}
|
||||
|
@ -104,9 +106,7 @@ export default class CanGraph extends Component {
|
|||
}
|
||||
|
||||
const dataChanged = this.dataChanged(this.props, nextProps);
|
||||
if(dataChanged) {
|
||||
this.view.run();
|
||||
}
|
||||
|
||||
return dataChanged
|
||||
|| JSON.stringify(this.state) !== JSON.stringify(nextState)
|
||||
|| this.visualChanged(this.props, nextProps);
|
||||
|
@ -114,7 +114,8 @@ export default class CanGraph extends Component {
|
|||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if(this.dataChanged(prevProps, this.props)) {
|
||||
this.view.run();
|
||||
this.view.remove('table', () => true).run();
|
||||
this.view.insert('table', this.props.data).run();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -211,7 +212,7 @@ export default class CanGraph extends Component {
|
|||
<span className='fa fa-bars'></span>
|
||||
</div>
|
||||
{this.props.plottedSignals.map(({ messageId, signalName, messageName }) => {
|
||||
const color = this.props.messages[messageId].signals[signalName].colors();
|
||||
const color = this.props.messages[messageId].frame.signals[signalName].colors();
|
||||
|
||||
return (
|
||||
<div className='cabana-explorer-visuals-plot-header'
|
||||
|
@ -243,10 +244,11 @@ export default class CanGraph extends Component {
|
|||
className='cabana-explorer-visuals-plot-container'>
|
||||
<CanPlot
|
||||
logLevel={0}
|
||||
data={{table: this.props.data}}
|
||||
data={{table: this.state.initialData}}
|
||||
onNewView={this.onNewView}
|
||||
onSignalClickTime={this.onSignalClickTime}
|
||||
onSignalSegment={this.onSignalSegment}
|
||||
renderer={'canvas'}
|
||||
/>
|
||||
</div>);
|
||||
}
|
||||
|
|
|
@ -124,7 +124,7 @@ export default class CanGraphList extends Component {
|
|||
messages={this.props.messages}
|
||||
messageId={messageId}
|
||||
messageName={msg.frame ? msg.frame.name : null}
|
||||
signalSpec={Object.assign(Object.create(msg.signals[signalName]), msg.signals[signalName])}
|
||||
signalSpec={Object.assign(Object.create(msg.frame.signals[signalName]), msg.frame.signals[signalName])}
|
||||
onSegmentChanged={this.props.onSegmentChanged}
|
||||
segment={this.props.segment}
|
||||
data={this.props.graphData[index]}
|
||||
|
@ -136,6 +136,7 @@ export default class CanGraphList extends Component {
|
|||
dragPos={isDragging ? this.state.dragPos : null}
|
||||
canReceiveGraphDrop={canReceiveGraphDrop}
|
||||
plottedSignals={plottedSignals}
|
||||
live={this.props.live}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -52,7 +52,8 @@ export default class CanLog extends Component {
|
|||
const curMessageLength = this.props.message ? this.props.message.entries.length : 0;
|
||||
const nextMessageLength = nextProps.message ? nextProps.message.entries.length : 0;
|
||||
|
||||
const shouldUpdate = nextMessageLength != curMessageLength
|
||||
const shouldUpdate = this.props.message !== nextProps.message
|
||||
|| nextMessageLength != curMessageLength
|
||||
|| nextProps.messageIndex != this.props.messageIndex
|
||||
|| nextProps.plottedSignals.length != this.props.plottedSignals.length
|
||||
|| JSON.stringify(nextProps.segmentIndices) != JSON.stringify(this.props.segmentIndices)
|
||||
|
@ -60,13 +61,11 @@ export default class CanLog extends Component {
|
|||
|| this.props.message != nextProps.message
|
||||
|| (this.props.message !== undefined
|
||||
&& nextProps.message !== undefined
|
||||
&& this.props.message.frame !== undefined
|
||||
&& nextProps.message.frame !== undefined
|
||||
&&
|
||||
(
|
||||
(this.props.message.signals
|
||||
&& nextProps.message.signals
|
||||
&& JSON.stringify(this.props.message.signals) != JSON.stringify(nextProps.message.signals))
|
||||
||
|
||||
(JSON.stringify(this.props.message.frame) != JSON.stringify(nextProps.message.frame))
|
||||
(JSON.stringify(this.props.message.frame) !== JSON.stringify(nextProps.message.frame))
|
||||
));
|
||||
|
||||
return shouldUpdate;
|
||||
|
@ -115,7 +114,7 @@ export default class CanLog extends Component {
|
|||
|
||||
toggleExpandPacketSignals(msg) {
|
||||
const msgIsExpanded = this.state.allPacketsExpanded || this.isMessageExpanded(msg);
|
||||
const msgHasSignals = Object.keys(msg.signals).length > 0;
|
||||
const msgHasSignals = Object.keys(this.props.message.frame.signals).length > 0;
|
||||
if (msgIsExpanded && msgHasSignals) {
|
||||
this.setState({expandedMessages: this.state.expandedMessages
|
||||
.filter((expMsgTime) => expMsgTime !== msg.time)})
|
||||
|
@ -132,7 +131,6 @@ export default class CanLog extends Component {
|
|||
{ Object.entries(msg.signals).map(([name, value]) => {
|
||||
return [name, value, this.isSignalPlotted(message.id, name)]
|
||||
}).map(([name, value, isPlotted]) => {
|
||||
const signalValue = msg.signals[name];
|
||||
const plottedButtonClass = isPlotted ? null : 'button--alpha';
|
||||
const plottedButtonText = isPlotted ? 'Hide Plot' : 'Show Plot';
|
||||
|
||||
|
@ -146,11 +144,11 @@ export default class CanLog extends Component {
|
|||
</div>
|
||||
<div className='signals-log-list-signal-value'>
|
||||
<span>
|
||||
(<strong>{ this.signalValuePretty(signalValue, value) }</strong> { unit })
|
||||
(<strong>{ this.signalValuePretty(signal, value) }</strong> { unit })
|
||||
</span>
|
||||
</div>
|
||||
<div className='signals-log-list-signal-action'
|
||||
onClick={ () => { this.toggleSignalPlot(this.props.message.id, name, isPlotted) } }>
|
||||
onClick={ () => { this.toggleSignalPlot(message.id, name, isPlotted) } }>
|
||||
<button className={ cx('button--tiny', plottedButtonClass) }>
|
||||
<span>{ plottedButtonText }</span>
|
||||
</button>
|
||||
|
@ -163,6 +161,7 @@ export default class CanLog extends Component {
|
|||
}
|
||||
|
||||
renderLogListItemMessage(msg, key) {
|
||||
const { message } = this.props;
|
||||
const msgIsExpanded = this.state.allPacketsExpanded || this.isMessageExpanded(msg);
|
||||
const msgHasSignals = Object.keys(msg.signals).length > 0;
|
||||
const hasSignalsClass = msgHasSignals ? 'has-signals' : null;
|
||||
|
@ -172,7 +171,7 @@ export default class CanLog extends Component {
|
|||
<div className='signals-log-list-item-header'
|
||||
onClick={ () => { this.toggleExpandPacketSignals(msg) } }>
|
||||
<div className='signals-log-list-message'>
|
||||
<strong>{(this.props.message.frame ? this.props.message.frame.name : null) || this.props.message.id}</strong>
|
||||
<strong>{(message.frame ? message.frame.name : null) || message.id}</strong>
|
||||
</div>
|
||||
<div className='signals-log-list-time'>
|
||||
<span>[{msg.relTime.toFixed(3)}]</span>
|
||||
|
|
|
@ -25,6 +25,11 @@
|
|||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.cabana-modal.cabana-modal--not-closable {
|
||||
.cabana-modal-backdrop {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -71,7 +71,8 @@ export default class GithubDbcList extends Component {
|
|||
.map((path) => {
|
||||
return (
|
||||
<div className={ cx('cabana-dbc-list-file', {'is-selected': this.state.selectedPath === path})}
|
||||
onClick={ () => { this.selectPath(path) } }>
|
||||
onClick={ () => { this.selectPath(path) } }
|
||||
key={path}>
|
||||
<span>{ path }</span>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,90 +1,96 @@
|
|||
import React, {Component} from 'react';
|
||||
import { StyleSheet, css } from 'aphrodite/no-important';
|
||||
import PropTypes from 'prop-types';
|
||||
import VisibilitySensor from 'react-visibility-sensor';
|
||||
|
||||
export default class MessageBytes extends Component {
|
||||
static propTypes = {
|
||||
seekTime: PropTypes.number.isRequired,
|
||||
message: PropTypes.object.isRequired,
|
||||
maxByteStateChangeCount: PropTypes.number.isRequired
|
||||
seekIndex: PropTypes.number,
|
||||
maxByteStateChangeCount: PropTypes.number.isRequired,
|
||||
live: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
byteColors: []
|
||||
byteColors: [],
|
||||
isVisible: true,
|
||||
lastUpdatedMillis: 0,
|
||||
};
|
||||
|
||||
this.onVisibilityChange = this.onVisibilityChange.bind(this);
|
||||
this.onCanvasRefAvailable = this.onCanvasRefAvailable.bind(this);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if(nextProps.live) {
|
||||
const nextLastEntry = nextProps.message.entries[nextProps.message.entries.length - 1];
|
||||
const curLastEntry = this.props.message.entries[this.props.message.entries.length - 1];
|
||||
|
||||
return (nextProps.hexData !== curLastEntry.hexData);
|
||||
} else {
|
||||
return nextProps.seekTime !== this.props.seekTime
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {message} = nextProps;
|
||||
|
||||
if(message && this.props.message && (message != this.props.message
|
||||
|| JSON.stringify(message.byteStateChangeCounts) != JSON.stringify(this.props.message.byteStateChangeCounts))) {
|
||||
this.updateByteColors(message);
|
||||
}
|
||||
this.updateCanvas(nextProps);
|
||||
}
|
||||
|
||||
updateByteColors(message) {
|
||||
const {maxByteStateChangeCount} = this.props;
|
||||
updateCanvas(props) {
|
||||
const {message, live, seekTime} = props;
|
||||
if(!this.canvas || message.entries.length === 0) return;
|
||||
|
||||
const byteColors = message.byteStateChangeCounts.map((count) =>
|
||||
Math.min(255, 75 + 180 * (count / maxByteStateChangeCount)) // TODO dynamic
|
||||
).map((red) =>
|
||||
'rgb(' + Math.round(red) + ',0,0)'
|
||||
);
|
||||
|
||||
this.setState({byteColors});
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.updateByteColors(this.props.message);
|
||||
}
|
||||
|
||||
renderBytes() {
|
||||
const {seekTime, message} = this.props;
|
||||
const {byteColors} = this.state;
|
||||
let mostRecentMsg = message.entries[message.entries.length - 1];
|
||||
if(!live) {
|
||||
|
||||
let mostRecentMsgIndex = message.entries.findIndex((e) =>
|
||||
e.relTime > seekTime) - 1;
|
||||
mostRecentMsgIndex = Math.max(0, mostRecentMsgIndex);
|
||||
const mostRecentMsg = message.entries[mostRecentMsgIndex];
|
||||
mostRecentMsg = message.entries.find((e) => e.relTime >= seekTime);
|
||||
|
||||
const msgSize = message.frame ? message.frame.size : 8;
|
||||
|
||||
let byteOpacities;
|
||||
if(mostRecentMsg.byteStateChangeTimes.every((time) => time == message.entries[0].relTime)) {
|
||||
byteOpacities = Array(msgSize).fill(0.1);
|
||||
} else {
|
||||
byteOpacities = mostRecentMsg.byteStateChangeTimes.map((time) =>
|
||||
Math.max(0.1, Math.min(1, 1.1 - ((seekTime - time))))
|
||||
);
|
||||
if(!mostRecentMsg) {
|
||||
mostRecentMsg = message.entries[0];
|
||||
}
|
||||
}
|
||||
|
||||
const bytes = byteOpacities.map((opacity, idx) =>
|
||||
<div key={idx} className={css(Styles.byte)}
|
||||
style={{opacity, backgroundColor: byteColors[idx]}}>{mostRecentMsg.hexData.substr(idx * 2, 2)}</div>
|
||||
);
|
||||
const ctx = this.canvas.getContext('2d');
|
||||
ctx.clearRect(0,0,180,15);
|
||||
for(let i = 0; i < message.byteStateChangeCounts.length; i++) {
|
||||
const hexData = mostRecentMsg.hexData.substr(i * 2, 2);
|
||||
ctx.fillStyle = mostRecentMsg.byteStyles[i].backgroundColor;
|
||||
|
||||
return bytes;
|
||||
ctx.fillRect(i * 20, 0, 20, 15);
|
||||
|
||||
ctx.font = '12px Courier';
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillText(hexData, i * 20 + 2, 12);
|
||||
}
|
||||
}
|
||||
|
||||
onVisibilityChange(isVisible) {
|
||||
if(isVisible !== this.state.isVisible) {
|
||||
this.setState({isVisible});
|
||||
}
|
||||
}
|
||||
|
||||
onCanvasRefAvailable(ref) {
|
||||
if(!ref) return;
|
||||
|
||||
this.canvas = ref;
|
||||
this.canvas.width = 160 * window.devicePixelRatio;
|
||||
this.canvas.height = 15 * window.devicePixelRatio;
|
||||
const ctx = this.canvas.getContext('2d');
|
||||
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {message} = this.props;
|
||||
let bytes = null;
|
||||
if(message.entries.length > 0) {
|
||||
bytes = this.renderBytes();
|
||||
}
|
||||
|
||||
return <div className={css(Styles.bytes)}>{bytes}</div>
|
||||
return (<canvas ref={this.onCanvasRefAvailable}
|
||||
className='cabana-meta-messages-list-item-bytes-canvas'></canvas>);
|
||||
}
|
||||
}
|
||||
|
||||
const Styles = StyleSheet.create({
|
||||
bytes: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
byte: {
|
||||
width: 20,
|
||||
height: 15,
|
||||
|
|
|
@ -76,6 +76,13 @@
|
|||
cursor: pointer;
|
||||
position: relative;
|
||||
transition-duration: .1s;
|
||||
&-bytes {
|
||||
width: 160px;
|
||||
&-canvas {
|
||||
width: 160px;
|
||||
height: 15px;
|
||||
}
|
||||
}
|
||||
&:not(.is-selected):hover {
|
||||
background: rgba(0,0,0,.05);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
|
||||
import Modal from '../Modals/baseModal';
|
||||
|
||||
export default class OnboardingModal extends Component {
|
||||
static propTypes = {
|
||||
handlePandaConnect: PropTypes.func,
|
||||
attemptingPandaConnection: PropTypes.bool
|
||||
};
|
||||
|
||||
static instructionalImages = {
|
||||
step2: require("../../images/webusb-enable-experimental-features.png"),
|
||||
step3: require("../../images/webusb-enable-webusb.png"),
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
webUsbEnabled: !!navigator.usb,
|
||||
viewingUsbInstructions: false,
|
||||
pandaConnected: false,
|
||||
authenticatedUser: {},
|
||||
chffrDrives: [],
|
||||
}
|
||||
|
||||
this.attemptPandaConnection = this.attemptPandaConnection.bind(this);
|
||||
this.toggleUsbInstructions = this.toggleUsbInstructions.bind(this);
|
||||
}
|
||||
|
||||
attemptPandaConnection() {
|
||||
this.props.handlePandaConnect();
|
||||
}
|
||||
|
||||
toggleUsbInstructions() {
|
||||
this.setState({ viewingUsbInstructions: !this.state.viewingUsbInstructions });
|
||||
}
|
||||
|
||||
navigateToDrivingExplorer() {
|
||||
window.location.href = 'https://community.comma.ai/explorer.php';
|
||||
}
|
||||
|
||||
renderPandaEligibility() {
|
||||
const { webUsbEnabled, pandaConnected } = this.state;
|
||||
const { attemptingPandaConnection } = this.props;
|
||||
if (!webUsbEnabled) {
|
||||
return (
|
||||
<p>
|
||||
<i className='fa fa-exclamation-triangle'></i>
|
||||
<a onClick={ this.toggleUsbInstructions }>
|
||||
<span>WebUSB is not enabled in your Chrome settings</span>
|
||||
</a>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
else if (!pandaConnected && attemptingPandaConnection) {
|
||||
return (
|
||||
<p>
|
||||
<i className='fa fa-spinner animate-spin'></i>
|
||||
<span className='animate-pulse-opacity'>Waiting for panda USB connection</span>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
renderOnboardingOptions() {
|
||||
return (
|
||||
<div className='cabana-onboarding-modes'>
|
||||
<div className='cabana-onboarding-mode'>
|
||||
<button onClick={ this.navigateToDrivingExplorer }
|
||||
className='button--primary button--kiosk'>
|
||||
<i className='fa fa-video-camera'></i>
|
||||
<strong>Load Drive From chffr</strong>
|
||||
<sup>Click <em>[cabana]</em> from a drive in your driving explorer</sup>
|
||||
</button>
|
||||
</div>
|
||||
<div className='cabana-onboarding-mode'>
|
||||
<button className='button--secondary button--kiosk'
|
||||
onClick={ this.attemptPandaConnection }
|
||||
disabled={ !this.state.webUsbEnabled || this.props.attemptingPandaConnection }>
|
||||
<i className='fa fa-bolt'></i>
|
||||
<strong>Launch Realtime Streaming</strong>
|
||||
<sup>Interactively stream car data over USB with <em>panda</em></sup>
|
||||
{ this.renderPandaEligibility() }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderUsbInstructions() {
|
||||
return (
|
||||
<div className='cabana-onboarding-instructions'>
|
||||
<button className='button--small button--inverted' onClick={ this.toggleUsbInstructions }>
|
||||
<i className='fa fa-chevron-left'></i>
|
||||
<span> Go back</span>
|
||||
</button>
|
||||
<h3>Follow these directions to enable WebUSB:</h3>
|
||||
<ol className='cabana-onboarding-instructions-list list--bubbled'>
|
||||
<li>
|
||||
<p><strong>Open your Chrome settings:</strong></p>
|
||||
<div className='inset'>
|
||||
<span>chrome://flags/#enable-experimental-web-platform-features</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Enable Expirimental Platform features:</strong></p>
|
||||
<img src={ OnboardingModal.instructionalImages.step2 } />
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Enable WebUSB:</strong></p>
|
||||
<img src={ OnboardingModal.instructionalImages.step3 } />
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>Relaunch your Chrome browser and try enabling live mode again.</strong></p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderModalContent() {
|
||||
if (this.state.viewingUsbInstructions) {
|
||||
return this.renderUsbInstructions();
|
||||
}
|
||||
else {
|
||||
return this.renderOnboardingOptions();
|
||||
}
|
||||
}
|
||||
|
||||
renderModalFooter() {
|
||||
return (
|
||||
<p>
|
||||
<span>Don{"\'"}t have a <a href='https://panda.comma.ai' target='_blank'>panda</a>? </span>
|
||||
<span><a href='https://panda.comma.ai' target='_blank'>Get one here</a> </span>
|
||||
<span>or <a href='https://community.comma.ai/cabana/?demo=1'>try the demo</a>.</span>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal
|
||||
title='Welcome to cabana'
|
||||
subtitle='Get started by viewing your chffr drives or enabling live mode'
|
||||
footer={ this.renderModalFooter() }
|
||||
disableClose={ true }
|
||||
variations={['wide', 'dark']}>
|
||||
{ this.renderModalContent() }
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,39 +1,95 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import Measure from 'react-measure';
|
||||
|
||||
export default class Modal extends Component {
|
||||
static PropTypes = {
|
||||
variations: PropTypes.array,
|
||||
disableClose: PropTypes.bool,
|
||||
handleClose: PropTypes.func,
|
||||
handleSave: PropTypes.func,
|
||||
title: PropTypes.string,
|
||||
subtitle: PropTypes.string,
|
||||
navigation: PropTypes.node,
|
||||
actions: PropTypes.node,
|
||||
footer: PropTypes.node,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
windowHeight: {},
|
||||
modalHeight: {},
|
||||
}
|
||||
|
||||
this.updateHeights = this.updateHeights.bind(this);
|
||||
}
|
||||
|
||||
updateHeights(contentRect) {
|
||||
this.setState({ windowHeight: window.innerHeight });
|
||||
this.setState({ modalHeight: contentRect.bounds.height });
|
||||
}
|
||||
|
||||
readVariationClasses() {
|
||||
if (this.props.variations) {
|
||||
const { variations } = this.props;
|
||||
let classes = '';
|
||||
variations.map((variation) => {
|
||||
classes = classes += `cabana-modal--${ variation } `;
|
||||
})
|
||||
return classes;
|
||||
}
|
||||
}
|
||||
|
||||
checkClosability() {
|
||||
return (this.props.disableClose || false);
|
||||
}
|
||||
|
||||
checkYScrollability() {
|
||||
return (this.state.modalHeight > this.state.windowHeight);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className='cabana-modal'>
|
||||
<div className='cabana-modal-container'>
|
||||
<div className='cabana-modal-close-icon'
|
||||
onClick={ this.props.handleClose }></div>
|
||||
<div className='cabana-modal-header'>
|
||||
<h1>{ this.props.title }</h1>
|
||||
<p>{ this.props.subtitle }</p>
|
||||
<div className={ cx(
|
||||
'cabana-modal',
|
||||
this.readVariationClasses(), {
|
||||
'cabana-modal--not-closable': this.checkClosability(),
|
||||
'cabana-modal--scrollable-y': this.checkYScrollability(),
|
||||
}) }>
|
||||
<Measure
|
||||
bounds
|
||||
onResize={(contentRect) => {
|
||||
this.updateHeights(contentRect);
|
||||
}}>
|
||||
{({ measureRef }) =>
|
||||
<div ref={ measureRef } className='cabana-modal-container'>
|
||||
<div className='cabana-modal-close-icon'
|
||||
onClick={ this.props.handleClose }></div>
|
||||
<div className='cabana-modal-header'>
|
||||
<h1>{ this.props.title }</h1>
|
||||
<p>{ this.props.subtitle }</p>
|
||||
</div>
|
||||
<div className='cabana-modal-navigation'>
|
||||
{ this.props.navigation }
|
||||
</div>
|
||||
<div className='cabana-modal-body'>
|
||||
<div className='cabana-modal-body-window'>
|
||||
{ this.props.children }
|
||||
</div>
|
||||
<div className='cabana-modal-body-gradient'></div>
|
||||
</div>
|
||||
<div className='cabana-modal-actions'>
|
||||
{ this.props.actions }
|
||||
</div>
|
||||
<div className='cabana-modal-footer'>
|
||||
{ this.props.footer }
|
||||
</div>
|
||||
</div>
|
||||
<div className='cabana-modal-navigation'>
|
||||
{ this.props.navigation }
|
||||
</div>
|
||||
<div className='cabana-modal-body'>
|
||||
{ this.props.children }
|
||||
</div>
|
||||
<div className='cabana-modal-actions'>
|
||||
{ this.props.actions }
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</Measure>
|
||||
<div className='cabana-modal-backdrop'
|
||||
onClick={ this.props.handleClose }>
|
||||
</div>
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
@import '../../styles/_global/all';
|
||||
|
||||
$cabana-modal-padding--wide: 76px;
|
||||
|
||||
.cabana-modal {
|
||||
&-backdrop {
|
||||
background: rgba(233,233,233,.8);
|
||||
|
@ -32,7 +34,7 @@
|
|||
position: absolute;
|
||||
top: 50%;
|
||||
width: 600px;
|
||||
transition: ease-in-out .2s;
|
||||
// transition: ease-in-out .2s;
|
||||
z-index: 101;
|
||||
}
|
||||
&-header {
|
||||
|
@ -73,11 +75,14 @@
|
|||
border-style: solid;
|
||||
border-width: 1px 0;
|
||||
padding: 34px 50px;
|
||||
position: relative;
|
||||
}
|
||||
&-actions {
|
||||
text-align: right;
|
||||
padding: 22px 50px;
|
||||
width: 100%;
|
||||
&:not(:empty) {
|
||||
padding: 22px 50px;
|
||||
}
|
||||
button {
|
||||
display: inline-block;
|
||||
&:not(:last-child) {
|
||||
|
@ -85,13 +90,56 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
&-footer {
|
||||
background: $color-grey-20;
|
||||
color: $color-grey-70;
|
||||
font-size: 14px;
|
||||
line-height: 70px;
|
||||
text-align: center;
|
||||
a {
|
||||
color: inherit;
|
||||
font-weight: 600;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Functional Variations
|
||||
&.cabana-modal--not-closable {
|
||||
.cabana-modal-backdrop {
|
||||
pointer-events: none;
|
||||
}
|
||||
.cabana-modal-close-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
&.cabana-modal--scrollable-y {
|
||||
.cabana-modal-container {
|
||||
transform: translate(-50%, 0);
|
||||
top: 5%;
|
||||
margin-bottom: 5%;
|
||||
}
|
||||
}
|
||||
// Size Variations
|
||||
&.cabana-modal--small {
|
||||
width: 480px;
|
||||
}
|
||||
&.cabana-modal--large {
|
||||
width: 800px;
|
||||
&.cabana-modal--wide {
|
||||
.cabana-modal-container {
|
||||
width: 820px;
|
||||
}
|
||||
.cabana-modal-header,
|
||||
.cabana-modal-body {
|
||||
padding-left: $cabana-modal-padding--wide;
|
||||
padding-right: $cabana-modal-padding--wide;
|
||||
}
|
||||
}
|
||||
// Color variations
|
||||
&.cabana-modal--dark {
|
||||
.cabana-modal-backdrop {
|
||||
background: rgba(0,0,0,.85);
|
||||
}
|
||||
}
|
||||
|
||||
// Overrides
|
||||
.cabana-tabs-navigation {
|
||||
padding: 0 44px;
|
||||
|
|
|
@ -43,6 +43,7 @@ export default class RouteSeeker extends Component {
|
|||
this.onClick = this.onClick.bind(this);
|
||||
this.onPlay = this.onPlay.bind(this);
|
||||
this.onPause = this.onPause.bind(this);
|
||||
this.executePlayTimer = this.executePlayTimer.bind(this);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
|
@ -73,7 +74,7 @@ export default class RouteSeeker extends Component {
|
|||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.clearInterval(this.playTimer);
|
||||
window.cancelAnimationFrame(this.playTimer);
|
||||
}
|
||||
|
||||
mouseEventXOffsetPercent(e) {
|
||||
|
@ -137,28 +138,9 @@ export default class RouteSeeker extends Component {
|
|||
this.props.onUserSeek(ratio);
|
||||
}
|
||||
|
||||
|
||||
onPlay() {
|
||||
window.clearInterval(this.playTimer);
|
||||
this.playTimer = window.setInterval(() => {
|
||||
const {videoElement} = this.props;
|
||||
if(videoElement === null) return;
|
||||
|
||||
const {currentTime} = videoElement;
|
||||
let newRatio = this.props.segmentProgress(currentTime);
|
||||
if(newRatio === this.state.ratio) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(newRatio >= 1) {
|
||||
newRatio = 0;
|
||||
this.props.onUserSeek(newRatio);
|
||||
}
|
||||
|
||||
if(newRatio >= 0) {
|
||||
this.updateSeekedBar(newRatio);
|
||||
this.props.onPlaySeek(currentTime);
|
||||
}
|
||||
}, 30);
|
||||
this.playTimer = window.requestAnimationFrame(this.executePlayTimer);
|
||||
let {ratio} = this.state;
|
||||
if(ratio >= 1) {
|
||||
ratio = 0;
|
||||
|
@ -167,8 +149,36 @@ export default class RouteSeeker extends Component {
|
|||
this.props.onPlay();
|
||||
}
|
||||
|
||||
executePlayTimer() {
|
||||
const {videoElement} = this.props;
|
||||
if(videoElement === null) {
|
||||
this.playTimer = window.requestAnimationFrame(this.executePlayTimer);
|
||||
return;
|
||||
}
|
||||
|
||||
const {currentTime} = videoElement;
|
||||
let newRatio = this.props.segmentProgress(currentTime);
|
||||
|
||||
if(newRatio === this.state.ratio) {
|
||||
this.playTimer = window.requestAnimationFrame(this.executePlayTimer);
|
||||
return;
|
||||
}
|
||||
|
||||
if(newRatio >= 1) {
|
||||
newRatio = 0;
|
||||
this.props.onUserSeek(newRatio);
|
||||
}
|
||||
|
||||
if(newRatio >= 0) {
|
||||
this.updateSeekedBar(newRatio);
|
||||
this.props.onPlaySeek(currentTime);
|
||||
}
|
||||
|
||||
this.playTimer = window.requestAnimationFrame(this.executePlayTimer);
|
||||
}
|
||||
|
||||
onPause() {
|
||||
window.clearInterval(this.playTimer);
|
||||
window.cancelAnimationFrame(this.playTimer);
|
||||
this.setState({isPlaying: false});
|
||||
this.props.onPause();
|
||||
}
|
||||
|
|
|
@ -330,7 +330,8 @@ export default class SignalLegendEntry extends Component {
|
|||
<div className='signals-legend-entry-form'>
|
||||
{SignalLegendEntry.fields.map((field) => {
|
||||
return (
|
||||
<div className='signals-legend-entry-form-field'>
|
||||
<div className='signals-legend-entry-form-field'
|
||||
key={field.field}>
|
||||
{this.renderFieldNode(field, signal.name)}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -13,6 +13,7 @@ import CanLog from './CanLog';
|
|||
import RouteSeeker from './RouteSeeker';
|
||||
import Entries from '../models/can/entries';
|
||||
import debounce from '../utils/debounce';
|
||||
import ArrayUtils from '../utils/array';
|
||||
import CommonStyles from '../styles/styles';
|
||||
import Images from '../styles/images';
|
||||
import PartSelector from './PartSelector';
|
||||
|
@ -22,6 +23,7 @@ export default class Explorer extends Component {
|
|||
static propTypes = {
|
||||
selectedMessage: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
live: PropTypes.bool,
|
||||
messages: PropTypes.objectOf(PropTypes.object),
|
||||
onConfirmedSignalChange: PropTypes.func,
|
||||
canFrameOffset: PropTypes.number,
|
||||
|
@ -38,7 +40,7 @@ export default class Explorer extends Component {
|
|||
const msg = props.messages[props.selectedMessage];
|
||||
|
||||
const ShowAddSignal = (
|
||||
msg && Object.keys(msg.signals).length === 0);
|
||||
msg && Object.keys(msg.frame.signals).length === 0);
|
||||
|
||||
this.state = {
|
||||
plottedSignals: [],
|
||||
|
@ -85,9 +87,10 @@ export default class Explorer extends Component {
|
|||
componentDidUpdate(prevProps, prevState) {
|
||||
if(this.props.selectedMessage === prevProps.selectedMessage
|
||||
&& this.props.messages[this.props.selectedMessage]
|
||||
&& prevProps.messages[prevProps.selectedMessage]) {
|
||||
const nextSignalNames = Object.keys(this.props.messages[this.props.selectedMessage].signals);
|
||||
const currentSignalNames = Object.keys(prevProps.messages[prevProps.selectedMessage].signals);
|
||||
&& prevProps.messages[prevProps.selectedMessage]
|
||||
&& this.props.messages[this.props.selectedMessage].frame !== undefined) {
|
||||
const nextSignalNames = Object.keys(this.props.messages[this.props.selectedMessage].frame.signals);
|
||||
const currentSignalNames = Object.keys(prevProps.messages[prevProps.selectedMessage].frame.signals);
|
||||
|
||||
const newSignalNames = nextSignalNames.filter((s) => currentSignalNames.indexOf(s) === -1);
|
||||
for(let i = 0; i < newSignalNames.length; i++) {
|
||||
|
@ -100,30 +103,27 @@ export default class Explorer extends Component {
|
|||
componentWillReceiveProps(nextProps) {
|
||||
const nextMessage = nextProps.messages[nextProps.selectedMessage];
|
||||
const curMessage = this.props.messages[this.props.selectedMessage];
|
||||
const {graphData} = this.state;
|
||||
let {plottedSignals, graphData} = this.state;
|
||||
|
||||
if(Object.keys(nextProps.messages).length === 0) {
|
||||
this.resetSegment();
|
||||
}
|
||||
if(nextMessage && nextMessage !== curMessage) {
|
||||
const nextSignalNames = Object.keys(nextMessage.signals);
|
||||
if(nextMessage && nextMessage.frame && nextMessage !== curMessage) {
|
||||
const nextSignalNames = Object.keys(nextMessage.frame.signals);
|
||||
|
||||
// this.setState({signals: Object.assign({}, nextMessage.signals)});
|
||||
if(nextSignalNames.length === 0) {
|
||||
this.setState({shouldShowAddSignal: true});
|
||||
}
|
||||
}
|
||||
|
||||
let {plottedSignals} = this.state;
|
||||
// unplot signals that have been removed
|
||||
|
||||
// remove plottedSignals that no longer exist
|
||||
plottedSignals = plottedSignals.map((plot) => plot.filter(({messageId, signalName}, index) => {
|
||||
const messageExists = Object.keys(nextProps.messages).indexOf(messageId) !== -1;
|
||||
let signalExists = true;
|
||||
if(!messageExists) {
|
||||
graphData.splice(index, 1);
|
||||
} else {
|
||||
const signalNames = Object.keys(nextProps.messages[messageId].signals);
|
||||
const signalNames = Object.keys(nextProps.messages[messageId].frame.signals);
|
||||
signalExists = signalNames.indexOf(signalName) !== -1;
|
||||
|
||||
if(!signalExists) {
|
||||
|
@ -133,7 +133,6 @@ export default class Explorer extends Component {
|
|||
|
||||
return messageExists && signalExists;
|
||||
})).filter((plot) => plot.length > 0);
|
||||
|
||||
this.setState({plottedSignals, graphData});
|
||||
|
||||
if(nextProps.selectedMessage && nextProps.selectedMessage != this.props.selectedMessage) {
|
||||
|
@ -177,10 +176,20 @@ export default class Explorer extends Component {
|
|||
userSeekTime: nextSeekTime})
|
||||
}
|
||||
|
||||
if(nextMessage && curMessage) {
|
||||
// refresh graph data if the message entry lengths
|
||||
// do not match
|
||||
this.refreshGraphData(nextProps.messages, plottedSignals);
|
||||
if(plottedSignals.length > 0) {
|
||||
if(graphData.length === plottedSignals.length) {
|
||||
if(plottedSignals.some((plot) =>
|
||||
plot.some(({messageId, signalName}) =>
|
||||
nextProps.messages[messageId].frame.signals[signalName] !== this.props.messages[messageId].frame.signals[signalName]
|
||||
))) {
|
||||
this.refreshGraphData(nextProps.messages, plottedSignals);
|
||||
} else {
|
||||
graphData = this.appendNewGraphData(plottedSignals, graphData, nextProps.messages);
|
||||
this.setState({graphData});
|
||||
}
|
||||
} else {
|
||||
this.refreshGraphData(nextProps.messages, plottedSignals);
|
||||
}
|
||||
}
|
||||
|
||||
if(JSON.stringify(nextProps.currentParts) !== JSON.stringify(this.props.currentParts)) {
|
||||
|
@ -190,6 +199,92 @@ export default class Explorer extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
appendNewGraphData(plottedSignals, graphData, messages) {
|
||||
const messagesPerPlot = plottedSignals.map((plottedMessages) =>
|
||||
plottedMessages.reduce((messages,
|
||||
{messageId, signalName}) => {
|
||||
messages.push(messageId);
|
||||
return messages;
|
||||
}, [])
|
||||
);
|
||||
|
||||
const extendedPlots = messagesPerPlot
|
||||
.map((plottedMessageIds, index) => {return {plottedMessageIds, index}}) // preserve index so we can look up graphData
|
||||
.filter(({plottedMessageIds, index}) => {
|
||||
let maxGraphTime = 0;
|
||||
if(graphData.length > 0) {
|
||||
maxGraphTime = graphData[index][graphData[index].length - 1].relTime;
|
||||
}
|
||||
|
||||
return plottedMessageIds.some((messageId) =>
|
||||
messages[messageId].entries.some((e) => e.relTime > maxGraphTime));
|
||||
}).map(({plottedMessageIds, index}) => {
|
||||
plottedMessageIds = plottedMessageIds.reduce((arr, messageId) => {
|
||||
if(arr.indexOf(messageId) === -1) {
|
||||
arr.push(messageId);
|
||||
}
|
||||
return arr;
|
||||
}, []);
|
||||
return {plottedMessageIds, index};
|
||||
});
|
||||
|
||||
extendedPlots.forEach(({plottedMessageIds, index}) => {
|
||||
const signalNamesByMessageId = plottedSignals[index].reduce((obj, {messageId, signalName}) => {
|
||||
if(!obj[messageId]) {
|
||||
obj[messageId] = []
|
||||
}
|
||||
obj[messageId].push(signalName);
|
||||
return obj;
|
||||
}, {});
|
||||
const graphDataMaxMessageTimes = plottedMessageIds.reduce((obj, messageId) => {
|
||||
const signalNames = signalNamesByMessageId[messageId];
|
||||
const maxIndex = ArrayUtils.findIndexRight(graphData[index], (entry) => {
|
||||
return signalNames.indexOf(entry.signalName) !== -1
|
||||
});
|
||||
if(maxIndex) {
|
||||
obj[messageId] = graphData[index][maxIndex].relTime;
|
||||
} else {
|
||||
obj[messageId] = graphData[index][graphData[index].length - 1].relTime;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
let newGraphData = [];
|
||||
plottedMessageIds.map((messageId) => {
|
||||
return { messageId, entries: messages[messageId].entries }
|
||||
}).filter(({messageId, entries}) => // Filter to only messages with stale graphData
|
||||
entries[entries.length - 1].relTime > graphDataMaxMessageTimes[messageId])
|
||||
.forEach(({messageId, entries}) => { // Compute and append new graphData
|
||||
let firstNewEntryIdx = entries.findIndex((entry) =>
|
||||
entry.relTime > graphDataMaxMessageTimes[messageId]);
|
||||
|
||||
const newEntries = entries.slice(firstNewEntryIdx);
|
||||
|
||||
signalNamesByMessageId[messageId].forEach((signalName) => {
|
||||
const signalGraphData = this._calcGraphData({...messages[messageId],
|
||||
entries: newEntries}, signalName);
|
||||
newGraphData = newGraphData.concat(signalGraphData);
|
||||
});
|
||||
});
|
||||
|
||||
const messageIdOutOfBounds = plottedMessageIds.find((messageId) =>
|
||||
graphData[index][0].relTime < messages[messageId].entries[0].relTime);
|
||||
|
||||
graphData[index] = graphData[index].concat(newGraphData)
|
||||
if(messageIdOutOfBounds) {
|
||||
const graphDataLowerBound = graphData[index].findIndex(
|
||||
(e) => e.relTime > messages[messageIdOutOfBounds].entries[0].relTime);
|
||||
|
||||
if(graphDataLowerBound) {
|
||||
graphData[index] = graphData[index].slice(graphDataLowerBound);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return graphData;
|
||||
}
|
||||
|
||||
timeWindow() {
|
||||
const {route, currentParts} = this.props;
|
||||
if(route) {
|
||||
|
@ -219,30 +314,33 @@ export default class Explorer extends Component {
|
|||
|
||||
return samples.map((entry) => {
|
||||
return {x: entry.time,
|
||||
xRel: entry.time - this.props.firstCanTime,
|
||||
relTime: entry.time - this.props.firstCanTime,
|
||||
y: entry.signals[signalName],
|
||||
unit: msg.signals[signalName].unit,
|
||||
color: `rgba(${msg.signals[signalName].colors().join(",")}, 0.5)`,
|
||||
unit: msg.frame.signals[signalName].unit,
|
||||
color: `rgba(${msg.frame.signals[signalName].colors().join(",")}, 0.5)`,
|
||||
signalName}
|
||||
});
|
||||
}
|
||||
|
||||
sortGraphData(graphData) {
|
||||
return graphData.sort((entry1, entry2) => {
|
||||
if(entry1.relTime < entry2.relTime) {
|
||||
return -1;
|
||||
} else if(entry1.relTime > entry2.relTime) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
calcGraphData(signals, messages) {
|
||||
if(typeof messages === 'undefined') {
|
||||
messages = this.props.messages;
|
||||
}
|
||||
|
||||
return signals.map(({messageId, signalName}) => this._calcGraphData(messages[messageId], signalName))
|
||||
.reduce((combined, signalData) => combined.concat(signalData), [])
|
||||
.sort((entry1, entry2) => {
|
||||
if(entry1.xRel < entry2.xRel) {
|
||||
return -1;
|
||||
} else if(entry1.xRel > entry2.xRel) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
return this.sortGraphData(signals.map(({messageId, signalName}) => this._calcGraphData(messages[messageId], signalName))
|
||||
.reduce((combined, signalData) => combined.concat(signalData), []));
|
||||
}
|
||||
|
||||
onSignalPlotPressed(messageId, signalName) {
|
||||
|
@ -253,6 +351,7 @@ export default class Explorer extends Component {
|
|||
|
||||
graphData = [this.calcGraphData([{messageId, signalName}]), ...graphData];
|
||||
plottedSignals = [[{messageId, signalName}], ...plottedSignals];
|
||||
|
||||
this.setState({plottedSignals, graphData});
|
||||
// }
|
||||
}
|
||||
|
@ -265,6 +364,7 @@ export default class Explorer extends Component {
|
|||
plottedSignals = this.state.plottedSignals;
|
||||
}
|
||||
let graphData = Array(plottedSignals.length);
|
||||
|
||||
plottedSignals.forEach((plotSignals, index) => {
|
||||
const plotGraphData = this.calcGraphData(plotSignals, messages);
|
||||
graphData[index] = plotGraphData;
|
||||
|
@ -315,6 +415,8 @@ export default class Explorer extends Component {
|
|||
// returns index guaranteed to be in [0, entries.length - 1]
|
||||
|
||||
const {entries} = this.props.messages[this.props.selectedMessage];
|
||||
if(entries.length === 0) return null;
|
||||
|
||||
const {segmentIndices} = this.state;
|
||||
let segmentLength, offset;
|
||||
if(segmentIndices.length === 2) {
|
||||
|
@ -345,11 +447,15 @@ export default class Explorer extends Component {
|
|||
|
||||
const {entries} = message;
|
||||
const userSeekIndex = this.indexFromSeekTime(time);
|
||||
const seekTime = entries[userSeekIndex].relTime;
|
||||
if(userSeekIndex) {
|
||||
const seekTime = entries[userSeekIndex].relTime;
|
||||
|
||||
this.setState({userSeekIndex, userSeekTime: seekTime});
|
||||
this.props.onUserSeek(seekTime);
|
||||
this.props.onSeek(userSeekIndex, seekTime);
|
||||
this.setState({userSeekIndex, userSeekTime: seekTime});
|
||||
this.props.onSeek(userSeekIndex, seekTime);
|
||||
} else {
|
||||
this.props.onUserSeek(time);
|
||||
this.setState({userSeekTime: time});
|
||||
}
|
||||
}
|
||||
|
||||
onPlaySeek(time) {
|
||||
|
@ -556,29 +662,33 @@ export default class Explorer extends Component {
|
|||
: this.renderSelectMessagePrompt()}
|
||||
</div>
|
||||
<div className='cabana-explorer-visuals'>
|
||||
<div className='cabana-explorer-visuals-header'>
|
||||
{this.timeWindow()}
|
||||
<PartSelector
|
||||
onPartChange={this.props.onPartChange}
|
||||
partsCount={this.props.partsCount}
|
||||
/>
|
||||
</div>
|
||||
<RouteVideoSync
|
||||
message={this.props.messages[this.props.selectedMessage]}
|
||||
secondsLoaded={this.secondsLoaded()}
|
||||
startOffset={this.startOffset()}
|
||||
seekIndex={this.props.seekIndex}
|
||||
userSeekIndex={this.state.userSeekIndex}
|
||||
playing={this.state.playing}
|
||||
url={this.props.url}
|
||||
canFrameOffset={this.props.canFrameOffset}
|
||||
firstCanTime={this.props.firstCanTime}
|
||||
onVideoClick={this.onVideoClick}
|
||||
onPlaySeek={this.onPlaySeek}
|
||||
onUserSeek={this.onUserSeek}
|
||||
onPlay={this.onPlay}
|
||||
onPause={this.onPause}
|
||||
userSeekTime={this.state.userSeekTime} />
|
||||
{ this.props.route !== null ?
|
||||
<div>
|
||||
<div className='cabana-explorer-visuals-header'>
|
||||
{this.timeWindow()}
|
||||
<PartSelector
|
||||
onPartChange={this.props.onPartChange}
|
||||
partsCount={this.props.partsCount}
|
||||
/>
|
||||
</div>
|
||||
<RouteVideoSync
|
||||
message={this.props.messages[this.props.selectedMessage]}
|
||||
secondsLoaded={this.secondsLoaded()}
|
||||
startOffset={this.startOffset()}
|
||||
seekIndex={this.props.seekIndex}
|
||||
userSeekIndex={this.state.userSeekIndex}
|
||||
playing={this.state.playing}
|
||||
url={this.props.url}
|
||||
canFrameOffset={this.props.canFrameOffset}
|
||||
firstCanTime={this.props.firstCanTime}
|
||||
onVideoClick={this.onVideoClick}
|
||||
onPlaySeek={this.onPlaySeek}
|
||||
onUserSeek={this.onUserSeek}
|
||||
onPlay={this.onPlay}
|
||||
onPause={this.onPause}
|
||||
userSeekTime={this.state.userSeekTime} />
|
||||
</div>
|
||||
: null }
|
||||
{this.state.segment.length > 0 ?
|
||||
<div className={css(CommonStyles.button, Styles.resetSegment)}
|
||||
onClick={() => {this.resetSegment()}}>
|
||||
|
@ -594,7 +704,8 @@ export default class Explorer extends Component {
|
|||
onSegmentChanged={this.onSegmentChanged}
|
||||
onSignalUnplotPressed={this.onSignalUnplotPressed}
|
||||
segment={this.state.segment}
|
||||
mergePlots={this.mergePlots} />
|
||||
mergePlots={this.mergePlots}
|
||||
live={this.props.live} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -4,6 +4,7 @@ import cx from 'classnames';
|
|||
import PropTypes from 'prop-types';
|
||||
import Clipboard from 'clipboard';
|
||||
require('core-js/fn/array/includes');
|
||||
const {ckmeans} = require('simple-statistics');
|
||||
|
||||
import {modifyQueryParameters} from '../utils/url';
|
||||
import LoadDbcModal from './LoadDbcModal';
|
||||
|
@ -33,6 +34,7 @@ export default class Meta extends Component {
|
|||
seekTime: PropTypes.number,
|
||||
loginWithGithub: PropTypes.element,
|
||||
isDemo: PropTypes.bool,
|
||||
live: PropTypes.bool,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
@ -41,12 +43,14 @@ export default class Meta extends Component {
|
|||
this.state = {
|
||||
filterText: 'Filter',
|
||||
lastSaved: dbcLastSaved !== null ? this.props.dbcLastSaved.fromNow() : null,
|
||||
hoveredMessages: []
|
||||
hoveredMessages: [],
|
||||
orderedMessageKeys: [],
|
||||
};
|
||||
this.onFilterChanged = this.onFilterChanged.bind(this);
|
||||
this.onFilterFocus = this.onFilterFocus.bind(this);
|
||||
this.onFilterUnfocus = this.onFilterUnfocus.bind(this);
|
||||
this.msgKeyFilter = this.msgKeyFilter.bind(this);
|
||||
this.msgFilter = this.msgFilter.bind(this);
|
||||
this.renderMessageBytes = this.renderMessageBytes.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
|
@ -70,10 +74,60 @@ export default class Meta extends Component {
|
|||
if(JSON.stringify(nextMsgKeys) != JSON.stringify(Object.keys(this.props.messages))) {
|
||||
let {selectedMessages} = this.props;
|
||||
selectedMessages = selectedMessages.filter((m) => nextMsgKeys.indexOf(m) !== -1);
|
||||
this.setState({hoveredMessages: []});
|
||||
|
||||
const orderedMessageKeys = this.sortMessages(nextProps.messages);
|
||||
this.setState({hoveredMessages: [], orderedMessageKeys});
|
||||
} else if((this.state.orderedMessageKeys.length === 0)
|
||||
|| (!this.props.live && this.props.messages && nextProps.messages
|
||||
&& this.byteCountsDidUpdate(this.props.messages, nextProps.messages))) {
|
||||
const orderedMessageKeys = this.sortMessages(nextProps.messages);
|
||||
this.setState({orderedMessageKeys});
|
||||
}
|
||||
}
|
||||
|
||||
byteCountsDidUpdate(prevMessages, nextMessages) {
|
||||
return Object.entries(nextMessages).some(([msgId, msg]) =>
|
||||
JSON.stringify(msg.byteStateChangeCounts)
|
||||
!== JSON.stringify(prevMessages[msgId].byteStateChangeCounts));
|
||||
}
|
||||
|
||||
sortMessages(messages) {
|
||||
// Returns list of message keys, ordered as follows:
|
||||
// messages are binned into at most 10 bins based on entry count
|
||||
// each bin is sorted by message CAN address
|
||||
// then the list of bins is flattened and reversed to
|
||||
// yield a count-descending, address-ascending order.
|
||||
|
||||
if(Object.keys(messages).length === 0) return [];
|
||||
const messagesByEntryCount = Object.entries(messages).reduce((partialMapping, [msgId, msg]) => {
|
||||
const entryCountKey = msg.entries.length.toString(); // js object keys are strings
|
||||
if( !partialMapping[entryCountKey] ) {
|
||||
partialMapping[entryCountKey] = [msg];
|
||||
} else {
|
||||
partialMapping[entryCountKey].push(msg);
|
||||
}
|
||||
return partialMapping;
|
||||
}, {});
|
||||
|
||||
const entryCounts = Object.keys(messagesByEntryCount).map((count) => parseInt(count));
|
||||
const binnedEntryCounts = ckmeans(entryCounts, Math.min(entryCounts.length, 10));
|
||||
const sortedKeys = binnedEntryCounts.map((bin) =>
|
||||
bin.map((entryCount) => messagesByEntryCount[entryCount.toString()])
|
||||
.reduce((messages, partial) => messages.concat(partial), [])
|
||||
.sort((msg1, msg2) => {
|
||||
if(msg1.address < msg2.address) {
|
||||
return 1;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
})
|
||||
.map((msg) => msg.id)
|
||||
).reduce((keys, bin) => keys.concat(bin), [])
|
||||
.reverse();
|
||||
|
||||
return sortedKeys;
|
||||
}
|
||||
|
||||
onFilterChanged(e) {
|
||||
let val = e.target.value;
|
||||
if(val.trim() === 'Filter') val = '';
|
||||
|
@ -93,14 +147,13 @@ export default class Meta extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
msgKeyFilter(key) {
|
||||
msgFilter(msg) {
|
||||
const {filterText} = this.state;
|
||||
const msg = this.props.messages[key];
|
||||
const msgName = (msg.frame ? msg.frame.name : '');
|
||||
|
||||
return (filterText == 'Filter'
|
||||
|| filterText == ''
|
||||
|| key.toLowerCase().indexOf(filterText.toLowerCase()) !== -1
|
||||
|| msg.id.toLowerCase().indexOf(filterText.toLowerCase()) !== -1
|
||||
|| msgName.toLowerCase().indexOf(filterText.toLowerCase()) !== -1);
|
||||
}
|
||||
|
||||
|
@ -140,50 +193,43 @@ export default class Meta extends Component {
|
|||
}
|
||||
|
||||
orderedMessages() {
|
||||
const {orderedMessageKeys} = this.state;
|
||||
const {messages} = this.props;
|
||||
const keys = Object.keys(messages)
|
||||
.filter(this.msgKeyFilter)
|
||||
.sort((key1, key2) => {
|
||||
const msg1 = messages[key1], msg2 = messages[key2];
|
||||
if(msg1.entries.length < msg2.entries.length) {
|
||||
return 1;
|
||||
} else if(msg1.entries.length === msg2.entries.length) {
|
||||
return 0;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
});
|
||||
|
||||
let bins = [];
|
||||
keys.forEach((key, idx) => {
|
||||
const msg = messages[key];
|
||||
let bin = bins.find((bin) =>
|
||||
bin.some((binMsg) =>
|
||||
Math.abs(binMsg.entries.length - msg.entries.length) < 100
|
||||
)
|
||||
);
|
||||
if(bin) {
|
||||
bin.push(msg);
|
||||
} else {
|
||||
bins.push([msg]);
|
||||
}
|
||||
});
|
||||
bins = bins.map((bin) => bin.sort((msg1, msg2) => {
|
||||
if(msg1.address < msg2.address) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return bins.reduce((arr, bin) => arr.concat(bin), []);
|
||||
return orderedMessageKeys.map((key) => messages[key]);
|
||||
}
|
||||
|
||||
selectedMessageClass(messageId) {
|
||||
return (this.props.selectedMessages.includes(messageId) ? 'is-selected' : null);
|
||||
}
|
||||
|
||||
renderMessageBytes(msg) {
|
||||
return (
|
||||
<tr onClick={() => {this.onMessageSelected(msg.id)}}
|
||||
key={msg.id}
|
||||
className={cx('cabana-meta-messages-list-item', this.selectedMessageClass(msg.id))}>
|
||||
<td>{msg.frame ? msg.frame.name : 'undefined'}</td>
|
||||
<td>{msg.id}</td>
|
||||
<td>{msg.entries.length}</td>
|
||||
<td>
|
||||
<div className='cabana-meta-messages-list-item-bytes'>
|
||||
<MessageBytes
|
||||
key={msg.id}
|
||||
message={msg}
|
||||
seekIndex={this.props.seekIndex}
|
||||
seekTime={this.props.seekTime}
|
||||
live={this.props.live}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
renderMessages() {
|
||||
return this.orderedMessages()
|
||||
.filter(this.msgFilter)
|
||||
.map(this.renderMessageBytes);
|
||||
}
|
||||
|
||||
renderAvailableMessagesList() {
|
||||
if(Object.keys(this.props.messages).length === 0) {
|
||||
return <p>Loading messages...</p>;
|
||||
|
@ -199,24 +245,7 @@ export default class Meta extends Component {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.orderedMessages()
|
||||
.map((msg) => {
|
||||
return (
|
||||
<tr onClick={() => {this.onMessageSelected(msg.id)}}
|
||||
key={msg.id}
|
||||
className={cx('cabana-meta-messages-list-item', this.selectedMessageClass(msg.id))}>
|
||||
<td>{msg.frame ? msg.frame.name : 'undefined'}</td>
|
||||
<td>{msg.id}</td>
|
||||
<td>{msg.entries.length}</td>
|
||||
<td>
|
||||
<MessageBytes
|
||||
message={msg}
|
||||
seekTime={this.props.seekTime}
|
||||
maxByteStateChangeCount={this.props.maxByteStateChangeCount} />
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
{this.renderMessages()}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
@ -258,14 +287,15 @@ export default class Meta extends Component {
|
|||
<div className='cabana-meta-header-action'>
|
||||
<button onClick={this.props.showLoadDbc}>Load DBC</button>
|
||||
</div>
|
||||
<div className='cabana-meta-header-action'
|
||||
data-clipboard-text={this.shareUrl()}
|
||||
data-clipboard-action='copy'
|
||||
ref={(ref) => ref ? new Clipboard(ref) : null}>
|
||||
<a className='button'
|
||||
href={this.shareUrl()}
|
||||
onClick={(e) => e.preventDefault()}>Copy Share Link</a>
|
||||
</div>
|
||||
{this.props.route ?
|
||||
<div className='cabana-meta-header-action'
|
||||
data-clipboard-text={this.shareUrl()}
|
||||
data-clipboard-action='copy'
|
||||
ref={(ref) => ref ? new Clipboard(ref) : null}>
|
||||
<a className='button'
|
||||
href={this.shareUrl()}
|
||||
onClick={(e) => e.preventDefault()}>Copy Share Link</a>
|
||||
</div> : null}
|
||||
<div className='cabana-meta-header-action'>
|
||||
<button onClick={this.props.showSaveDbc}>Save DBC</button>
|
||||
</div>
|
||||
|
|
|
@ -20,4 +20,6 @@ export const LOGENTRIES_TOKEN = '4bc98019-8277-4fe0-867c-ed21ea843cc5';
|
|||
|
||||
export const PART_SEGMENT_LENGTH = 3;
|
||||
|
||||
export const CAN_GRAPH_MAX_POINTS = 10000;
|
||||
export const CAN_GRAPH_MAX_POINTS = 10000;
|
||||
|
||||
export const STREAMING_WINDOW = 60;
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 68 KiB |
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
26
src/index.js
26
src/index.js
|
@ -15,36 +15,34 @@ import './index.css';
|
|||
|
||||
const routeFullName = getUrlParameter('route');
|
||||
let isDemo = !routeFullName;
|
||||
let props = {autoplay: false, isDemo};
|
||||
let props = {autoplay: true, isDemo};
|
||||
let persistedDbc = null;
|
||||
|
||||
if(routeFullName) {
|
||||
const [dongleId, route] = routeFullName.split('|');
|
||||
props.dongleId = dongleId;
|
||||
props.name = route;
|
||||
|
||||
const persistedDbc = fetchPersistedDbc(routeFullName);
|
||||
if(persistedDbc) {
|
||||
const {dbcFilename, dbc} = persistedDbc;
|
||||
props.dbc = dbc;
|
||||
props.dbcFilename = dbcFilename;
|
||||
}
|
||||
persistedDbc = fetchPersistedDbc(routeFullName);
|
||||
|
||||
let max = getUrlParameter('max'), url = getUrlParameter('url');
|
||||
if(max && url) {
|
||||
props.max = max;
|
||||
props.url = url;
|
||||
}
|
||||
} else if(getUrlParameter('prius')) {
|
||||
props.autoplay = true;
|
||||
props.dongleId = 'b67ff0c1d78774da';
|
||||
props.name = '2017-06-30--17-37-49';
|
||||
} else {
|
||||
props.autoplay = true;
|
||||
} else if(getUrlParameter('demo')) {
|
||||
props.dongleId = 'cb38263377b873ee';
|
||||
props.name = '2017-06-12--18-51-47';
|
||||
props.dbc = AcuraDbc;
|
||||
props.dbcFilename = 'acura_ilx_2016_can.dbc';
|
||||
}
|
||||
|
||||
if(persistedDbc) {
|
||||
const {dbcFilename, dbc} = persistedDbc;
|
||||
props.dbc = dbc;
|
||||
props.dbcFilename = dbcFilename;
|
||||
}
|
||||
|
||||
const authTokenQueryParam = getUrlParameter(GITHUB_AUTH_TOKEN_KEY);
|
||||
if(authTokenQueryParam !== null) {
|
||||
props.githubAuthToken = authTokenQueryParam;
|
||||
|
@ -67,4 +65,4 @@ if(routeFullName || isDemo) {
|
|||
|
||||
document.getElementById('root').appendChild(img);
|
||||
document.getElementById('root').appendChild(comment);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
@import './styles/base/lists';
|
||||
@import './styles/base/typography';
|
||||
@import './styles/base/utils';
|
||||
@import './styles/base/animations';
|
||||
@import './styles/base/insets';
|
||||
|
||||
// Libraries
|
||||
@import './styles/lib/index';
|
||||
|
|
|
@ -40,6 +40,10 @@ class CloudLog {
|
|||
warn(message) {
|
||||
this.emit(message, 'warn');
|
||||
}
|
||||
|
||||
error(message) {
|
||||
this.emit(message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
export default (new CloudLog());
|
||||
|
|
|
@ -173,8 +173,9 @@ export default class DBC {
|
|||
setSignals(msgId, signals) {
|
||||
const msg = this.messages.get(msgId);
|
||||
if(msg) {
|
||||
msg.signals = signals;
|
||||
this.messages.set(msgId, msg);
|
||||
const newMsg = Object.assign(Object.create(msg), msg);
|
||||
newMsg.signals = signals;
|
||||
this.messages.set(msgId, newMsg);
|
||||
} else {
|
||||
const msg = this.createFrame(msgId);
|
||||
msg.signals = signals;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// All Global Variables and Mixins
|
||||
@import 'colors';
|
||||
@import 'mixins';
|
||||
@import '../../../node_modules/font-awesome/scss/variables';
|
||||
@import '../../../node_modules/font-awesome/scss/_variables';
|
||||
|
|
|
@ -9,4 +9,4 @@ $color-grey-70: #6f6f6f;
|
|||
$color-grey-80: #484848;
|
||||
$color-grey-90: #2d2d2d;
|
||||
|
||||
$color-blue-50: #378FFF;
|
||||
$color-blue-50: #258FDA;
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
|
||||
Animations
|
||||
~~~~~~~~~~
|
||||
|
||||
*/
|
||||
|
||||
// Spinning Animation
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.animate-spin {
|
||||
animation-name: spin;
|
||||
animation-duration: 1000ms;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear;
|
||||
}
|
|
@ -34,3 +34,12 @@ hr {
|
|||
margin: 22px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
|
|
@ -165,6 +165,16 @@ a.button {
|
|||
font-size: 12px;
|
||||
line-height: $input-height--tiny - 2px;
|
||||
}
|
||||
// Button Disabled Variations
|
||||
&[disabled] {
|
||||
background: $button-background--disabled;
|
||||
border-color: $button-border--disabled;
|
||||
border-bottom-width: 1px;
|
||||
color: $button-color--disabled;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
text-shadow: none;
|
||||
}
|
||||
// Button Color Variations
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~
|
||||
&.button--primary {
|
||||
|
@ -177,6 +187,13 @@ a.button {
|
|||
background: darken($button-background--primary, 8%);
|
||||
}
|
||||
}
|
||||
&.button--secondary {
|
||||
background: linear-gradient(-180deg, #F9F9F9 0%, #F0F0F0 100%);
|
||||
border-color: $color-grey-40;
|
||||
border-radius: 5px;
|
||||
color: $color-grey-80;
|
||||
text-shadow: none;
|
||||
}
|
||||
&.button--alpha {
|
||||
background: $button-background--alpha;
|
||||
border-color: $color-grey-30;
|
||||
|
@ -207,14 +224,84 @@ a.button {
|
|||
background: darken($button-background--dark, 10%);
|
||||
}
|
||||
}
|
||||
// Button Disabled Variations
|
||||
&[disabled] {
|
||||
background: $button-background--disabled;
|
||||
border-color: $button-border--disabled;
|
||||
border-bottom-width: 1px;
|
||||
color: $button-color--disabled;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
text-shadow: none;
|
||||
// Major variations
|
||||
&.button--kiosk {
|
||||
box-shadow: 0 1px 3px 0 rgba(105,105,105,0.2);
|
||||
height: 144px;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 34px;
|
||||
padding-left: 160px;
|
||||
padding-top: 31px;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
&:active {
|
||||
> i {
|
||||
top: 35px;
|
||||
}
|
||||
}
|
||||
&[disabled],
|
||||
&.is-disabled {
|
||||
// it is expected when kiosk buttons are disabled
|
||||
// that a context <p> will be provided
|
||||
border-width: 1px;
|
||||
box-shadow: none;
|
||||
cursor: default;
|
||||
padding-top: 22px;
|
||||
strong,
|
||||
sup,
|
||||
> i {
|
||||
opacity: .3;
|
||||
}
|
||||
sup {
|
||||
font-size: 16px;
|
||||
}
|
||||
p {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
span {
|
||||
padding-left: 8px;
|
||||
}
|
||||
}
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
pointer-events: auto;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
> i {
|
||||
background: rgba(0,0,0,.2);
|
||||
border: 1px solid rgba(0,0,0,.1);
|
||||
border-radius: 100%;
|
||||
font-size: 36px;
|
||||
line-height: 73px;
|
||||
text-align: center;
|
||||
left: 50px;
|
||||
top: 34px;
|
||||
opacity: .3;
|
||||
position: absolute;
|
||||
width: 73px;
|
||||
}
|
||||
strong,
|
||||
sup {
|
||||
display: block;
|
||||
line-height: normal;
|
||||
}
|
||||
strong {
|
||||
font-size: 26px;
|
||||
letter-spacing: .0125em;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
sup {
|
||||
font-size: 18px;
|
||||
font-weight: normal;
|
||||
}
|
||||
em {
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
// Insets
|
||||
// ~~~~~~
|
||||
|
||||
@import '../_global/all';
|
||||
|
||||
.inset {
|
||||
background: $color-grey-20;
|
||||
border: 1px solid $color-grey-30;
|
||||
border-radius: 3px;
|
||||
color: $color-grey-70;
|
||||
padding: 12px;
|
||||
width: 100%;
|
||||
}
|
|
@ -1,8 +1,46 @@
|
|||
// Lists
|
||||
// ~~~~~
|
||||
|
||||
@import '../_global/all';
|
||||
|
||||
$list-bubble-size: 25px;
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ol {
|
||||
margin: 0;
|
||||
&.list--bubbled {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
li {
|
||||
counter-increment: list-counter;
|
||||
min-height: $list-bubble-size;
|
||||
margin-bottom: 2%;
|
||||
padding-bottom: 2%;
|
||||
padding-top: 4px;
|
||||
padding-left: $list-bubble-size * 1.6;
|
||||
position: relative;
|
||||
&:before {
|
||||
background: #fff;
|
||||
border: 1px solid $color-grey-30;
|
||||
border-radius: 100%;
|
||||
content: counter(list-counter);
|
||||
font-size: 12px;
|
||||
line-height: $list-bubble-size;
|
||||
margin-right: 5px;
|
||||
left: 0;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
width: $list-bubble-size;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 3%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,13 @@
|
|||
export function elementWiseEquals(arr1, arr2) {
|
||||
function elementWiseEquals(arr1, arr2) {
|
||||
return arr1.length === arr2.length && arr1.every((ele, idx) => arr2[idx] === ele);
|
||||
}
|
||||
|
||||
function findIndexRight(arr, condition) {
|
||||
for(let i = arr.length - 1; i >= 0; i--) {
|
||||
if(condition(arr[i])) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {elementWiseEquals, findIndexRight};
|
123
src/utils/dbc.js
123
src/utils/dbc.js
|
@ -1,42 +1,100 @@
|
|||
require('core-js/fn/array/from');
|
||||
|
||||
function findMaxByteStateChangeCount(messages) {
|
||||
return Object.values(messages).map((m) => m.byteStateChangeCounts)
|
||||
.reduce((counts, countArr) => counts.concat(countArr), []) // flatten arrays
|
||||
.reduce((count1, count2) => count1 > count2 ? count1 : count2, 0); // find max
|
||||
}
|
||||
|
||||
function addCanMessage([address, busTime, data, source], dbc, canStartTime, messages, prevMsgEntries, byteStateChangeCountsByMessage) {
|
||||
var id = source + ":" + address.toString(16);
|
||||
|
||||
if (messages[id] === undefined) messages[id] = createMessageSpec(dbc, address, id, source);
|
||||
|
||||
const prevMsgEntry = messages[id].entries.length > 0 ?
|
||||
messages[id].entries[messages[id].entries.length - 1]
|
||||
:
|
||||
(prevMsgEntries[id] || null);
|
||||
|
||||
if(byteStateChangeCountsByMessage[id] && messages[id].byteStateChangeCounts.every((c) => c === 0)) {
|
||||
messages[id].byteStateChangeCounts = byteStateChangeCountsByMessage[id]
|
||||
}
|
||||
|
||||
const {msgEntry,
|
||||
byteStateChangeCounts} = parseMessage(dbc,
|
||||
busTime,
|
||||
address,
|
||||
data,
|
||||
canStartTime,
|
||||
prevMsgEntry);
|
||||
|
||||
messages[id].byteStateChangeCounts = byteStateChangeCounts.map((count, idx) =>
|
||||
messages[id].byteStateChangeCounts[idx] + count
|
||||
);
|
||||
|
||||
messages[id].entries.push(msgEntry);
|
||||
|
||||
return msgEntry;
|
||||
}
|
||||
|
||||
function createMessageSpec(dbc, address, id, bus) {
|
||||
const frame = dbc.messages.get(address);
|
||||
const size = frame ? frame.size : 8;
|
||||
|
||||
return {address: address,
|
||||
id: id,
|
||||
bus: bus,
|
||||
entries: [],
|
||||
frame: dbc.messages.get(address),
|
||||
byteColors: Array(size).fill(0),
|
||||
byteStateChangeCounts: Array(size).fill(0)}
|
||||
}
|
||||
|
||||
function determineByteStateChangeTimes(hexData, time, msgSize, lastParsedMessage) {
|
||||
const byteStateChangeCounts = Array(msgSize).fill(0);
|
||||
let byteStateChangeTimes;
|
||||
const byteStateChangeCounts = Array(msgSize).fill(0);
|
||||
let byteStateChangeTimes;
|
||||
|
||||
if(!lastParsedMessage) {
|
||||
byteStateChangeTimes = Array(msgSize).fill(time);
|
||||
} else {
|
||||
byteStateChangeTimes = Array.from(lastParsedMessage.byteStateChangeTimes);
|
||||
if(!lastParsedMessage) {
|
||||
byteStateChangeTimes = Array(msgSize).fill(time);
|
||||
} else {
|
||||
byteStateChangeTimes = Array.from(lastParsedMessage.byteStateChangeTimes);
|
||||
|
||||
for(let i = 0; i < byteStateChangeTimes.length; i++) {
|
||||
const currentData = hexData.substr(i * 2, 2),
|
||||
prevData = lastParsedMessage.hexData.substr(i * 2, 2);
|
||||
for(let i = 0; i < byteStateChangeTimes.length; i++) {
|
||||
const currentData = hexData.substr(i * 2, 2),
|
||||
prevData = lastParsedMessage.hexData.substr(i * 2, 2);
|
||||
|
||||
if(currentData != prevData) {
|
||||
byteStateChangeTimes[i] = time;
|
||||
byteStateChangeCounts[i] = 1;
|
||||
if(currentData != prevData) {
|
||||
byteStateChangeTimes[i] = time;
|
||||
byteStateChangeCounts[i] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {byteStateChangeTimes, byteStateChangeCounts};
|
||||
return {byteStateChangeTimes, byteStateChangeCounts};
|
||||
}
|
||||
|
||||
function parseMessage(dbc, time, address, data, timeStart, lastParsedMessage) {
|
||||
const hexData = Buffer.from(data).toString('hex');
|
||||
let hexData;
|
||||
if(typeof data === 'string') {
|
||||
hexData = data;
|
||||
data = Buffer.from(data, 'hex');
|
||||
} else {
|
||||
hexData = Buffer.from(data).toString('hex');
|
||||
}
|
||||
const msgSpec = dbc.messages.get(address);
|
||||
const msgSize = msgSpec ? msgSpec.size : 8;
|
||||
const relTime = time - timeStart;
|
||||
|
||||
const {byteStateChangeTimes, byteStateChangeCounts} = determineByteStateChangeTimes(hexData,
|
||||
relTime,
|
||||
msgSize,
|
||||
lastParsedMessage)
|
||||
const {byteStateChangeTimes,
|
||||
byteStateChangeCounts} = determineByteStateChangeTimes(hexData,
|
||||
relTime,
|
||||
msgSize,
|
||||
lastParsedMessage);
|
||||
const msgEntry = {time: time,
|
||||
signals: dbc.getSignalValues(address, data),
|
||||
relTime,
|
||||
hexData,
|
||||
byteStyles: new Array(8).fill({}),
|
||||
byteStateChangeTimes}
|
||||
|
||||
return {msgEntry, byteStateChangeCounts};
|
||||
|
@ -50,11 +108,34 @@ for(let i = 0; i < 64; i += 8) {
|
|||
}
|
||||
|
||||
function bigEndianBitIndex(matrixBitIndex) {
|
||||
return BIG_ENDIAN_START_BITS.indexOf(matrixBitIndex);
|
||||
return BIG_ENDIAN_START_BITS.indexOf(matrixBitIndex);
|
||||
}
|
||||
|
||||
function matrixBitNumber(bigEndianIndex) {
|
||||
return BIG_ENDIAN_START_BITS[bigEndianIndex];
|
||||
}
|
||||
|
||||
export default {bigEndianBitIndex, matrixBitNumber, parseMessage}
|
||||
function setMessageByteColors(message, maxByteStateChangeCount) {
|
||||
message.byteColors = message.byteStateChangeCounts.map((count) =>
|
||||
isNaN(count) ? 0 : Math.min(255, 75 + 180 * (count / maxByteStateChangeCount))
|
||||
).map((red) =>
|
||||
'rgb(' + Math.round(red) + ',0,0)'
|
||||
);
|
||||
|
||||
for(let i = 0; i < message.entries.length; i++) {
|
||||
message.entries[i].byteStyles = message.entries[i].byteStyles.map((style, idx) => {
|
||||
return {backgroundColor: message.byteColors[idx], ...style};
|
||||
});
|
||||
}
|
||||
|
||||
return message;
|
||||
|
||||
}
|
||||
|
||||
export default {bigEndianBitIndex,
|
||||
addCanMessage,
|
||||
createMessageSpec,
|
||||
matrixBitNumber,
|
||||
parseMessage,
|
||||
findMaxByteStateChangeCount,
|
||||
setMessageByteColors};
|
||||
|
|
|
@ -15,4 +15,4 @@ export function fromArray(arr) {
|
|||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,8 @@ export default createClassFromSpec('CanPlot', {
|
|||
},
|
||||
{"name": "videoTime"},
|
||||
{"name": "segment",
|
||||
"value": {"data": "table", "field": "xRel"}}
|
||||
"value": {"data": "table", "field": "relTime"}
|
||||
}
|
||||
],
|
||||
"data": [
|
||||
{
|
||||
|
@ -34,11 +35,11 @@ export default createClassFromSpec('CanPlot', {
|
|||
"transform": [
|
||||
{
|
||||
"type": "filter",
|
||||
"expr": "abs(datum.xRel - tipTime) <= 0.1"
|
||||
"expr": "abs(datum.relTime - tipTime) <= 0.1"
|
||||
},
|
||||
{
|
||||
"type": "aggregate",
|
||||
"fields": ["xRel", "y", "unit"],
|
||||
"fields": ["relTime", "y", "unit"],
|
||||
"ops": ["min", "argmin", "argmin"],
|
||||
"as": ["min", "argmin", "argmin"]
|
||||
}
|
||||
|
@ -50,7 +51,7 @@ export default createClassFromSpec('CanPlot', {
|
|||
"transform": [
|
||||
{
|
||||
"type": "filter",
|
||||
"expr": "length(segment) != 2 || (datum.xRel >= segment[0] && datum.xRel <= segment[1])"
|
||||
"expr": "length(segment) != 2 || (datum.relTime >= segment[0] && datum.relTime <= segment[1])"
|
||||
},
|
||||
{"type": "extent", "field": "y", "signal": "ySegment"}
|
||||
]
|
||||
|
@ -68,7 +69,7 @@ export default createClassFromSpec('CanPlot', {
|
|||
"name": "xrelscale",
|
||||
"type": "linear",
|
||||
"range": "width",
|
||||
"domain": {"data": "table", "field": "xRel"},
|
||||
"domain": {"data": "table", "field": "relTime"},
|
||||
"zero": false,
|
||||
"clamp": true,
|
||||
"domainRaw": {"signal": "segment"}
|
||||
|
@ -167,7 +168,8 @@ export default createClassFromSpec('CanPlot', {
|
|||
"interactive": true,
|
||||
"encode": {
|
||||
"update": {
|
||||
"x": {"scale": "xrelscale", "field": "xRel"},
|
||||
"interpolate": {"value": "step"},
|
||||
"x": {"scale": "xrelscale", "field": "relTime"},
|
||||
"y": {"scale": "yscale", "field": "y"}
|
||||
},
|
||||
"hover": {
|
||||
|
@ -246,7 +248,7 @@ export default createClassFromSpec('CanPlot', {
|
|||
"from": {"data": "tooltip"},
|
||||
"encode": {
|
||||
"update": {
|
||||
"x": {"scale": "xrelscale", "field": "argmin.xRel"},
|
||||
"x": {"scale": "xrelscale", "field": "argmin.relTime"},
|
||||
"y": {"scale": "yscale", "field": "argmin.y"},
|
||||
"size": {"value": 50},
|
||||
"fill": {"value": "black"}
|
||||
|
@ -260,8 +262,8 @@ export default createClassFromSpec('CanPlot', {
|
|||
"name": "tooltipGroup",
|
||||
"encode": {
|
||||
"update": {
|
||||
"x": [{"test": "inrange(datum.argmin.xRel + 80, domain('xrelscale'))", "scale": "xrelscale", "field": "argmin.xRel"},
|
||||
{"scale": "xrelscale", "field": "argmin.xRel", "offset": -80}],
|
||||
"x": [{"test": "inrange(datum.argmin.relTime + 80, domain('xrelscale'))", "scale": "xrelscale", "field": "argmin.relTime"},
|
||||
{"scale": "xrelscale", "field": "argmin.relTime", "offset": -80}],
|
||||
"y": {"scale": "yscale", "field": "argmin.y"},
|
||||
"height": {"value": 20},
|
||||
"width": {"value": 80},
|
||||
|
@ -277,7 +279,7 @@ export default createClassFromSpec('CanPlot', {
|
|||
"interactive": false,
|
||||
"encode": {
|
||||
"update": {
|
||||
"text": {"signal": "format(parent.argmin.xRel, ',.2f') + ': ' + format(parent.argmin.y, ',.2f') + ' ' + parent.argmin.unit"},
|
||||
"text": {"signal": "format(parent.argmin.relTime, ',.2f') + ': ' + format(parent.argmin.y, ',.2f') + ' ' + parent.argmin.unit"},
|
||||
"fill": {"value": "black"},
|
||||
"fontWeight": {"value": "bold"},
|
||||
"y": {"value": 20}
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
import DBC from '../models/can/dbc';
|
||||
import DbcUtils from '../utils/dbc';
|
||||
|
||||
|
||||
function processStreamedCanMessages(newCanMessages,
|
||||
prevMsgEntries,
|
||||
firstCanTime,
|
||||
dbc,
|
||||
lastBusTime,
|
||||
byteStateChangeCountsByMessage,
|
||||
maxByteStateChangeCount) {
|
||||
const messages = {};
|
||||
let lastCanTime;
|
||||
|
||||
for(let batch = 0; batch < newCanMessages.length; batch++) {
|
||||
let {time, canMessages} = newCanMessages[batch];
|
||||
canMessages = canMessages.sort((msg1, msg2) => {
|
||||
if(msg1[1] < msg2[1]) {
|
||||
return -1;
|
||||
} else if(msg1[1] > msg2[1]) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
let busTimeSum = 0;
|
||||
|
||||
for(let i = 0; i < canMessages.length; i++) {
|
||||
let [address, busTime, data, source] = canMessages[i];
|
||||
let prevBusTime;
|
||||
if(i === 0) {
|
||||
if(lastBusTime === null) {
|
||||
prevBusTime = 0;
|
||||
} else {
|
||||
prevBusTime = lastBusTime;
|
||||
}
|
||||
} else {
|
||||
prevBusTime = canMessages[i - 1][1];
|
||||
}
|
||||
|
||||
if(busTime >= prevBusTime) {
|
||||
busTimeSum += busTime - prevBusTime;
|
||||
} else {
|
||||
busTimeSum += (0x10000 - prevBusTime) + busTime;
|
||||
}
|
||||
const message = [...canMessages[i]];
|
||||
message[1] = time + busTimeSum / 500000.0;
|
||||
|
||||
if(firstCanTime === 0) {
|
||||
firstCanTime = message[1];
|
||||
}
|
||||
|
||||
const msgEntry = DbcUtils.addCanMessage(message, dbc, firstCanTime, messages, prevMsgEntries, byteStateChangeCountsByMessage);
|
||||
if(i === canMessages.length - 1) {
|
||||
lastCanTime = msgEntry.relTime;
|
||||
}
|
||||
}
|
||||
|
||||
lastBusTime = canMessages[canMessages.length - 1][1];
|
||||
const newMaxByteStateChangeCount = DbcUtils.findMaxByteStateChangeCount(messages);
|
||||
|
||||
if(newMaxByteStateChangeCount > maxByteStateChangeCount) {
|
||||
maxByteStateChangeCount = newMaxByteStateChangeCount;
|
||||
}
|
||||
|
||||
Object.keys(messages).forEach((key) => {
|
||||
messages[key] = DbcUtils.setMessageByteColors(messages[key], maxByteStateChangeCount);
|
||||
});
|
||||
}
|
||||
|
||||
self.postMessage({newMessages: messages, seekTime: lastCanTime, lastBusTime, firstCanTime, maxByteStateChangeCount});
|
||||
}
|
||||
|
||||
self.onmessage = function(e) {
|
||||
const {newCanMessages, prevMsgEntries, firstCanTime, dbcText, lastBusTime, byteStateChangeCountsByMessage, maxByteStateChangeCount} = e.data;
|
||||
const dbc = new DBC(dbcText);
|
||||
processStreamedCanMessages(newCanMessages,
|
||||
prevMsgEntries,
|
||||
firstCanTime,
|
||||
dbc,
|
||||
lastBusTime,
|
||||
byteStateChangeCountsByMessage,
|
||||
maxByteStateChangeCount);
|
||||
}
|
|
@ -9,25 +9,7 @@ import * as CanApi from '../api/can';
|
|||
|
||||
const Int64LE = require('int64-buffer').Int64LE
|
||||
|
||||
function createMessageSpec(dbc, address, id, bus) {
|
||||
const frame = dbc.messages.get(address);
|
||||
const size = frame ? frame.size : 8;
|
||||
|
||||
return {address: address,
|
||||
id: id,
|
||||
bus: bus,
|
||||
entries: [],
|
||||
frame: dbc.messages.get(address),
|
||||
byteStateChangeCounts: Array(size).fill(0)}
|
||||
}
|
||||
|
||||
function findMaxByteStateChangeCount(messages) {
|
||||
return Object.values(messages).map((m) => m.byteStateChangeCounts)
|
||||
.reduce((counts, countArr) => counts.concat(countArr), []) // flatten arrays
|
||||
.reduce((count1, count2) => count1 > count2 ? count1 : count2, 0); // find max
|
||||
}
|
||||
|
||||
async function loadCanPart(dbc, base, num, canStartTime, prevMsgEntries) {
|
||||
async function loadCanPart(dbc, base, num, canStartTime, prevMsgEntries, maxByteStateChangeCount) {
|
||||
var messages = {};
|
||||
const {times,
|
||||
sources,
|
||||
|
@ -44,7 +26,7 @@ async function loadCanPart(dbc, base, num, canStartTime, prevMsgEntries) {
|
|||
|
||||
var addressNum = address.toNumber();
|
||||
var data = datas.slice(i*8, (i+1)*8);
|
||||
if (messages[id] === undefined) messages[id] = createMessageSpec(dbc, address.toNumber(), id, src);
|
||||
if (messages[id] === undefined) messages[id] = DbcUtils.createMessageSpec(dbc, address.toNumber(), id, src);
|
||||
|
||||
const prevMsgEntry = messages[id].entries.length > 0 ?
|
||||
messages[id].entries[messages[id].entries.length - 1]
|
||||
|
@ -65,15 +47,23 @@ async function loadCanPart(dbc, base, num, canStartTime, prevMsgEntries) {
|
|||
messages[id].entries.push(msgEntry);
|
||||
}
|
||||
|
||||
const maxByteStateChangeCount = findMaxByteStateChangeCount(messages);
|
||||
const newMaxByteStateChangeCount = DbcUtils.findMaxByteStateChangeCount(messages);
|
||||
if(newMaxByteStateChangeCount > maxByteStateChangeCount) {
|
||||
maxByteStateChangeCount = newMaxByteStateChangeCount;
|
||||
}
|
||||
|
||||
Object.keys(messages).forEach((key) => {
|
||||
messages[key] = DbcUtils.setMessageByteColors(messages[key], maxByteStateChangeCount);
|
||||
});
|
||||
|
||||
self.postMessage({newMessages: messages,
|
||||
maxByteStateChangeCount});
|
||||
self.close();
|
||||
}
|
||||
|
||||
self.onmessage = function(e) {
|
||||
const {dbcText, base, num, canStartTime, prevMsgEntries} = e.data;
|
||||
const {dbcText, base, num, canStartTime, prevMsgEntries, maxByteStateChangeCount} = e.data;
|
||||
|
||||
const dbc = new DBC(dbcText);
|
||||
loadCanPart(dbc, base, num, canStartTime, prevMsgEntries);
|
||||
loadCanPart(dbc, base, num, canStartTime, prevMsgEntries, maxByteStateChangeCount);
|
||||
}
|
||||
|
|
|
@ -11,17 +11,21 @@ function reparseEntry(entry, address, dbc, canStartTime, prevMsgEntry) {
|
|||
}
|
||||
|
||||
self.onmessage = function(e) {
|
||||
const {message, dbcText, canStartTime} = e.data;
|
||||
const {messages, dbcText, canStartTime} = e.data;
|
||||
const dbc = new DBC(dbcText);
|
||||
for(var i = 0; i < message.entries.length; i++) {
|
||||
const entry = message.entries[i];
|
||||
const prevMsgEntry = i > 0 ? message.entries[i - 1] : null;
|
||||
Object.keys(messages).forEach((messageId) => {
|
||||
const message = messages[messageId];
|
||||
for(var i = 0; i < message.entries.length; i++) {
|
||||
const entry = message.entries[i];
|
||||
const prevMsgEntry = i > 0 ? message.entries[i - 1] : null;
|
||||
|
||||
const {msgEntry} = reparseEntry(entry, message.address, dbc, canStartTime, prevMsgEntry);
|
||||
const {msgEntry} = reparseEntry(entry, message.address, dbc, canStartTime, prevMsgEntry);
|
||||
|
||||
message.entries[i] = msgEntry;
|
||||
}
|
||||
message.entries[i] = msgEntry;
|
||||
}
|
||||
messages[messageId] = message;
|
||||
});
|
||||
|
||||
self.postMessage(message);
|
||||
self.postMessage(messages);
|
||||
self.close();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue