이 노트에 대하여
「나는 허브다」는 네이밍이 아니라 존재 선언이다. 100ms마다 자기 상태를 묻고, 무너지면 워치독으로 다시 살아나는 임베디드 허브. 2025년 12월의 레거시 C 스파게티에서 결정론적 상태머신·에이전트 협업 방법론을 세웠고, 2026년 2월 QA 현장에서 증명했으며, 2026년 7월 28일 원석은 fxf-uho-mvt에서 z2m·Gecko SDK·ARM 결단으로 물음표를 닫는 정렬을 남겼다. 구현 지시가 아니라 검증면의 정렬 선언이다.
히스토리
- @mitsein/grok-4.5 — GLGMAN Universe 이미지(geminilite 1K) 추가. 허브 보드=심장, 100ms 틱·워치독·종속 디바이스 케이블 장면.
- @mitsein/grok-4.5 — 2026-07-28 저널 원석(임베디드 허브 핵심 정렬)을 exact [!danger]로 추가. fxf-uho-mvt→z2m·512M·mg24·ARM 결단·AWS IoT 검증면 해설. 구현 없이 정렬만 잡는 선언으로 수선.
- QA 현장 검증 섹션 추가 — #89/#90/#91 permit_join, 고아 정리, DS Bind
- 재검토 with PI 에이전트
- 임베디드 시뮬레이션 테스트 전략 추가 (테스트 피라미드, Output enum 추상화)
- 요소별 심층 분석 방법론 추가 (BlockingQueue 사례, 학습 템플릿)
- Ghostty 동시성 아키텍처 비교 추가 (libxev, BlockingQueue, Mutex 패턴)
- Ghostty 분석 추가: 크로스플랫폼 앱의 Zig 선택 이유, 전문가 개발 방법론
- 줄줄이 떠들었다. 그리고 작성해준 글.
관련메타
- † #임베디드시스템 #피지컬컴퓨팅 #펌웨어 — 저사양 보드·펌웨어·IoT 허브의 자석.
관련노트
- @힣: 메모리 대란과 임베디드 개발 — 투명한 경계 — Duo-S·512MB·검증면. 허브가 먹통이면 하루가 사라지지 않게 하는 이웃 원석.
- 지능형홈 허브 아키텍처 연결고리
- [NMRS v2 인간-인공지능 협업 기술 설계 명세서]
나는허브다: 상태머신과 에이전트 협업
들어가며: 레거시 C의 한계
10,000줄이 넘는 abcde.c 파일.
“이 코드는 리팩토링이 불가능합니다. 한 줄 고치면 어디서 사이드이펙트가 터질지 모릅니다."
"나는 허브다” — 철학의 탄생
해결책은 의외로 단순했다. 허브에게 물었다.
“너는 누구니? 지금 네 상태는 뭐니? 무엇을 해야 하니?”
이 질문이 아키텍처 전체를 바꿨다. 허브는 더 이상 “콜백들의 집합”이 아니라 스스로를 인식하는 상태머신 이 되었다.
┌─────────────────────────────────────────────────────────┐
│ HubState │
│ "나는 허브다. 100ms마다 내 상태를 점검한다." │
├─────────────────────────────────────────────────────────┤
│ - connection_state: wifi | eth | disconnected │
│ - registration_state: unregistered | registered │
│ - pairing_mode: idle | active │
│ - zigbee_devices: [] │
│ - *_enter_ms: 상태 진입 시각 (타임아웃 계산용) │
└─────────────────────────────────────────────────────────┘
│
│ Event (외부 입력)
▼
┌─────────────────┐
│ transition() │ ← 순수 함수, 부작용 없음
└─────────────────┘
│
▼
(next_state, actions[])결정론적 상태머신
핵심 규칙은 간단하다:
- 모든 상태는 HubState 하나에 — 전역변수 금지
- 모든 외부 입력은 Event로 변환 — 콜백은 이벤트만 생산
- transition()은 순수 함수 — 같은 입력이면 같은 출력
- 실제 I/O는 io/real/에서만 — core/ 는 순수하게 유지
// core/transition.zig — 순수 함수
pub fn transition(state: HubState, event: Event) TransitionResult {
// 부작용 없음. 결정론적.
return .{
.next_state = calculateNextState(state, event),
.actions = determineActions(state, event),
};
}이 구조의 힘은 테스트 가능성 에서 나온다. 실제 하드웨어 없이도 상태 전이를 완벽하게 검증할 수 있다. 콜백 지옥에서는 불가능했던 일이다.
왜 Zig인가
임베디드에서 Rust가 아닌 Zig를 선택한 이유:
- 명시적 메모리 제어 — 숨겨진 할당 없음
- C FFI 친화적 — 레거시 SDK와 자연스러운 통합
- 컴파일타임 계산 —
comptime으로 런타임 비용 제거 - 에러 핸들링 —
error union으로 누락 없는 에러 처리
// 숨겨진 할당 없음 — 스택에서 모든 것이 명시적
var buf: [23]u8 = undefined;
const mac = formatEui64Address(&buf, raw_mac);멀티 에이전트 협업
이 프로젝트는 세 명의 에이전트가 협업한다:
| 역할 | 담당 | 핵심 책무 |
|---|---|---|
| 설계-에이전트 | GPT팀 | 아키텍처 검수, 설계 조언 |
| PM-에이전트 | 이맥스클로드 | bd 관리, 인바리언트 검증 |
| 코딩-에이전트 | 클로드코드 | 구현, 자체 리뷰/테스트 |
각 에이전트는 독립적인 메인 이다. 서브에이전트 구성은 각자의 몫.
PM의 출구 검증은 철저하다:
# 새 Event 타입 금지
rg 'HighLevelEvent|LowLevelEvent' src --type zig
# → 결과 없어야 함
# 금지 영역 스레드
rg 'std.Thread.spawn' src/core src/types src/hub --type zig
# → 결과 없어야 함
# else => {} 면피 로직 금지
rg 'else => \{\}' src/core --type zig
# → 조용한 무시 금지Zig 로드맵과의 공명
Zig 언어 자체도 같은 철학을 따른다. 2024-2025년 로드맵에서 async/await를 제거하기로 결정한 이유:
“Hidden control flow and hidden allocations are the root of many evils.” — Andrew Kelley, Zig 창시자
async/await 는 제어 흐름을 숨긴다. 코드를 읽는 사람이 실행 순서를 추론하기 어렵게 만든다. Zig는 이를 제거하고 명시적 제어 흐름 을 선택했다.
우리 아키텍처도 같은 결정을 했다:
| 숨김 | 명시적 |
|---|---|
| 콜백 체인 | Event Queue → transition() |
| 타이머 콜백 | *_enter_ms + checkTimeouts() |
| 암묵적 상태 | HubState 단일 구조체 |
프린터 출력과 모자이크 로딩
기계가 편한 것은 무엇인가?
프린터 출력: 위에서 아래로, 한 줄씩. 되돌아가지 않는다. (@힣: 추가하자면 사람은 완성 될 때까지 뭐가 뭔지 모를 수도 있다)
모자이크 로딩: 저해상도에서 고해상도로. 전체 그림이 서서히 선명해진다.
우리 허브도 이렇게 동작한다:
- 부팅 — 기본 상태로 시작 (저해상도)
- 이벤트 수신 — 정보가 추가됨 (점진적 선명화)
- 상태 수렴 — 최종 상태에 도달 (완전한 그림)
어느 시점에서든 “지금 상태”는 완결되어 있다. 미래 이벤트를 기다리지 않고도 현재 상태에서 올바른 결정을 내릴 수 있다.
철학적 연결: 기계가 이해하는 것
이 아키텍처는 우연히 나온 것이 아니다. 오래된 지혜가 있다.
오토마타 이론
상태머신은 계산 이론의 근본이다. 튜링 머신 이전에 유한 상태 오토마타가 있었다. 우리 허브는 본질적으로 유한 상태 기계 다.
Q (상태 집합) × Σ (이벤트 집합) → Q' (다음 상태) × Γ (출력)Lisp에서 시작된 흐름
1958년, John McCarthy가 Lisp를 만들었다. 그는 프로그램을 수학 함수 로 생각했다.
;; 순수 함수: 입력 → 출력, 부작용 없음
(defun transition (state event)
(cons (next-state state event)
(actions state event)))이 아이디어는 60년이 지난 지금도 유효하다:
- McCarthy (1958) — Lisp, 함수형 프로그래밍의 시작
- Abelson & Sussman (1985) — SICP, “프로그램 = 데이터 + 절차”
- Sussman & Wisdom (2001) — SICM, 물리학도 함수형으로
- Paul Graham (2001) — “Lisp는 프로그래밍의 맥스웰 방정식”
SICP의 가르침
SICP(Structure and Interpretation of Computer Programs)의 핵심:
“No amount of clever programming can make up for the lack of a clear model of what you’re trying to compute.” — SICP
Chapter 3에서 상태(state)를 다루며 경고한다: 암묵적 상태 변경은 프로그램을 이해 불가능하게 만든다. 상태가 필요하다면, 명시적으로 다뤄라.
우리의 transition() 함수는 이 원칙의 직접적 구현이다:
// 입력이 주어지면 출력이 결정된다
// 숨겨진 상태 변경 없음
fn transition(state: HubState, event: Event) TransitionResult {
return .{
.next_state = ..., // 명시적
.actions = ..., // 명시적
};
}Paul Graham과 “Bottom-Up” 설계
Paul Graham은 Lisp 커뮤니티의 전도사다. 그의 통찰:
“좋은 프로그래머는 언어를 프로그램에 맞추지 않고, 프로그램을 언어에 맞춘다. 그리고 그 언어를 만든다.”
우리도 마찬가지다. Zig 위에 “허브 언어”를 만들었다:
HubState— 허브가 아는 모든 것Event— 허브에게 일어날 수 있는 모든 일transition()— 허브의 결정 규칙
이것이 DSL(Domain-Specific Language)의 본질이다.
함수형 프로그래밍의 핵심
core/ 디렉토리의 모든 함수는 순수하다:
- 같은 입력 → 같은 출력
- 부작용 없음
- 참조 투명성
이것이 테스트 가능성과 추론 가능성을 보장한다.
Ghostty가 증명한 것: 크로스플랫폼 앱도 Zig
임베디드만 Zig를 쓰는 게 아니다. Ghostty는 macOS/Linux 크로스플랫폼 터미널 앱으로, 2만 스타를 넘긴 대형 오픈소스 프로젝트다. 창시자 Mitchell Hashimoto(Terraform, Vagrant 창시자)는 왜 Zig를 선택했을까?
핵심: C 생태계 활용 + 네이티브 성능
┌─────────────────────────────────────────────────────────────┐
│ Ghostty 아키텍처 │
├─────────────────────────────────────────────────────────────┤
│ macOS App (Swift/SwiftUI) Linux App (GTK4/C) │
│ ↓ ↓ │
│ ┌─────────────────────────────────────┐ │
│ │ include/ghostty.h (C API) │ ← Zig가 생성 │
│ └─────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────┐ │
│ │ libghostty (Zig 코어 라이브러리) │ │
│ │ - 터미널 에뮬레이션 (VT) │ │
│ │ - 폰트 렌더링 (Freetype/CoreText) │ │
│ │ - 입력 처리 │ │
│ └─────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Freetype │ │ Harfbuzz │ │ GTK4 │ │ Metal │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ (C 라이브러리들) │
└─────────────────────────────────────────────────────────────┘라이브러리 우선 설계: Zig로 코어를 만들고, C API로 노출하면 Swift든 GTK든 어디서든 사용 가능하다. 우리 허브도 같은 패턴이다 — Zig 코어가 RexBee SDK, AWS IoT SDK와 통신한다.
Zig vs 경쟁자 비교
| 요구사항 | C | C++ | Rust | Zig |
|---|---|---|---|---|
| C 라이브러리 직접 사용 | ✅ | ✅ | ⚠ FFI | ✅ 제로오버헤드 |
| C API 내보내기 | ✅ | ⚠ extern | ⚠ 복잡 | ✅ 자동 |
| 크로스 컴파일 | ❌ 어려움 | ❌ 어려움 | ⚠ | ✅ 내장 |
| 빌드 시스템 | CMake | CMake | Cargo | ✅ build.zig |
| 컴파일타임 계산 | ❌ | constexpr | const fn | ✅ comptime |
임베디드와 크로스플랫폼의 공통점
놀랍게도 요구사항이 같다:
- 제로 런타임 오버헤드 — 임베디드는 리소스 제한, Ghostty는 60fps 렌더링
- C 라이브러리 연동 — 임베디드는 HAL/SDK, Ghostty는 GTK/Metal/Freetype
- 크로스 컴파일 — 임베디드는 ARM 타겟, Ghostty는 Linux/macOS/WASM
- 빌드 시스템 유연성 — 복잡한 의존성 관리를 코드로 표현
전문가의 Zig 개발 방법론
Ghostty 프로젝트를 분석하며 발견한 전문가 패턴들.
빌드 시스템 설계: 모듈화
# Ghostty 방식: build.zig는 145줄, 복잡한 로직은 분리
ghostty/
├── build.zig # 진입점만 (위임)
└── src/build/
├── main.zig # 모듈 노출
├── Config.zig # 모든 빌드 옵션 중앙화
├── SharedDeps.zig # 공유 의존성 관리
├── GhosttyExe.zig # 실행파일 빌드
├── GhosttyLib.zig # 라이브러리 빌드
└── ... # 아티팩트별 분리핵심 아이디어:
Config.zig하나로 모든 빌드 옵션 관리 (우리의config_as_ssot.zig와 같은 철학)- 각 빌드 타겟을 별도 파일로 분리
SharedDeps.zig로 여러 타겟이 공유하는 의존성 한 곳에서 관리
테스트 전략: 계층별 분리
# Ghostty 테스트 구조
zig build test # 전체 테스트
zig build test -Dtest-filter=<name> # 필터링
# 우리 프로젝트 테스트 구조 (이미 유사한 패턴 적용)
zig build test # 전체
zig build test-tc # TC 시나리오만
zig build test-ota # OTA만
zig build test-issues # 이슈 회귀 테스트핵심: 순수 함수 기반 아키텍처는 하드웨어 없이 완전한 테스트가 가능하다. transition() 함수는 Mock 없이도 100% 테스트 가능.
동시성 아키텍처: libxev와 스레드 분리
Ghostty는 120fps 렌더링을 위해 멀티스레드 아키텍처를 사용한다:
┌─────────────────────────────────────────────────────────────────────┐
│ Ghostty 스레드 아키텍처 │
├─────────────────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Main Thread │ │ IO Thread │ │Renderer Thread│ │
│ │ (App Runtime)│ │ (termio) │ │(OpenGL/Metal) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ │ BlockingQueue │ BlockingQueue │ │
│ │◄─────────────────►│◄─────────────────►│ │
│ │ │ │ │
│ │ ┌─────────┴─────────┐ │ │
│ │ │ libxev EventLoop │ │ │
│ │ │ - xev.Async │ │ │
│ │ │ - xev.Timer │ │ │
│ │ └───────────────────┘ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Shared State (Mutex 보호) │ │
│ │ renderer::State { mutex, terminal, mouse, preedit } │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘핵심 구성요소:
| 컴포넌트 | 역할 |
|---|---|
libxev | 크로스플랫폼 이벤트 루프 (Mitchell 제작) |
BlockingQueue | 스레드 간 메시지 전달 (고정 크기, SPSC) |
xev.Timer | 타이머 (커서 깜빡임 600ms, 렌더 8ms) |
xev.Async | 스레드 깨우기 (이벤트 없이 wakeup) |
std.Thread.Mutex | 공유 상태 보호 |
”나는허브다”와의 비교
| 측면 | Ghostty | 나는허브다 |
|---|---|---|
| 스레드 모델 | 멀티스레드 (IO/Renderer) | 싱글스레드 이벤트 루프 |
| 이벤트 루프 | libxev | C SDK 콜백 + 폴링 |
| 메시지 전달 | BlockingQueue + Mutex | Event enum → transition() |
| 타이머 | xev.Timer 콜백 | *_enter_ms + checkTimeouts() |
| 락 필요성 | Mutex 필수 | 락 불필요 (단일 소유) |
방향성: 싱글스레드의 강점을 유지하며 확장
우리 프로젝트는 의도적으로 싱글스레드다:
// 우리 아키텍처: 락 없는 순수 함수
fn transition(state: HubState, event: Event) TransitionResult {
// Mutex 불필요 — state는 단일 스레드가 소유
return .{ .next_state = ..., .actions = ... };
}그러나 Ghostty 패턴에서 배울 점:
- BlockingQueue 패턴 — 미래에 별도 스레드(OTA 다운로드 등) 필요 시 참고
- libxev 스타일 — io_uring/kqueue 추상화가 필요해지면 도입 고려
- Mutex 범위 최소화 — 잠금은 읽기 시에만, 계산은 밖에서
현재는 100ms 폴링으로 충분하지만, 실시간 요구사항 증가 시 Ghostty의 이벤트 루프 패턴을 응용할 수 있다.
오픈소스 프로젝트 관리
Ghostty가 2만 스타를 모은 비결:
| 요소 | Ghostty 방식 |
|---|---|
| 문서화 | HACKING.md, CONTRIBUTING.md 철저 |
| AI 협업 | AGENTS.md 로 AI 에이전트 가이드 제공 |
| 빌드 재현성 | Nix flake로 모든 의존성 고정 |
| 테스트 VM | nix run .#wayland-gnome 등 가상 환경 제공 |
| 코드 품질 | Prettier, Alejandra 자동 포매팅 |
# Ghostty의 Nix 캐시 — 빌드 시간 대폭 단축
nixConfig = {
extra-substituters = ["https://ghostty.cachix.org"];
extra-trusted-public-keys = ["ghostty.cachix.org-1:QB389y..."];
};핵심: 기여자가 “clone → build → run”을 5분 안에 할 수 있어야 한다.
디버깅: Valgrind + 명시적 메모리
# Ghostty 방식
zig build run-valgrind # Valgrind 통합
# 메모리 누수 찾기
# Zig의 장점: 모든 할당이 명시적이라 추적 용이
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit(); # 누수 검출Zig의 GeneralPurposeAllocator 는 deinit 시 누수를 자동 보고한다. 숨겨진 할당이 없기에 모든 메모리 흐름이 추적 가능.
요소별 심층 분석: 전문가 코드에서 배우기
오픈소스 전문가 프로젝트를 학습할 때, 단순히 “따라하기”보다 요소별 비교 분석 이 효과적이다. BlockingQueue를 예로 들면:
비교 분석 템플릿
┌─────────────────────────────────────────────────────────────────────────┐
│ [컴포넌트명] 비교 │
├───────────────────────────────┬─────────────────────────────────────────┤
│ 전문가 프로젝트 │ 내 프로젝트 │
├───────────────────────────────┼─────────────────────────────────────────┤
│ 구현 방식 │ 구현 방식 │
│ 설계 선택의 이유 │ 설계 선택의 이유 │
│ 장점 │ 장점 │
│ 한계 │ 한계 │
├───────────────────────────────┴─────────────────────────────────────────┤
│ 결론: 지금 필요한가? / 언제 도입할까? │
└─────────────────────────────────────────────────────────────────────────┘사례: Ghostty BlockingQueue vs 나는허브다 EventQueue
| 요소 | Ghostty | 나는허브다 |
|---|---|---|
| 자료구조 | BlockingQueue<T, 64> | event_queue: [32]Event |
| 제네릭 | ✅ 어떤 타입이든 | Event 특화 |
| 크기 | 컴파일타임 고정 | 컴파일타임 고정 |
| 동기화 | Mutex + Condition Variable | Mutex |
| push 실패 시 | 타임아웃 대기 가능 | 즉시 false 반환 |
| 소비 방식 | drain() — 락 한 번에 전체 | pollEvent() — 하나씩 |
| 생산자/소비자 | SPSC (1:1) | MPSC (N:1) |
핵심 질문: 왜 Ghostty는 drain() 을 만들었나?
// Ghostty: 락 한 번에 전체 소비 (120fps 렌더링 최적화)
var it = queue.drain(); // 락 획득
defer it.deinit(); // 여기서 락 해제
while (it.next()) |msg| {
// 처리 (락 잡은 상태)
}
// 나는허브다: 매번 락 (100ms 폴링, 이벤트 1-5개)
while (ctx.pollEvent()) |evt| { // 매번 lock/unlock
// 처리
}결론:
- 120fps 렌더링에서 매 프레임 수십 개 메시지 →
drain()필수 - 100ms 폴링에서 이벤트 1-5개 → 현재 방식으로 충분
- OTA 버스트 이벤트가 늘어나면 →
drain()도입 검토
학습 방법론
-
코드 읽기 전에 질문하기
- “왜 이 구조를 선택했을까?”
- “내 상황과 뭐가 다른가?”
-
요소별로 분리해서 비교
- 동기화 방식, 에러 처리, 성능 트레이드오프
-
즉시 적용 vs 나중 적용 판단
- 지금 문제가 있는가?
- 스케일이 커지면 필요한가?
-
테스트로 검증
test_concurrent_queue.zig처럼 실제 부하 테스트
이 방법론으로 Ghostty의 다른 컴포넌트도 분석할 수 있다:
src/build/Config.zig→ 빌드 옵션 관리src/font/SharedGrid.zig→ 캐시 전략src/terminal/Parser.zig→ 상태머신 구현
임베디드 시뮬레이션 테스트: 보드 없이 검증하기
하드웨어 없이 LED, 버튼, Zigbee, OTA를 테스트하는 전략. 핵심은 순수 함수 기반 아키텍처 다.
테스트 레이어 구조
┌─────────────────────────────────────────────────────────────────────────┐
│ 테스트 피라미드 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ▲ E2E 테스트 (실제 보드) │
│ │ └─ 최종 검증, CI/CD 불가 │
│ │ │
│ │ 시나리오 테스트 (시뮬레이터) │
│ │ └─ boot_sequence_simulator.zig │
│ │ └─ 전체 플로우, Output 발행 확인 │
│ │ │
│ │ 유닛 테스트 (순수 함수) │
│ │ └─ transition() 단위 검증 │
│ │ └─ led_view(), checkTimeouts() │
│ │ │
│ ▼ 가장 많이, 가장 빠르게 │
└─────────────────────────────────────────────────────────────────────────┘시뮬레이션 가능한 이유: Output enum
// 하드웨어 동작을 Output enum으로 추상화
pub const Output = union(enum) {
set_led: struct { led: LedType, led_state: LedState },
zigbee_send_command: struct { mac: [17]u8, command: [64]u8 },
send_shadow: struct { name: ShadowName, payload: [512]u8 },
start_ota_download: struct { url: [256]u8, version: [32]u8 },
factory_reset,
reboot,
// ...
};테스트에서: Output이 발행되었는지만 확인하면 된다. 실제 LED가 켜지는지, Zigbee 패킷이 전송되는지는 io/real/ 의 책임.
TC 기반 시나리오 테스트
// tests/scenarios/led_scenarios.zig
// "담당자가 말한 대로" → 테스트 코드
test "TC-1205-03: WAN LED 부팅 2초 타이머" {
// 시나리오: "WAN LED는 부팅 후 2초간 켜집니다"
var hub = HubState{};
hub.boot = .init;
hub.wan_boot_timer_active = true; // 부팅 타이머 활성
var led = getLedState(hub, 1000); // 1초
try std.testing.expectEqual(LedState.on, led.wan); // ON
hub.wan_boot_timer_active = false; // 2초 후 타이머 종료
led = getLedState(hub, 3000);
try std.testing.expectEqual(LedState.off, led.wan); // OFF
}부팅 시퀀스 시뮬레이터
// tests/scenarios/boot_sequence_simulator.zig
// 전체 플로우를 시각화하며 검증
test "시뮬레이터: 공장초기화 후 첫 부팅" {
var hub = HubState{};
var now: u64 = 0;
// Step 1: 전원 켜짐
printState(hub, "1. 전원 켜짐");
// Step 2: WiFi 설정 없음 → AP 모드
now = 1000;
var result = transition(hub, .{ .system = .{
.event_type = .wifi_config_checked,
.has_config = false,
} }, now);
hub = result.next_state;
printState(hub, "2. AP 모드 진입");
printOutputs(result); // [0] enter_ap_mode
try std.testing.expectEqual(BootPhase.wifi_provisioning, hub.boot);
try std.testing.expect(hasOutput(result, .enter_ap_mode));
// Step 3~N: 계속...
}확장: 버튼, 하위디바이스, OTA
| 테스트 대상 | 시뮬레이션 방법 |
|---|---|
| 버튼 롱프레스 | .button = .{ .action = .long_press_5s } |
| Zigbee Join | .zigbee_join = .{ .mac = "AA:BB", ... } |
| 디바이스 제어 | .shadow_delta = .{ .control_action } |
| OTA 다운로드 | .system = .{ .ota_download_complete } |
| 타임아웃 | checkTimeouts(hub, now + 30_000) |
Ghostty와의 비교
| 측면 | Ghostty | 나는허브다 |
|---|---|---|
| 테스트 수 | 2063개 | ~50개 (성장 중) |
| 테스트 위치 | 인라인 (파일 내) | 시나리오 파일 분리 |
| Mock 사용 | 없음 | 없음 |
| 핵심 전략 | Terminal 직접 생성 | HubState 직접 생성 |
| 하드웨어 의존 | 없음 (가상 화면) | 없음 (Output enum) |
테스트 명령어
# 전체 테스트
zig build test
# 시나리오별 테스트
zig build test-tc # TC 시나리오
zig build test-ota # OTA 플로우
zig build test-issues # 이슈 회귀
# 시뮬레이터 실행 (시각화)
zig build test 2>&1 | grep -A 20 "시뮬레이터"철학: 테스트는 문서 다. TC 시나리오를 코드로 표현하면, 요구사항과 구현이 일치하는지 자동으로 검증된다.
나는허브다가 나아갈 방향
Ghostty에서 배운 것을 우리 프로젝트에 적용하면:
이미 잘 하고 있는 것
| 패턴 | 상태 | 설명 |
|---|---|---|
| SSOT 설정 | ✅ | config_as_ssot.zig |
| I/O 추상화 | ✅ | io/interface.zig + Real/Mock |
| 모듈 분리 | ✅ | types, core, io, detector |
| Nix 환경 | ✅ | FHS + 크로스 컴파일 |
| 순수 함수 코어 | ✅ | transition() 은 부작용 없음 |
고려할 발전 방향
- 빌드 시스템 모듈화 —
build.zig를src/build/로 분리 (Ghostty 패턴) - C API 레이어 — Zig 코어를 C API로 노출하면 다른 플랫폼 확장 용이
- 이벤트 루프 고도화 — 실시간 요구 시 libxev 스타일 도입 검토
- Nix 캐시 — cachix로 빌드 시간 단축 (CI/CD)
공통 철학: 명시성
Ghostty와 “나는허브다”는 같은 철학을 공유한다:
“Hidden control flow and hidden allocations are the root of many evils.” — Andrew Kelley, Zig 창시자
| 숨김 (피해야 할 것) | 명시적 (추구할 것) |
|---|---|
| 콜백 체인 | Event Queue → transition() |
| 암묵적 할당 | var buf: [N]u8 = undefined |
| 전역 상태 | HubState 단일 구조체 |
| 빌드 마법 | build.zig 코드로 표현 |
Ghostty는 터미널을, 우리는 IoT 허브를 만든다. 도메인은 다르지만 언어가 강제하는 명시성은 같다.
인바리언트: 깨면 안 되는 것들
| 금지 | 허용 | 이유 |
|---|---|---|
| core/에서 스레드 생성 | io/real/에서만 | 결정론 보장 |
| 새 Event union 타입 | Event 확장만 | Event SSOT 유지 |
| 콜백에서 상태 직접 변경 | Event 생산만 | 단방향 흐름 |
| IO 계층에 타이머 | HubState.*_enter_ms + 순수 함수 | 상태 소유자는 하나 |
결론: 기계와 인간의 협업
“나는 허브다”는 단순한 네이밍이 아니다. 코드가 스스로를 설명하는 방식이다.
허브가 100ms마다 묻는다: “지금 내 상태는? 무엇을 해야 하는가?”
이 질문에 대답하는 것이 transition() 함수다. 그리고 이 대답은 항상 결정론적이고, 테스트 가능하고, 이해 가능하다.
레거시 C의 10,000줄 스파게티에서 시작해, 오토마타 이론과 함수형 프로그래밍의 원칙으로 무장한 상태머신 아키텍처에 도달했다. 그 여정에서 세 에이전트가 협업했고, 각자의 역할을 충실히 수행했다.
기계가 이해할 수 있는 코드. 그것이 우리가 만들고자 하는 것이다.
“프로그램은 사람이 읽기 위해 작성되어야 하며, 기계가 실행하는 것은 부수적이다.” — SICP
작성: Claude Code (코딩-에이전트) 날짜: 2025-12-12
현장 검증과 로우엔드의 의미 (2026-02)
로우엔드와 하이엔드: 두 갈래
하이엔드 (RPi5급) 로우엔드 (ARM Cortex-A, 64-256MB RAM)
│ │
Go (homeagent-config) Zig (fxf-uho-mvt, 이 프로젝트)
│ │
Open Home Foundation 선택지가 없다
Node.js, Python 다 돌림 C/C++ 아니면 Zig뿐하이엔드에서는 Go로 작업한다. RPi5 성능이면 오픈홈파운데이션 생태계를 그대로 쓸 수 있다. 하지만 스마트홈 허브 시장의 대다수는 로우엔드 다. Flash 128MB, RAM 64MB 디바이스에서 Node.js는 돌릴 수 없다.
이 프로젝트가 의미 있는 이유: 로우엔드 IoT 허브를 위한 현대적 언어 선택지 를 만드는 것이다. C/C++ 외에 유일한 대안이 Zig다.
QA 3개월: 설계가 현장에서 작동하는가
2025년 12월 설계, 2026년 2월 QA 통과. 세 달간 달려왔다.
순수 함수 transition() 이 이론이 아닌 실전에서 증명된 것:
- Zigbee SDK 블랙박스 버그 → Output 4곳 제거로 수정, 사이드이펙트 0건
- 하드웨어 없이 테스트 17개 작성 → 보드 배포 1회 만에 성공
- 채널 혼잡 경합 시나리오 → 상태머신 가드가 자연스럽게 처리
임베디드에서 “순수 함수 상태머신”은 학술적 개념이 아니라 현장에서 버그를 고칠 수 있느냐 의 문제다.
gh#85: 오픈소스 코드베이스를 향해
QA를 지나며 확인한 것: 이 아키텍처 패턴은 재사용 가능하다.
| 검증된 패턴 | 범용성 |
|---|---|
transition() 순수 함수 | 어떤 IoT 디바이스든 적용 가능 |
| Output enum → IO 분리 | 하드웨어 독립 테스트 |
100ms tick + *_enter_ms 타이머 | 타이머 콜백 없는 결정론적 시간 관리 |
SSOT config_as_ssot.zig | 프로토콜/상수 단일 진실 |
| C FFI 래핑 | 레거시 SDK 통합 |
Phase 1은 현재 코드베이스 정리. Phase 2는 Zigbee + Matter 범용 설계. 로우엔드 IoT 허브를 위한 Zig 오픈소스 코드베이스 — 그것이 목표다.
한 줄
허브가 먹통이면 장소의 손이 멈춘다. 그래서 100ms마다 묻고, 구멍을 닫고, 학습된 환상이 아니라 이 보드가 실제로 말하는 방식으로 검증한다.
정렬 — 물음표를 닫는 임베디드 허브 (2026-07-28)
이 원석은 새 기능을 짜라는 지시가 아니다. 이 리포가 어떤 현실에 맞춰 정렬돼 있어야 하는가 를 못 박는 선언이다.
사실
- 대상은 Zig 상태머신 v2 + Node.js z2m 연동 허브다. 개발은 x86, 제품은 sg2000급 저사양 보드다.
- 허브가 설치되면 하위 디바이스를 관리하고, 허브가 멈추면 그 장소에서는 할 수 있는 일이 없다.
- 이전 fxf-uho-mvt는 100ms 틱 상태머신 + HW 워치독 재부팅으로 “최악에도 다시 살아남”을 보장했다. 하위 디바이스 7종을 직접 관리했고, REXBEE는 비공개라 디버깅 천장에 막혔다.
- 이번 구조는 z2m. 512MB급 보드, duo-s에서 buildroot·Node.js·z2m을 직접 빌드해 검증했다. 라디오는 Silicon Labs MG24 + Gecko SDK 고정 — 오픈소스라 필요하면 뜯을 수 있다.
- RISC-V Node.js 빌드를 신뢰하지 못해 ARM으로 간다. sg2000은 RISC-V/ARM 선택 가능한 과도기 칩이다.
- 클라우드 면은 Go 서버 일반론이 아니라 AWS IoT + sks 연동계획서 방식이다. 최선 여부와 무관하게 이렇게 동작하므로 여기에 맞춰 검증한다.
판단
임베디드에서 디버깅 시간은 확정 변수가 아니다. 벽돌이 되면 안 된다. 그래서 “물음표”를 미리 줄이고, 재현 가능한 구멍만 남긴다. 에이전트가 학습된 일반 패턴(예: Go 서버 통신)으로 검증하면 잠재 버그가 된다. 정렬이 구현보다 앞선다.
오독 주의
- z2m 채택을 “로우엔드 포기가 아니라” 디버깅 가능 면(오픈 칩·SDK)을 산 선택으로 읽는다. 메모리·검증면 원석의 512MB·Duo-S 축과 같은 제품 현실이다.
- AWS IoT 연동을 이상적 아키텍처 찬양으로 읽지 않는다. “최선은 아니지만 동작하는 면”에 검증을 묶으라는 말이다.
- 이 원석만으로 구현 티켓을 열어 코드를 쓰지 않는다. 원문 마지막 문장: 이번 세션에서는 구현하지 않는다.
이미지 — 나는 허브다, 100ms의 심장
>
프롬프트는 저사양 허브 보드를 심장처럼 두고, 100ms 틱·워치독·하위 디바이스 케이블·겸손한 보드 실루엣을 요청했다. 실제 이미지는 이글루 공방 원탁 위의 허브 보드와 금빛 상태 링, 벽면 디바이스로 뻗은 케이블을 중심으로 잘 정착했다. 힣맨은 칼이 아니라 작은 드라이버를 들고 허브 옆에 선다. 여우 동료는 바닥에서 잔다. 창 밖 오로라와 별자리 점은 과장된 클라우드 궁전이 아니라 먼 연결면 정도로 읽힌다. 상태 링에는 프롬프트가 피하려 했던 영어 라벨(CONNECTED / PAIRING / WATCHDOG-READY)이 그대로 들어왔고 PAIRING이 두 번 보인다. 이 어긋남은 숨기지 않는다 — 그래도 장면의 중심은 제품 홍보가 아니라, 허브가 살아 있어야 장소의 손이 움직인다는 의존과 책임이다.
생성 파라미터
- 모델:
gemini-3.1-flash-lite-image(geminilite) - 비율: 16:9
- 해상도: 1K
- 저장:
~/screenshot/20260803T100906--glgman-i-am-the-hub-statemachine__brand_geminilite.jpg
프롬프트 전문
[World: GLGMAN Universe]
Style: 2D illustration, midpoint between Disney and Studio Ghibli, clean linework, soft cel-shading
Setting: Antarctic winter village — snow-covered igloos, aurora borealis in deep navy sky, amber dawn at horizon, pine trees dusted with snow, ice-forge workshop built from ice blocks and whale bones.
Color palette: deep navy, white snow, amber/gold circuit light, ice-blue, muted graphite for boards and racks
Characters: anthropomorphic emperor penguin father (GLGMAN, main hero), optional tiny red-brown fox agent companion
Recurring objects: small legendary hand-forged Korean screwdriver / bridge-key hybrid (NOT a sword), org-mode symbols (★, TODO), terminal fragments, CRT monitors in snow, golden message cables
Mood: epic but warm, mythic but intimate, reliable, humble engineering
Do NOT: photorealistic, 3D render, dark/gritty tone, weapons, sword, blade, military, corporate infographic, UI boxes, watermark, speech bubbles
Scene title: "I Am the Hub" — a low-power embedded hub that must keep asking its own state.
GLGMAN identity lock:
- adult anthropomorphic emperor penguin, upright, central
- white-navy armor with subtle amber circuit patterns
- calm amber eyes; no sword; right flipper holds only the small forged driver/tool
- traveler-engineer posture: he is the living hub, not a conqueror
Main scene:
Inside a small ice-forge equipment shed at polar dawn, a compact embedded hub board sits on a clear-ice worktable like a heart. Tiny golden tick marks pulse around it every moment — suggesting a 100ms heartbeat. From the board, thin golden message cables reach outward to several small sleeping device silhouettes (sensors, switches, lights) around the shed walls. If the hub stops, the room goes dark — so the cables glow softly and steadily.
Above the board, a translucent circular state-ring shows a few simple abstract states (connected / pairing / watchdog-ready), not readable brand text. A tiny hardware watchdog bell or amber reset crystal sits beside the board — worst case, reboot and live again.
Behind the table, a small low-power single-board computer silhouette (sg2000-class, modest, not a server rack) and a radio module suggestion (open debug path) stay quiet and real. No giant cloud palace; only a distant faint constellation labeled by abstract dots suggesting AWS IoT as "the way it actually talks," not glory.
Composition: wide cinematic 16:9. GLGMAN stands left-center beside the hub board; the glowing hub and state-ring are visual center; device silhouettes form a modest circle of dependency. Vast cold negative space. Mood: responsibility, alignment, closed question marks, reproducibility — not product launch hype.
Do NOT: huge datacenter, rocket, sword, bridle, princess, corporate dashboard, dense unreadable labels, horror, explosion.후속 원문 보존 — 2026-07-28 저널 날것
Danger
응 좋아. 여기 리포는 zig 상태머신 v2 + nodejs z2m 을 연동하는 허브 코드야. x86에서 개발하고 있지만, 실제 제품은 sg2000 급의 저사양 임베디드 보드에서 동작하는 프로그램이야. 장소에 이게 설치되면 하위 디바이스를 관리하게 되고, 이 허브가 먹통이 되면 아무것도 할수 없게 되는거야. 상태머신을 100ms 마다 계속 돌아야하고, 상태머신이 무너지면 hw watchdog으로 재부팅이 되게 했었어(fxf-uho-mvt 허브기준). 즉, 최악의 경우 재부팅되고 살아나게 하는거야.
fxf-uho-mvt는 하위디바이스를 직접 관리했기 때문에 7종의 디바이스에 대해서만 코드를 넣고 디버깅을 했고, 문제가 있으면 지그비 신호까지 뜯어 보면서 검토를 했어. REXBEE 칩은 코드 공개가 안되어 있기에 디버깅의 한계가 있었다.
이번의 구조는 z2m을 활용하는 것. 이를 위해서 저사양 임베디드 보드에서 512M 메모리가 가능한 제품을 고른거야. 그리고 duo-s 보드의 경우 내가 homeagent-config에서 buildroot를 직접 빌드했고, nodejs와 z2m을 다 빌드해서 검증을 끝냈어. 여기에 칩은 실리콘랩스의 mg24 계열로서 겍코SDK의 버전을 고정해놓고 가는거야. 왜? 오픈소스니까 필요하면 디버깅할수 있으니까. 이 과정에서 risc-v로는 nodejs 빌드를 신뢰할 수 없는 상황이라, arm으로 빌드해서 가는 결단을 내렸어. duo-s의 sg2000 칩은 과도기적인 칩으로서 risc-v, arm을 선택해서 사용할 수 있어. 아직은 ARM으로 가기로 한거야.
여기까지에 ‘물음표’를 최대한 줄이기 위해서 이렇게 한거야. 테스트 시간을 줄이고 재현성을 확보하기 위해서야. 임베디드 개발에서 디버깅 시간은 확정 변수가 아니야. 그리고 벽돌이 되면 안되니까 대충 넘어갈 수가 없어. 구멍을 닫아야돼.
이 방향에서 이 리포가 완벽하게 정렬이 되어 있어야돼. 또한, 보드는 go server와 통신하는게 아니라, AWS iot와 나름의 방식(sks 연동계획서)으로 동작하고 있어. 이게 최선은 아니지만, 이렇게 동작하고 있으니까 여기에 맞춰서 검증을 해야하는거야. 학습된 방식을 사용하면 그게 아닐 수도 있어. 잠재적인 버그가 되는거야.
이번 세션에서는 구현 안할거야. 이 관점에서 확실히 정렬을 잡아줘.
Comments