펌웨어
b3ff05c 기준)xiaozhi-esp32는 마이크·스피커·화면·네트워크를 묶어 AI 음성 단말로 만드는 ESP-IDF 기반 펌웨어입니다. 여기에 MCP 서버를 넣어, AI가 장치의 기능을 발견하고 호출할 수 있게 합니다. 예를 들어 “소리를 줄여 줘”라는 요청은 최종적으로 볼륨을 설정하는 C++ 함수 호출로 이어집니다.
이 저장소에서 가장 많이 배울 것은 AI 모델 자체보다, AI 응답을 실제 기기의 동작으로 연결하는 시스템 설계입니다. 일반적인 대화 경로에서는 서버가 음성 인식·언어 모델·음성 합성을 맡고, ESP32는 녹음·전처리·압축·전송·재생을 맡습니다. 로컬 깨우기 단어 인식과 LLM 추론은 서로 다른 일입니다.
Application에 전달됩니다.hello를 교환하며, 음성 형식과 MCP 지원 여부를 알립니다. 이미 열린 채널은 재사용합니다.stt, 합성 음성 상태는 tts, 표정 정보는 llm 메시지로 들어옵니다.이 경로는 main/application.cc의 InitializeProtocol()과 main/audio/audio_service.cc에서 확인할 수 있습니다. 서버가 보내는 llm 메시지를 처리한다는 사실이, 장치 안에 LLM 추론 엔진이 있다는 뜻은 아닙니다.
안내 창구를 떠올려 보세요. 창구 직원은 손님의 말을 듣고, 전문 상담 부서에 전달하고, 답을 읽어 주며, 필요하면 조명 스위치를 누릅니다. 여기서 기기는 창구 직원이고, 서버의 AI는 전문 상담 부서입니다.
MCP는 “어떤 스위치를 어떤 인자로 누를 수 있는가”를 적은 업무 목록입니다. 목록을 이해한 AI가 요청을 만들면, 기기는 허용된 함수와 인자에 맞는지 확인한 뒤 실행합니다.
main/main.cc의 app_main() → main/application.cc의 Initialize()·Run() → main/audio/audio_service.cc의 입출력 태스크를 따라가세요. 처음부터 모든 보드 드라이버를 읽을 필요는 없습니다.
2026-10-23 기준 GitHub Trending C++ Weekly 2위, 스타 31,804개, 포크 7,436개입니다. 이 순위와 누적 관심은 확인할 수 있지만, 특정 영상이나 업데이트가 순위 상승의 원인이라고 단정할 근거는 없습니다. 여기서는 소스에서 관찰되는 매력으로 주목받는 배경을 해석합니다.
첫째는 MCP와 하드웨어의 연결입니다. McpServer::AddCommonTools()는 상태 조회와 볼륨 조절을 등록하고, 화면 밝기·카메라 도구는 해당 장치가 있을 때 추가합니다. 둘째는 확장 가능한 보드 구조입니다. 같은 대화 로직을 다양한 디스플레이·코덱·통신 장치와 결합하는 사례가 풍부합니다.
화려한 대화 시연 뒤에서, 서로 다른 하드웨어와 느린 네트워크를 견디는 공통 코드를 읽을 수 있다는 점이 이 저장소의 강점입니다. 특히 오디오 큐의 지연 제한과 콜백 실행 위치는 작은 예제에서 보기 어려운 실전 주제입니다.
| 선택지 | 중심 역할 | 학습할 때의 차이 |
|---|---|---|
| xiaozhi-esp32 | AI 대화용 기기 펌웨어와 장치 측 MCP | C++ 소스에서 음성 태스크·전송·보드 이식·도구 호출을 함께 추적할 수 있습니다. |
| ESPHome Voice Assistant | 기기 음성을 Home Assistant의 Assist로 전달 | Home Assistant 연동과 설정 중심 접근을 익히기 좋습니다. xiaozhi는 자체 대화 프로토콜과 펌웨어 내부 설계를 읽는 데 초점이 있습니다. |
| 일반 버튼·GPIO 예제 | 입력에 따라 핀 한 개를 제어 | 시작은 쉽지만, 음성 스트림·동시성·서버 도구 호출까지는 별도 설계가 필요합니다. |
| 대화 서버 프로젝트 | ASR·LLM·TTS와 모델 제공자 연결 | 서버 구성과 모델 통합을 배웁니다. 이 저장소는 그 서버와 통신하는 단말 쪽을 보여 줍니다. |
ESPHome의 역할은 공식 Voice Assistant 문서 기준입니다. 어느 쪽이 무조건 빠르거나 음질이 좋다는 성능 비교는 아닙니다. 기존 Home Assistant 환경을 확장하려는지, 펌웨어와 대화 프로토콜을 직접 공부하려는지에 따라 선택이 달라집니다.
| 층 | 확인되는 내용 | 해석 |
|---|---|---|
| 기기 소스 | Opus, 음성 엔진, 화면, 상태 머신, MCP 디스패처 | 이 저장소에서 구현을 직접 읽을 수 있습니다. |
| 보드별 선택 기능 | 카메라, AEC, 배터리, 4G, Ethernet 등 | 모든 보드가 모든 기능을 가진 것은 아닙니다. Kconfig와 보드 구현을 확인해야 합니다. |
| 서버 연동 기능 | Qwen·DeepSeek 연계, 화자 인식, 클라우드 MCP | README의 전체 서비스 기능 설명입니다. 모델 추론·화자 인식 서버 구현이 이 펌웨어에 포함된다는 의미는 아닙니다. |
README는 공식 서버 연결과 별도의 Python·Java·Go 서버 프로젝트를 소개합니다. 이 생태계는 활용 범위를 넓히지만, 서버의 배포 방법·서비스 정책·실제 호환성은 각각 확인해야 합니다.
같은 리모컨 설계를 여러 TV에 맞추는 일과 비슷합니다. 버튼의 뜻은 같아도 각 TV의 신호 방식은 다릅니다. 이 프로젝트의 보드 계층은 하드웨어 차이를 흡수하고, 대화 로직은 “듣기·말하기·제어하기”라는 공통 동작에 집중하게 합니다.
이 저장소에는 웹 API 서버의 실행 진입점이나 모델 제공자용 백엔드가 없습니다. 서버와 통신하고 기기 동작을 조정하는 코어는 Application, AudioService, Protocol, McpServer입니다. 장치 측 MCP 서버와 대화 백엔드는 역할이 다르며, 여기서 “서버”는 도구를 제공하는 쪽이라는 뜻으로도 쓰입니다.
| 기술 | 선언·설정 | 실제 쓰임 |
|---|---|---|
| C++ | main/CMakeLists.txt: -std=gnu++23 | 상태·콜백·RAII·이동 소유권·std::expected를 사용하는 펌웨어 구현 |
| ESP-IDF | main/idf_component.yml: >=6.0.1 | FreeRTOS, GPIO, I2S, I2C, 네트워크, OTA, 플래시 API 제공 |
| ESP-SR | espressif/esp-sr: ==2.4.7 | WakeNet·MultiNet과 AFE 음성 전처리 경로 |
| 오디오 코덱 | espressif/esp_audio_codec: ~2.5.0 | Opus 인코딩·디코딩. 일반 PC용 libopus 버전을 별도로 고정한 선언은 아닙니다. |
| 오디오 효과 | espressif/esp_audio_effects: ~1.3.0 | 입출력 샘플레이트 변환 등 |
| 하드웨어 코덱 | espressif/esp_codec_dev: ~1.6.2 | 보드의 실제 ADC·DAC·오디오 코덱 제어 기반 |
| JSON·암호화 | cJSON, Mbed TLS/PSA Crypto 사용 | 제어 메시지 파싱과 MQTT/UDP 음성 암복호화. 독립 버전은 이 매니페스트에 명시되지 않습니다. |
위 의존성 버전은 모두 main/idf_component.yml의 선언값입니다. ==는 정확한 버전 고정이고, ~·^는 호환 범위 표현입니다. 추적 파일에 dependencies.lock이 없어, 범위 선언을 실제 설치된 정확한 버전처럼 읽으면 안 됩니다.
사용자 접점은 main/display/, main/led/, 보드의 버튼 콜백입니다. React나 Vue 앱 구조가 아니며, package.json·Cargo.toml·pyproject.toml·go.mod도 추적 파일에 없습니다. Python은 주로 빌드·리소스·테스트 도구에 쓰입니다.
| 화면·입력 요소 | 의존성 선언 | 읽을 위치 |
|---|---|---|
| 그래픽 UI | lvgl/lvgl: ~9.5.0esp_lvgl_port: ~2.9.0 | main/display/의 LCD·LVGL 경로 |
| 폰트 | 78/xiaozhi-fonts: ~2.0.0 | CMake의 폰트 선택과 main/assets/ |
| 버튼·회전 입력 | espressif/button: ~4.2.1espressif/knob: ^1.1.0 | main/boards/common/button.*와 보드별 초기화 |
| 카메라 | espressif/esp32-camera: ^2.1.7espressif/esp_video: ^2.4.1 | 대상 칩 조건이 붙은 선택 기능. 모든 빌드에 같은 경로가 포함되지는 않습니다. |
| 네트워크 연결 | 78/esp-wifi-connect: ~3.3.178/esp-ml307: ~3.8.1 | Wi-Fi 설정·연결과 모뎀 계층 |
sdkconfig.defaults에는 # LVGL 9.2.2라는 주석이 남아 있지만, 현재 의존성 선언은 ~9.5.0입니다. 버전 판정에는 오래된 설명 주석보다 실제 매니페스트를 우선해야 합니다.
CONFIG_... 값이 되어 CMake의 소스 선택과 C++의 조건부 컴파일에 영향을 줍니다. 실행 중 메뉴 선택과는 다릅니다.| 계층 | 실제 파일·값 | 배울 점 |
|---|---|---|
| 루트 빌드 | CMakeLists.txt: CMake 3.16 이상, MINIMAL_BUILD ON, PROJECT_VER "2.5.3" | SDK를 불러온 뒤 필요한 컴포넌트만 포함합니다. |
| 보드 빌드 | scripts/build.py: _collect_variants(), build_board() | JSON 설정·칩·언어·깨우기 모델을 빌드 설정으로 연결합니다. |
| Docker | docker/firmware-builder/Dockerfile: espressif/idf:release-v6.1 | 대화 서버 이미지가 아니라 펌웨어 빌더 이미지입니다. |
| 빌드 CI | .github/workflows/build.yml: 같은 IDF 이미지, actions/checkout@v7, actions/upload-artifact@v7 | 변경 파일에 따라 빌드 대상을 선택하고 바이너리를 보관합니다. |
| 릴리스 CI | .github/workflows/release.yml: scripts/release.py 호출 | 릴리스 목록 준비 → 변형별 패키징 → 누락 검증·게시로 단계를 나눕니다. |
| 호스트 검사 | scripts/tests/의 테스트 파일 10개 | 장치 없이 빌드 선택·리소스·MCP 일부를 검사하는 코드가 있습니다. |
.github/workflows/build.yml은 수동 실행에서 전체 목록을 선택합니다. 일반 변경에서는 _select_variants_for_changes()가 공통 코드 변경에 대표 변형을, 보드 변경에 해당 보드의 변형을 고릅니다. 모든 푸시에서 모든 보드를 빌드한다는 구조는 아닙니다.
# main/idf_component.yml의 핵심 선언
espressif/esp-sr: ==2.4.7
espressif/esp_audio_codec: ~2.5.0
lvgl/lvgl: ~9.5.0
esp_lvgl_port: ~2.9.0
idf:
version: '>=6.0.1'
# CMakeLists.txt의 펌웨어 개발 버전
set(PROJECT_VER "2.5.3")
최신 공개 릴리스는 v2.5.2(2026-10-11), 분석 커밋의 프로젝트 버전은 2.5.3, 최소 SDK는 6.0.1입니다. README와 빌더는 ESP-IDF의 release/v6.1 브랜치를 권장합니다. ESP32-S31은 README 기준 IDF 6.1 이상이 필요합니다.
아래에서 상자 안은 펌웨어, 상자 밖의 대화 서버는 별도 시스템입니다. 화살표는 데이터나 제어 흐름입니다. 네트워크 수신 콜백과 메인 루프가 항상 같은 태스크에서 실행되는 것은 아니라는 점을 기억하세요.
사용자 음성 사용자 버튼
| |
v v
+---------------- ESP32 펌웨어 ------------------------+
| AudioCodec --> AudioInputTask --> AudioEngine |
| | |
| 깨우기/VAD 이벤트 -------------+ |
| | | 16 kHz PCM |
| v v |
| Application 인코딩 큐 |
| 상태 머신 | |
| 메인 루프 OpusCodecTask |
| | | |
| +----------- 송신 큐 |
| | |
| v |
| Protocol: WebSocket 또는 MQTT + UDP |
| | 음성 수신 |
| v |
| 디코딩 큐 --> OpusCodecTask |
| | |
| 재생 큐 |
| v |
| AudioOutputTask --> 스피커 |
| |
| 제어 JSON --> Application --> 화면·표정 |
| | |
| +--> McpServer --> Schedule() |
| | |
| 메인 태스크의 도구 실행 |
| v |
| Board --> 볼륨·조명·카메라 |
+----------------------|------------------------------+
| Opus / 제어 JSON / MCP
v
별도의 대화 서버
ASR + LLM + TTS 또는 실시간 음성 모델
|
모델·외부 도구 서비스
main/main.cc의 app_main()은 먼저 NVS를 초기화하고, Application::GetInstance()를 얻어 Initialize()와 Run()을 호출합니다. Run()은 종료되지 않는 메인 이벤트 루프입니다. 부팅의 입구를 알면 수천 줄짜리 파일도 읽을 순서를 잡을 수 있습니다.
Application::Run()은 xEventGroupWaitBits()로 이벤트를 기다립니다. 네트워크 연결, 깨우기 단어, 오디오 송신 준비, 상태 변경 등이 비트로 표현됩니다. 콜백에서 할 일이 생기면 Schedule()이 작업을 큐에 넣고 MAIN_EVENT_SCHEDULE을 켭니다.
// main/application.cc: 작업 등록
void Application::Schedule(std::function<void()>&& callback) {
{
std::lock_guard<std::mutex> lock(mutex_);
main_tasks_.push_back(std::move(callback));
}
xEventGroupSetBits(event_group_, MAIN_EVENT_SCHEDULE);
}
// Run()의 핵심: 잠금을 푼 뒤 콜백 실행
auto tasks = std::move(main_tasks_);
lock.unlock();
for (auto& task : tasks) {
task();
}
콜백을 실행하는 동안 큐의 잠금을 붙들지 않는 것이 포인트입니다. 콜백이 다시 작업을 등록하더라도 같은 잠금을 기다리며 멈추는 일을 피합니다. 이벤트 비트는 “할 일이 있다”는 신호이고, 실제 여러 작업은 main_tasks_가 보관합니다. 같은 비트가 두 번 켜졌다고 두 개의 이벤트 객체가 쌓이는 것은 아닙니다.
상태는 main/device_state_machine.cc의 IsValidTransition()에서 제한합니다. Application::SetDeviceState()는 TransitionTo()로 위임하며, 허용되지 않은 전이는 경고 후 false를 돌려줍니다.
대표적인 대화 경로
unknown --> starting --> activating --> idle
|
v
connecting
|
v
listening <--> speaking
| |
+----> idle <-+
설정·업데이트 경로도 별도 존재
starting --> wifi_configuring --> activating --> upgrading
idle --> notifying --> idle
이 그림은 전체 전이표의 일부입니다. 코드에서는 connecting → speaking을 바로 허용하지 않고 listening을 거치게 합니다. 같은 상태로의 요청은 성공하는 무동작입니다. current_state_의 원자적 읽기·쓰기가 모든 복합 동작을 자동으로 보호하는 것은 아니므로, 앱 변경을 메인 태스크로 모으는 규칙과 함께 이해해야 합니다.
이벤트는 호출벨이고, 작업 큐는 주문서이며, 상태 머신은 주방의 작업 규칙입니다. 호출벨만으로 주문 내용을 알 수 없고, 주문서가 있다고 조리 중인 기계를 아무 상태로나 바꿀 수 있는 것도 아닙니다. 세 장치가 서로 다른 문제를 해결합니다.
main/boards/common/board.h의 Board는 GetAudioCodec(), GetDisplay(), GetNetwork(), StartNetwork() 등의 인터페이스를 제공합니다. 공통 앱은 구체적인 보드 이름이나 핀 번호 대신 이 인터페이스를 호출합니다.
DECLARE_BOARD(CompactWifiBoard)는 create_board() 팩터리를 정의합니다. Board::GetInstance()가 이를 통해 보드 하나를 만듭니다. CMake는 선택한 BOARD_DIR의 *.cc·*.c를 포함하므로, 빌드 하나에 보드 팩터리도 하나여야 합니다.
// main/boards/common/board.h의 팩터리 매크로
#define DECLARE_BOARD(BOARD_CLASS_NAME) \
void* create_board() { \
return new BOARD_CLASS_NAME(); \
}
// 보드 구현 끝에서 구체 클래스를 연결
DECLARE_BOARD(CompactWifiBoard);
선택 기능의 처리도 좋은 예제입니다. main/boards/common/board.cc의 기본 GetDisplay()는 NoDisplay, GetLed()는 NoLed를 반환하고 GetCamera()는 nullptr를 반환합니다. 화면·LED에는 빈 구현을, 카메라에는 존재 여부 검사를 사용하는 서로 다른 방식이 공존합니다.
학습할 때는 main/boards/bread-compact-wifi/compact_wifi_board.cc를 한 보드 사례로 고르면 좋습니다. 생성자에서 디스플레이·버튼·도구를 초기화하고, GetAudioCodec()에서 I2S 연결 방식에 맞는 코덱 구현을 반환합니다. NoAudioCodecSimplex라는 이름은 오디오 자체가 없다는 뜻이 아니라, 별도 제어형 코덱 칩 없이 쓰는 I2S 경로입니다.
main/protocols/protocol.h는 OpenAudioChannel(), SendAudio(), CloseAudioChannel()을 순수 가상 함수로 정의합니다. Application::InitializeProtocol()은 OTA 응답에서 MQTT 설정을 먼저 확인하고, 없으면 WebSocket 설정을 확인합니다. 둘 다 없으면 MQTT를 선택합니다.
| 구현 | 제어 메시지 | 음성 데이터 | 핵심 함수 |
|---|---|---|---|
| WebSocket | 텍스트 프레임의 JSON | 바이너리 프레임의 Opus | WebsocketProtocol::OpenAudioChannel()·SendAudio() |
| MQTT + UDP | MQTT의 JSON | AES-CTR 처리한 UDP 패킷 | MqttProtocol::OpenAudioChannel()·CryptAesCtr() |
Protocol::SendMcpMessage()는 JSON-RPC 본문을 type: "mcp" 봉투 안에 넣습니다. 즉 MCP가 오디오 압축을 대체하거나, 모든 데이터를 MQTT로 보내는 구조가 아닙니다. 메시지 의미와 전송 수단을 나눠 둔 것입니다.
WebSocket의 GetHelloMessage()는 format: "opus", sample_rate: 16000, channels: 1, frame_duration: 60을 알립니다. OpenAudioChannel()은 서버의 hello를 최대 10초 기다립니다. 반대로 서버가 보낼 음성의 기본값은 Protocol에서 24 kHz·60 ms이며, 실제 응답값으로 갱신할 수 있습니다.
MQTT/UDP 수신 경로는 16바이트 헤더의 길이·종류·페이로드 길이를 검사합니다. 이미 받은 번호 이하의 패킷은 버리고, 순번이 건너뛰면 경고한 뒤 새 패킷을 처리합니다. 이것은 패킷 정렬 버퍼나 재전송 구현과는 다릅니다.
MQTT/UDP 음성 패킷 헤더: 총 16바이트 +------+-------+-------------+------+-----------+----------+ | type | flags | payload_len | ssrc | timestamp | sequence | | 1 B | 1 B | 2 B | 4 B | 4 B | 4 B | +------+-------+-------------+------+-----------+----------+ | 이어지는 데이터: 암호화된 Opus payload | +--------------------------------------------------------+
AudioService는 입력·출력·Opus 처리를 별도 태스크로 나눕니다. AudioInputTask()가 녹음하면 음성 엔진이 처리하고, OpusCodecTask()가 압축하거나 압축을 풀며, AudioOutputTask()가 스피커로 내보냅니다.
| 큐 | 설정상 상한 | 포화 시 정책 |
|---|---|---|
| 인코딩 대기 | PCM 작업 2개 | PushTaskToEncodeQueue()에서 가장 오래된 작업을 버립니다. |
| 송신 대기 | 2400 / 60 = 40패킷 | 오래된 패킷을 버리고 새 패킷을 넣습니다. |
| 일반 디코딩 대기 | 1200 / 60 = 20패킷 | wait가 참이면 공간을 기다리고, 거짓이면 실패를 반환합니다. |
| 재생 대기 | PCM 작업 2개 | 공간이 생길 때까지 디코딩 진행을 제한합니다. |
상한은 main/audio/audio_service.h에 있습니다. 40패킷은 60 ms 전송 프레임 기준 최대 2.4초 분량이고, 이것이 곧 실제 왕복 지연이라는 뜻은 아닙니다. 디코딩 큐의 내부 저장 용량은 10초 오디오 테스트 큐와 교환할 수 있게 더 크게 잡혀 있지만, 일반 수신의 제한은 20패킷입니다. 서버가 다른 프레임 길이를 쓰면 패킷 수와 초 단위 관계도 달라집니다.
실시간 음성에서는 오래된 소리를 전부 보존하는 것보다, 메모리와 지연이 끝없이 늘어나지 않게 하는 선택이 더 중요할 수 있습니다. 이 코드의 송신·인코딩 경로는 그 선택을 명시합니다. 반면 재생 쪽에는 기다리는 정책이 있으므로, 모든 큐가 같은 방식으로 동작한다고 일반화하면 안 됩니다.
main/audio/fixed_queue.h의 FixedQueue는 std::array<std::optional<T>, Capacity>를 사용합니다. 큐 노드가 계속 늘어나는 할당은 피하지만, 패킷의 std::vector 버퍼와 패킷 객체 할당까지 사라지는 것은 아닙니다. “고정 용량 큐”와 “할당 없는 전체 경로”를 구별하세요.
재생 취소도 흥미롭습니다. ResetDecoder()는 playback_generation_을 올리고 큐를 비웁니다. PushPacketToDecodeQueue()는 기다리는 동안 세대가 바뀌면 실패합니다. IsPlaybackDrainedLocked()는 큐뿐 아니라 디코딩·출력 중인 작업도 확인하므로, 큐가 비었다는 이유만으로 소리가 끝났다고 판단하지 않습니다.
다음은 서버가 보내는 MCP 도구 호출의 예시입니다. session_id는 설명용 값이며, 자연어에서 도구를 선택하는 과정은 서버의 역할입니다. 기기는 아래처럼 구조화된 요청을 처리합니다.
{
"session_id": "example-session",
"type": "mcp",
"payload": {
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "self.audio_speaker.set_volume",
"arguments": { "volume": 30 }
}
}
}
Application::InitializeProtocol()에 등록된 JSON 콜백이 type == "mcp"를 보고 McpServer::ParseMessage(payload)를 호출합니다.ParseMessage()는 JSON-RPC 버전 2.0, 메서드, 객체형 인자, 숫자형 요청 ID를 검사합니다.DoToolCall()이 도구 이름을 찾아 PropertyList를 복사하고 필수 인자·타입·범위를 확인합니다. 볼륨의 선언 범위는 0~100입니다.Application::Schedule()에 등록됩니다. 수신 JSON 포인터를 나중까지 그대로 붙잡는 방식이 아닙니다.Board::GetInstance().GetAudioCodec()->SetOutputVolume(30)으로 이어집니다.ReplyResult()가 같은 ID로 응답을 만들고 Protocol::SendMcpMessage() 경로로 돌려보냅니다.initialize 응답은 protocolVersion: "2024-11-05"를 선언합니다. 도구 목록은 GetToolsList()가 8,000바이트를 기준으로 페이지를 나누고 nextCursor를 보냅니다. MCP의 모든 기능을 구현한 범용 서버로 가정하기보다, initialize·tools/list·tools/call 중심의 장치용 구현으로 읽는 것이 정확합니다.
xiaozhi-esp32/ ├── CMakeLists.txt 프로젝트·SDK 진입점 ├── sdkconfig.defaults* 공통·칩별 기본 설정 ├── main/ │ ├── main.cc app_main() 진입점 │ ├── application.* 이벤트 루프와 대화 동작 │ ├── device_state* 상태 이름·전이 규칙 │ ├── mcp_server.* 도구 등록·검증·실행 요청 │ ├── ota.* 버전 조회·활성화·업데이트 │ ├── settings.* NVS 설정 접근 │ ├── idf_component.yml 외부 컴포넌트 의존성 │ ├── Kconfig.projbuild 보드·언어·기능 설정 │ ├── CMakeLists.txt 소스·리소스 선택 │ ├── audio/ │ │ ├── audio_service.* 오디오 태스크와 큐 │ │ ├── audio_codec.* 하드웨어 오디오 인터페이스 │ │ ├── fixed_queue.h 고정 용량 큐 │ │ ├── codecs/ 하드웨어별 코덱 구현 │ │ └── engines/ AFE·경량 음성 엔진 │ ├── protocols/ 공통 API·WebSocket·MQTT/UDP │ ├── boards/ │ │ ├── common/ 보드·버튼·통신 공통 계층 │ │ └── bread-compact-wifi/ 한 보드의 구현·핀·변형 │ ├── display/ OLED·LCD·LVGL 화면 │ ├── led/ LED 표시 │ ├── notify/ 알림 음성·자막 재생 │ └── assets/ 효과음·언어 리소스 ├── partitions/v2/ 플래시 분할표 ├── scripts/ │ ├── build.py 보드 변형 빌드 도구 │ ├── build_default_assets.py 리소스 묶음 생성 │ ├── gen_lang.py 언어 헤더 생성 │ ├── release.py 릴리스 준비·검증·게시 │ ├── ci/ 대표 빌드 변형 목록 │ └── tests/ 호스트 측 테스트 ├── docker/firmware-builder/ 펌웨어 빌더 컨테이너 ├── .github/workflows/ 빌드·릴리스·MCP 검사 └── docs/ 보드·통신·MCP 문서
| 궁금한 동작 | 읽는 순서 |
|---|---|
| 버튼으로 대화 시작 | 보드의 InitializeButtons() → Application::ToggleChatState() → Run() → HandleToggleChatEvent() |
| 음성 전송 | AudioInputTask() → 음성 엔진의 출력 → 인코딩 큐 → OpusCodecTask() → 송신 큐 → Protocol::SendAudio()의 실제 구현 |
| 응답 자막 | 통신의 JSON 수신 → InitializeProtocol()의 tts/sentence_start 분기 → Schedule() → SetChatMessage() |
| 조명 제어 | 보드의 InitializeTools() → main/boards/common/lamp_controller.h → MCP 등록 → gpio_set_level() |
| 보드 선택 | 보드의 config.json → scripts/build.py → Kconfig → CMake → 보드 소스와 config.h |
커밋 b3ff05c의 Git 추적 파일을 확장자별로 분류한 물리적 줄 수입니다. 빈 줄·주석을 포함하며, 다운로드한 외부 컴포넌트와 빌드 산출물은 포함하지 않습니다. 헤더에는 C/C++ 선언과 데이터가 함께 있으므로 C++ 구현 수치와 분리했습니다.
| 분류 | 파일 수 | 줄 수 |
|---|---|---|
| C++ 구현 (.cc·.cpp) | 287 | 86,089 |
| 헤더 (.h·.hpp) | 357 | 29,541 |
| C 구현 (.c) | 18 | 6,657 |
| Python (.py) | 31 | 12,770 |
| CMake | 2 | 1,530 |
| Shell (.sh) | 2 | 13 |
이 기준의 C/C++ 구현·헤더 합계는 122,287줄입니다. main/boards/ 아래 관련 코드만 103,064줄인 반면, main/audio/는 5,502줄, main/protocols/는 1,265줄입니다. 전체 줄 수의 상당 부분은 보드 지원에 있으므로, 핵심 원리를 공부하려면 공통 계층부터 좁혀 읽는 편이 좋습니다.
Python 테스트 파일 10개의 합계는 3,946줄입니다. 이는 테스트가 존재하는 범위를 보여 주는 수치이며, 펌웨어의 전체 테스트 커버리지나 실제 장치 품질을 나타내는 비율은 아닙니다.
bread-compact-wifi, WebSocket, 볼륨 도구 세 가지를 고르면 충분합니다. 이 경로를 설명할 수 있게 된 뒤 다른 코덱·모뎀·화면 구현을 비교하면, 반복되는 인터페이스와 달라지는 하드웨어 코드를 쉽게 구분할 수 있습니다.
AudioStreamPacket은 std::unique_ptr로 전달됩니다. std::move(packet)가 보이면 “비싼 데이터를 복사하지 않는다”에서 멈추지 말고, 이제 누가 패킷을 파괴할 책임을 가지는가를 질문하세요. Protocol::SendAudio()와 AudioService::PopPacketFromSendQueue()가 좋은 읽기 대상입니다.
main/mcp_server.h는 성공 시 반환할 값의 타입을 std::variant로, 성공 또는 오류를 std::expected로 표현합니다. sdkconfig.defaults는 C++ 예외와 RTTI를 끄므로, 예외 대신 반환값으로 실패를 전달하는 방식도 함께 볼 수 있습니다.
// main/mcp_server.h
using ReturnValue = std::variant<bool, int, std::string, cJSON*, ImageContent*>;
using ToolResult = std::expected<ReturnValue, std::string>;
using ToolCallback = std::function<ToolResult(const PropertyList&)>;
미니 실습: DoToolCall()에서 arguments가 복사되는 지점과 람다로 이동하는 지점을 표시하세요. 원본 수신 JSON이 해제되어도 지연 실행이 가능한 이유를 객체 수명으로 설명해 보세요. 포인터형 반환값에는 별도 소유권 약속도 있으므로, McpTool::Call()의 직렬화·해제 경로까지 확인하면 좋습니다.
lock_guard는 범위를 벗어나면 잠금을 풀고, unique_ptr는 소유한 객체를 정리합니다. 이동은 이 책임을 다른 객체에 넘기는 일입니다. 실제 버퍼가 항상 통째로 복사된다는 뜻은 아닙니다.AudioService::Start()에서 입력 태스크 우선순위는 8, 출력은 4, Opus 처리는 2입니다. 오디오 프로세서를 쓸 때 입력은 코어 0에 고정합니다. 메인 루프는 Application::Run()에서 우선순위를 10으로 설정합니다. 이 숫자는 CPU 사용률이나 지연 보장이 아니라, 실행 가능한 태스크 사이의 스케줄링 우선순위입니다.
미니 실습: “네트워크 콜백 → 이벤트 비트 → 메인 루프 → 큐 소비”를 네 칸으로 그리세요. 이벤트 비트가 여러 번 합쳐져도 MAIN_EVENT_SEND_AUDIO 분기에서 while로 큐를 비우는 이유를 설명해 보세요. 공식 배경은 ESP-IDF FreeRTOS 문서에서 이어갈 수 있습니다.
16 kHz·16비트·단일 채널 PCM이면 초당 16,000 × 2 = 32,000바이트입니다. 60 ms는 960샘플, 1,920바이트입니다. 이것은 압축 전 PCM 계산이며, 실제 Opus 전송량은 설정과 음성 내용에 따라 달라집니다.
AS_OPUS_ENC_CONFIG()는 complexity = 0, enable_fec = false, enable_dtx = true, enable_vbr = true를 설정합니다. 자동 비트레이트를 쓰므로 특정 고정 전송률을 임의로 붙이면 안 됩니다. Opus의 배경은 공식 코덱 소개를 참고하세요.
미니 실습: 2초의 PCM이 64,000바이트라는 것을 계산하고, main/audio/README.md의 깨우기 음성 캐시 설명과 비교하세요. 전송 패킷에 담긴 압축 음성 용량을 같은 수치로 추정하면 왜 틀리는지도 적어 보세요.
BinaryProtocol2·BinaryProtocol3는 서로 다른 바이너리 헤더를 정의합니다. 송신 코드의 htons()·htonl(), 수신 코드의 ntohs()·ntohl()는 다중 바이트 정수의 순서를 맞춥니다. 문자 JSON과 바이너리 음성은 같은 연결 위에서도 파싱 규칙이 다릅니다.
미니 실습: MQTT/UDP 헤더의 필드를 바이트 오프셋으로 적어 보세요. payload_len은 2, timestamp는 8, sequence는 12에서 시작합니다. 헤더 길이를 먼저 검사해야 하는 이유를 포인터 접근과 연결해서 설명하세요.
Property는 타입, 기본값, 정수 범위, 문자열 최대 길이를 표현합니다. Property::Validate()와 set_value()는 선언을 실행 시 검사로 연결합니다. 도구 설명은 모델이 선택할 때 참고하는 정보이고, 실제 인자 검사는 별개의 책임입니다.
미니 실습: 볼륨 도구에 대해 누락, 문자열 "30", 정수 30, 범위 밖 101의 예상 결과를 표로 만드세요. 도구가 선언한 스키마와 디스패처의 검사를 각각 찾아 근거를 달아 보세요.
정밀하게 보면 한계도 있습니다. DoToolCall()은 정수 인자를 cJSON_IsNumber()로 확인한 뒤 valueint로 읽으며, 소수 부분이 없는지 별도로 검사하지 않습니다. 스키마의 integer 선언만 보고 모든 숫자 입력이 엄격히 검사된다고 가정하면 안 됩니다. 선언과 구현을 비교하는 연습에 좋은 지점입니다.
scripts/tests/test_mcp_server.py는 플랫폼 의존 부분을 대체하되 실제 mcp_server.cc와 press-to-talk 도구를 호스트 C++23 컴파일러로 컴파일하도록 설계되어 있습니다. test_tool_error_message_round_trips()는 따옴표·줄바꿈 등이 들어간 오류도 JSON 왕복이 되는지 검사합니다.
미니 실습: 이 테스트에서 진짜 코드인 부분과 대체한 하드웨어 부분을 두 열로 나눠 보세요. 테스트가 통과하더라도 마이크 배선·음향 에코·무선 끊김을 검증할 수 없는 이유를 정리하세요.
좋은 학습 결과는 “이 라이브러리를 안다”가 아니라, 데이터의 소유자·실행 태스크·실패 정책을 소스에서 설명할 수 있는 상태입니다.
| 목표 | 필요한 것 | 주의할 구분 |
|---|---|---|
| 소스 학습 | 텍스트 편집기, Git, JSON·C++를 읽을 기초 | 실제 보드나 모델 API가 없어도 호출 흐름·계산·설계 과제를 할 수 있습니다. |
| 펌웨어 빌드 | 선택한 칩을 지원하는 ESP-IDF, Python과 SDK 도구 체인 | 매니페스트는 IDF 6.0.1 이상, README·CI는 release/v6.1 사용을 안내합니다. |
| MCP 호스트 테스트 | C++23 컴파일러, Python, cJSON·Mbed TLS 개발 라이브러리 | .github/workflows/test-mcp.yml은 Ubuntu 24.04에서 g++·libcjson-dev·libmbedtls-dev를 사용합니다. |
| 음성 단말 동작 | 맞는 ESP32 보드, 마이크·출력 회로·스피커, 전원, 네트워크, 호환 서버 | 코드가 컴파일되는 것과 보드의 배선·음향이 정상인 것은 별도 조건입니다. |
| 선택 기능 | 화면, 카메라, PSRAM, AEC 기준 신호, 모뎀 등 | 보드·칩·기능별 조건이 다릅니다. 공통 필수 부품 목록이 아닙니다. |
sdkconfig.defaults의 공통값은 16 MB 플래시와 partitions/v2/16m.csv입니다. 반면 main/boards/espressif/esp-hi/config.json은 4 MB 플래시와 partitions/v2/4m.csv를 선택합니다. 따라서 “이 프로젝트는 무조건 16 MB가 필요하다”는 설명은 틀립니다.
| 16 MB 기본 분할 | 실제 CSV 값 | 역할 |
|---|---|---|
| nvs | 0x4000 = 16 KiB | 설정 저장 |
| otadata | 0x2000 = 8 KiB | OTA 슬롯 상태 |
| ota_0·ota_1 | 각 0x3f0000 = 3.9375 MiB | 번갈아 사용하는 펌웨어 영역 |
| assets | 0x800000 위치, 크기 8 MiB | 폰트·표정·깨우기 모델 등의 리소스 |
partitions/v2/README.md는 앱 영역을 약 4 MB로 요약하지만 실제 크기는 위 CSV 값입니다. 또한 4 MB 분할은 factory 앱 하나와 1 MiB 리소스를 사용하므로, 모든 보드가 두 OTA 슬롯을 가진다고 가정할 수 없습니다.
main/boards/bread-compact-wifi/config.h는 입력 16 kHz·출력 24 kHz를 사용합니다. 기본 simplex 배선은 마이크 WS/SCK/DIN이 GPIO 4/5/6, 스피커 DOUT/BCLK/LRCK가 GPIO 7/15/16입니다. 이 값은 그 보드 정의의 예시이며 다른 보드에 그대로 적용할 공통 핀 표가 아닙니다.
AfeAudioEngine의 채널 형식에서 M은 마이크, R은 재생 기준 신호입니다. 두 번째 마이크를 R로 연결하면 같은 의미가 되지 않습니다. USE_DEVICE_AEC도 기본값은 꺼져 있고 지원 보드 목록으로 제한됩니다. 서버 AEC 옵션에는 불안정 표시와 서버 지원 조건이 있습니다.
전이중 대화는 옵션 하나의 효과가 아니라, 재생 기준 신호·음향 구조·보드 지원·서버 동작이 함께 맞아야 가능한 결과입니다. 현재 AFE 코드는 ns_init = false로 잡음 억제를 끄고 있으므로, AFE 사용을 곧 모든 잡음 제거 기능 활성화로 설명하면 안 됩니다.
깨우기 단어 인식은 기기에서 할 수 있지만, 일반적인 자유 대화에는 호환 대화 서버가 필요합니다. Ota::GetCheckVersionUrl()는 NVS의 wifi/ota_url을 먼저 읽고, 없으면 CONFIG_OTA_URL을 사용합니다. 기본 URL은 main/Kconfig.projbuild에 정의되어 있고 버전·서버 주소 조회에 쓰입니다.
서버를 바꿀 때는 접속 주소 하나뿐 아니라 OTA 응답 형식, 인증, hello, 음성 프레임, 제어 메시지, MCP 호환성을 함께 봐야 합니다. 특정 GPU가 이 펌웨어의 필수 요구사항은 아니며, 서버를 직접 운영할 경우 필요한 자원은 선택한 모델과 서버 구현에 달려 있습니다.
모든 과제에서 입력·중간 경로·예상 결과를 먼저 적으면, 코드를 바꾸기 전에 이해의 빈틈을 찾을 수 있습니다. 아래 1~3번은 빌드 없이 완료할 수 있습니다. 4~5번은 설계와 예제 작성까지 진행하고, 실행 검증은 별도의 개발 환경과 필요 하드웨어를 갖춘 뒤 확장하세요.
목표: UI 이벤트가 코어 로직으로 들어가는 경로를 이해합니다.
bread-compact-wifi의 InitializeButtons()에서 부트 버튼 콜백을 찾습니다.ToggleChatState() → 이벤트 비트 → Run() → HandleToggleChatEvent()를 연결합니다.산출물: 함수 이름과 파일 경로가 적힌 5칸 흐름도. 완료 기준: 버튼 콜백이 곧바로 네트워크 연결을 수행하는지, 메인 루프에 요청하는지 근거를 들어 설명할 수 있습니다.
# 소스만 검색하는 명령
rg -n 'InitializeButtons|ToggleChatState' main/boards/bread-compact-wifi
rg -n 'MAIN_EVENT_TOGGLE_CHAT|HandleToggleChatEvent' main/application.*
목표: 설정 한 줄이 코드와 자원 선택으로 이어지는 과정을 배웁니다.
main/Kconfig.projbuild에서 LANGUAGE_KO_KR를 찾습니다.main/CMakeLists.txt에서 LANG_DIR "ko-KR"와 LANG_JSON을 찾습니다.main/assets/locales/ko-KR/language.json의 키 세 개와 대응하는 화면 문구를 골라 번역표를 만듭니다.산출물: 설정 → 디렉토리 → JSON → 생성 헤더의 연결도와 문구 3개. 완료 기준: 한국어 UI 설정과 한국어 음성 인식 모델·한국어 깨우기 단어가 서로 다른 설정이라는 점을 설명할 수 있습니다.
목표: 용량 제한과 오래된 데이터 폐기의 효과를 계산합니다.
OpusCodecTask()의 송신 큐 분기와 비교합니다.산출물: 시점·입력 번호·큐 앞/뒤 번호·폐기 수 표. 완료 기준: 최종 큐는 11~50번, 폐기 10개, 보관 분량은 2.4초가 됩니다. 압축률·패킷 손실·입력 큐 지연은 이 단순 모형에서 제외했다고 명시하세요.
목표: 도구의 설명·인자 스키마·콜백을 한 세트로 설계합니다.
self.study.add라는 도구 초안을 작성합니다. 두 입력의 스키마는 -100~100의 정수로 선언합니다.InitializeTools() 쪽에 두는 이유를 정리합니다.산출물: 도구 등록 코드 초안과 요청·예상 응답 4쌍. 완료 기준: 아래 예시의 누락·타입·범위 오류는 콜백 실행 전 거부되며, 정상 입력의 합은 -200~200 범위임을 설명합니다. 소수 입력에 대한 검사 한계는 6-5절과 함께 보세요.
// 연습용 도구 등록 예시
auto& server = McpServer::GetInstance();
server.AddTool(
"self.study.add", "두 정수를 더합니다.",
PropertyList({
Property("left", kPropertyTypeInteger, -100, 100),
Property("right", kPropertyTypeInteger, -100, 100)
}),
[](const PropertyList& args) -> ReturnValue {
return args["left"].value<int>() + args["right"].value<int>();
});
| 입력 예시 | 예상 동작 |
|---|---|
{"left": 2, "right": 3} | 성공. 결과 5를 도구 응답의 텍스트 콘텐츠로 직렬화합니다. |
{"left": 2} | 필수 인자 누락, JSON-RPC 오류 코드 -32602 |
{"left": "2", "right": 3} | 인자 타입 불일치, -32602 |
{"left": 101, "right": 3} | 인자 범위 초과, -32602 |
참고할 테스트는 scripts/tests/test_mcp_server.py입니다. 새 도구를 실제로 검사하려면 테스트 드라이버에 등록하고 요청을 보내는 구성이 필요합니다. 위 표는 디스패처 로직에 따른 예상 결과이며 실행 측정값은 아닙니다.
목표: 핀 번호 몇 개를 바꾸는 것보다 넓은 보드 이식의 범위를 익힙니다.
docs/custom-board.md와 기존 보드 하나를 읽고, 고유한 보드 디렉토리·type·빌드 name을 설계합니다.config.json, Kconfig, CMake, config.h, 보드 클래스, DECLARE_BOARD의 변경 계획을 작성합니다.산출물: 보드 이식 설계서 1장과 파일별 변경 계획. 완료 기준: 기존 보드 핀을 덮어쓰지 않고, OTA 호환성에 쓰이는 보드 식별자를 구분하며, 해당 빌드에서 팩터리가 하나만 생기도록 설명합니다.
1~3번으로 호출 경로와 단위를 익힌 뒤 4번에서 입력 계약을, 5번에서 제품 구성을 다루세요. 실행 환경을 갖췄다면 설계한 예상 결과를 실제 관찰과 비교하되, 빌드 성공과 하드웨어 성공은 따로 기록하는 습관이 좋습니다.
기초 C++ 문법과 함수 호출을 읽을 수 있다면 시작할 수 있습니다. 처음 보는 문법이 많으면 class, 참조, 람다, 템플릿을 먼저 복습하세요. 주차는 속도 경쟁을 위한 기한이 아니라, 다음 주제로 넘어갈 때 확인할 이해 기준입니다.
| 주차 | 학습 주제·소스 | 만들 결과와 통과 기준 |
|---|---|---|
| 1주차 | 펌웨어 입구, 함수 호출, 설정main/main.cc·main/application.*·보드 config.json | 부팅 흐름과 버튼 흐름도. 진입점, 네트워크 시작, 메인 루프를 구별합니다. 파일은 main/ 아래에서 찾습니다. |
| 2주차 | 현대 C++ 소유권과 실패 표현main/protocols/protocol.h·main/mcp_server.h | 패킷 수명도와 ToolResult 해설. 참조·이동·소유 포인터의 차이를 한 사례로 설명합니다. |
| 3주차 | FreeRTOS·이벤트·상태 머신main/application.cc·main/device_state_machine.cc | 상태 전이표와 태스크별 책임표. 잘못된 전이와 잠금 안의 느린 작업이 왜 문제인지 설명합니다. |
| 4주차 | PCM·Opus·큐 정책main/audio/audio_service.*·main/audio/fixed_queue.h | 60 ms 프레임의 메모리 계산과 송신 큐 모형. 압축 전 바이트 수와 압축 후 길이를 구분합니다. |
| 5주차 | WebSocket·MQTT/UDP·MCPmain/protocols/·main/mcp_server.cc | 도구 호출 요청·응답 예제와 오류 표. 서버가 도구를 고르는 일과 기기가 실행하는 일을 분리합니다. |
| 6주차 | 보드 추상화·빌드·검증main/boards/common/·scripts/build.py·CI | 보드 이식 설계서와 검증 계획. 공통 변경이 어떤 대표 변형에 영향을 주는지 설명합니다. |
각 주차의 결과에 파일 경로·함수 이름·입력과 출력의 형식을 함께 적어 두세요. 다음 주에 다른 계층을 읽을 때, 이미 이해한 연결 지점을 다시 사용할 수 있습니다.
다음 단계로 넘어갈 기준은 파일을 다 읽었는지가 아니라, 하나의 입력이 어느 태스크와 큐를 거쳐 어떤 출력이 되는지 설명할 수 있는지입니다. 음성이나 화면이 없는 프로젝트에서도 이 기준은 그대로 쓸 수 있습니다.
도시 전체의 골목을 외우기보다 집에서 학교까지의 길 하나를 먼저 익히는 방식입니다. 한 경로를 정확히 알면, 다른 보드와 다른 통신 방식은 “어느 교차로에서 갈라지는가”라는 비교 문제로 바뀝니다.
| 용어 | 쉬운 설명 |
|---|---|
| ESP32 | 기기의 입력·출력과 통신을 제어하는 마이크로컨트롤러 계열. 칩별 자원과 기능이 다릅니다. |
| ESP-IDF | ESP32용 공식 개발 프레임워크. 드라이버·RTOS·빌드 도구 등을 제공합니다. |
| 펌웨어 | 기기의 플래시에 기록되어 부팅 후 하드웨어를 제어하는 프로그램입니다. |
| FreeRTOS 태스크 | 스케줄러가 번갈아 또는 여러 코어에서 실행하는 작업 단위. 이 코드에서는 녹음·재생·코덱 처리를 나눕니다. |
| 이벤트 그룹 | 여러 종류의 신호를 비트로 모아 기다리는 FreeRTOS 기능. 같은 비트는 횟수별로 쌓이지 않습니다. |
| 상태 머신 | 현재 상태와 허용된 이동 규칙으로 동작을 관리하는 구조. 듣기·말하기·연결 중 같은 상태를 제한합니다. |
| 콜백 | 이벤트가 생겼을 때 호출하도록 미리 등록한 함수. 어느 태스크에서 불리는지도 중요합니다. |
| 뮤텍스 | 공유 데이터를 동시에 바꾸지 않도록 한 번에 하나의 실행 흐름에 접근을 허용하는 잠금입니다. |
| 생산자·소비자 | 데이터를 만드는 작업과 사용하는 작업을 큐로 연결하는 방식. 처리 속도가 달라도 조율할 수 있습니다. |
| 포화 정책 | 큐가 가득 찼을 때 기다릴지, 새 데이터를 거절할지, 오래된 데이터를 버릴지 정한 규칙입니다. |
| PCM | 시간마다 측정한 소리의 진폭을 숫자로 담은 데이터. 압축 전 오디오 표현입니다. |
| 샘플레이트 | 1초에 소리를 몇 번 측정하는지 나타내는 값. 16 kHz는 초당 16,000개 샘플입니다. |
| Opus | 음성·음악 전송에 쓰는 오디오 압축 형식. 이 펌웨어는 기본 60 ms 단위로 인코딩합니다. |
| VAD | 현재 오디오에 사람의 발화가 있는지 감지하는 기능. 무엇을 말했는지 알아듣는 ASR과 다릅니다. |
| AFE | 인식·전송 전에 오디오를 처리하는 전처리 계층. 기능별 활성화 여부는 설정과 구현에 달려 있습니다. |
| AEC | 스피커에서 나와 마이크에 다시 들어온 소리를 줄이는 에코 제거. 재생 기준 신호가 중요합니다. |
| WakeNet·MultiNet | ESP-SR의 음성 모델 계열. 이 프로젝트에서는 깨우기 감지와 사용자 깨우기 단어 경로에 활용합니다. |
| ASR·TTS | ASR은 말을 글자로, TTS는 글자를 말소리로 바꾸는 기술. 일반 대화 경로에서는 서버가 담당합니다. |
| LLM | 문맥을 바탕으로 답변과 도구 호출 등을 생성하는 언어 모델. 이 저장소는 주로 그 결과를 받아 동작합니다. |
| MCP | AI 애플리케이션과 도구·데이터를 연결하는 공통 규칙. 여기서는 ESP32가 도구를 제공하는 서버 역할을 합니다. |
| JSON-RPC 2.0 | JSON으로 함수 이름·인자·요청 ID·결과 또는 오류를 주고받는 메시지 형식입니다. |
| 도구 스키마 | 도구 이름과 인자의 타입·필수 여부·범위를 적은 계약. 실행 전에 입력을 검증하는 근거입니다. |
| WebSocket | 연결을 유지하면서 양쪽이 메시지를 보낼 수 있는 통신 방식. 여기서는 JSON과 바이너리 음성을 나눠 보냅니다. |
| MQTT·UDP | MQTT는 발행·구독형 메시지 통신, UDP는 데이터그램 전송 방식. 이 프로젝트는 제어와 음성 경로로 조합합니다. |
| AES-CTR·nonce | AES-CTR은 암호화 방식이고 nonce는 암호화 입력 구성에 쓰는 값입니다. 암호화가 곧 메시지 인증을 의미하지는 않습니다. |
| I2S·I2C | I2S는 디지털 오디오 전달, I2C는 디스플레이·코덱 등의 제어에 흔히 쓰는 하드웨어 통신 규격입니다. |
| HAL·팩터리 | HAL은 하드웨어 차이를 감추는 인터페이스, 팩터리는 사용할 구체 객체를 만드는 함수입니다. |
| NVS | 재부팅 후에도 남는 키·값 저장소. Wi-Fi 설정, 장치 UUID, 접속 설정 등에 사용합니다. |
| OTA·파티션 | OTA는 네트워크 업데이트, 파티션은 플래시의 구역입니다. 앱·설정·리소스를 다른 구역에 배치합니다. |
| PSRAM | 기기에 추가된 실행용 RAM. 플래시 용량과 다르며, 음성 모델·화면 버퍼 등의 조건에 영향을 줍니다. |
| RAII·unique_ptr | 객체 수명에 자원 해제를 묶는 원칙과, 객체 하나의 소유 책임을 관리하는 C++ 포인터입니다. |
| std::expected | 성공 결과나 오류 중 하나를 반환하는 C++ 타입. 이 코드에서는 MCP 실행 실패 등을 전달합니다. |
| Kconfig·빌드 변형 | Kconfig는 기능 설정 체계, 빌드 변형은 칩·화면·메모리 등의 조합으로 구별하는 펌웨어 구성입니다. |
| LVGL | 임베디드 기기에서 화면 위젯·스타일·글꼴 등을 다루는 그래픽 UI 라이브러리입니다. |
처음에는 태스크·큐·상태·소유권·도구 스키마 다섯 단어부터 정확히 연결해 보세요. 나머지 용어는 이 다섯 개가 움직이는 환경과 데이터 형식을 설명합니다.
Run(), Schedule(), InitializeProtocol()별도 서버가 궁금하면 README가 소개하는 Python 서버 프로젝트부터 역할의 차이를 비교해 보세요. 서버의 내부 구조와 지원 펌웨어 버전은 해당 프로젝트의 문서를 참고하세요.
| 항목 | 상태와 기준 |
|---|---|
| 선정 | GitHub Trending C++ Weekly #2 · 2026-10-23 |
| 스타 / 포크 | 31,804 / 7,436 · 2026-10-23 GitHub 기준 |
| 주 언어 / 설명 | C++ · MCP 기반 챗봇 |
| 생성일 | 2024-08-31 |
| 최근 push | 2026-10-23 · GitHub 기준 |
| 최신 릴리스 | v2.5.2 · 2026-10-11 |
| 분석 기준 최근 커밋 | b3ff05c · 2026-10-23 · Merge pull request #2263 from goodtft/main |
| 소스의 개발 버전 | 2.5.3 · 루트 CMakeLists.txt |
| 코드 규모 | C++ 구현 86,089줄, 헤더 29,541줄, C 6,657줄, Python 12,770줄. 빈 줄·주석 포함, Git 추적 파일 기준 |
| 보드·변형 | 보드 config.json 159개, 그 안의 builds 항목 합계 193개. SDK 조건 적용 전 원시 집계 |
| 언어 리소스 | language.json 40개, CMake 언어 매핑도 40개. 실제 안내 음성의 구비 여부와는 별도 |
| SDK | ESP-IDF >=6.0.1 · 권장 브랜치 release/v6.1 · S31은 README 기준 6.1 이상 |
| 테스트 코드 | scripts/tests/test_*.py 10개, 3,946줄. 실행 성공·장치 검증을 뜻하지 않음 |
| 라이선스 | MIT · 의존 컴포넌트의 라이선스는 별도 확인 대상 |
README와 집계 차이: README에는 보드 디렉토리 138개·릴리스 변형 171개·UI 언어 39개로 적혀 있습니다. 분석 커밋의 설정·리소스 원시 집계는 위 표와 다릅니다. _get_builds_for_idf()는 변형의 idf_version 조건을 필터링하므로, 원시 193개를 특정 SDK에서 검증된 릴리스 수로 바꾸어 말할 수는 없습니다.
동작·성능: 호출 흐름과 설정은 커밋 b3ff05c 기준입니다. 실제 빌드·호스트 테스트 결과와 보드의 지연·음질·소비 전력은 개발 환경과 하드웨어에 따라 달라집니다. 전체 기여자 수·주간 증가량은 전체 이력과 기간별 통계가 필요한 별도 지표입니다.
서버 경계: 모델 추론·화자 인식·클라우드 MCP와 공식 서버의 무료 이용 안내는 README의 서비스 설명입니다. 서버 소스·모델 가중치·모든 외부 도구가 이 저장소에 들어 있는 것은 아니며, 서비스 조건과 서버 호환성은 별도 확인이 필요합니다.
하드웨어 경계: 칩 지원 목록이 모든 보드의 카메라·AEC·PSRAM 지원을 의미하지 않습니다. UI 언어가 있다는 사실도 해당 언어의 로컬 깨우기 모델을 보장하지 않습니다. 실제 보드의 핀·전원·오디오 기준 신호와 Kconfig 조건을 함께 확인하세요.
MCP·통신 경계: 현재 디스패처는 숫자형 요청 ID와 일부 메서드를 처리합니다. user_only는 도구 목록 필터이며, DoToolCall() 자체의 사용자 인증 장벽으로 볼 수 없습니다. MQTT/UDP의 길이 검사를 근거로 다른 전송 경로까지 같은 검증을 갖췄다고 가정해서도 안 됩니다.
버전 경계: 공개 릴리스와 개발 커밋의 설정은 다를 수 있습니다. 의존성 범위·가변 SDK 브랜치는 시간이 지나면 해석 결과가 바뀔 수 있으므로, 재현할 때는 펌웨어 커밋·SDK 리비전·의존성 잠금 정보를 함께 기록해야 합니다.