Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions .github/workflows/CI.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
name: CI

on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read

jobs:
linux:
runs-on: ubuntu-latest
strategy:
matrix:
target: [x86_64, aarch64]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist
manylinux: auto
- uses: actions/upload-artifact@v4
with:
name: wheels-linux-${{ matrix.target }}
path: dist

macos:
runs-on: macos-latest
strategy:
matrix:
target: [x86_64, aarch64]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist
- uses: actions/upload-artifact@v4
with:
name: wheels-macos-${{ matrix.target }}
path: dist

windows:
runs-on: windows-latest
strategy:
matrix:
target: [x64]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
architecture: ${{ matrix.target }}
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
args: --release --out dist
- uses: actions/upload-artifact@v4
with:
name: wheels-windows-${{ matrix.target }}
path: dist

sdist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build sdist
uses: PyO3/maturin-action@v1
with:
command: sdist
args: --out dist
- uses: actions/upload-artifact@v4
with:
name: wheels-sdist
path: dist

release:
name: Release to PyPI
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
needs: [linux, macos, windows, sdist]
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
pattern: wheels-*
merge-multiple: true
path: dist
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
24 changes: 12 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
[package]
name = "mcap-player"
name = "tachy-mcap-reader"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
description = "Zero-service MCAP playback library for ROS2"

[lib]
name = "_mcap_player_core"
name = "_core"
crate-type = ["cdylib"]

[dependencies]
Expand Down
105 changes: 105 additions & 0 deletions README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
# tachy-reader

Zero-service MCAP playback library for ROS2. Rust core (via PyO3) with a pure-Python API.

**No DDS services are registered** — all playback control happens via in-process method calls, keeping the ROS2 graph clean.

## Features

- **Rust-powered core**: memory-mapped MCAP reading with zero-copy message access
- **Zero ROS2 services**: no parameter/logger/type-description services polluting the graph
- **Simple API**: context-manager pattern, blocking or async playback
- **Playback control**: play, pause, resume, stop, seek, rate adjustment
- **Topic filtering**: include/exclude specific topics
- **QoS overrides**: per-topic QoS profile configuration
- **Callbacks**: `on_publish` and `on_complete` hooks

## Installation

### From source (requires Rust toolchain)

```bash
pip install git+https://github.com/tachycode/tachy-reader.git@dev
```

### Local development

```bash
git clone https://github.com/tachycode/tachy-reader.git
cd tachy-reader
pip install -e .
```

> **Prerequisites**: Rust toolchain (`rustup`), Python ≥ 3.8, maturin ≥ 1.5

## Quick Start

```python
from mcap_player import McapPlayer

with McapPlayer("/path/to/recording.mcap") as player:
# Blocking playback at 2x speed
player.play(rate=2.0)
```

### Non-blocking playback

```python
with McapPlayer("recording.mcap") as player:
player.play_async(rate=1.0)

while player.state != "finished":
print(f"Progress: {player.progress:.1%}")
time.sleep(1.0)
```

### Topic filtering

```python
player = McapPlayer(
"recording.mcap",
topics=["/camera/image_raw", "/imu/data"],
topic_prefix="/replay",
)
```

### Playback control

```python
player.play_async()
player.pause()
player.seek(10.5) # jump to 10.5 seconds
player.set_rate(0.5) # half speed
player.resume()
player.stop()
```

## API Reference

### `McapPlayer`

| Parameter | Type | Description |
|-----------|------|-------------|
| `mcap_path` | `str` | Path to `.mcap` file |
| `node_name` | `str` | ROS2 node name (default: `"mcap_player"`) |
| `topic_prefix` | `str` | Prefix for published topics |
| `topics` | `list[str]` | Whitelist of topics to publish |
| `exclude_topics` | `list[str]` | Topics to skip |
| `qos_overrides` | `dict` | Per-topic QoS profiles |
| `on_publish` | `Callable` | Called after each message publish |
| `on_complete` | `Callable` | Called when playback finishes |
| `node` | `Node` | Existing rclpy Node (optional) |

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `current_time` | `float` | Current position in seconds |
| `progress` | `float` | Playback progress (0.0 – 1.0) |
| `state` | `str` | `"idle"`, `"playing"`, `"paused"`, `"finished"` |
| `duration` | `float` | Total file duration in seconds |
| `topics_info` | `dict` | Topic metadata `{name: (msg_type, encoding)}` |

## License

Apache-2.0
53 changes: 53 additions & 0 deletions docs/devlog/DEVLOG-002-ccg-raw-reader-api-design.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
---
id: DEVLOG-002
title: CCG 토론 — McapRawReader API 설계 분석
task_type: feature
status: completed
complexity: high
created: 2026-03-19
duration_estimate: 2h
tags: [ccg, codex, api-design, rosbag2_py, architecture, mmap, zero-copy]
---

## 목표 (Goal)
- rosbag2_py SequentialReader를 tachy-reader로 대체 가능한지 분석
- Codex/Gemini CCG 토론을 통해 API 설계 방향 결정
- 성능 이득 여부, 메모리 이슈, 테스트 전략 수립

## 접근 과정 (Approach Log)

### 1차 시도 — omc ask 명령
- **방법**: `omc ask codex/gemini` 스킬 호출
- **결과**: 실패 — `omc` 명령어 미설치
- **원인**: oh-my-claudecode CLI 미설치 환경

### 2차 시도 — codex/gemini CLI 직접 호출
- **방법**: `codex exec --full-auto`, `gemini -p` 직접 실행
- **결과**: Codex 성공, Gemini 실패 (API key 미설정)
- **원인**: Codex는 `--full-auto` 플래그로 비대화형 실행 가능. Gemini는 `GEMINI_API_KEY` 미설정

### 3차 시도 — Codex + Claude 듀얼 분석
- **방법**: Codex(GPT-5.4) 분석 결과 + Claude(Opus 4.6) 독립 분석 합성
- **결과**: 성공 — 4개 주제 모두 합의 도출

## 최종 해결 (Final Solution)
- **API 설계**: Option B (별도 McapRawReader pyclass) 만장일치
- Option A (McapCore에 추가): 두 소비 모델 혼재 위험
- Option C (rate=inf): semantic hack, Scheduler condvar 불필요하게 통과
- **성능**: 현재 코드는 "mmap+zero-copy"가 아닌 "mmap+eager full-copy+per-message clone"
- reader.rs:134에서 `msg.data.to_vec()` — 전체 payload 복사
- reader.rs:169에서 다시 clone — per-message 이중 복사
- **메모리**: 10GB+ 파일에서 OOM 위험 → Phase 2 lazy chunk decoding 필요
- **버그 2건 발견**:
1. scheduler.rs:88 — double ns 변환 (lib.rs:66에서 이미 변환된 값을 재변환)
2. scheduler.rs:204 — pause/seek 시 이미 consume된 메시지 유실

## 교훈 (Lessons Learned)
- Codex CLI: `codex exec --full-auto "prompt"` 형태로 비대화형 실행
- Gemini CLI: `gemini -p "prompt"` — `GEMINI_API_KEY` 환경변수 필수
- CCG 패턴: 한 모델 불가 시 나머지 + Claude 합성으로 충분히 유의미한 분석 가능
- tachy-reader의 "zero-copy" 마케팅과 실제 구현 사이에 갭 존재 — open() 시 전체 파일 메모리 적재

## 변경 파일 (Changed Files)
- `.omc/artifacts/ask/codex-raw-reader-api.md` — Codex 분석 결과
- `.omc/artifacts/ask/claude-raw-reader-synthesis.md` — 종합 분석 및 액션 체크리스트
57 changes: 57 additions & 0 deletions docs/devlog/DEVLOG-003-implement-mcap-raw-reader.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
---
id: DEVLOG-003
title: McapRawReader pyclass 구현 및 scheduler 버그 수정
task_type: feature
status: completed
complexity: medium
created: 2026-03-19
duration_estimate: 1h
tags: [mcap-raw-reader, pyo3, rust, iterator, scheduler-bug, maturin]
---

## 목표 (Goal)
- Scheduler를 우회하는 McapRawReader pyclass 구현 (타이밍 없는 순차 읽기)
- scheduler.rs double ns 변환 버그 수정
- Python export 및 type stubs 업데이트
- 정확성 테스트 및 벤치마크 스크립트 작성

## 접근 과정 (Approach Log)

### 1차 시도 — McapRawReader 구현 + 빌드
- **방법**: lib.rs에 McapRawReader pyclass 추가, parking_lot::Mutex로 McapReader 래핑, `__iter__`/`__next__` 구현
- **결과**: 코드 작성 성공, 빌드 환경 문제 발생
- **원인**: Rust 툴체인 미설치, python3-venv 미설치, sudo 불가

### 2차 시도 — 환경 구축
- **방법**: rustup 설치 → maturin pip 설치 → maturin develop 시도
- **결과**: `maturin develop`는 venv 필요하여 실패
- **원인**: python3-venv 패키지 미설치 + sudo 권한 없음

### 3차 시도 — maturin build + pip install wheel
- **방법**: `maturin build --release` → wheel 생성 → `pip3 install --break-system-packages --no-deps` wheel
- **결과**: 성공 — 빌드, 설치, 테스트 실행 가능
- **원인**: `maturin build`는 venv 불필요, `--no-deps`로 rosidl_runtime_py 의존성 우회

### 버그 수정 — scheduler.rs:88
- **방법**: `file_start + (start_ns as f64 * 1_000_000_000.0)` → `file_start + start_ns`
- **결과**: 성공 — lib.rs:66에서 이미 초→ns 변환 완료된 값이므로 재변환 제거

## 최종 해결 (Final Solution)
- McapRawReader: McapReader를 Mutex로 직접 감싸서 Scheduler 완전 우회
- Python iterator protocol 지원: `for topic, msg_type, data, ts in McapRawReader("file.mcap")`
- topics(), duration_ns(), message_count(), reset(), seek() 메서드 제공
- 10개 테스트 작성, 벤치마크 스크립트 작성

## 교훈 (Lessons Learned)
- **Rust 환경 부트스트래핑**: `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y` 로 즉시 설치
- **venv 없는 환경에서 maturin**: `maturin build` → `pip install wheel` 패턴이 유효
- **`--break-system-packages`**: PEP 668 제한을 우회하나, 프로덕션에서는 비권장
- **ROS2 의존성 우회**: `--no-deps`로 rosidl_runtime_py 없이 설치 가능 (McapRawReader는 ROS2 불필요)

## 변경 파일 (Changed Files)
- `src/lib.rs` — McapRawReader pyclass 추가, _mcap_player_core module 등록
- `src/scheduler.rs` — line 88: double ns 변환 버그 수정
- `python/mcap_player/__init__.py` — McapRawReader export 추가
- `python/mcap_player/_core.pyi` — McapRawReader type stubs 추가
- `tests/test_raw_reader.py` — 10개 정확성 테스트
- `tests/bench_raw_reader.py` — rosbag2_py 대비 성능 벤치마크
Loading