ros-tviewer 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ros_tviewer-0.1.0/.env.example +9 -0
- ros_tviewer-0.1.0/.github/workflows/ci.yml +42 -0
- ros_tviewer-0.1.0/.github/workflows/publish.yml +38 -0
- ros_tviewer-0.1.0/.gitignore +116 -0
- ros_tviewer-0.1.0/.python-version +1 -0
- ros_tviewer-0.1.0/AGENTS.md +153 -0
- ros_tviewer-0.1.0/CHANGELOG.md +28 -0
- ros_tviewer-0.1.0/CONTRIBUTING.md +33 -0
- ros_tviewer-0.1.0/LICENSE +21 -0
- ros_tviewer-0.1.0/PKG-INFO +124 -0
- ros_tviewer-0.1.0/README.md +96 -0
- ros_tviewer-0.1.0/SECURITY.md +25 -0
- ros_tviewer-0.1.0/config.toml +16 -0
- ros_tviewer-0.1.0/pyproject.toml +56 -0
- ros_tviewer-0.1.0/pyrightconfig.json +9 -0
- ros_tviewer-0.1.0/src/ros_tviewer/__init__.py +1 -0
- ros_tviewer-0.1.0/src/ros_tviewer/convert.py +85 -0
- ros_tviewer-0.1.0/src/ros_tviewer/main.py +233 -0
- ros_tviewer-0.1.0/src/ros_tviewer/node.py +223 -0
- ros_tviewer-0.1.0/src/ros_tviewer/ros_env.py +98 -0
- ros_tviewer-0.1.0/tests/e2e.sh +29 -0
- ros_tviewer-0.1.0/tests/fakes.py +51 -0
- ros_tviewer-0.1.0/tests/test_convert.py +131 -0
- ros_tviewer-0.1.0/tests/test_main.py +239 -0
- ros_tviewer-0.1.0/tests/test_node.py +98 -0
- ros_tviewer-0.1.0/tests/test_ros2_e2e.py +163 -0
- ros_tviewer-0.1.0/tests/test_ros_env.py +93 -0
- ros_tviewer-0.1.0/tests/test_smoke.py +8 -0
- ros_tviewer-0.1.0/tests/tools/dummy_publisher.py +78 -0
- ros_tviewer-0.1.0/uv.lock +302 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
ROS_TVIEWER_APP__DEFAULT_TARGET=world
|
|
2
|
+
ROS_TVIEWER_LOGGING__LEVEL=WARNING
|
|
3
|
+
ROS_TVIEWER_CAMERA__TOPIC=/camera/image_raw
|
|
4
|
+
ROS_TVIEWER_CAMERA__FPS=30
|
|
5
|
+
ROS_TVIEWER_CAMERA__ROTATE=0
|
|
6
|
+
ROS_TVIEWER_CAMERA__STRETCH=false
|
|
7
|
+
ROS_TVIEWER_CAMERA__COMPRESSED=false
|
|
8
|
+
ROS_TVIEWER_CAMERA__TIMEOUT=5.0
|
|
9
|
+
ROS_TVIEWER_CAMERA__FRAMES=0
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
check:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
steps:
|
|
13
|
+
- name: Checkout
|
|
14
|
+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
15
|
+
with:
|
|
16
|
+
persist-credentials: false
|
|
17
|
+
|
|
18
|
+
- name: Install uv
|
|
19
|
+
uses: astral-sh/setup-uv@8d55fbecc275b1c35dbe060458839f8d30439ccf # v3
|
|
20
|
+
|
|
21
|
+
- name: Install system dependencies
|
|
22
|
+
# tcamviewer sdist 빌드에 cmake + FFmpeg dev 라이브러리 필요
|
|
23
|
+
run: |
|
|
24
|
+
sudo apt-get update
|
|
25
|
+
sudo apt-get install -y --no-install-recommends cmake libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: |
|
|
29
|
+
if [ -f uv.lock ]; then
|
|
30
|
+
uv sync --locked --group dev
|
|
31
|
+
else
|
|
32
|
+
uv sync --group dev
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
- name: Lint
|
|
36
|
+
run: uv run ruff check .
|
|
37
|
+
|
|
38
|
+
- name: Format
|
|
39
|
+
run: uv run ruff format --check .
|
|
40
|
+
|
|
41
|
+
- name: Test
|
|
42
|
+
run: uv run pytest
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch: # 수동 재배포 (릴리스 재실행은 원본 워크플로우 SHA를 재사용함)
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
# PyPI Trusted Publishing (OIDC) — 비밀 토큰 불필요
|
|
14
|
+
id-token: write
|
|
15
|
+
steps:
|
|
16
|
+
- name: Checkout
|
|
17
|
+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
18
|
+
with:
|
|
19
|
+
persist-credentials: false
|
|
20
|
+
|
|
21
|
+
- name: Install uv
|
|
22
|
+
uses: astral-sh/setup-uv@8d55fbecc275b1c35dbe060458839f8d30439ccf # v3
|
|
23
|
+
|
|
24
|
+
- name: Install system dependencies
|
|
25
|
+
# tcamviewer sdist 빌드에 cmake + FFmpeg dev 라이브러리 필요
|
|
26
|
+
run: |
|
|
27
|
+
sudo apt-get update
|
|
28
|
+
sudo apt-get install -y cmake libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev
|
|
29
|
+
|
|
30
|
+
- name: Build
|
|
31
|
+
run: uv build
|
|
32
|
+
env:
|
|
33
|
+
UV_NO_CACHE: "1" # 캐시 독립 빌드 (cache poisoning 공격 표면 제거)
|
|
34
|
+
|
|
35
|
+
- name: Publish to PyPI
|
|
36
|
+
# v1.14.2 — 이전 버전(v1.12.x)의 twine은 hatchling이 생성하는
|
|
37
|
+
# Metadata-Version 2.5를 거부하므로 신규 버전 유지 필수
|
|
38
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# --- OS Specific ---
|
|
2
|
+
# macOS
|
|
3
|
+
.DS_Store
|
|
4
|
+
.AppleDouble
|
|
5
|
+
.LSOverride
|
|
6
|
+
Icon
|
|
7
|
+
._*
|
|
8
|
+
.DocumentRevisions-V100
|
|
9
|
+
.fseventsd
|
|
10
|
+
.Spotlight-V100
|
|
11
|
+
.TemporaryItems
|
|
12
|
+
.Trashes
|
|
13
|
+
.VolumeIcon.icns
|
|
14
|
+
.com.apple.timemachine.donotpresent
|
|
15
|
+
|
|
16
|
+
# Windows
|
|
17
|
+
Thumbs.db
|
|
18
|
+
Thumbs.db:encryptable
|
|
19
|
+
ehthumbs.db
|
|
20
|
+
ehthumbs_vista.db
|
|
21
|
+
*.stackdump
|
|
22
|
+
[Dd]esktop.ini
|
|
23
|
+
$RECYCLE.BIN/
|
|
24
|
+
*.cab
|
|
25
|
+
*.msi
|
|
26
|
+
*.msix
|
|
27
|
+
*.msm
|
|
28
|
+
*.msp
|
|
29
|
+
*.lnk
|
|
30
|
+
|
|
31
|
+
# Linux
|
|
32
|
+
*~
|
|
33
|
+
.fuse_hidden*
|
|
34
|
+
.directory
|
|
35
|
+
.Trash-*
|
|
36
|
+
.nfs*
|
|
37
|
+
|
|
38
|
+
# --- Python General ---
|
|
39
|
+
__pycache__/
|
|
40
|
+
*.py[cod]
|
|
41
|
+
*$py.class
|
|
42
|
+
*.so
|
|
43
|
+
.Python
|
|
44
|
+
build/
|
|
45
|
+
develop-eggs/
|
|
46
|
+
dist/
|
|
47
|
+
downloads/
|
|
48
|
+
eggs/
|
|
49
|
+
.eggs/
|
|
50
|
+
lib/
|
|
51
|
+
lib64/
|
|
52
|
+
parts/
|
|
53
|
+
sdist/
|
|
54
|
+
var/
|
|
55
|
+
wheels/
|
|
56
|
+
share/python-wheels/
|
|
57
|
+
*.egg-info/
|
|
58
|
+
.installed.cfg
|
|
59
|
+
*.egg
|
|
60
|
+
MANIFEST
|
|
61
|
+
|
|
62
|
+
# --- Environments & Secrets ---
|
|
63
|
+
.env
|
|
64
|
+
.venv
|
|
65
|
+
env/
|
|
66
|
+
venv/
|
|
67
|
+
ENV/
|
|
68
|
+
env.bak/
|
|
69
|
+
venv.bak/
|
|
70
|
+
# Keep .env.example for documentation
|
|
71
|
+
|
|
72
|
+
# --- Tooling & Linting ---
|
|
73
|
+
# Ruff
|
|
74
|
+
.ruff_cache/
|
|
75
|
+
|
|
76
|
+
# Mypy / Pyright
|
|
77
|
+
.mypy_cache/
|
|
78
|
+
.dmypy.json
|
|
79
|
+
dmypy.json
|
|
80
|
+
# pyrightconfig.json is usually kept if shared, but can be ignored if local-only
|
|
81
|
+
# pyrightconfig.json
|
|
82
|
+
|
|
83
|
+
# Pytest / Coverage
|
|
84
|
+
.pytest_cache/
|
|
85
|
+
.coverage
|
|
86
|
+
.coverage.*
|
|
87
|
+
htmlcov/
|
|
88
|
+
.tox/
|
|
89
|
+
.nox/
|
|
90
|
+
nosetests.xml
|
|
91
|
+
coverage.xml
|
|
92
|
+
*.cover
|
|
93
|
+
.hypothesis/
|
|
94
|
+
cover/
|
|
95
|
+
|
|
96
|
+
# --- Package Managers (uv / Poetry / PDM) ---
|
|
97
|
+
# uv: uv.lock should be committed for reproducibility
|
|
98
|
+
.pdm.toml
|
|
99
|
+
__pypackages__/
|
|
100
|
+
poetry.toml
|
|
101
|
+
|
|
102
|
+
# --- IDEs ---
|
|
103
|
+
# Visual Studio Code
|
|
104
|
+
.vscode/*
|
|
105
|
+
!.vscode/settings.json
|
|
106
|
+
!.vscode/tasks.json
|
|
107
|
+
!.vscode/launch.json
|
|
108
|
+
!.vscode/extensions.json
|
|
109
|
+
!.vscode/*.code-snippets
|
|
110
|
+
.history/
|
|
111
|
+
|
|
112
|
+
# PyCharm / JetBrains
|
|
113
|
+
.idea/
|
|
114
|
+
|
|
115
|
+
# --- Project Specific ---
|
|
116
|
+
*.log
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# AGENTS.md — ros-tviewer 개발 가이드
|
|
2
|
+
|
|
3
|
+
ROS 2 카메라 토픽을 터미널에서 재생하는 CLI 뷰어. 이 문서는 에이전트(및 개발자)가
|
|
4
|
+
**일관성 있게** 개발하기 위한 아키텍처 명세와 TDD 규칙이다. 코드 변경 전 반드시 읽는다.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. 프로젝트 개요
|
|
9
|
+
|
|
10
|
+
| 항목 | 내용 |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| 목적 | ROS 2 `sensor_msgs/msg/Image`, `CompressedImage` 토픽을 터미널(ANSI TrueColor Half-Block)로 실시간 재생 |
|
|
13
|
+
| 렌더링 | [tcamviewer](https://pypi.org/project/tcamviewer/) `>=0.2.3` (C-ABI 코어 + ctypes 바인딩, PyPI 배포됨) |
|
|
14
|
+
| CLI | `wpycli` (cobra 스타일 Command), 설정 `wpyconf`, 로깅 `wpylog` |
|
|
15
|
+
| 실행 | `uv run ros-tviewer play /camera/image_raw` 또는 ROS env source 후 `uvx ros-tviewer ...` |
|
|
16
|
+
| Python | 3.12 (ROS 2 Jazzy ABI 매칭 — `requires-python = ">=3.12"`) |
|
|
17
|
+
| 테스트 | `pytest` (단위), E2E는 ROS 환경에서 `bash tests/e2e.sh` |
|
|
18
|
+
|
|
19
|
+
## 2. 아키텍처
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
┌──────────────────────────────┐
|
|
23
|
+
│ main.py (wpycli Command) │ play / topics / config
|
|
24
|
+
└──────┬───────────────────────┘
|
|
25
|
+
│ 1) ros_env.ensure_rclpy() ← 반드시 가장 먼저
|
|
26
|
+
▼
|
|
27
|
+
┌──────────────────────────────┐
|
|
28
|
+
│ ros_env.py │ rclpy 부트스트랩
|
|
29
|
+
│ (시스템 ROS 탐지 + re-exec) │ (pip 설치 불가 → 시스템 전용)
|
|
30
|
+
└──────┬───────────────────────┘
|
|
31
|
+
▼
|
|
32
|
+
┌──────────────────────────────┐
|
|
33
|
+
│ node.py (CameraViewer) │ rclpy 구독 + 스로틀 + 렌더 루프
|
|
34
|
+
│ - 최신 프레임만 유지(Drop) │ QoS: sensor_data (depth 1)
|
|
35
|
+
└──────┬───────────────────────┘
|
|
36
|
+
▼
|
|
37
|
+
┌────────────────────┴────────────────────┐
|
|
38
|
+
▼ ▼
|
|
39
|
+
┌──────────────────┐ ┌──────────────────────┐
|
|
40
|
+
│ convert.py │ │ tcamviewer (PyPI) │
|
|
41
|
+
│ Image/Compressed│ ── RGB24 numpy ──▶│ TerminalRenderer │
|
|
42
|
+
│ → RGB24 (H,W,3) │ │ render_rgb() │
|
|
43
|
+
└──────────────────┘ └──────────────────────┘
|
|
44
|
+
numpy + opencv만 사용. cv_bridge 사용 금지.
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 모듈 계약 (변경 금지 원칙)
|
|
48
|
+
|
|
49
|
+
| 모듈 | 책임 | 금지 사항 |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| `main.py` | wpycli 커맨드 정의, ctx → 파라미터 변환 | ROS/rclpy 직접 임포트 금지, 렌더 로직 금지 |
|
|
52
|
+
| `ros_env.py` | rclpy 가용성 확인, sys.path 주입, 환경변수 구성 후 `os.execve` 재실행 | rclpy를 모듈 레벨에서 import 금지(항상 함수 내 지연 임포트) |
|
|
53
|
+
| `node.py` | rclpy 노드/구독/스핀 루프, 프레임 슬롯·레이트리미터 | 픽셀 변환 로직 금지(convert에 위임), 렌더러 생성만 |
|
|
54
|
+
| `convert.py` | 메시지 → RGB24 numpy. 순수 함수. rclpy 무의존 (테스트 가능) | rclpy/노드 상태 금지. cv2 임포트는 함수 내부 지연. 채널 재배치는 cv2.cvtColor 사용 |
|
|
55
|
+
| `tests/` | 단위 테스트는 rclpy 없이 실행 가능해야 함 | ROS 미설치 환경에서 fail 금지 → `skipif` 사용 |
|
|
56
|
+
|
|
57
|
+
### 데이터 규약
|
|
58
|
+
|
|
59
|
+
- **내부 프레임 표준**: `numpy.ndarray`, `dtype=uint8`, `shape=(H, W, 3)`, **RGB24**, C-contiguous.
|
|
60
|
+
- 렌더러가 BGR을 원하면 `render_bgr`을 쓰지 말고 convert에서 RGB로 통일한다.
|
|
61
|
+
- `convert.image_to_rgb(data, encoding, width, height, step)`는 순수 함수 — 부작용 없음.
|
|
62
|
+
- 지원 encoding: `rgb8`, `bgr8`, `rgba8`, `bgra8`, `mono8`, `8UC1/3/4`. 추가 시
|
|
63
|
+
`_SUPPORTED_ENCODINGS` 테이블 + 테스트를 **함께** 추가한다 (TDD).
|
|
64
|
+
- 지원하지 않는 encoding은 `UnsupportedEncodingError`, 데이터 손상은 `FrameDecodeError`.
|
|
65
|
+
|
|
66
|
+
### rclpy 다루기 (중요)
|
|
67
|
+
|
|
68
|
+
- **rclpy는 pip 설치 불가** — 시스템 ROS 2(`/opt/ros/<distro>`)에만 존재.
|
|
69
|
+
- 규칙:
|
|
70
|
+
1. rclpy import는 **함수 내부 지연 임포트**만 허용 (`ros_env.ensure_rclpy()` 이후).
|
|
71
|
+
2. CLI 런 핸들러 첫 줄에서 `ensure_rclpy()` 호출. 미설치 환경에서는
|
|
72
|
+
명확한 `_HELP_MESSAGE`와 함께 `RuntimeError`.
|
|
73
|
+
3. `ensure_rclpy()`는 find_spec 기반 확인 → sys.path 주입 → 실패 시 환경변수 구성 후
|
|
74
|
+
**re-exec**(`os.execve`, 마커 `ROS_TVIEWER_REEXEC`로 무한루프 방지) 순서를 유지.
|
|
75
|
+
4. ruff/pyright: `import rclpy` 라인에는 `# type: ignore[import-not-found]` 허용.
|
|
76
|
+
|
|
77
|
+
## 3. TDD 규칙 (Red → Green → Refactor)
|
|
78
|
+
|
|
79
|
+
1. **새 기능 = 실패하는 테스트부터**. 구현 전 테스트가 반드시 `pytest`에서 fail/red를
|
|
80
|
+
확인한 뒤 구현한다.
|
|
81
|
+
2. 테스트 분류:
|
|
82
|
+
- `tests/test_convert.py` — 변환 순수함수. rclpy 불필요, 항상 실행.
|
|
83
|
+
- `tests/test_main.py` — CLI 커맨드/플래그 구조. rclpy 불필요, 항상 실행.
|
|
84
|
+
- `tests/test_ros_env.py` — 부트스트랩. `os.execve`는 **반드시 monkeypatch**로 가로챔
|
|
85
|
+
(테스트 프로세스 교체 방지).
|
|
86
|
+
- `tests/test_ros2_e2e.py` — 실제 rclpy 구독/렌더. `pytest.mark.skipif`:
|
|
87
|
+
rclpy 미탑재 환경에서는 skip. 서브프로세스로 `source /opt/ros/*/setup.bash` 후 실행.
|
|
88
|
+
3. Fake 메시지: `sensor_msgs`를 임포트하지 말고 `types.SimpleNamespace`로
|
|
89
|
+
`data/encoding/width/height/step` 필드를 만든다 (`tests/fakes.py` 헬퍼 재사용).
|
|
90
|
+
4. 커버리지 목표: convert 100%, ros_env 분기 커버, main 플래그 파싱 스냅샷.
|
|
91
|
+
5. 커밋 단위: Red(테스트 추가) → Green(최소 구현) → Refactor → `uv run ruff check .` → 반복.
|
|
92
|
+
|
|
93
|
+
## 4. 명령어
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
uv sync # 의존성 설치 (tcamviewer는 sdist → 시스템에 cmake+FFmpeg dev 필요)
|
|
97
|
+
uv run pytest # 단위 테스트
|
|
98
|
+
uv run pytest tests/test_ros2_e2e.py -v # ROS E2E (source 없이도 skip만 됨)
|
|
99
|
+
uv run ruff check . && uv run ruff format --check . # 린트/포맷
|
|
100
|
+
bash tests/e2e.sh # ROS 환경 E2E (더미 퍼블리셔 → play 60프레임)
|
|
101
|
+
uv run ros-tviewer play /camera/image_raw # 실행
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## 5. 설정 참조 (wpyconf)
|
|
105
|
+
|
|
106
|
+
`config.toml` / `.env` / CLI 플래그 우선순위: **CLI > env > file > defaults**.
|
|
107
|
+
|
|
108
|
+
| 키 (config) | env | CLI 플래그 | 기본값 |
|
|
109
|
+
| --- | --- | --- | --- |
|
|
110
|
+
| `camera.topic` | `ROS_TVIEWER_CAMERA__TOPIC` | (위치인자 `[topic]`) | `/camera/image_raw` |
|
|
111
|
+
| `camera.fps` | `ROS_TVIEWER_CAMERA__FPS` | `--fps/-f` | 30 |
|
|
112
|
+
| `camera.rotate` | `ROS_TVIEWER_CAMERA__ROTATE` | `--rotate/-r` | 0 |
|
|
113
|
+
| `camera.stretch` | `ROS_TVIEWER_CAMERA__STRETCH` | `--stretch/-s` | false |
|
|
114
|
+
| `camera.compressed` | `ROS_TVIEWER_CAMERA__COMPRESSED` | `--compressed/-z` | false(→None, 자동감지) |
|
|
115
|
+
| `camera.timeout` | `ROS_TVIEWER_CAMERA__TIMEOUT` | `--timeout/-t` | 5.0 |
|
|
116
|
+
| `camera.frames` | `ROS_TVIEWER_CAMERA__FRAMES` | `--frames/-n` | 0(무제한) |
|
|
117
|
+
| `logging.level` | `ROS_TVIEWER_LOGGING__LEVEL` | `--log-level` | WARNING |
|
|
118
|
+
|
|
119
|
+
- `compressed`는 트라이스테이트다: 플래그/설정 true → 강제, 미지정 → **None(자동 감지)**.
|
|
120
|
+
bool로 강제하면 CompressedImage 토픽에 Image로 구독하는 미스매치로 무한 대기한다 (E2E 회귀 1회).
|
|
121
|
+
- 로그는 터미널 렌더를 오염시키므로 **기본 WARNING 이상** 유지. 렌더 루프 안에서 로그 금지.
|
|
122
|
+
- 새 플래그 추가 시: main.py 플래그 + defaults + config.toml + .env.example + AGENTS.md 표 — 5곳 동시 갱신.
|
|
123
|
+
|
|
124
|
+
## 6. 실행 모델 & 성능
|
|
125
|
+
|
|
126
|
+
- 구독 QoS: `qos_profile_sensor_data` (best effort, depth 1) — 프레임 밀림 방지.
|
|
127
|
+
- 콜백은 **변환 없이 원본 메시지만** `FrameSlot`에 저장(drop)한다. 픽셀 변환은 렌더 직전에만
|
|
128
|
+
수행 — 수신률이 렌더율보다 높으면 수신 프레임 상당수가 렌더 없이 버려지므로, 변환 비용을
|
|
129
|
+
렌더량에 비례시킨다.
|
|
130
|
+
- 손상 프레임(`FrameDecodeError`/`UnsupportedEncodingError`)은 렌더 루프에서 스킵하고
|
|
131
|
+
종료 시 `bad_frames` 카운트만 로그에 남긴다.
|
|
132
|
+
- FPS 스로틀은 `RateLimiter`(모노토닉 클럭)로 구현 — `time.sleep` 사용 금지.
|
|
133
|
+
- 변환 성능(1080p, ms/frame): rgb8 0.2 / bgr8 0.3 / rgba·bgra 0.8 / mono 0.25 /
|
|
134
|
+
CompressedImage(jpeg) ~20(디코딩 자체가 지배). 채널 재배치에 numpy 음수 스트라이드 복사
|
|
135
|
+
(예: `[..., ::-1]`)을 쓰지 않는다 — `cv2.cvtColor`(SIMD)가 수십 배 빠르다.
|
|
136
|
+
- 터미널 리사이즈: 매 프레임 `get_terminal_size()` 비교, 변경 시 `renderer.resize()`.
|
|
137
|
+
- 렌더러는 `use_diff=True, alt_screen=True, hide_cursor=True` 고정.
|
|
138
|
+
종료 시(Signal 포함) `renderer.close()` 보장 — 터미널 복구 필수.
|
|
139
|
+
|
|
140
|
+
## 7. 자주 하는 실수 (Do NOT)
|
|
141
|
+
|
|
142
|
+
- ❌ 모듈 레벨 `import rclpy` → ROS 미설치 머신의 단위 테스트 전부 붕괴.
|
|
143
|
+
- ❌ `cv_bridge` 사용 → ROS 전용 의존성, uvx 실행이 깨짐. convert.py (numpy/cv2)만 사용.
|
|
144
|
+
- ❌ BGR 프레임을 `render_rgb`로 전달 → 색 반전. RGB24로 정규화 후 전달.
|
|
145
|
+
- ❌ 렌더 콜백 안에서 `print`/`logging.info` → 화면 깨짐.
|
|
146
|
+
- ❌ `except Exception: pass` 같은 무시 처리 → 에러는 명시적 예외 타입 + 안내 메시지.
|
|
147
|
+
- ❌ `time.sleep` 기반 프레임 페이싱 → 지연 누적. 모노토닉 기반 RateLimiter 사용.
|
|
148
|
+
|
|
149
|
+
## 8. 배포
|
|
150
|
+
|
|
151
|
+
- `uv build` → `uv publish` (PyPI, 패키지명 `ros_tviewer` / 실행명 `ros-tviewer`).
|
|
152
|
+
- 배포 전 체크리스트: `uv run pytest`, `uv run ruff check .`, `bash tests/e2e.sh`, README 실행 예시 최신화.
|
|
153
|
+
- tcamviewer 하위호환: 구버전(≤0.2.2) 휠의 `.so` 이중 중첩 버그가 있으므로 하한 `>=0.2.3` 고정.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
이 프로젝트의 모든 주목할 만한 변경 사항이 이 파일에 기록됩니다.
|
|
4
|
+
|
|
5
|
+
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)을 따르며,
|
|
6
|
+
버전 관리는 [Semantic Versioning](https://semver.org/lang/ko/)을 따릅니다.
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `play` 커맨드: ROS 2 카메라 토픽(Image/CompressedImage)을 tcamviewer
|
|
13
|
+
Half-Block TrueColor 렌더러로 터미널에 실시간 재생 — `--fps`, `--rotate`,
|
|
14
|
+
`--stretch`, `--compressed`, `--timeout`, `--frames` 플래그
|
|
15
|
+
- `topics` 커맨드: 광고 중인 sensor_msgs 카메라 토픽 나열
|
|
16
|
+
- `config` / `version` 커맨드
|
|
17
|
+
- 최신 프레임 우선(drop) 구독 + 모노토닉 FPS 스로틀, 렌더 직전 변환
|
|
18
|
+
- 시스템 ROS 2 자동 부트스트랩 (sys.path 주입 → re-exec)
|
|
19
|
+
- 단위 테스트(ROS 불필요), ROS E2E 테스트, CI (ruff lint/format + pytest)
|
|
20
|
+
|
|
21
|
+
## [0.1.0] - 2025-09-09
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- 최초 릴리스 (위 Unreleased 항목 포함)
|
|
26
|
+
|
|
27
|
+
[Unreleased]: https://github.com/wkqco33/ros_tviewer/compare/v0.1.0...HEAD
|
|
28
|
+
[0.1.0]: https://github.com/wkqco33/ros_tviewer/releases/tag/v0.1.0
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# 기여 가이드 (Contributing)
|
|
2
|
+
|
|
3
|
+
ros-tviewer에 관심을 가져주셔서 감사합니다. 아래 규칙을 따라주시면 리뷰가 빨라집니다.
|
|
4
|
+
|
|
5
|
+
## 개발 환경
|
|
6
|
+
|
|
7
|
+
- Python 3.12, [uv](https://docs.astral.sh/uv/), ROS 2 (Jazzy/Humble, 테스트 실행 시)
|
|
8
|
+
- 빌드 도구: `cmake`, FFmpeg dev 라이브러리 (`libavcodec-dev` 등) — tcamviewer sdist 빌드용
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
uv sync # 의존성 설치
|
|
12
|
+
uv run pytest # 단위 테스트 (ROS 미설치 환경에서도 실행 가능)
|
|
13
|
+
uv run ruff check . # 린트
|
|
14
|
+
uv run ruff format --check . # 포맷 검사
|
|
15
|
+
uvx pyright --project . # 타입 체크
|
|
16
|
+
bash tests/e2e.sh # ROS 환경 E2E (더미 퍼블리셔 → play 60프레임)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 규칙
|
|
20
|
+
|
|
21
|
+
- **[AGENTS.md](AGENTS.md)를 먼저 읽어주세요** — 아키텍처·모듈 계약·TDD 규칙이 정리되어 있습니다.
|
|
22
|
+
- 새 기능은 **실패하는 테스트부터** 추가합니다 (Red → Green → Refactor).
|
|
23
|
+
- 단위 테스트는 rclpy/sensor_msgs 없이 실행 가능해야 합니다 (`tests/fakes.py` 헬퍼 사용).
|
|
24
|
+
rclpy가 필요한 경로는 `pytest.mark.skipif` 또는 서브프로세스 E2E로 검증합니다.
|
|
25
|
+
- 모듈 레벨 `import rclpy` 금지 (함수 내부 지연 임포트만 허용).
|
|
26
|
+
- 렌더 루프 안에서 `print`/로깅 금지 (터미널 오염).
|
|
27
|
+
- 커밋 전에 린트·포맷·테스트를 모두 통과해야 합니다.
|
|
28
|
+
|
|
29
|
+
## PR 프로세스
|
|
30
|
+
|
|
31
|
+
1. 이슈를 먼저 생성해 방향을 맞추는 것을 권장합니다.
|
|
32
|
+
2. 작은 단위의 PR을 선호합니다 (한 PR = 한 목적).
|
|
33
|
+
3. PR 설명에 변경 요약과 검증 방법(`pytest`, E2E 등)을 적어주세요.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 seo.youngchae
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ros_tviewer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ROS 2 카메라 토픽을 터미널에서 재생하는 뷰어
|
|
5
|
+
Project-URL: Homepage, https://github.com/wkqco33/ros_tviewer
|
|
6
|
+
Project-URL: Repository, https://github.com/wkqco33/ros_tviewer
|
|
7
|
+
Project-URL: Issues, https://github.com/wkqco33/ros_tviewer/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/wkqco33/ros_tviewer/blob/main/CHANGELOG.md
|
|
9
|
+
Author: seo.youngchae
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: camera,rclpy,ros2,terminal,tui,viewer
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Multimedia :: Video :: Capture
|
|
19
|
+
Classifier: Topic :: Terminals
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Requires-Dist: numpy
|
|
22
|
+
Requires-Dist: opencv-python-headless
|
|
23
|
+
Requires-Dist: tcamviewer>=0.2.3
|
|
24
|
+
Requires-Dist: wpycli
|
|
25
|
+
Requires-Dist: wpyconf
|
|
26
|
+
Requires-Dist: wpylog
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# ros-tviewer
|
|
30
|
+
|
|
31
|
+
[](https://github.com/wkqco33/ros_tviewer/actions/workflows/ci.yml)
|
|
32
|
+
[](https://pypi.org/project/ros-tviewer/)
|
|
33
|
+
[](https://www.python.org/)
|
|
34
|
+
[](LICENSE)
|
|
35
|
+
|
|
36
|
+
ROS 2 카메라 토픽을 터미널에서 재생하는 CLI 뷰어.
|
|
37
|
+
|
|
38
|
+
[sensor_msgs/msg/Image](https://docs.ros.org/) 및 `sensor_msgs/msg/CompressedImage` 토픽을
|
|
39
|
+
[tcamviewer](https://pypi.org/project/tcamviewer/)의 Half-Block TrueColor(24-bit ANSI) 렌더러로
|
|
40
|
+
터미널에 실시간(30~60+ FPS) 재생한다.
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
uv run ros-tviewer play /camera/image_raw
|
|
44
|
+
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 요구 사항
|
|
48
|
+
|
|
49
|
+
| 항목 | 설명 |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| OS | Linux |
|
|
52
|
+
| Python | 3.12 (ROS 2 Jazzy ABI 매칭) |
|
|
53
|
+
| ROS 2 | Jazzy/Humble (`rclpy`는 시스템 ROS에서 로드 — pip 설치 불가) |
|
|
54
|
+
| 빌드 도구 | `cmake`, FFmpeg dev 라이브러리 (`libavcodec-dev` 등) — tcamviewer sdist 빌드용 |
|
|
55
|
+
| 터미널 | TrueColor 지원 (`echo $COLORTERM` → `truecolor`/`24bit`) |
|
|
56
|
+
|
|
57
|
+
## 시작하기
|
|
58
|
+
|
|
59
|
+
### 설치
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# uv tool (권장)
|
|
63
|
+
uv tool install ros-tviewer
|
|
64
|
+
|
|
65
|
+
# 또는 pipx / pip
|
|
66
|
+
pipx install ros-tviewer
|
|
67
|
+
pip install ros-tviewer
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
PyPI 배포 전 개발 트리에서는 저장소 클론 후 실행한다:
|
|
71
|
+
|
|
72
|
+
### 개발 트리에서 실행
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
uv sync
|
|
76
|
+
|
|
77
|
+
# 1) 광고 중인 카메라 토픽 확인
|
|
78
|
+
uv run ros-tviewer topics
|
|
79
|
+
|
|
80
|
+
# 2) 재생 (ROS 환경 미 source 시 자동 감지·재실행)
|
|
81
|
+
source /opt/ros/jazzy/setup.bash # 권장 — 미실행 시 자동 부트스트랩이 시도됨
|
|
82
|
+
uv run ros-tviewer play /camera/image_raw
|
|
83
|
+
|
|
84
|
+
# 옵션 예시
|
|
85
|
+
uv run ros-tviewer play /camera/image_raw/compressed --fps 30 --rotate 90 --stretch
|
|
86
|
+
uv run ros-tviewer play /camera/image_raw --frames 100 # 100 프레임 렌더 후 종료 (테스트/데모)
|
|
87
|
+
uv run ros-tviewer config # 현재 설정 출력
|
|
88
|
+
uv run ros-tviewer version # 앱·의존성·런타임 리포트
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- `q`/`Ctrl+C`로 종료 (터미널 상태 자동 복구).
|
|
92
|
+
- `--compressed` 미지정 시 토픽 타입을 자동 감지한다.
|
|
93
|
+
- 설정 우선순위: **CLI 플래그 > `.env` > `config.toml` > 기본값**.
|
|
94
|
+
|
|
95
|
+
## E2E 테스트
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
uv run pytest # 단위 테스트 (ROS 미설치 환경에서도 실행 가능, ROS 테스트는 skip)
|
|
99
|
+
bash tests/e2e.sh # 더미 퍼블리셔 → play 60프레임 데모
|
|
100
|
+
uv run pytest tests/test_ros2_e2e.py -v # 실제 rclpy 구독 + 렌더 E2E
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## 구성
|
|
104
|
+
|
|
105
|
+
| 모듈 | 책임 |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| `main.py` | wpycli 커맨드 (`play`/`topics`/`config`), 플래그·설정 파싱 |
|
|
108
|
+
| `ros_env.py` | rclpy 부트스트랩 (시스템 ROS 탐지 → sys.path 주입 → re-exec) |
|
|
109
|
+
| `node.py` | 구독(QoS sensor_data), 최신 메시지 유지(drop), FPS 스로틀, 렌더 직전 변환 루프 |
|
|
110
|
+
| `convert.py` | Image/CompressedImage → RGB24 numpy (numpy/opencv만 사용, cv_bridge 없이) |
|
|
111
|
+
|
|
112
|
+
## 개발
|
|
113
|
+
|
|
114
|
+
- 아키텍처·TDD 규칙·컨벤션: **[AGENTS.md](AGENTS.md)** — 코드 변경 전 필독.
|
|
115
|
+
- 린트: `uv run ruff check .`
|
|
116
|
+
- 타입 체크: `uvx pyright --project .`
|
|
117
|
+
- 테스트: `uv run pytest`
|
|
118
|
+
|
|
119
|
+
## 기여 및 보안
|
|
120
|
+
|
|
121
|
+
- 기여 방법: [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
122
|
+
- 취약점 보고: [SECURITY.md](SECURITY.md) — 공개 이슈로 보고하지 마세요.
|
|
123
|
+
- 변경 이력: [CHANGELOG.md](CHANGELOG.md)
|
|
124
|
+
- 라이선스: [MIT](LICENSE)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# ros-tviewer
|
|
2
|
+
|
|
3
|
+
[](https://github.com/wkqco33/ros_tviewer/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/ros-tviewer/)
|
|
5
|
+
[](https://www.python.org/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
ROS 2 카메라 토픽을 터미널에서 재생하는 CLI 뷰어.
|
|
9
|
+
|
|
10
|
+
[sensor_msgs/msg/Image](https://docs.ros.org/) 및 `sensor_msgs/msg/CompressedImage` 토픽을
|
|
11
|
+
[tcamviewer](https://pypi.org/project/tcamviewer/)의 Half-Block TrueColor(24-bit ANSI) 렌더러로
|
|
12
|
+
터미널에 실시간(30~60+ FPS) 재생한다.
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
uv run ros-tviewer play /camera/image_raw
|
|
16
|
+
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 요구 사항
|
|
20
|
+
|
|
21
|
+
| 항목 | 설명 |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| OS | Linux |
|
|
24
|
+
| Python | 3.12 (ROS 2 Jazzy ABI 매칭) |
|
|
25
|
+
| ROS 2 | Jazzy/Humble (`rclpy`는 시스템 ROS에서 로드 — pip 설치 불가) |
|
|
26
|
+
| 빌드 도구 | `cmake`, FFmpeg dev 라이브러리 (`libavcodec-dev` 등) — tcamviewer sdist 빌드용 |
|
|
27
|
+
| 터미널 | TrueColor 지원 (`echo $COLORTERM` → `truecolor`/`24bit`) |
|
|
28
|
+
|
|
29
|
+
## 시작하기
|
|
30
|
+
|
|
31
|
+
### 설치
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# uv tool (권장)
|
|
35
|
+
uv tool install ros-tviewer
|
|
36
|
+
|
|
37
|
+
# 또는 pipx / pip
|
|
38
|
+
pipx install ros-tviewer
|
|
39
|
+
pip install ros-tviewer
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
PyPI 배포 전 개발 트리에서는 저장소 클론 후 실행한다:
|
|
43
|
+
|
|
44
|
+
### 개발 트리에서 실행
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv sync
|
|
48
|
+
|
|
49
|
+
# 1) 광고 중인 카메라 토픽 확인
|
|
50
|
+
uv run ros-tviewer topics
|
|
51
|
+
|
|
52
|
+
# 2) 재생 (ROS 환경 미 source 시 자동 감지·재실행)
|
|
53
|
+
source /opt/ros/jazzy/setup.bash # 권장 — 미실행 시 자동 부트스트랩이 시도됨
|
|
54
|
+
uv run ros-tviewer play /camera/image_raw
|
|
55
|
+
|
|
56
|
+
# 옵션 예시
|
|
57
|
+
uv run ros-tviewer play /camera/image_raw/compressed --fps 30 --rotate 90 --stretch
|
|
58
|
+
uv run ros-tviewer play /camera/image_raw --frames 100 # 100 프레임 렌더 후 종료 (테스트/데모)
|
|
59
|
+
uv run ros-tviewer config # 현재 설정 출력
|
|
60
|
+
uv run ros-tviewer version # 앱·의존성·런타임 리포트
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- `q`/`Ctrl+C`로 종료 (터미널 상태 자동 복구).
|
|
64
|
+
- `--compressed` 미지정 시 토픽 타입을 자동 감지한다.
|
|
65
|
+
- 설정 우선순위: **CLI 플래그 > `.env` > `config.toml` > 기본값**.
|
|
66
|
+
|
|
67
|
+
## E2E 테스트
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
uv run pytest # 단위 테스트 (ROS 미설치 환경에서도 실행 가능, ROS 테스트는 skip)
|
|
71
|
+
bash tests/e2e.sh # 더미 퍼블리셔 → play 60프레임 데모
|
|
72
|
+
uv run pytest tests/test_ros2_e2e.py -v # 실제 rclpy 구독 + 렌더 E2E
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 구성
|
|
76
|
+
|
|
77
|
+
| 모듈 | 책임 |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `main.py` | wpycli 커맨드 (`play`/`topics`/`config`), 플래그·설정 파싱 |
|
|
80
|
+
| `ros_env.py` | rclpy 부트스트랩 (시스템 ROS 탐지 → sys.path 주입 → re-exec) |
|
|
81
|
+
| `node.py` | 구독(QoS sensor_data), 최신 메시지 유지(drop), FPS 스로틀, 렌더 직전 변환 루프 |
|
|
82
|
+
| `convert.py` | Image/CompressedImage → RGB24 numpy (numpy/opencv만 사용, cv_bridge 없이) |
|
|
83
|
+
|
|
84
|
+
## 개발
|
|
85
|
+
|
|
86
|
+
- 아키텍처·TDD 규칙·컨벤션: **[AGENTS.md](AGENTS.md)** — 코드 변경 전 필독.
|
|
87
|
+
- 린트: `uv run ruff check .`
|
|
88
|
+
- 타입 체크: `uvx pyright --project .`
|
|
89
|
+
- 테스트: `uv run pytest`
|
|
90
|
+
|
|
91
|
+
## 기여 및 보안
|
|
92
|
+
|
|
93
|
+
- 기여 방법: [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
94
|
+
- 취약점 보고: [SECURITY.md](SECURITY.md) — 공개 이슈로 보고하지 마세요.
|
|
95
|
+
- 변경 이력: [CHANGELOG.md](CHANGELOG.md)
|
|
96
|
+
- 라이선스: [MIT](LICENSE)
|