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

* Add rlog thumbnails to cabana

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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