cabana/src/CanExplorer.js

669 lines
24 KiB
JavaScript
Raw Normal View History

2017-08-03 15:41:52 -06:00
import React, { Component } from 'react';
2017-06-13 18:40:05 -06:00
import Moment from 'moment';
import PropTypes from 'prop-types';
2017-08-03 15:41:52 -06:00
import {USE_UNLOGGER, PART_SEGMENT_LENGTH, STREAMING_WINDOW} from './config';
2017-06-16 23:28:51 -06:00
import * as GithubAuth from './api/github-auth';
import cx from 'classnames';
import Modal from './components/Modals/baseModal';
2017-06-13 18:40:05 -06:00
import DBC from './models/can/dbc';
import Meta from './components/meta';
import Explorer from './components/explorer';
2017-06-13 18:40:05 -06:00
import * as Routes from './api/routes';
2017-08-03 15:41:52 -06:00
import OnboardingModal from './components/Modals/OnboardingModal';
2017-06-13 18:40:05 -06:00
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');
2017-08-03 15:41:52 -06:00
const CanStreamerWorker = require('./workers/CanStreamerWorker.worker.js');
import debounce from './utils/debounce';
import EditMessageModal from './components/EditMessageModal';
2017-06-27 17:28:42 -06:00
import LoadingBar from './components/LoadingBar';
2017-08-03 15:41:52 -06:00
import {persistDbc, fetchPersistedDbc} from './api/localstorage';
import OpenDbc from './api/opendbc';
import UnloggerClient from './api/unlogger';
2017-08-03 15:41:52 -06:00
import PandaReader from './api/panda-reader';
import * as ObjectUtils from './utils/object';
import {hash} from './utils/string';
2017-08-03 15:41:52 -06:00
import DbcUtils from './utils/dbc';
export default class CanExplorer extends Component {
static propTypes = {
dongleId: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
dbc: PropTypes.instanceOf(DBC),
dbcFilename: PropTypes.string,
2017-07-05 16:57:03 -06:00
githubAuthToken: PropTypes.string,
autoplay: PropTypes.bool,
max: PropTypes.number,
url: PropTypes.string,
};
constructor(props) {
super(props);
this.state = {
messages: {},
selectedMessages: [],
2017-08-03 15:41:52 -06:00
route: null,
2017-06-13 18:40:05 -06:00
canFrameOffset: -1,
firstCanTime: 0,
2017-08-03 15:41:52 -06:00
lastBusTime: null,
selectedMessage: null,
currentParts: [0,0],
2017-08-03 15:41:52 -06:00
showOnboarding: false,
2017-06-13 18:40:05 -06:00
showLoadDbc: false,
showSaveDbc: false,
showEditMessageModal: false,
editMessageModalMessage: null,
2017-08-03 15:41:52 -06:00
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,
2017-06-27 17:28:42 -06:00
maxByteStateChangeCount: 0,
isLoading: true,
partsLoaded: 0,
spawnWorkerHash: null,
2017-08-03 15:41:52 -06:00
attemptingPandaConnection: false,
pandaNoDeviceSelected: false,
};
this.openDbcClient = new OpenDbc(props.githubAuthToken);
if(USE_UNLOGGER) {
this.unloggerClient = new UnloggerClient();
}
2017-08-03 15:41:52 -06:00
this.pandaReader = new PandaReader();
this.showOnboarding = this.showOnboarding.bind(this);
this.hideOnboarding = this.hideOnboarding.bind(this);
2017-06-13 18:40:05 -06:00
this.showLoadDbc = this.showLoadDbc.bind(this);
this.hideLoadDbc = this.hideLoadDbc.bind(this);
this.showSaveDbc = this.showSaveDbc.bind(this);
this.hideSaveDbc = this.hideSaveDbc.bind(this);
this.showEditMessageModal = this.showEditMessageModal.bind(this);
this.hideEditMessageModal = this.hideEditMessageModal.bind(this);
2017-06-13 18:40:05 -06:00
this.onDbcSelected = this.onDbcSelected.bind(this);
this.onDbcSaved = this.onDbcSaved.bind(this);
this.onConfirmedSignalChange = this.onConfirmedSignalChange.bind(this);
this.onPartChange = this.onPartChange.bind(this);
2017-06-20 17:46:07 -06:00
this.onMessageFrameEdited = this.onMessageFrameEdited.bind(this);
this.onSeek = this.onSeek.bind(this);
this.onUserSeek = this.onUserSeek.bind(this);
this.onMessageSelected = this.onMessageSelected.bind(this);
this.onMessageUnselected = this.onMessageUnselected.bind(this);
this.initCanData = this.initCanData.bind(this);
this.updateSelectedMessages = this.updateSelectedMessages.bind(this);
2017-08-03 15:41:52 -06:00
this.handlePandaConnect = this.handlePandaConnect.bind(this);
this.processStreamedCanMessages = this.processStreamedCanMessages.bind(this);
this.onStreamedCanMessagesProcessed = this.onStreamedCanMessagesProcessed.bind(this);
this.showingModal = this.showingModal.bind(this);
2017-08-03 15:41:52 -06:00
this.lastMessageEntriesById = this.lastMessageEntriesById.bind(this);
}
2017-06-13 18:40:05 -06:00
componentWillMount() {
2017-08-03 15:41:52 -06:00
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)]};
this.setState(newState, this.initCanData);
} else {
this.showOnboarding();
}
2017-08-03 15:41:52 -06:00
});
} else {
this.showOnboarding();
}
}
initCanData() {
const {route} = this.state;
const offsetFinder = new CanOffsetFinder();
offsetFinder.postMessage({partCount: route.proclog,
base: route.url});
offsetFinder.onmessage = (e) => {
const {canFrameOffset, firstCanTime} = e.data;
this.setState({canFrameOffset, firstCanTime}, () => {
this.spawnWorker(this.state.currentParts);
});
};
}
onDbcSelected(dbcFilename, dbc) {
2017-08-03 15:41:52 -06:00
const {route, messages} = this.state;
2017-06-13 18:40:05 -06:00
this.hideLoadDbc();
2017-08-03 15:41:52 -06:00
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);
});
} else {
persistDbc('live', {dbcFilename, dbc});
2017-06-13 18:40:05 -06:00
2017-08-03 15:41:52 -06:00
this.setState({dbc, dbcFilename, dbcText: dbc.text(), messages: {}});
}
}
2017-06-13 18:40:05 -06:00
onDbcSaved(dbcFilename) {
const dbcLastSaved = Moment();
this.setState({dbcLastSaved, dbcFilename})
this.hideSaveDbc();
}
2017-08-03 15:41:52 -06:00
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}
2017-06-27 17:28:42 -06:00
if(!this.state.isLoading) {
this.setState({isLoading: true});
}
const [minPart, maxPart] = parts;
let part = minPart, prevMsgEntries = {}, prepend = false, spawnWorkerHash;
if(options) {
if(options.part) part = options.part;
if(options.prevMsgEntries) prevMsgEntries = options.prevMsgEntries;
if(options.spawnWorkerHash) {
spawnWorkerHash = options.spawnWorkerHash;
}
}
if(!spawnWorkerHash) {
spawnWorkerHash = hash(Math.random().toString(16));
this.setState({spawnWorkerHash});
}
if(part === minPart) {
this.setState({partsLoaded: 0});
}
2017-08-03 15:41:52 -06:00
const {dbc, dbcFilename, route, firstCanTime, canFrameOffset, maxByteStateChangeCount} = this.state;
2017-06-13 18:40:05 -06:00
var worker = new CanFetcher();
worker.onmessage = (e) => {
if(spawnWorkerHash !== this.state.spawnWorkerHash) {
// Parts changed, stop spawning workers.
return;
}
2017-06-16 23:28:51 -06:00
if(this.state.dbcFilename != dbcFilename) {
// DBC changed while this worker was running
// -- don't update messages and halt recursion.
return;
}
2017-06-13 18:40:05 -06:00
2017-08-03 15:41:52 -06:00
let {newMessages, maxByteStateChangeCount} = e.data;
if(maxByteStateChangeCount > this.state.maxByteStateChangeCount) {
this.setState({maxByteStateChangeCount});
2017-08-03 15:41:52 -06:00
} else {
maxByteStateChangeCount = this.state.maxByteStateChangeCount;
}
2017-08-03 15:41:52 -06:00
const messages = this.addAndRehydrateMessages(newMessages, maxByteStateChangeCount);
const prevMsgEntries = {};
for(let key in newMessages) {
const msg = newMessages[key];
2017-08-03 15:41:52 -06:00
prevMsgEntries[key] = msg.entries[msg.entries.length - 1];
}
2017-08-03 15:41:52 -06:00
2017-06-13 18:40:05 -06:00
this.setState({messages,
partsLoaded: this.state.partsLoaded + 1}, () => {
if(part < maxPart) {
this.spawnWorker(parts, {part: part + 1, prevMsgEntries, spawnWorkerHash, prepend});
2017-06-27 17:28:42 -06:00
} else {
this.setState({isLoading: false});
2017-06-13 18:40:05 -06:00
}
})
2017-06-13 18:40:05 -06:00
}
2017-06-16 23:28:51 -06:00
worker.postMessage({dbcText: dbc.text(),
base: route.url,
2017-06-13 18:40:05 -06:00
num: part,
canStartTime: firstCanTime - canFrameOffset,
2017-08-03 15:41:52 -06:00
prevMsgEntries,
maxByteStateChangeCount
});
}
2017-06-13 18:40:05 -06:00
showingModal() {
const {
2017-08-03 15:41:52 -06:00
showOnboarding,
showLoadDbc,
showSaveDbc,
showAddSignal,
showEditMessageModal,
} = this.state;
2017-08-03 15:41:52 -06:00
return showOnboarding || showLoadDbc || showSaveDbc || showAddSignal || showEditMessageModal;
}
showOnboarding() {
this.setState({ showOnboarding: true });
}
hideOnboarding() {
this.setState({ showOnboarding: false });
}
2017-06-13 18:40:05 -06:00
showLoadDbc() {
this.setState({showLoadDbc: true});
}
hideLoadDbc() {
this.setState({showLoadDbc: false});
}
showSaveDbc() {
this.setState({showSaveDbc: true})
}
hideSaveDbc() {
this.setState({showSaveDbc: false})
}
2017-08-03 15:41:52 -06:00
reparseMessages(messages) {
this.setState({isLoading: true});
2017-06-13 18:40:05 -06:00
2017-08-03 15:41:52 -06:00
const {dbc} = this.state;
2017-06-13 18:40:05 -06:00
var worker = new MessageParser();
worker.onmessage = (e) => {
2017-08-03 15:41:52 -06:00
let messages = e.data;
messages = this.addAndRehydrateMessages(messages, {replace: true});
2017-06-13 18:40:05 -06:00
2017-06-27 17:28:42 -06:00
this.setState({messages, isLoading: false})
2017-06-13 18:40:05 -06:00
}
2017-08-03 15:41:52 -06:00
worker.postMessage({messages,
2017-06-13 18:40:05 -06:00
dbcText: dbc.text(),
canStartTime: this.state.firstCanTime});
}
2017-08-03 15:41:52 -06:00
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);
}, 500);
onPartChange(part) {
let {currentParts, partsLoaded, canFrameOffset, route, messages} = this.state;
2017-07-16 21:19:17 -06:00
if(canFrameOffset === -1 || part + PART_SEGMENT_LENGTH >= route.proclog) {
return
}
// determine new parts to load, whether to prepend or append
const currentPartSpan = currentParts[1] - currentParts[0] + 1;
// update current parts
currentParts = [part, part + currentPartSpan - 1];
// update messages to only preserve entries in new part range
const messagesKvPairs = Object.entries(messages)
.map(([messageId, message]) =>
[messageId, {...message,
entries: []
}
]);
messages = ObjectUtils.fromArray(messagesKvPairs);
// update state then load new parts
this.setState({currentParts, messages, seekTime: part * 60}, this.partChangeDebounced);
}
showEditMessageModal(msgKey) {
2017-06-20 17:46:07 -06:00
const msg = this.state.messages[msgKey];
if(!msg.frame) {
msg.frame = this.state.dbc.createFrame(msg.address);
}
this.setState({showEditMessageModal: true,
2017-06-20 17:46:07 -06:00
editMessageModalMessage: msgKey,
2017-08-03 15:41:52 -06:00
messages: this.state.messages,
dbcText: dbc.text()});
}
hideEditMessageModal() {
this.setState({showEditMessageModal: false});
}
2017-06-20 17:46:07 -06:00
onMessageFrameEdited(messageFrame) {
const {messages,
route,
dbcFilename,
dbc,
editMessageModalMessage} = this.state;
const message = Object.assign({}, messages[editMessageModalMessage]);
message.frame = messageFrame;
2017-07-04 19:21:36 -06:00
dbc.messages.set(messageFrame.id, messageFrame);
persistDbc(route.fullname,
{dbcFilename, dbc});
2017-06-20 17:46:07 -06:00
messages[editMessageModalMessage] = message;
2017-08-03 15:41:52 -06:00
this.setState({messages, dbc, dbcText: dbc.text()});
this.hideEditMessageModal();
}
onSeek(seekIndex, seekTime) {
this.setState({seekIndex, seekTime});
}
onUserSeek(seekTime) {
if(USE_UNLOGGER) {
this.unloggerClient.seek(this.props.dongleId, this.props.name, seekTime);
}
const msg = this.state.messages[this.state.selectedMessage];
let seekIndex;
if(msg) {
seekIndex = msg.entries.findIndex((e) => e.relTime >= seekTime);
if(seekIndex === -1) {
seekIndex = 0
}
} else {
seekIndex = 0
}
this.setState({seekIndex, seekTime});
}
onMessageSelected(msgKey) {
let {seekTime, seekIndex, messages} = this.state;
const msg = messages[msgKey];
if(seekTime > 0 && msg.entries.length > 0) {
seekIndex = msg.entries.findIndex((e) => e.relTime >= seekTime);
if(seekIndex === -1) {
seekIndex = 0;
}
seekTime = msg.entries[seekIndex].relTime;
}
this.setState({seekTime, seekIndex, selectedMessage: msgKey});
}
updateSelectedMessages(selectedMessages) {
this.setState({selectedMessages});
}
onMessageUnselected(msgKey) {
this.setState({selectedMessage: null});
}
2017-06-30 22:27:04 -06:00
loginWithGithub() {
2017-08-03 15:41:52 -06:00
const {route} = this.state;
return (
2017-08-03 15:41:52 -06:00
<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>
</a>
)
2017-06-30 22:27:04 -06:00
}
2017-08-03 15:41:52 -06:00
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() }) }>
{this.state.isLoading ?
<LoadingBar
isLoading={this.state.isLoading}
/> : null}
<div className='cabana-header'>
<a className='cabana-header-logo' href=''>Comma Cabana</a>
<div className='cabana-header-account'>
{this.props.githubAuthToken ?
<p>GitHub Authenticated</p>
: this.loginWithGithub()
}
</div>
</div>
<div className='cabana-window'>
2017-08-03 15:41:52 -06:00
<Meta url={this.state.route ? this.state.route.url : null}
messages={this.state.messages}
selectedMessages={this.state.selectedMessages}
updateSelectedMessages={this.updateSelectedMessages}
showEditMessageModal={this.showEditMessageModal}
currentParts={this.state.currentParts}
onMessageSelected={this.onMessageSelected}
onMessageUnselected={this.onMessageUnselected}
showLoadDbc={this.showLoadDbc}
showSaveDbc={this.showSaveDbc}
dbcFilename={this.state.dbcFilename}
dbcLastSaved={this.state.dbcLastSaved}
dongleId={this.props.dongleId}
name={this.props.name}
route={this.state.route}
seekTime={this.state.seekTime}
2017-08-03 15:41:52 -06:00
seekIndex={this.state.seekIndex}
maxByteStateChangeCount={this.state.maxByteStateChangeCount}
isDemo={this.props.isDemo}
2017-08-03 15:41:52 -06:00
live={this.state.live}
/>
2017-08-03 15:41:52 -06:00
{this.state.route || this.state.live ?
<Explorer
2017-08-03 15:41:52 -06:00
url={this.state.route ? this.state.route.url : null}
live={this.state.live}
messages={this.state.messages}
selectedMessage={this.state.selectedMessage}
onConfirmedSignalChange={this.onConfirmedSignalChange}
onSeek={this.onSeek}
onUserSeek={this.onUserSeek}
canFrameOffset={this.state.canFrameOffset}
firstCanTime={this.state.firstCanTime}
seekTime={this.state.seekTime}
seekIndex={this.state.seekIndex}
currentParts={this.state.currentParts}
partsLoaded={this.state.partsLoaded}
autoplay={this.props.autoplay}
showEditMessageModal={this.showEditMessageModal}
onPartChange={this.onPartChange}
route={this.state.route}
2017-08-03 15:41:52 -06:00
partsCount={this.state.route ? this.state.route.proclog : 0}
/>
: null}
</div>
2017-08-03 15:41:52 -06:00
{ this.state.showOnboarding ?
<OnboardingModal
handlePandaConnect={ this.handlePandaConnect }
attemptingPandaConnection= { this.state.attemptingPandaConnection }
/> : null }
{this.state.showLoadDbc ?
<LoadDbcModal
onDbcSelected={this.onDbcSelected}
handleClose={this.hideLoadDbc}
openDbcClient={this.openDbcClient}
loginWithGithub={this.loginWithGithub()}
/> : null}
{this.state.showSaveDbc ?
<SaveDbcModal
dbc={this.state.dbc}
sourceDbcFilename={this.state.dbcFilename}
onDbcSaved={this.onDbcSaved}
handleClose={this.hideSaveDbc}
openDbcClient={this.openDbcClient}
hasGithubAuth={this.props.githubAuthToken !== null}
loginWithGithub={this.loginWithGithub()}
/> : null}
{this.state.showEditMessageModal ?
<EditMessageModal
handleClose={this.hideEditMessageModal}
handleSave={this.onMessageFrameEdited}
message={this.state.messages[this.state.editMessageModalMessage]}
/> : null}
2017-08-03 15:41:52 -06:00
</div>
);
}
}