Обзор кода прошивки: структура каталогов, конечный автомат и правила разработки

Обзор кода прошивки: структура каталогов, конечный автомат и правила разработки

Прошивка XiaoZhi — это C/C++ проект на ESP-IDF, который поддерживает множество чипов, плат, экранов, аудиоустройств и сетевых каналов. Одна сборка выбирает ровно одну реализацию платы. Страница составлена по AGENTS.md, main/audio/README.md и исходному коду upstream v2.5.2, чтобы вы нашли нужное место до того, как менять код.

1. Карта исходного кода

РасположениеНазначение
main/main.ccИнициализирует NVS, создаёт Application и входит в главный цикл Run()
main/application.*Главный цикл событий, жизненный цикл протокола, высокоуровневое поведение (пробуждение, диалог, обновление, уведомления)
main/device_state_machine.*, device_state.hДопустимые рабочие состояния и переходы между ними
main/boards/common/Интерфейс платы и повторно используемые вспомогательные классы для оборудования и сети
main/boards/<производитель>/<плата>/Выводы, инициализация и варианты сборки каждой платы (config.h, config.json)
main/audio/Кодеки, аудиозадачи, входные движки, слова пробуждения и очереди
main/protocols/Не зависящий от транспорта интерфейс Protocol и две реализации: WebSocket и MQTT/UDP
main/display/, main/led/Общие реализации LCD, OLED, отображения эмодзи и светодиодов
main/mcp_server.*Инструменты MCP на стороне устройства и их диспетчеризация
main/ota.*, main/assets.*, main/settings.*Проверка версии OTA и активация, обновление прошивки, раздел assets, настройки NVS
main/Kconfig.projbuild, main/CMakeLists.txtТип платы и конфигурация функций; выбор исходников, плат, языков, шрифтов и ресурсов
scripts/build.pyОфициально рекомендуемая точка входа для сборки платы или варианта

2. Ход выполнения

После инициализации NVS main.cc вызывает Application::GetInstance().Initialize() и Run(); Run() — главный цикл событий, который никогда не возвращается. Рабочим состоянием управляет DeviceStateMachine, а DeviceState принимает следующие значения:

СостояниеЗначение
kDeviceStateStartingЗапуск
kDeviceStateWifiConfiguringРежим настройки сети, см. Первый запуск
kDeviceStateIdleОжидание слова пробуждения или кнопки
kDeviceStateConnectingУстановка соединения с сервером
kDeviceStateListeningЗапись и отправка звука на сервер
kDeviceStateSpeakingВоспроизведение ответа
kDeviceStateNotifyingПоказ уведомления
kDeviceStateUpgradingОбновление прошивки или ресурсов
kDeviceStateActivatingПроцесс активации
kDeviceStateAudioTestingПроверка аудио
kDeviceStateFatalErrorКритическая ошибка

Application предоставляет точки входа ToggleChatState(), StartListening(), StopListening(), WakeWordInvoke(), Alert(), Schedule(), SendMcpMessage() и другие. Код платы (например, обработчики кнопок) запускает поведение через них.

3. Абстракция платы

Board (main/boards/common/board.h) — интерфейс, от которого зависит основной код. Главные чисто виртуальные методы: GetBoardType(), GetAudioCodec(), GetNetwork(), StartNetwork(), GetNetworkStateIcon(), SetPowerSaveLevel(), GetBoardJson(), GetDeviceStatusJson(); подсветка, светодиод, дисплей, камера и батарея — необязательные возможности.

В boards/common/ есть базовые классы для разных типов сети: WifiBoard, Ml307Board, DualNetworkBoard, EthernetBoard, RndisBoard, Nt26Board, а также вспомогательные классы для кнопок, управления питанием и таймеров сна. Каждая конкретная плата наследует один из базовых классов и экспортируется через DECLARE_BOARD(ИмяКласса); в сборке должен быть ровно один такой экспорт.

Полную процедуру добавления платы см. в руководстве по своей плате.

4. Аудио-путь

Аудио управляется AudioService: ввод-вывод кодека, кодирование и декодирование Opus и сетевые очереди отделены от обработки речи, зависящей от чипа.

  • Отправка: микрофон → AudioCodec → AudioInputTask → один из AudioEngine (AfeAudioEngine на S3 / P4 / S31, LiteAudioEngine на остальных чипах) → моно PCM 16 кГц → кодирование в OpusCodecTask → очередь отправки → сеть.
  • Приём: сеть → очередь декодирования → декодирование в OpusCodecTask → очередь воспроизведения → AudioOutputTask → кодек → динамик.
  • Длительность кадра Opus — 60 мс; очереди используют хранилище, ёмкость которого фиксируется при компиляции.

Соответствие движков и чипов см. в разделе Поддержка чипов и спецификации хранилища.

5. Протокольный слой

Protocol абстрагирует действия: открытие и закрытие аудиоканала, отправку аудио, отправку слова пробуждения, начало и остановку прослушивания, прерывание воспроизведения и сообщения MCP. Конкретные транспорты — WebsocketProtocol и MqttProtocol. Подробности протоколов см. в разделах Протокол связи WebSocket, Гибридный протокол MQTT + UDP и Протокол MCP.

6. Правила разработки

Правила взяты из upstream AGENTS.md. Сверьтесь с ними перед изменением кода:

  • Держите изменения сфокусированными и не добавляйте постороннее форматирование. Форматируйте по .clang-format репозитория только изменённые файлы C/C++, см. руководство по стилю кода.
  • В сборке должен экспортироваться ровно один DECLARE_BOARD(...).
  • Не меняйте выводы существующей платы ради другого оборудования. Вместо этого добавьте новую плату или вариант релиза с уникальным именем, потому что идентификатор типа платы влияет на совместимость OTA.
  • Основной код зависит только от интерфейса Board, а не от конкретного класса платы или config.h отдельной платы.
  • Камера, подсветка, дисплей, светодиод и батарея считаются необязательными возможностями.
  • Рабочее состояние меняйте только через Application::SetDeviceState() и конечный автомат.
  • Обратные вызовы могут выполняться не в главной задаче: перед изменением состояния приложения вернитесь в главную задачу через Application::Schedule() или биты событий.
  • Не блокируйте главный цикл событий и аудиозадачи; в аудио-пути избегайте неограниченных очередей и повторных выделений больших блоков.
  • Общую семантику сообщений размещайте в Protocol; при изменении контракта протокола проверяйте и WebSocket, и MQTT/UDP.
  • Проверяйте сетевой ввод и соблюдайте соглашения о владении cJSON; ключи NVS — это постоянный API, их изменение требует миграции.
  • Функции, зависящие от цели, ограничивайте через Kconfig или правила компонентов; не считайте, что у каждого чипа есть PSRAM или ресурсы S3/P4.
  • Не правьте вручную сгенерированные файлы и вывод сторонних компонентов: build/, releases/, managed_components/, components/, sdkconfig*, main/assets/lang_config.h и т. п.

7. Сборка и проверка

Сначала загрузите среду ESP-IDF (source /path/to/esp-idf/export.sh, проверьте командой idf.py --version), затем используйте команды, рекомендованные upstream:

# Discover exact board and variant names
python3 scripts/build.py --list-boards

# Canonical variant build
python3 scripts/build.py <board-directory> --name <variant-name>

# Host-side build tests
python3 -m unittest discover -s scripts/tests -v

scripts/build.py перезаписывает локальный sdkconfig и состояние сборки. После смены платы не считайте, что старый каталог сборки всё ещё соответствует прежней цели.

Upstream требует следующей проверки:

  • Изменения только в коде платы: соберите затронутые варианты и проведите дымовое тестирование на реальном оборудовании.
  • Изменения в ядре, общем коде плат, аудио, протоколе, дисплее, зависимостях, Kconfig или CMake: запустите тесты на хосте и соберите репрезентативные варианты по чипам и сетям.
  • Изменения аудио: проверьте захват, воспроизведение, пробуждение/VAD, перебивание, повторное подключение и применимые режимы AEC.
  • Изменения UI или assets: проверьте варианты без экрана, с OLED и с LVGL, а также размеры разделов.
  • Успешная сборка — это не проверка на оборудовании. В отчёте укажите, что именно проверено и что ещё требует реального оборудования.

Связанные материалы

Источники: AGENTS.md, main/audio/README.md, main/application.h, main/device_state.h, main/boards/common/board.h и main/protocols/protocol.h в 78/xiaozhi-esp32 v2.5.2.