import { MessageReader } from '@foxglove/rosmsg-serialization';
import { compare } from '@foxglove/rostime';
import { Bag } from '@foxglove/rosbag';
import { BlobReader } from '@foxglove/rosbag/web';
import { parse as parseMessageDefinition } from '@foxglove/rosmsg';
import { Topic } from '@/foxglove/packages/studio-base/players/types';
import {
GetBackfillMessagesArgs,
MessageIteratorArgs
} from '@/foxglove/packages/studio-base/players/IterablePlayer/IIterableSource';
import { IteratorResult } from '@/foxglove/packages/studio-base/players/IterablePlayer/IIterableSource';
import { MessageEvent } from '@/foxglove/packages/studio/src';
type BagSource = { type: 'file'; file: File };
export default class BagIterableSource {
#abort: Promise<boolean>;
#abortResolve: (value: boolean | PromiseLike<boolean>) => void;
#source: BagSource;
#bag: Bag;
#topics: Map<string, Topic>;
#readersByConnectionId: Map<number, MessageReader>;
constructor(source: BagSource) {
this.#abort = new Promise(r => {
this.#abortResolve = r;
});
this.#source = source;
}
public async initialize(): Promise<any> {
this.#bag = new Bag(new BlobReader(this.#source.file));
const bag = this.#bag;
await bag.open();
console.log('open bag', bag);
const readersByConnectionId = new Map<number, MessageReader>();
const topics = new Map<string, Topic>();
for (const [id, connection] of bag.connections) {
const schemaName = connection.type;
if (!schemaName) {
continue;
}
const parsedDefinition = parseMessageDefinition(
connection.messageDefinition
);
const reader = new MessageReader(parsedDefinition);
readersByConnectionId.set(id, reader);
const existingTopic = topics.get(connection.topic);
if (!existingTopic) {
topics.set(connection.topic, { name: connection.topic, schemaName });
}
}
this.#topics = topics;
this.#readersByConnectionId = readersByConnectionId;
return {
topics: Array.from(topics.values()),
start: this.#bag.startTime ?? { sec: 0, nsec: 0 },
end: this.#bag.endTime ?? { sec: 0, nsec: 0 },
profile: 'ros1'
};
}
public async *messageIterator(
opt: MessageIteratorArgs
): AsyncIterableIterator<Readonly<IteratorResult>> {
yield* this.#messageIterator({ ...opt, reverse: false });
}
async *#messageIterator(
opt: MessageIteratorArgs & { reverse: boolean }
): AsyncGenerator<Readonly<IteratorResult>> {
const iterator = this.#bag.messageIterator({
topics: Array.from(this.#topics.keys())
});
const end = this.#bag.endTime;
for await (const bagMsgEvent of iterator) {
if (end && compare(bagMsgEvent.timestamp, end) > 0) {
return;
}
const connectionId = bagMsgEvent.connectionId;
const reader = this.#readersByConnectionId.get(connectionId);
if (reader) {
const dataCopy = bagMsgEvent.data.slice();
const parsedMessage = reader.readMessage(dataCopy);
return {
type: 'message-event',
connectionId,
msgEvent: {
topic: bagMsgEvent.topic,
receiveTime: bagMsgEvent.timestamp,
sizeInBytes: bagMsgEvent.data.byteLength,
message: parsedMessage,
schemaName: ''
}
};
}
}
}
public async getBackfillMessages({
topics,
time
}: GetBackfillMessagesArgs): Promise<MessageEvent[]> {
const messages: MessageEvent[] = [];
for (const topic of topics) {
for await (const result of this.#messageIterator({
topics: [topic],
start: time,
reverse: true
})) {
if (result.type === 'message-event') {
messages.push(result.msgEvent);
}
break;
}
}
messages.sort((a, b) => compare(a.receiveTime, b.receiveTime));
return messages;
}
id: number = 0;
async *start() {
for (;;) {
if (this.id === 100) {
return this.id;
}
await wait((Math.random() * 3 + 0) * 1000);
await this.#abort;
this.id++;
yield this.id;
}
}
async next() {
return await this.start().next();
}
stop() {
this.#abort = new Promise(r => {
this.#abortResolve = r;
});
}
open() {
this.#abortResolve(true);
}
}
function wait(arg0: number) {
return new Promise<void>(r => {
setTimeout(() => {
r();
}, arg0);
});
}
import * as Comlink from 'comlink';
import BagIterableSource from './BagIterableSource';
import { DataSourceFactoryInitializeArgs } from '@/foxglove/packages/studio-base/context/PlayerSelectionContext';
import { Player } from '@/foxglove/packages/studio-base/players/types';
import { WorkerIterableSource } from './WorkerIterableSource';
import { IterablePlayer } from './IterablePlayer';
import { IIterableSource } from '@/foxglove/packages/studio-base/players/IterablePlayer/IIterableSource';
export default class Ros1LocalBagDataSourceFactory {
public id = 'ros1-local-bagfile';
public initialize(args: DataSourceFactoryInitializeArgs): Player | undefined {
const file = args.file;
if (!file) {
return;
}
const source = new WorkerIterableSource({
initWorker: () => {
return new Worker(
new URL('./BagIterableSourceWorker.Worker.ts', import.meta.url),
{
type: 'module'
}
);
},
initArgs: { file }
}) as unknown as IIterableSource;
return new IterablePlayer({
source,
name: file.name,
sourceId: this.id
});
}
}
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext", "Webworker"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"paths": {
"@/*": ["3D/*"]
},
"baseUrl": ".",
"outDir": "./dist",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"references": [{ "path": "./tsconfig.node.json" }],
"include": [
"3D/**/*.ts",
"3D/**/*.d.ts",
"3D/**/*.tsx",
"./typings/**/*.d.ts"
, "3D/rosPlay/player/BagIterableSourceWorker.Worker.js" ],
"exclude": ["node_modules", "dist"]
}