AI로 IoT 펌웨어 개발하기: MCP 서버와 자동화 테스트
2024년 12월 1일
15분
개요
임베디드 시스템 개발은 전통적으로 하드웨어와 소프트웨어의 긴밀한 상호작용이 필요한 영역입니다. 이 글에서는 Jest가 MCP 서버를 통해 실제 nRF52840 하드웨어와 통신하고, AI(Claude Code)가 테스트 결과를 확인하며 펌웨어를 개발하는 방법을 소개합니다.
프로젝트 구성
이 시스템은 두 개의 핵심 프로젝트로 구성됩니다:
1. iot-ai (펌웨어)
- 플랫폼: nRF52840 (Nordic Semiconductor)
- RTOS: Zephyr RTOS
- 언어: C++
- 주요 기능: BLE 통신, 센서 데이터 수집, 다중 디바이스 연결
2. nrf-mcp-server (MCP 서버)
- 언어: TypeScript / Node.js
- 역할: Jest 테스트와 하드웨어 간의 브릿지
- 기능: 시리얼 통신, 펌웨어 플래싱, 로그 스트리밍
아키텍처
Jest 테스트
MCP 서버를 통해 명령 전송
↓
nRF52840 펌웨어 (C++)
로그 출력
↓
Jest ← 로그 패턴 매칭
테스트 결과 & 로그
↓
Claude Code
테스트 결과와 로그를 분석하여 코드 수정
MCP 서버의 핵심 도구들
MCP 서버는 Jest 테스트가 하드웨어를 제어할 수 있도록 7가지 도구를 제공합니다:
| 도구 | 설명 |
|---|---|
list_devices | 연결된 nRF 디바이스 목록 조회 |
connect_serial | 시리얼 포트 연결 및 로그 수집 시작 |
disconnect_serial | 시리얼 연결 해제 |
flash_firmware | 펌웨어 빌드 및 플래싱 |
write_serial | 디바이스에 명령 전송 |
get_serial_status | 연결 상태 확인 |
reset_devices | 하드웨어 리셋 |
테스트 주도 개발 (TDD) 워크플로우
이 시스템의 핵심은 로그 기반 테스트입니다.
1단계: 테스트 의도 정의 (사람)
# 재시도 테스트 의도
- Central → Peripheral 메시지 전송 중 실패 시 자동 재시도
- 최대 3회 재시도 후 실패 콜백 호출
- 재시도 간격은 100ms
2단계: 테스트 코드 작성 (AI)
AI가 테스트 의도를 기반으로 Jest 테스트 코드를 작성합니다:
test("comm 레이어 재시도 테스트", async () => {
await pairDevices("기기1", "기기2", passkey);
// 패킷 거부 설정
await writeSerial("기기1", "peripheral reject");
await waitForNewLog("기기1", /Packet acceptance set to: false/);
// comm을 통한 데이터 전송
await writeSerial("기기2", "comm send 1 TestData");
await waitForNewLog("기기2", /Comm: Data enqueued/);
// 패킷 거부 및 재시도 확인
await waitForNewLog("기기1", /Comm: Write rejected/);
await waitForNewLog("기기2", /Scheduling retry/);
// 패킷 승낙으로 전환
await writeSerial("기기1", "peripheral accept");
await waitForNewLog("기기1", /Packet acceptance set to: true/);
// 재시도 후 전송 성공 확인
await waitForNewLog("기기2", /Send success to device ID 1/);
await waitForNewLog("기기1", /Data received from device ID 1/);
});
3단계: 펌웨어 구현 (AI)
Claude Code가 테스트 요구사항을 읽고 C++ 코드를 생성합니다:
void Comm::send_with_retry(uint8_t device_id, const char* data) {
for (int attempt = 0; attempt < MAX_RETRIES; attempt++) {
LOG_INF("Retry attempt %d", attempt + 1);
if (send_internal(device_id, data)) {
LOG_INF("Send success to device ID %d", device_id);
return;
}
k_msleep(RETRY_INTERVAL_MS);
}
LOG_ERR("Send failed after %d attempts", MAX_RETRIES);
notify_failure(device_id);
}
4단계: 빌드 및 테스트 실행
npm run test:shell
# 1. CMake 빌드
# 2. nrfjprog으로 플래싱
# 3. Jest 테스트 실행
# 4. 로그 패턴 검증
펌웨어 레이어 아키텍처
펌웨어는 명확한 계층 구조로 설계되어 AI가 각 레이어를 독립적으로 수정할 수 있습니다:
Application (센서, 비즈니스 로직)
↓
Comm Layer (큐잉, 재시도, 브로드캐스트)
↓
Connectivity Layer (device_id 추상화)
↓
Peripheral/Central (GATT 서버/클라이언트)
↓
BtAuth (블루투스 페어링 & 보안)
↓
Zephyr BLE API
주요 설계 결정
1. 로그 기반 테스트를 선택한 이유
- 실제 하드웨어에서 실행되어 타이밍 이슈 발견 가능
- 모킹 없이 실제 BLE 통신 검증
- 로그 패턴 매칭으로 상태 변화 추적
2. Device ID 추상화
bt_conn포인터 대신 숫자 ID(1-N) 사용- 재연결 시에도 일관된 디바이스 식별
- MAC 주소 기반으로 NVS에 영구 저장
3. 지수 백오프 재시도
- 5단계 재시도: 100ms → 250ms → 500ms → 1s → 4s
- 실패 시 이벤트 리스너에 알림
- 브로드캐스트 시 특정 디바이스 제외 가능
실제 구현 예시
BLE 연결 관리
// connectivity.cpp
void Connectivity::on_connected(bt_conn* conn, uint8_t role) {
// MAC 주소로 기존 device_id 찾기
bt_addr_le_t addr;
bt_conn_get_info(conn, &info);
int device_id = find_by_address(&addr);
if (device_id < 0) {
device_id = allocate_new_id();
save_to_nvs(device_id, &addr);
}
LOG_INF("Connection completed - device_id=%d", device_id);
}
메시지 큐 시스템
// comm.cpp
class SendQueue {
struct QueueItem {
uint8_t device_id;
uint8_t data[MAX_DATA_LEN];
uint8_t retry_count;
int64_t next_retry_time;
};
std::array<QueueItem, QUEUE_SIZE> items;
void process() {
for (auto& item : items) {
if (k_uptime_get() >= item.next_retry_time) {
if (!try_send(item)) {
schedule_retry(item);
}
}
}
}
};
핵심 성과
| 지표 | 값 |
|---|---|
| 펌웨어 코드 | 6,277줄 (C++) |
| MCP 서버 코드 | 3,215줄 (TypeScript) |
| 테스트 파일 | 39개 Jest 테스트 |
| 최대 동시 연결 | 6개 (5 peripheral + 1 central) |
| 보안 레벨 | BLE Level 4 (Authenticated SC) |
결론
이 프로젝트는 Jest 테스트가 하드웨어와 통신하고, AI가 그 결과를 확인하며 임베디드 시스템을 개발할 수 있음을 보여줍니다.
핵심 인사이트:
- 레이어 분리가 AI 개발을 가능하게 함: 명확한 계층 구조 덕분에 Claude가 한 레이어를 수정해도 다른 부분이 깨지지 않음
- 로그 기반 테스트가 효과적: 복잡한 모킹 없이도 실제 하드웨어 동작을 검증 가능
- MCP가 하드웨어 접근을 추상화: Jest 테스트가 MCP 서버를 통해 시리얼 포트, 플래싱 등 저수준 작업을 수행
- 사람은 "무엇을", AI는 "어떻게": 테스트 의도와 아키텍처는 사람이, 구현 세부사항은 AI가 담당
이 접근법은 임베디드 시스템 개발의 새로운 가능성을 열어줍니다.