30분 | 중상급 | WiFi CSI 포즈 추정 원리 파악, Python/Rust 듀얼 버전 배포, ESP32 감지 노드 네트워크 접속 및 엔드 투 엔드(End-to-End) 파이프라인 실행
대상 독자
- WiFi 감지 및 유비쿼터스 컴퓨팅에 관심 있는 개발자
- Python 기초가 있으며 Rust 기반 고성능 추론을 알고 싶은 엔지니어
- 카메라를 사용하지 않는 비접촉식 인체 감지 솔루션을 탐구하려는 연구자(Researcher)
최소 요구 사항: Python 클래스와 기본적인 Rust 문법을 이해할 수 있으며, Linux/macOS 또는 WSL2 환경을 보유해야 함.
핵심 의존성 및 환경
| 의존성 | 요구 버전 | 역할 |
|---|---|---|
| Python | ≥ 3.9 | 메인 코드베이스 v1 |
| Rust | ≥ 1.75 | 고성능 추론 포트 |
| PyTorch | ≥ 2.1 | 신경망 추론 |
| NumPy + SciPy | 최신 안정판 | CSI 신호 처리 |
| OpenCV | ≥ 4.8 | 포즈 시각화 |
| ESP32-S3 개발보드 | 8MB Flash | WiFi CSI 하드웨어 노드 (선택 사항) |
| AP 지원 라우터 | 802.11n 이상 | CSI 데이터 소스 |
WARNING
일반 ESP32(1세대) 및 ESP32-C3는 CSI 수집을 지원하지 않습니다. 이 프로젝트는 ESP32-S3(Xtensa 듀얼 코어) 또는 ESP32-C6 + 60GHz mmWave 모듈이 반드시 필요합니다.
전체 프로젝트 구조
RuView/
├── v1/ # Python 메인 코드베이스
│ ├── src/
│ │ ├── core/
│ │ │ ├── csi_processor.py # CSI 데이터 구조 및 처리
│ │ │ ├── phase_sanitizer.py # 위상 노이즈 보정
│ │ │ └── router_interface.py # 라우터/ESP32 통신
│ │ ├── hardware/
│ │ │ ├── csi_extractor.py # ESP32 시리얼 데이터 추출
│ │ │ └── router_interface.py # WiFi CSI 프레임 캡처
│ │ └── services/
│ │ └── ... # 신호 처리 파이프라인
│ ├── data/
│ │ └── proof/
│ │ ├── sample_csi_data.json # 1000프레임 확정적 합성 CSI (seed=42)
│ │ └── verify.py # SHA-256 파이프라인 검증 스크립트
│ └── tests/ # pytest 테스트 수트
├── rust-port/wifi-densepose-rs/ # Rust 이식 버전
│ └── crates/
│ ├── wifi-densepose-core/ # 핵심 타입, CSI 프레임 기본형
│ ├── wifi-densepose-signal/ # RuvSense SOTA 신호 처리 (14개 모듈)
│ ├── wifi-densepose-nn/ # ONNX/PyTorch/Candle 추론 백엔드
│ ├── wifi-densepose-train/ # RuVector 훈련 파이프라인
│ ├── wifi-densepose-hardware/ # ESP32 TDM 프로토콜
│ └── wifi-densepose-api/ # Axum REST API
├── firmware/esp32-csi-node/ # ESP32-S3 펌웨어 소스 코드
├── docs/adr/ # 43개의 아키텍처 결정 기록 (ADR)
└── pyproject.toml # Python 의존성 정의
단계별 가이드
1단계: WiFi CSI 원리 이해 (하드웨어 없이 Python 시뮬레이션 실행)
WiFi CSI의 본질은 무선 신호가 공간에서 전파될 때, 인체에 반사되어 발생하는 진폭(Amplitude)과 위상(Phase)의 변화를 이용해 비접촉 감지를 구현하는 것입니다.
WiFi 장치(라우터)와 수신단 사이에 사람이 움직이면, CSI 보고서에 각 부반송파(Subcarrier)의 복소수 값인 진폭과 위상이 기록됩니다. 이러한 변화는 미세하지만 인체의 포즈와 동작을 유추하기에 충분합니다.
RuView의 강점은 CSI 신호를 일련의 파이프라인(위상 보정 → 다중 경로 억제 → 신경망 추론)을 통해 17개 주요 포인트(Keypoint)의 포즈 추정값으로 변환한다는 점입니다.
하드웨어를 연결하기 전, 내장된 합성 데이터를 사용하여 전체 프로세스를 실행해 보세요:
# 프로젝트 클론
git clone https://github.com/ruvnet/RuView.git
cd RuView
# Python 의존성 설치
cd v1
pip install -e ".[dev]"
# 확정적 파이프라인 검증 실행 (하드웨어 불필요)
python data/proof/verify.py
정상적으로 작동한다면 다음과 같은 화면이 출력됩니다:
===== WiFi-DensePose Pipeline Verification =====
Generator: generate_reference_signal.py v1.0.0
Seed: 42
Frames: 1000 | Antennas: 3 | Subcarriers: 56
Loading sample CSI data... done
Running pipeline...
[1/4] Phase sanitization ............ OK
[2/4] Multistatic signal processing .. OK
[3/4] Neural network inference ....... OK
[4/4] Keypoint extraction ........... OK
Computing reference hash (SHA-256)...
VERDICT: PASS
TIP
이 검증 스크립트는 seed=42로 생성된 합성 CSI 데이터를 사용하므로 결과가 완전히 재현 가능합니다. 이 단계가 성공하면 Python 환경에 문제가 없음을 의미하며 다음 단계로 넘어갈 수 있습니다.
2단계: 환경 준비 (Python + Rust + WSL2)
Python 환경 (venv 권장)
# 호환성이 가장 좋은 Python 3.10 또는 3.11 사용 권장
python3.10 -m venv .venv
source .venv/bin/activate # Linux/macOS
# .venv\Scripts\activate # Windows PowerShell
pip install --upgrade pip
pip install -e ".[gpu]" # NVIDIA GPU가 있는 경우
pip install -e ".[dev]" # 개발용 의존성 (pytest 포함)
Rust 환경 (고성능 추론 포트용)
# Rust 설치 (미설치 시)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 검증
rustc --version # rustc 1.75+ 출력 확인
cargo --version # cargo 1.75+ 출력 확인
# ESP32 개발을 위한 Rust 툴체인 지원 (하드웨어가 있는 경우만 선택 사항)
rustup target add xtensa-esp32-elf riscv32imc-esp-elf
Windows 사용자: WSL2 필수
# PowerShell에서 관리자 권한으로 실행
wsl --install
# 재부팅 후 WSL 접속
wsl -d Ubuntu-22.04
WARNING
ESP-IDF(ESP32 펌웨어 컴파일 툴체인)는 Windows 네이티브 Git Bash에서 환경 변수 충돌(MSYSTEM 변수가 ESP-IDF v5.4를 방해)이 발생할 수 있습니다. Windows에서 ESP32 펌웨어를 컴파일하려면 WSL2 또는 순수 PowerShell 사용을 권장합니다.
3단계: Python 버전 포즈 추정 파이프라인 실행
이제 실제 코드를 사용하여 CSI 데이터를 추론 파이프라인에 입력해 보겠습니다.
3.1 CSI 데이터 구조
# v1/src/core/csi_processor.py
from __future__ import annotations
import numpy as np
from dataclasses import dataclass
from typing import List
@dataclass
class CSIFrame:
"""단일 CSI 프레임 — WiFi 채널 측정 1회에 해당"""
timestamp_s: float
amplitude: np.ndarray # shape: (num_antennas, num_subcarriers)
phase: np.ndarray # shape: (num_antennas, num_subcarriers)
subcarrier_indices: np.ndarray # 부반송파 인덱스
@classmethod
def from_json(cls, data: dict) -> CSIFrame:
return cls(
timestamp_s=data["timestamp_s"],
amplitude=np.array(data["amplitude"], dtype=np.float32),
phase=np.zeros_like(data["amplitude"], dtype=np.float32), # JSON에 위상이 없으므로 수동 초기화
subcarrier_indices=np.arange(56), # 기본 56개 부반송파
)
@dataclass
class PoseKeypoints:
"""17개 COCO 주요 포인트(Keypoints) 포즈 결과"""
keypoints: List[np.ndarray] # 각 keypoint의 (x, y, confidence)
pose_id: int
def get_skeleton(self) -> np.ndarray:
"""모든 주요 포인트의 (N, 2) 좌표 배열 반환, 시각화에 용이"""
return np.array([p[:2] for p in self.keypoints])
3.2 위상 보정 (LO 주파수 편차 제거)
원본 CSI 위상은 LO(국부 발진기) 노이즈에 의해 심하게 오염되어 있으므로 사용 전 보정이 필요합니다:
# v1/src/core/phase_sanitizer.py
import numpy as np
def sanitize_phase(csi: np.ndarray) -> np.ndarray:
"""
반복적 LO 위상 오프셋 추정 + 원형 평균(Circular Mean) 보정
참고 문헌: ADR-014 SOTA Signal Processing
매개변수:
csi: shape (num_antennas, num_subcarriers)인 복소수 CSI 행렬
반환값:
보정된 위상 행렬 (동일한 shape)
"""
num_antennas, num_subcarriers = csi.shape
phase = np.angle(csi) # 원본 위상 [-π, π]
# 반복적 위상 오프셋 추정 (일반적으로 3회 반복 시 수렴)
for _ in range(3):
# 각 부반송파에서 인접 안테나와의 위상차를 이용해 오프셋 추정
phase_offset = np.zeros(num_antennas)
for a in range(1, num_antennas):
delta = phase[a] - phase[0]
# 원형 평균 (위상 랩핑 고려)
delta_mean = np.arctan2(
np.mean(np.sin(delta)),
np.mean(np.cos(delta))
)
phase_offset[a] = delta_mean
phase[a] -= phase_offset[a]
return phase
def extract_clean_amplitude(csi: np.ndarray) -> np.ndarray:
"""노이즈가 심한 부반송파를 제외하고 깨끗한 진폭 추출"""
amplitude = np.abs(csi)
# Z-score 필터링: 진폭이 비정상적으로 낮은 부반송파(간섭 가능성) 제거
z_scores = (amplitude - np.mean(amplitude, axis=1, keepdims=True)) \
/ (np.std(amplitude, axis=1, keepdims=True) + 1e-8)
mask = z_scores > -2.5
amplitude = np.where(mask, amplitude, 0)
return amplitude
3.3 엔드 투 엔드 추론 프로세스
# v1/demo_pipeline.py
import json
import numpy as np
from pathlib import Path
from src.core.csi_processor import CSIFrame, PoseKeypoints
from src.core.phase_sanitizer import sanitize_phase, extract_clean_amplitude
def load_csi_frames(json_path: str) -> list[CSIFrame]:
"""JSON 파일에서 CSI 프레임 시퀀스 로드"""
with open(json_path) as f:
data = json.load(f)
return [CSIFrame.from_json(frame) for frame in data["frames"]]
def run_pipeline(csi_frame: CSIFrame) -> PoseKeypoints:
"""
전체 CSI → Pose 추론 파이프라인
프로세스:
1. 위상 보정 (LO 노이즈 제거)
2. 진폭 정리 (이상치 필터링)
3. 다중 경로 억제 (RuvSense 신호 처리 핵심)
4. 신경망 추론 (단순화된 주요 포인트 출력)
5. 칼만 필터 추적 (17-keypoint 트래커)
"""
# 1단계: 위상 보정
# 데모용으로 진폭에 위상을 입혀 원본 CSI 모사
csi = csi_frame.amplitude * np.exp(1j * csi_frame.phase)
clean_phase = sanitize_phase(csi)
# 2단계: 깨끗한 진폭 추출
clean_amplitude = extract_clean_amplitude(csi)
# 3단계: 다중 경로 억제 (단순화 버전 — 공간 평균 취득)
suppressed = clean_amplitude.mean(axis=0) # (56,) 안테나 차원 평균
# 4단계: 단순화된 포즈 추론 (데모용)
# 실제로는 PyTorch 모델 호출: self.model(suppressed.unsqueeze(0))
# 여기서는 17개 포인트의 (x, y, conf) 가짜 출력 생성
keypoints = []
for i in range(17):
x = 0.5 + 0.05 * np.sin(i * 0.5) + np.random.normal(0, 0.02)
y = 0.5 + 0.3 * np.cos(i * 0.3) + np.random.normal(0, 0.02)
conf = 0.9 + np.random.normal(0, 0.05)
keypoints.append(np.array([x, y, np.clip(conf, 0, 1)]))
return PoseKeypoints(keypoints=keypoints, pose_id=0)
def main():
csi_path = Path(__file__).parent / "data/proof/sample_csi_data.json"
frames = load_csi_frames(str(csi_path))
print(f"Loaded {len(frames)} CSI frames")
# 앞선 30프레임 추론 실행
for i, frame in enumerate(frames[:30]):
pose = run_pipeline(frame)
skeleton = pose.get_skeleton()
if i % 10 == 0:
print(f"Frame {i}: {len(skeleton)} keypoints, "
f"avg confidence={np.mean([k[2] for k in pose.keypoints]):.3f}")
print("Pipeline run complete.")
if __name__ == "__main__":
main()
실행:
python demo_pipeline.py
# 출력 예시:
# Loaded 1000 CSI frames
# Frame 0: 17 keypoints, avg confidence=0.901
# Frame 10: 17 keypoints, avg confidence=0.898
# ...
# Pipeline run complete.
4단계: Rust 버전 컴파일 및 테스트 (초고속 추론)
Python 버전으로 로직의 정확성을 확인했다면, 이제 밀리초(ms) 단위의 지연 시간을 제공하는 생산 환경용 Rust 버전으로 전환해 보겠습니다.
cd rust-port/wifi-densepose-rs
# 컴파일 확인 (GPU 불필요, 단일 크레이트 빠른 검증)
cargo check -p wifi-densepose-core --no-default-features
cargo check -p wifi-densepose-signal --no-default-features
# 전체 테스트 수트 실행 (1,031개 이상의 테스트, 약 2분 소요)
cargo test --workspace --no-default-features
TIP
--no-default-features는 GPU/CUDA 관련 코드를 건너뛰므로 NVIDIA 드라이버가 없는 기기에서도 실행 가능합니다. 테스트 통과는 핵심 파이프라인 로직이 완벽함을 의미합니다.
핵심 Rust 코드 구조는 다음과 같으며, Python 버전과 설계 사상이 일치함을 알 수 있습니다:
// crates/wifi-densepose-core/src/types.rs
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
// WiFi CSI 프레임 — Rust 버전은 강력한 타입을 사용하여 Python의 dataclass 대체
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CSIFrame {
pub timestamp_us: u64,
pub amplitude: Vec<Vec<f32>>, // (antennas, subcarriers)
pub phase_raw: Vec<Vec<f32>>, // 원본 위상 (미보정)
pub subcarrier_idx: Vec<i32>,
}
// RuvSense 처리 후의 포즈 주요 포인트
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PoseKeypoints {
pub keypoints: Vec<Keypoint>,
pub pose_id: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Keypoint {
pub x: f32,
pub y: f32,
pub confidence: f32,
pub label: &'static str, // "nose", "left_shoulder" 등
}
// 에러 타입 — Rust 스타일
#[derive(Debug, thiserror::Error)]
pub enum CSIPipelineError {
#[error("Insufficient antennas: got {0}, need >= 2")]
InsufficientAntennas(usize),
#[error("NaN detected in CSI data")]
NaNInCSI,
#[error("Phase sanitization failed: {0}")]
PhaseSanitization(String),
}
// crates/wifi-densepose-signal/src/ruvsense/phase_align.rs
// 위상 보정 — Rust 고성능 버전 (SIMD 가속)
use std::f32::consts::PI;
pub fn circular_mean(samples: &[f32]) -> f32 {
// 원형 평균 = atan2(sum(sin), sum(cos))
let (sin_sum, cos_sum) = samples.iter()
.map(|&x| (x.sin(), x.cos()))
.fold((0.0_f32, 0.0_f32), |(s, c), (si, co)| (s + si, c + co));
sin_sum.atan2(cos_sum)
}
/// 반복적 LO 위상 오프셋 보정 (Python의 sanitize_phase에 해당)
pub fn align_phase(phase: &mut [[f32; 56]; 3]) {
for _iter in 0..3 {
for ant in 1..3 {
let delta: [f32; 56] = std::array::from_fn(|sc| phase[ant][sc] - phase[0][sc]);
let offset = circular_mean(&delta);
for sc in 0..56 {
phase[ant][sc] -= offset;
// [-π, π] 범위로 조정
phase[ant][sc] = ((phase[ant][sc] + PI) % (2.0 * PI)) - PI;
}
}
}
}
5단계: ESP32-S3 펌웨어 플래싱 및 WiFi 네트워크 접속
하드웨어를 보유한 사용자를 위한 단계입니다. 하드웨어가 없다면 6단계로 바로 넘어가세요.
5.1 펌웨어 컴파일
WARNING
아래 명령은 Windows WSL2 또는 Linux/macOS에서 실행하십시오. Windows 네이티브 Git Bash에서 ESP-IDF를 실행하지 마십시오.
cd firmware/esp32-csi-node
# 설정 — 8MB Flash 버전 (표준)
cp sdkconfig.defaults.8mb sdkconfig.defaults
# 펌웨어 컴파일
# 먼저 esp-idf 환경을 source해야 합니다.
. /path/to/esp-idf/export.sh # Linux/macOS/WSL2
# C:\Espressif\esp-idf\export.ps1 # Windows PowerShell
idf.py build
TIP
ESP-IDF v5.4 + ESP32-S3의 WiFi CSI 기능을 위해서는 특정 Kconfig 설정(CONFIG_ESP_WIFI_CSI_ENABLED=y)이 필요하며, 기본 템플릿에 이미 적용되어 있습니다.
5.2 ESP32-S3에 굽기(Flash)
# 시리얼 포트 확인 (Linux/macOS: /dev/ttyUSB0, Windows: COM7)
idf.py -p /dev/ttyUSB0 flash monitor
5.3 WiFi 네트워크 설정
python firmware/esp32-csi-node/provision.py \
--port /dev/ttyUSB0 \
--ssid "YourRouterSSID" \
--password "YourRouterPassword" \
--target-ip 192.168.1.20 # ESP32가 연결될 CSI 수집단 IP
5.4 CSI 데이터 전송 확인
# 시리얼 모니터 열기
python -m serial.tools.miniterm /dev/ttyUSB0 115200
다음과 같은 내용이 지속적으로 출력되면 CSI 데이터가 정상적으로 전송되고 있는 것입니다:
CSI: ts=1234567890 ant=0 sc=0 amp=1.23 phase=-0.45
CSI: ts=1234567890 ant=0 sc=1 amp=1.45 phase=-0.32
CSI: ts=1234567890 ant=1 sc=0 amp=0.98 phase=0.12
...
6단계: 엔드 투 엔드 검증 — CSI 수집부터 포즈 출력까지
Python의 hardware/csi_extractor.py를 ESP32 데이터 스트림에 연결하거나, 라우터 미러 모드를 사용하여 실제 CSI를 수집합니다:
# CSI 수집 서비스 시작 (ESP32 노드 연결)
cd v1
python -m src.hardware.csi_extractor --port /dev/ttyUSB0 --baud 115200
# 다른 터미널에서 REST API 시작 (Rust Axum 서비스)
cd rust-port/wifi-densepose-rs
cargo run -p wifi-densepose-api
API 엔드포인트:
# 현재 감지된 인원수 확인
curl http://localhost:8080/api/v1/poses/current
# 최근 10프레임의 스켈레톤 좌표 가져오기
curl http://localhost:8080/api/v1/poses/history?limit=10
응답 예시:
{
"poses": [
{
"pose_id": 1,
"keypoints": [
{"label": "nose", "x": 0.52, "y": 0.15, "confidence": 0.94},
{"label": "left_shoulder", "x": 0.38, "y": 0.28, "confidence": 0.91},
{"label": "right_shoulder","x": 0.65, "y": 0.27, "confidence": 0.89},
...
],
"timestamp_us": 1712345678901234
}
]
}
7단계: 공식 테스트 수트 실행 (선택 사항)
# Python 테스트
cd v1
python -m pytest tests/ -x -q
# Rust 테스트
cd rust-port/wifi-densepose-rs
cargo test --workspace --no-default-features
# 전체 검증 패키지 생성 (ADR-028 표준)
cd ../..
bash scripts/generate-witness-bundle.sh
cd dist/witness-bundle-ADR028-*/
bash VERIFY.sh
성공 시 검증 패키지는 Rust 테스트 결과, Python proof 해시 및 펌웨어 파일 SHA-256을 포함하여 7/7 PASS를 출력합니다.
자주 발생하는 문제 해결(FAQ)
Q1: ESP32에서 CSI 데이터가 나오지 않고 빈 화면만 뜹니다.
WiFi CSI는 라우터와 ESP32 모두 802.11n을 지원해야 하며, 일부 라우터는 CSI 수집 기능을 수동으로 활성화해야 합니다(ASUS/Netgear 일부 모델 지원). 라우터 펌웨어를 확인하거나 ESP32를 AP 모드로 설정하여 데이터 소스로 사용해 보세요.
Q2: python data/proof/verify.py 실행 시 numpy.linalg.LinAlgError가 발생합니다.
보통 numpy/scipy 버전 호환성 문제입니다. numpy >= 1.24, scipy >= 1.11를 사용 중인지 확인하고, conda의 구버전 미러를 피하세요. venv를 사용하여 환경을 격리하는 것을 권장합니다.
Q3: Rust cargo test 중 linker cc not found 에러가 발생합니다.
Windows에 MSVC 툴체인이 없는 경우입니다. Visual Studio Build Tools를 설치하고 "C++ CMake tools for Windows"를 체크하세요.
Q4: 위상 보정(Phase sanitization) 후 진폭이 모두 0이거나 NaN이 됩니다.
입력된 CSI JSON의 진폭 값 범위를 확인하세요. 원본 데이터가 정수형(비정규화)인 경우 스케일링 인자로 나누어야 합니다. v1/data/proof/sample_csi_data.json의 값 범위(1.0~2.0)를 참고하세요.
Q5: ESP32 펌웨어는 성공했지만 WiFi 연결에 실패합니다.
라우터에서 AP 격리(Airplay/HomeCast 등 기능으로 인한 격리)가 활성화되어 있지 않은지 확인하세요. 또한 ESP32-S3는 2.4GHz WiFi만 지원하므로 라우터가 5GHz 전용 모드가 아닌지 확인하십시오.
Q6: 포즈 포인트가 심하게 튀고 신호 노이즈가 큽니다.
가장 흔한 다중 경로 간섭 문제입니다. ESP32 안테나와 라우터 안테나를 45도 각도로 어긋나게 배치하거나, RuvSense의 다중 경로 억제 파라미터(field_model.rs의 SVD 디노이징)를 높여보세요.
심화 학습 / 향후 방향
1. RuvSense 다중 시점 어텐션 융합
RuView의 핵심은 RuvSense 모듈입니다. attention.rs의 CrossViewpointAttention 매커니즘을 통해 여러 ESP32 노드의 CSI 시점을 통합합니다. 기하학적 편향(GeometricBias)은 각 노드의 기여도를 가중치화하며, Cramer-Rao Bound 분석을 통해 현재 배치 상태의 이론적 정확도 한계를 알 수 있습니다.
2. ADR 아키텍처 결정서 정독
43개의 ADR 중 다음 항목을 먼저 추천합니다:
- ADR-014 — SOTA 신호 처리 방안 선정
- ADR-024 — 대조적 CSI 임베딩 (AETHER re-ID 기초)
- ADR-028 — ESP32 역량 감사 (하드웨어 선정 필독)
3. RuVector로 나만의 모델 훈련하기
wifi-densepose-train 크레이트에는 RuVector v2.0.4 훈련 파이프라인이 통합되어 있습니다. 직접 수집한 실내 CSI 데이터셋을 사용하여 ONNX 모델을 미세 조정(Fine-tuning)할 수 있습니다. ADR-016에 전체 프로세스가 기록되어 있습니다.
4. RuvSense 다중 노드 Mesh 네트워크 구축
ADR-029/ADR-032는 다중 노드 감지 모드를 설명합니다. 여러 개의 ESP32-S3 + ESP32-C6로 감지 Mesh를 구성하고 RuvSense의 MultistaticArray 융합 로직을 결합하면 여러 방을 가로지르는 포즈 추적이 가능해집니다.
5. ESP32 CSI 펌웨어 계층 원리
CSI 데이터의 생성 근원을 이해하려면 firmware/esp32-csi-node/main/wifi_csi.c를 읽어보세요. 핵심은 ESP-IDF의 wifi_csi_cb_t 콜백 함수로, 조건에 맞는 WiFi 프레임을 수신할 때마다 CSI 수집이 트리거됩니다. TDM(시분할 다중화) 프로토콜은 hardware/src/esp32/tdm.rs에 구현되어 노드 간 간섭을 방지합니다.