import React, { Component } from 'react'; import Moment from 'moment'; import PropTypes from 'prop-types'; import cx from 'classnames'; import { createWriteStream } from 'streamsaver'; import Panda from '@commaai/pandajs'; import CommaAuth, { storage as CommaAuthStorage, config as AuthConfig } from '@commaai/my-comma-auth'; import { raw as RawDataApi, drives as DrivesApi } from '@commaai/comma-api'; import { timeout, interval } from 'thyming'; import { USE_UNLOGGER, PART_SEGMENT_LENGTH, STREAMING_WINDOW, GITHUB_AUTH_TOKEN_KEY } from './config'; import * as GithubAuth from './api/github-auth'; import DBC from './models/can/dbc'; import Meta from './components/Meta'; import Explorer from './components/Explorer'; import OnboardingModal from './components/Modals/OnboardingModal'; import SaveDbcModal from './components/SaveDbcModal'; import LoadDbcModal from './components/LoadDbcModal'; import debounce from './utils/debounce'; import EditMessageModal from './components/EditMessageModal'; import LoadingBar from './components/LoadingBar'; import { persistDbc, fetchPersistedDbc, unpersistGithubAuthToken } from './api/localstorage'; import OpenDbc from './api/OpenDbc'; import UnloggerClient from './api/unlogger'; import { hash } from './utils/string'; import { modifyQueryParameters } from './utils/url'; import DbcUtils from './utils/dbc'; import { demoLogUrls, demoRoute } from './demo'; const RLogDownloader = require('./workers/rlog-downloader.worker'); const LogCSVDownloader = require('./workers/dbc-csv-downloader.worker'); const MessageParser = require('./workers/message-parser.worker'); const CanStreamerWorker = require('./workers/CanStreamerWorker.worker'); const dataCache = {}; export default class CanExplorer extends Component { constructor(props) { super(props); this.state = { messages: {}, thumbnails: [], selectedMessages: [], route: null, canFrameOffset: 0, routeInitTime: 0, firstFrameTime: 0, firstCanTime: null, lastBusTime: null, selectedMessage: null, currentParts: [0, 0], currentPart: 0, currentWorkers: {}, loadingParts: [], loadedParts: [], showOnboarding: false, showLoadDbc: false, showSaveDbc: false, showEditMessageModal: false, editMessageModalMessage: null, 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: props.seekTime || 0, seekIndex: 0, maxByteStateChangeCount: 0, isLoading: true, partsLoaded: 0, spawnWorkerHash: null, attemptingPandaConnection: false, pandaNoDeviceSelected: false, live: false, isGithubAuthenticated: props.githubAuthToken !== null && props.githubAuthToken !== undefined, shareUrl: null, logUrls: null }; this.openDbcClient = new OpenDbc(props.githubAuthToken); if (USE_UNLOGGER) { this.unloggerClient = new UnloggerClient(); } 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); this.hideSaveDbc = this.hideSaveDbc.bind(this); this.showEditMessageModal = this.showEditMessageModal.bind(this); this.hideEditMessageModal = this.hideEditMessageModal.bind(this); this.onDbcSelected = this.onDbcSelected.bind(this); this.onDbcSaved = this.onDbcSaved.bind(this); this.onConfirmedSignalChange = this.onConfirmedSignalChange.bind(this); this.onPartChange = this.onPartChange.bind(this); 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); 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); this.githubSignOut = this.githubSignOut.bind(this); this.downloadLogAsCSV = this.downloadLogAsCSV.bind(this); this.pandaReader = new Panda(); this.pandaReader.onMessage(this.processStreamedCanMessages); } componentDidMount() { this.dataCacheTimer = interval(() => { const { currentParts } = this.state; let { loadedParts } = this.state; if (this.loadMessagesFromCacheRunning || loadedParts.length < 4) { return; } loadedParts.forEach((part) => { if (part >= currentParts[0] && part <= currentParts[1]) { return; } if (Date.now() - dataCache[part].lastUsed > 3 * 60 * 1000) { console.log('Decaching part', part); loadedParts = loadedParts.filter((p) => p !== part); this.setState({ loadedParts }, () => { delete dataCache[part]; }); } }); }, 10000); const { dongleId, name } = this.props; if (CommaAuth.isAuthenticated() && !name) { this.showOnboarding(); } else if (this.props.isDemo) { // is demo! const logUrls = demoLogUrls; const route = demoRoute; this.setState({ logUrls, route, currentParts: [0, 2], currentPart: 0 }, this.initCanData); } else if ( this.props.max && this.props.url && !this.props.exp && !this.props.sig ) { // legacy share? maybe dead code const { max, url } = this.props; const startTime = Moment(name, 'YYYY-MM-DD--H-m-s'); const route = { fullname: `${dongleId}|${name}`, proclog: max, url, start_time: startTime }; this.setState( { route, currentParts: [0, Math.min(max, PART_SEGMENT_LENGTH - 1)] }, this.initCanData ); } else if (dongleId && name) { const routeName = `${dongleId}|${name}`; let urlPromise; let logUrlsPromise; if (this.props.url) { urlPromise = Promise.resolve(this.props.url); } else { urlPromise = DrivesApi.getRouteInfo(routeName).then((route) => route.url); } if (this.props.sig && this.props.exp) { logUrlsPromise = RawDataApi.getLogUrls(routeName, { sig: this.props.sig, exp: this.props.exp }); } else { logUrlsPromise = RawDataApi.getLogUrls(routeName); } Promise.all([urlPromise, logUrlsPromise]) .then((initData) => { const [url, logUrls] = initData; const newState = { route: { fullname: routeName, proclog: logUrls.length - 1, start_time: Moment(name, 'YYYY-MM-DD--H-m-s'), url }, currentParts: [ 0, Math.min(logUrls.length - 1, PART_SEGMENT_LENGTH - 1) ], logUrls }; this.setState(newState, this.initCanData); DrivesApi.getShareSignature(routeName).then((shareSignature) => this.setState({ shareUrl: modifyQueryParameters({ add: { exp: shareSignature.exp, sig: shareSignature.sig, max: logUrls.length - 1, url }, remove: [GITHUB_AUTH_TOKEN_KEY] }) })); }) .catch((err) => { console.log(err); CommaAuthStorage.logOut().then(() => { CommaAuthStorage.isAuthed = false; this.showOnboarding(); }); }); } else { this.showOnboarding(); } } componentWillUnmount() { if (this.dataCacheTimer) { this.dataCacheTimer(); } } initCanData() { this.spawnWorker(this.state.currentParts); } onDbcSelected(dbcFilename, dbc) { const { route } = this.state; this.hideLoadDbc(); dbc.lastUpdated = Date.now(); this.persistDbc({ dbcFilename, dbc }); if (route) { this.setState( { dbc, dbcFilename, dbcText: dbc.text(), partsLoaded: 0, selectedMessage: null, messages: {} }, () => { this.loadMessagesFromCache(); } ); } else { this.setState({ dbc, dbcFilename, dbcText: dbc.text(), messages: {} }); } } onDbcSaved(dbcFilename) { const dbcLastSaved = Moment(); this.setState({ dbcLastSaved, dbcFilename }); this.hideSaveDbc(); } // async downloadDbcFile() { // const blob = new Blob([this.props.dbc.text()], {type: "text/plain;charset=utf-8"}); // const filename = this.state.dbcFilename.replace(/\.dbc/g, '') + '.dbc'; // FileSaver.saveAs(blob, filename, true); // } downloadLogAsCSV() { console.log('downloadLogAsCSV:start'); const { dbcFilename } = this.state; const fileStream = createWriteStream( `${dbcFilename.replace(/\.dbc/g, '-')}${+new Date()}.csv` ); const writer = fileStream.getWriter(); const encoder = new TextEncoder(); if (this.state.live) { return this.downloadLiveLogAsCSV(dataHandler); } return this.downloadRawLogAsCSV(dataHandler); function dataHandler(e) { const { logData, shouldClose, progress } = e.data; if (shouldClose) { console.log('downloadLogAsCSV:close'); writer.close(); return; } console.log('CSV export progress:', progress); const uint8array = encoder.encode(`${logData}\n`); writer.write(uint8array); } } downloadRawLogAsCSV(handler) { return this.downloadLiveLogAsCSV(handler); } downloadLiveLogAsCSV(handler) { // Trigger processing of in-memory data in worker // this method *could* just fetch the data needed for the worked, but // eventually this might be in it's own worker instead of the shared one const { firstCanTime, canFrameOffset } = this.state; const worker = new LogCSVDownloader(); worker.onmessage = handler; worker.postMessage({ data: Object.keys(this.state.messages).map((sourceId) => { const source = this.state.messages[sourceId]; return { id: source.id, bus: source.bus, address: source.address, entries: source.entries.slice() }; }), canStartTime: firstCanTime - canFrameOffset }); } mergeThumbnails(newThumbnails) { const { thumbnails } = this.state; if (!newThumbnails || !newThumbnails.length) { return thumbnails; } if (!thumbnails.length) { return newThumbnails; } let oldIndex = 0; let newIndex = 0; // is old immediately after new? if (newThumbnails[0].monoTime > thumbnails[thumbnails.length - 1]) { return thumbnails.concat(newThumbnails); } // is new immediately after old? if (newThumbnails[newThumbnails.length - 1] < thumbnails[0]) { return newThumbnails.concat(thumbnails); } let result = []; while (oldIndex < thumbnails.length && newIndex < newThumbnails.length) { if (thumbnails[oldIndex].monoTime < newThumbnails[newIndex].monoTime) { result.push(thumbnails[oldIndex]); oldIndex += 1; } else { result.push(newThumbnails[newIndex]); newIndex += 1; } } if (oldIndex < thumbnails.length) { result = result.concat(thumbnails.slice(oldIndex)); } else if (newIndex < newThumbnails.length) { result = result.concat(newThumbnails.slice(newIndex)); } return result; } cancelWorker(workerHash) { // actually don't... return; } spawnWorker(options) { let { currentParts, currentWorkers, loadingParts } = this.state; console.log('Checking worker for', currentParts); if (!this.state.isLoading) { this.setState({ isLoading: true }); } if (loadingParts.length > 1) { // only 2 workers at a time pls return; } const [minPart, maxPart] = currentParts; // updated worker list (post canceling, and this time a copy) currentWorkers = { ...this.state.currentWorkers }; const { loadedParts, currentPart } = this.state; let part = -1; const allWorkerParts = loadingParts.concat(loadedParts); for (let partOffset = 0; partOffset <= maxPart - minPart; ++partOffset) { let tempPart = currentPart + partOffset; if (tempPart > maxPart) { tempPart = minPart + ((tempPart - minPart) % (maxPart - minPart + 1)); } if (allWorkerParts.indexOf(tempPart) === -1) { part = tempPart; break; } } if (part === -1) { console.log('Loading complete'); this.setState({ isLoading: false }); return; } console.log('Starting worker for part', part); // options is object of {part, prevMsgEntries, spawnWorkerHash, prepend} options = options || {}; let { prevMsgEntries } = options; const prepend = false; const { dbc, route, firstCanTime, canFrameOffset } = this.state; let { maxByteStateChangeCount } = this.state; if (!prevMsgEntries) { // we have previous messages loaded const { messages } = this.state; prevMsgEntries = {}; Object.keys(messages).forEach((key) => { const { entries } = messages[key]; prevMsgEntries[key] = entries[entries.length - 1]; }); } // var worker = new CanFetcher(); const worker = new RLogDownloader(); const spawnWorkerHash = hash(Math.random().toString(16)); currentWorkers[spawnWorkerHash] = { part, worker }; loadingParts = [part, ...loadingParts]; this.setState({ currentWorkers, loadingParts }); worker.onmessage = (e) => { if (this.state.currentWorkers[spawnWorkerHash] === undefined) { console.log('Worker was canceled'); return; } maxByteStateChangeCount = e.data.maxByteStateChangeCount; const { newMessages, newThumbnails, isFinished, routeInitTime, firstFrameTime, } = e.data; if (maxByteStateChangeCount > this.state.maxByteStateChangeCount) { this.setState({ maxByteStateChangeCount }); } else { maxByteStateChangeCount = this.state.maxByteStateChangeCount; } if (routeInitTime !== this.state.routeInitTime) { this.setState({ routeInitTime }); } if (firstFrameTime && firstFrameTime !== this.state.firstFrameTime) { this.setState({ firstFrameTime }); } this.addMessagesToDataCache(part, newMessages, newThumbnails); // const messages = this.addAndRehydrateMessages( // newMessages, // maxByteStateChangeCount // ); // const prevMsgEntries = {}; // Object.keys(newMessages).forEach((key) => { // prevMsgEntries[key] = newMessages[key].entries[newMessages[key].entries.length - 1]; // }); // const thumbnails = this.mergeThumbnails(newThumbnails); if (isFinished) { const loadingParts = this.state.loadingParts.filter((p) => p !== part); const loadedParts = [part, ...this.state.loadedParts]; this.setState( { partsLoaded: this.state.partsLoaded + 1, loadingParts, loadedParts }, () => { this.spawnWorker({ prevMsgEntries, spawnWorkerHash, prepend }); if (window.dataCallback) { window.dataCallback(); window.dataCallback = null; } } ); } }; worker.postMessage({ // old stuff for reverse compatibility for easier testing base: route.url, num: part, // so that we don't try to read metadata about it... isDemo: this.props.isDemo, isLegacyShare: this.props.isLegacyShare, logUrls: this.state.logUrls, // data that is used dbcText: dbc.text(), route: route.fullname, part, canStartTime: firstCanTime != null ? firstCanTime - canFrameOffset : null, prevMsgEntries, maxByteStateChangeCount }); } addAndRehydrateMessages(newMessages, options) { // Adds new message entries to messages state // and "rehydrates" ES6 classes (message frame) // lost from JSON serialization in webworker data cloning. // handles merging the data in correct order options = options || {}; const messages = { ...this.state.messages }; Object.keys(newMessages).forEach((key) => { // add message if (options.replace !== true && key in messages) { // should merge here instead of concat // assumes messages are always sequential const msgEntries = messages[key].entries; const newMsgEntries = newMessages[key].entries; const msgLength = msgEntries.length; const newMsgLength = newMsgEntries.length; const entryLength = msgLength + newMsgLength; messages[key] = { ...messages[key], entries: Array(entryLength) }; let msgIndex = 0; let newMsgIndex = 0; for (let i = 0; i < entryLength; ++i) { if (newMsgIndex >= newMsgLength) { messages[key].entries[i] = msgEntries[msgIndex++]; } else if (msgIndex >= msgLength) { messages[key].entries[i] = newMsgEntries[newMsgIndex++]; } else if ( msgEntries[msgIndex].relTime <= newMsgEntries[newMsgIndex].relTime ) { messages[key].entries[i] = msgEntries[msgIndex++]; } else if ( msgEntries[msgIndex].relTime >= newMsgEntries[newMsgIndex].relTime ) { messages[key].entries[i] = newMsgEntries[newMsgIndex++]; } } messages[key].byteStateChangeCounts = newMessages[key].byteStateChangeCounts; } else { messages[key] = newMessages[key]; messages[key].frame = this.state.dbc.getMessageFrame( messages[key].address ); } }); const maxByteStateChangeCount = DbcUtils.findMaxByteStateChangeCount( messages ); this.setState({ maxByteStateChangeCount }); Object.keys(messages).forEach((key) => { // console.log(key); messages[key] = DbcUtils.setMessageByteColors( messages[key], maxByteStateChangeCount ); }); return messages; } async addMessagesToDataCache(part, newMessages, newThumbnails) { const { dbc, currentParts } = this.state; const entry = await this.getParseSegment(part); if (!entry) { // first chunk of data returned from this segment Object.keys(newMessages).forEach((key) => { newMessages[key] = this.parseMessageEntry(newMessages[key], dbc); }); dataCache[part] = { messages: newMessages, thumbnails: newThumbnails, lastUpdated: Date.now(), lastUsed: Date.now() }; if (part >= currentParts[0] && part <= currentParts[1]) { this.setState({ messages: this.addAndRehydrateMessages(newMessages) }); } return; } entry.lastUsed = Date.now(); // data is always append only, and always per segment Object.keys(newMessages).forEach((key) => { let msgs = newMessages[key]; if (!dataCache[part].messages[key]) { msgs = this.parseMessageEntry(msgs, dbc); dataCache[part].messages[key] = msgs; } else { let { entries } = dataCache[part].messages[key]; const lastEntry = entries.length ? entries[entries.length - 1] : null; msgs = this.parseMessageEntry(msgs, dbc, lastEntry); entries = entries.concat(msgs.entries); dataCache[part].messages[key].entries = entries; } newMessages[key] = msgs; }); dataCache[part].thumbnails = dataCache[part].thumbnails.concat(newThumbnails); if (part >= currentParts[0] && part <= currentParts[1]) { this.setState({ messages: this.addAndRehydrateMessages(newMessages) }); } } async loadMessagesFromCache() { // create a new messages object for state if (this.loadMessagesFromCacheRunning) { if (!this.loadMessagesFromCacheTimer) { this.loadMessagesFromCacheTimer = timeout(() => this.loadMessagesFromCache(), 10); } return; } this.loadMessagesFromCacheRunning = true; if (this.loadMessagesFromCacheTimer) { this.loadMessagesFromCacheTimer(); this.loadMessagesFromCacheTimer = null; } const { currentParts, dbc } = this.state; const { lastUpdated } = dbc; const [minPart, maxPart] = currentParts; const messages = {}; let thumbnails = []; let isCanceled = false; let start = performance.now(); const promises = []; for (let i = minPart, l = maxPart; i <= l; ++i) { promises.push(this.getParseSegment(i)); } await promises.reduce(async (prev, p) => { await prev; if (isCanceled) { return; } const cacheEntry = await p; if (this.state.dbc.lastUpdated !== lastUpdated) { if (!isCanceled) { isCanceled = true; this.loadMessagesFromCacheRunning = false; console.log('Canceling!'); this.loadMessagesFromCache(); } return; } if (cacheEntry) { const newMessages = cacheEntry.messages; thumbnails = thumbnails.concat(cacheEntry.thumbnails); Object.keys(newMessages).forEach((key) => { if (!messages[key]) { messages[key] = { ...newMessages[key] }; } else { const newMessageEntries = newMessages[key].entries; const messageEntries = messages[key].entries; if (newMessageEntries.length && newMessageEntries[0].relTime < messageEntries[messageEntries.length - 1].relTime) { console.error('Found out of order messages', newMessageEntries[0], messageEntries[messageEntries.length - 1]); } messages[key].entries = messages[key].entries.concat(newMessages[key].entries); } }); } console.log('Done with', performance.now() - start); start = performance.now(); }, Promise.resolve()); if (isCanceled) { return; } Object.keys(this.state.messages).forEach((key) => { if (!messages[key]) { messages[key] = this.state.messages[key]; messages[key].entries = []; } }); Object.keys(messages).forEach((key) => { messages[key].frame = dbc.getMessageFrame( messages[key].address ); }); const maxByteStateChangeCount = DbcUtils.findMaxByteStateChangeCount( messages ); this.setState({ maxByteStateChangeCount }); Object.keys(messages).forEach((key) => { // console.log(key); messages[key] = DbcUtils.setMessageByteColors( messages[key], maxByteStateChangeCount ); }); console.log('Done with old messages', performance.now() - start); this.setState({ messages, thumbnails }); this.loadMessagesFromCacheRunning = false; } async getParseSegment(part) { if (!dataCache[part]) { return null; } if (dataCache[part].promise) { await dataCache[part].promise; } dataCache[part].promise = this.getParseSegmentInternal(part); return dataCache[part].promise; } async getParseSegmentInternal(part) { const start = performance.now(); const { dbc } = this.state; if (!dbc.lastUpdated) { dbc.lastUpdated = Date.now(); } const { lastUpdated } = dbc; let { messages } = dataCache[part]; let reparseMessages = {}; // if (lastUpdated > dataCache[part].lastUpdated) { // dataCache[part].lastUpdated = Date.now(); // return await this.reparseMessages(messages); // } Object.keys(messages).forEach((key) => { if (messages[key].lastUpdated >= lastUpdated) { return; } reparseMessages[key] = messages[key]; }); if (Object.keys(reparseMessages).length) { console.log('Reparsing messages!', Object.keys(reparseMessages).length); reparseMessages = await this.reparseMessages(reparseMessages); } messages = { ...messages, ...reparseMessages }; dataCache[part].messages = messages; const end = performance.now(); if (end - start > 200) { // warn about anything over 200ms console.warn('getParseSegment took', part, end - start, Object.keys(messages).length); } return dataCache[part]; } decacheMessageId(messageId) { Object.keys(dataCache).forEach((part) => { if (dataCache[part].messages[messageId]) { dataCache[part].messages[messageId].lastUpdated = 0; } }); } async reparseMessages(_messages) { const messages = _messages; const { dbc } = this.state; dbc.lastUpdated = dbc.lastUpdated || Date.now(); Object.keys(messages).forEach((key) => { messages[key].frame = dbc.getMessageFrame(messages[key].address); }); return new Promise((resolve, reject) => { const worker = new MessageParser(); worker.onmessage = (e) => { const newMessages = e.data.messages; Object.keys(newMessages).forEach((key) => { newMessages[key].lastUpdated = dbc.lastUpdated; newMessages[key].frame = dbc.getMessageFrame(newMessages[key].address); }); resolve(newMessages); }; worker.postMessage({ messages, dbcText: dbc.text(), canStartTime: this.state.firstCanTime }); }); } parseMessageEntry(_entry, dbc, lastMsg) { const entry = _entry; dbc.lastUpdated = dbc.lastUpdated || Date.now(); entry.lastUpdated = dbc.lastUpdated; entry.frame = dbc.getMessageFrame( entry.address ); let prevMsgEntry = lastMsg || null; const byteStateChangeCounts = []; // entry.messages[id].byteStateChangeCounts = byteStateChangeCounts.map( // (count, idx) => entry.messages[id].byteStateChangeCounts[idx] + count // ); entry.entries = entry.entries.map((message) => { if (message.hexData) { prevMsgEntry = DbcUtils.reparseMessage(dbc, message, prevMsgEntry); } else { prevMsgEntry = DbcUtils.parseMessage( dbc, message.time, message.address, message.data, message.timeStart, prevMsgEntry ); } byteStateChangeCounts.push(prevMsgEntry.byteStateChangeCounts); prevMsgEntry = prevMsgEntry.msgEntry; return prevMsgEntry; }); entry.byteStateChangeCounts = byteStateChangeCounts.reduce((memo, val) => { if (!memo) { return val; } return memo.map((count, idx) => val[idx] + count); }, null); return entry; } showingModal() { const { showOnboarding, showLoadDbc, showSaveDbc, showAddSignal, showEditMessageModal } = this.state; return ( showOnboarding || showLoadDbc || showSaveDbc || showAddSignal || showEditMessageModal ); } showOnboarding() { if (!CommaAuth.isAuthenticated() && window.sessionStorage && window.location && window.location.pathname !== AuthConfig.AUTH_PATH) { window.sessionStorage.setItem('onboardingPath', window.location.href); } this.setState({ showOnboarding: true }); } hideOnboarding() { this.setState({ showOnboarding: false }); } showLoadDbc() { this.setState({ showLoadDbc: true }); } hideLoadDbc() { this.setState({ showLoadDbc: false }); } showSaveDbc() { this.setState({ showSaveDbc: true }); } hideSaveDbc() { this.setState({ showSaveDbc: false }); } updateMessageFrame(messageId, frame) { const { messages } = this.state; messages[messageId].frame = frame; this.setState({ messages }); } persistDbc({ dbcFilename, dbc }) { const { route } = this.state; if (route) { persistDbc(route.fullname, { dbcFilename, dbc }); } else { persistDbc('live', { dbcFilename, dbc }); } this.loadMessagesFromCache(); } onConfirmedSignalChange(message, signals) { const { dbc, dbcFilename } = this.state; dbc.setSignals(message.address, { ...signals }); this.persistDbc({ dbcFilename, dbc }); this.updateMessageFrame(message.id, dbc.getMessageFrame(message.address)); this.setState({ dbc, dbcText: dbc.text() }, () => { this.decacheMessageId(message.id); this.loadMessagesFromCache(); }); } partChangeDebounced = debounce(() => { this.loadMessagesFromCache(); this.spawnWorker(); }, 500); onPartChange(part) { let { currentParts, currentPart, canFrameOffset, route } = this.state; if (canFrameOffset === -1 || part === currentPart) { return; } // determine new parts to load, whether to prepend or append let maxPart = Math.min(route.proclog, part + 1); const minPart = Math.max(0, maxPart - PART_SEGMENT_LENGTH + 1); if (minPart === 0) { maxPart = Math.min(route.proclog, 2); } // update current parts currentParts = [minPart, maxPart]; currentPart = part; if ( currentPart !== this.state.currentPart || currentParts[0] !== this.state.currentParts[0] || currentParts[1] !== this.state.currentParts[1] ) { // update state then load new parts this.setState({ currentParts, currentPart }, this.partChangeDebounced); } } showEditMessageModal(msgKey) { const msg = this.state.messages[msgKey]; if (!msg.frame) { msg.frame = this.state.dbc.createFrame(msg.address); } this.setState({ showEditMessageModal: true, editMessageModalMessage: msgKey, messages: this.state.messages, dbcText: this.state.dbc.text() }); } hideEditMessageModal() { this.setState({ showEditMessageModal: false }); } onMessageFrameEdited(messageFrame) { const { messages, dbcFilename, dbc, editMessageModalMessage } = this.state; const message = { ...messages[editMessageModalMessage] }; message.frame = messageFrame; dbc.messages.set(messageFrame.id, messageFrame); this.persistDbc({ dbcFilename, dbc }); messages[editMessageModalMessage] = message; this.setState({ messages, dbc, dbcText: dbc.text() }); this.hideEditMessageModal(); } onSeek(seekIndex, seekTime) { this.setState({ seekIndex, seekTime }); const { currentPart } = this.state; const part = ~~(seekTime / 60); if (part !== currentPart) { this.onPartChange(part); } } 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.onSeek(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 }); } loginWithGithub() { const { route } = this.state; return ( Log in with Github ); } 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) { const 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, 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 && messages[selectedMessages[0]] !== undefined ) { seekIndex = Math.max(0, messages[selectedMessages[0]].entries.length - 1); } this.setState({ messages, seekTime, seekIndex, lastBusTime, firstCanTime, maxByteStateChangeCount }); } onStreamedCanMessagesProcessed(e) { this._onStreamedCanMessagesProcessed(e.data); } async 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; // if any errors go off during connection, mark as not trying to connect anymore... const unlisten = this.pandaReader.onError((err) => { console.error(err.stack || err); this.setState({ attemptingPandaConnection: false }); }); try { await this.pandaReader.start(); this.setState({ showOnboarding: false, showLoadDbc: true }); } catch (e) {} this.setState({ attemptingPandaConnection: false }); unlisten(); } githubSignOut(e, dataArray) { unpersistGithubAuthToken(); this.setState({ isGithubAuthenticated: false }); e.preventDefault(); } render() { const { route, messages, selectedMessages, currentParts, dbcFilename, dbcLastSaved, seekTime, seekIndex, shareUrl, maxByteStateChangeCount, live, thumbnails, selectedMessage, canFrameOffset, firstCanTime, currentPart, partsLoaded } = this.state; const { startTime, segments } = this.props; return (
{this.state.isLoading ? ( ) : null}
Comma Cabana
{this.state.isGithubAuthenticated ? (

GitHub Authenticated

Sign out

) : ( this.loginWithGithub() )}
{route || live ? ( ) : null}
{this.state.showOnboarding ? ( ) : null} {this.state.showLoadDbc ? ( ) : null} {this.state.showSaveDbc ? ( ) : null} {this.state.showEditMessageModal ? ( ) : null}
); } } CanExplorer.propTypes = { dongleId: PropTypes.string, name: PropTypes.string, dbc: PropTypes.instanceOf(DBC), dbcFilename: PropTypes.string, githubAuthToken: PropTypes.string, autoplay: PropTypes.bool, max: PropTypes.number, url: PropTypes.string, startTime: PropTypes.number, segments: PropTypes.array };