Add ROS2 MCAP file support (.mcap/.mcap.zstd)#10
Conversation
- Add MCAP parsing with @mcap/core, @foxglove/rosmsg2-serialization, fzstd - Abstract severity to string-based SeverityLevel type (ROS1/ROS2 unified) - Dynamic import for MCAP module to keep bundle split for bag-only users - Add E2E tests (20 cases) with MCAP fixture generated via ROS2 Jazzy Docker - Update file accept to .bag,.mcap, i18n text, and CLAUDE.md Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Add try/catch with detailed error logging (matching loadRosbagMessages style) - Show available topics when no rosout/diagnostics topics are found - Add console logging for load progress and success Co-Authored-By: Claude Opus 4.6 <[email protected]>
MCAP files recorded by some tools lack the summary/footer section, causing McapIndexedReader to fail. Detect the footer magic and fall back to McapStreamReader for incomplete files. Also decompress whole-file zstd-wrapped .mcap files by detecting the zstd magic at the start of the buffer. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Replace unreliable hasMcapFooter() check with try/catch fallback to streaming reader, since all valid MCAP files share the same trailing magic bytes regardless of index presence - Add state-change deduplication for diagnostics to match ROS1 loader behavior, preventing duplicate rows from repeated status samples Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The filter only compared level and message, causing entries with different values (e.g. packets_received changing) to be incorrectly skipped. This fixes the e2e M-5-3 name filter test failure. Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
Pull request overview
This PR adds ROS2 log/diagnostics ingestion via MCAP (.mcap / .mcap.zstd) alongside existing ROS1 .bag support, and standardizes rosout severity handling across ROS1/ROS2 by moving to a string-based SeverityLevel.
Changes:
- Introduces MCAP parsing (
src/mcapUtils.ts) and aloadMessages()dispatcher to choose ROS1 bag vs ROS2 MCAP at runtime. - Migrates rosout severity from numeric codes to
SeverityLevelstrings and updates exports/UI accordingly. - Adds Playwright E2E coverage and fixtures for MCAP, plus updates SQLite export schema and docs.
Reviewed changes
Copilot reviewed 13 out of 15 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/types.ts | Adds SeverityLevel and ROS1/ROS2 severity mappings; updates color/bg mappings to string keys. |
| src/rosbagUtils.ts | Maps ROS1 numeric severity to SeverityLevel; updates export formats and SQLite schema; adds loadMessages() dispatcher. |
| src/rosbagUtils.test.ts | Updates unit tests for string-based severities and updated SQLite schema. |
| src/mcapUtils.ts | New MCAP reader using @mcap/core, CDR deserialization, and zstd decompression; collects rosout + diagnostics. |
| src/i18n.ts | Updates upload file-type hint to include MCAP extensions. |
| src/App.tsx | Accepts MCAP uploads, uses loadMessages(), and updates severity filtering/UI to SeverityLevel. |
| package.json | Adds dependencies required for MCAP + ROS2 deserialization. |
| package-lock.json | Locks new MCAP/ROS2-related dependencies and transitive updates. |
| e2e/sqlite-export.spec.ts | Updates assertions for new SQLite severity column. |
| e2e/mcap.spec.ts | Adds Playwright E2E coverage for MCAP upload/filter/export/diagnostics. |
| e2e/fixtures/test_sample.mcap | Adds MCAP fixture used by E2E tests. |
| e2e/fixtures/generate_test_mcap.py | Adds generator script for the MCAP test fixture (ROS2). |
| e2e/fixtures/Dockerfile.mcap | Adds Docker image to generate the MCAP fixture using ROS2 Jazzy. |
| README.md | Updates supported formats/versions and dependency list to include MCAP. |
| CLAUDE.md | Updates repo guidance to reflect MCAP support and severity mapping changes. |
Comments suppressed due to low confidence (1)
src/i18n.ts:18
- The English i18n strings still say “Loading bag file…” / “Error loading bag file”, but the app now supports MCAP uploads too. Update these strings (and any similar bag-specific wording) to be format-agnostic (e.g., “Loading file…” / “Error loading file”).
'loading.message': 'Loading bag file...',
'error.title': 'Error loading bag file',
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "@foxglove/rosbag": "^0.4.1", | ||
| "@foxglove/rosmsg": "^5.0.5", | ||
| "@foxglove/rosmsg2-serialization": "^3.0.3", | ||
| "@mcap/core": "^2.2.0", | ||
| "fzstd": "^0.1.1", |
There was a problem hiding this comment.
@foxglove/rosmsg2-serialization declares engines.node >= 20 (see package-lock), but this repo’s package.json doesn’t declare an engines requirement. Adding an explicit engines field (or documenting the Node >=20 requirement) will prevent confusing install failures for developers on older Node versions.
There was a problem hiding this comment.
This is a browser-only app — Node is only used for dev tooling. The runtime doesn't depend on Node engine compatibility, so adding an engines field isn't necessary here.
| const decompressHandlers: DecompressHandlers = { | ||
| zstd: (data) => zstdDecompress(new Uint8Array(data)), | ||
| }; |
There was a problem hiding this comment.
decompressHandlers only registers a zstd handler. MCAP chunks can also be lz4-compressed; without an lz4 handler those files will fail to load. Consider adding an lz4 handler (you already depend on lz4js) or documenting that only zstd-compressed MCAPs are supported.
There was a problem hiding this comment.
Added lz4 handler in 6f09f3f. lz4js was already in dependencies (used by the ROS1 bag loader).
Add tests for .mcap.zstd (outer zstd compression) and truncated .mcap files to verify error handling and data recovery. Also update fixture generator to produce these variants automatically. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
fzstd.decompress() can return a Uint8Array with non-zero byteOffset, so using .buffer directly caused the MCAP reader to read from wrong offsets. Use .slice().buffer to get a properly aligned ArrayBuffer. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 19 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
src/i18n.ts:18
- UI strings still refer to a "bag file" in loading/error text, but the uploader now supports
.mcap/.mcap.zstdtoo. Please updateloading.messageand the Englisherror.titleto be format-neutral (e.g. "Loading file..." / "Error loading file") to avoid misleading users.
'upload.fileType': 'ROSbag / MCAP file (.bag, .mcap, .mcap.zstd)',
// Loading / Error
'loading.message': 'Loading bag file...',
'error.title': 'Error loading bag file',
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const magic = new Uint8Array(buffer, 0, 4); | ||
| if (magic[0] === 0x28 && magic[1] === 0xb5 && magic[2] === 0x2f && magic[3] === 0xfd) { | ||
| const decompressed = zstdDecompress(new Uint8Array(buffer)); | ||
| buffer = decompressed.slice().buffer as ArrayBuffer; |
There was a problem hiding this comment.
buffer = decompressed.slice().buffer makes an additional full copy of the decompressed data. For large .mcap.zstd files this can double peak memory usage. Consider reusing the Uint8Array’s underlying buffer when possible (or only copying when byteOffset/byteLength require it).
| buffer = decompressed.slice().buffer as ArrayBuffer; | |
| if (decompressed.byteOffset === 0 && decompressed.byteLength === decompressed.buffer.byteLength) { | |
| buffer = decompressed.buffer as ArrayBuffer; | |
| } else { | |
| buffer = decompressed.slice().buffer as ArrayBuffer; | |
| } |
There was a problem hiding this comment.
Fixed in 6f09f3f — now checks byteOffset === 0 && byteLength === buffer.byteLength before reusing the backing buffer directly, only copying when needed.
…n magic check - Remove unused channelsById map from McapMessageCollector - Add lz4 decompression handler for MCAP chunks - Add buffer.byteLength >= 4 guard before reading magic bytes - Avoid unnecessary buffer copy when byteOffset is already 0 - Tighten file picker accept to .mcap.zstd instead of .zstd Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…nknown severity - Fall back to streaming reader when indexed reader returns 0 messages (handles unchunked MCAPs where messages are not inside Chunk records) - Track pending channels and rebuild readers when their schema arrives later (handles streaming MCAPs with Channel before Schema order) - Map unmapped/unknown severity levels to 'UNKNOWN' instead of 'DEBUG' to preserve original data fidelity Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Summary
.mcap/.mcap.zstd) alongside existing ROS1.bagfilesSeverityLeveltype ('DEBUG'|'INFO'|'WARN'|'ERROR'|'FATAL') so both ROS1 (1/2/4/8/16) and ROS2 (10/20/30/40/50) are unified@mcap/corefor MCAP reading,@foxglove/rosmsg2-serializationfor CDR deserialization,fzstd(pure JS) for zstd decompressionChanged files
src/mcapUtils.ts— New MCAP parsing modulesrc/types.ts—SeverityLevelstring type,ROS1_SEVERITY/ROS2_SEVERITYmappingssrc/rosbagUtils.ts—loadMessagesdispatcher, severity string conversionsrc/App.tsx— Accept.mcap, useloadMessages, severity UI updatessrc/i18n.ts— Updated file type descriptione2e/mcap.spec.ts— 20 E2E test cases for MCAPe2e/fixtures/— MCAP fixture + Dockerfile (ROS2 Jazzy)Test plan
🤖 Generated with Claude Code