#include <Adafruit_BMP280.h>
#include <Adafruit_MPU6050.h>
#include <Arduino.h>
#include <Kant_inferencing.h>
#include <SD.h>
#include <SPI.h>
#include <SparkFun_Qwiic_Humidity_AHT20.h>
#include <TinyGPS++.h>
#include <Wire.h>
#include <driver/i2s_std.h>
#include “esp_task_wdt.h”
// =====================================================
// GPS
// =====================================================
#define GPS_RX_PIN 7
// =====================================================
// I2S INMP441
// =====================================================
#define I2S_MIC_PORT I2S_NUM_AUTO
#define I2S_MIC_SCK 4
#define I2S_MIC_WS 5
#define I2S_MIC_SD 6
// =====================================================
// I2S MAX98357A
// =====================================================
#define I2S_SPEAKER_PORT I2S_NUM_AUTO
#define I2S_TTS_BCLK 1
#define I2S_TTS_LRC 2
#define I2S_TTS_DIN 3
// =====================================================
// SD
// =====================================================
#define SD_CS 10
#define SD_SCK 12
#define SD_MISO 13
#define SD_MOSI 11
#define SD_FREQUENCY 4000000
// =====================================================
// РАСПОЗНАВАНИЕ
// =====================================================
#define WAKE_THRESHOLD 0.90f
#define COMMAND_THRESHOLD 0.90f
#define COMMAND_TIMEOUT_MS 7000
#define REQUIRED_CONFIRMATIONS 2
#define MIN_AUDIO_LEVEL 100
#define MIN_CONFIDENCE_GAP 0.20f
// =====================================================
// WAV
// =====================================================
#define WAV_SAMPLE_RATE 16000
#define AUDIO_IO_BUFFER_SIZE 2048
#define AUDIO_WRITE_CHUNK_SIZE 512
#define AUDIO_WRITE_TIMEOUT_MS 200
// =====================================================
// ОБЪЕКТЫ
// =====================================================
Adafruit_BMP280 bmp;
Adafruit_MPU6050 mpu;
AHT20 aht;
HardwareSerial gpsSerial(1);
TinyGPSPlus gps;
// =====================================================
// I2S-КАНАЛЫ
// =====================================================
static i2s_chan_handle_t mic_rx_handle = nullptr;
static i2s_chan_handle_t speaker_tx_handle = nullptr;
// =====================================================
// ДАТЧИКИ
// =====================================================
bool bmp_available = false;
bool aht_available = false;
bool mpu_available = false;
// =====================================================
// EDGE IMPULSE
// =====================================================
static int16_t audio_buffer[EI_CLASSIFIER_SLICE_SIZE];
static int32_t i2s_samples[256];
static ei_impulse_result_t result;
static signal_t my_signal;
static size_t microphone_samples_filled = 0;
// =====================================================
// СОСТОЯНИЕ АССИСТЕНТА
// =====================================================
enum AssistantState { WAIT_KANT, WAIT_COMMAND };
AssistantState assistant_state = WAIT_KANT;
uint32_t command_wait_started = 0;
uint8_t kant_confirmations = 0;
uint8_t weather_confirmations = 0;
uint8_t coordinates_confirmations = 0;
// =====================================================
// WAV-БУФЕРЫ
// =====================================================
static uint8_t wav_input_buffer[AUDIO_IO_BUFFER_SIZE];
static int16_t wav_stereo_buffer[AUDIO_IO_BUFFER_SIZE];
// =====================================================
// WAV ПАРАМЕТРЫ
// =====================================================
static uint8_t wav_channels = 0;
static uint8_t wav_bits_per_sample = 0;
static uint32_t wav_file_sample_rate = 0;
// =====================================================
// ПРЕДОБЪЯВЛЕНИЯ ФУНКЦИЙ
// =====================================================
void reset_microphone_slice();
// =====================================================
// LITTLE-ENDIAN
// =====================================================
uint16_t readLE16(File& file) {
uint8_t data[2];
if (file.read(data, sizeof(data)) != sizeof(data)) {
return 0;
}
return static_cast<uint16_t>(data[0]) | (static_cast<uint16_t>(data[1]) << 8);
}
uint32_t readLE32(File& file) {
uint8_t data[4];
if (file.read(data, sizeof(data)) != sizeof(data)) {
return 0;
}
return static_cast<uint32_t>(data[0]) |
(static_cast<uint32_t>(data\[1\]) << 8) |
(static_cast<uint32_t>(data\[2\]) << 16) |
(static_cast<uint32_t>(data\[3\]) << 24);
}
bool readChunkId(File& file, char* id) {
if (file.read(reinterpret_cast<uint8_t*>(id), 4) != 4) {
return false;
}
id[4] = ‘\0’;
return true;
}
// =====================================================
// I2S МИКРОФОН
// =====================================================
bool setup_i2s_microphone() {
i2s_chan_config_t channel_config =
I2S_CHANNEL_DEFAULT_CONFIG(I2S_MIC_PORT, I2S_ROLE_MASTER);
channel_config.auto_clear = true;
channel_config.dma_desc_num = 8;
channel_config.dma_frame_num = 128;
esp_err_t err = i2s_new_channel(&channel_config, nullptr, &mic_rx_handle);
if (err != ESP_OK) {
Serial.printf("Ошибка создания I2S RX: %s (%d)\\n", esp_err_to_name(err),
err);
return false;
}
i2s_std_config_t mic_config = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(EI_CLASSIFIER_FREQUENCY),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT,
I2S_SLOT_MODE_MONO),
.gpio_cfg = {.mclk = I2S_GPIO_UNUSED,
.bclk = static_cast<gpio_num_t>(I2S_MIC_SCK),
.ws = static_cast<gpio_num_t>(I2S_MIC_WS),
.dout = I2S_GPIO_UNUSED,
.din = static_cast<gpio_num_t>(I2S_MIC_SD),
.invert_flags = {
.mclk_inv = false, .bclk_inv = false, .ws_inv = false}}};
mic_config.slot_cfg.slot_mask = I2S_STD_SLOT_LEFT;
err = i2s_channel_init_std_mode(mic_rx_handle, &mic_config);
if (err != ESP_OK) {
Serial.printf("Ошибка инициализации I2S RX: %s (%d)\\n",
esp_err_to_name(err), err);
return false;
}
err = i2s_channel_enable(mic_rx_handle);
if (err != ESP_OK) {
Serial.printf("Ошибка включения I2S RX: %s (%d)\\n", esp_err_to_name(err),
err);
return false;
}
Serial.println(F(“I2S микрофон инициализирован”));
return true;
}
// =====================================================
// I2S MAX98357A
// =====================================================
bool setup_i2s_speaker() {
i2s_chan_config_t channel_config =
I2S_CHANNEL_DEFAULT_CONFIG(I2S_SPEAKER_PORT, I2S_ROLE_MASTER);
channel_config.auto_clear = true;
channel_config.dma_desc_num = 6;
channel_config.dma_frame_num = 64;
esp_err_t err = i2s_new_channel(&channel_config, &speaker_tx_handle, nullptr);
if (err != ESP_OK) {
Serial.printf("Ошибка создания I2S TX: %s (%d)\\n", esp_err_to_name(err),
err);
return false;
}
i2s_std_config_t speaker_config = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(WAV_SAMPLE_RATE),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT,
I2S_SLOT_MODE_STEREO),
.gpio_cfg = {.mclk = I2S_GPIO_UNUSED,
.bclk = static_cast<gpio_num_t>(I2S_TTS_BCLK),
.ws = static_cast<gpio_num_t>(I2S_TTS_LRC),
.dout = static_cast<gpio_num_t>(I2S_TTS_DIN),
.din = I2S_GPIO_UNUSED,
.invert_flags = {
.mclk_inv = false, .bclk_inv = false, .ws_inv = false}}};
err = i2s_channel_init_std_mode(speaker_tx_handle, &speaker_config);
if (err != ESP_OK) {
Serial.printf("Ошибка инициализации I2S TX: %s (%d)\\n",
esp_err_to_name(err), err);
return false;
}
err = i2s_channel_enable(speaker_tx_handle);
if (err != ESP_OK) {
Serial.printf("Ошибка включения I2S TX: %s (%d)\\n", esp_err_to_name(err),
err);
return false;
}
Serial.println(F(“I2S MAX98357A запущен”));
return true;
}
// =====================================================
// РАЗБОР WAV
// =====================================================
bool prepare_wav(File& file, uint32_t& data_size) {
char riff[5];
char wave[5];
if (!readChunkId(file, riff)) {
Serial.println(F("Не удалось прочитать RIFF"));
return false;
}
readLE32(file);
if (!readChunkId(file, wave)) {
Serial.println(F("Не удалось прочитать WAVE"));
return false;
}
if (strcmp(riff, “RIFF”) != 0 || strcmp(wave, “WAVE”) != 0) {
Serial.println(F("Файл не является RIFF WAV"));
return false;
}
bool fmt_found = false;
bool data_found = false;
wav_channels = 0;
wav_bits_per_sample = 0;
wav_file_sample_rate = 0;
data_size = 0;
while (file.available()) {
char chunk_id\[5\];
if (!readChunkId(file, chunk_id)) {
return false;
}
uint32_t chunk_size = readLE32(file);
uint32_t chunk_start = file.position();
if (strcmp(chunk_id, "fmt ") == 0) {
uint16_t audio_format = readLE16(file);
wav_channels = readLE16(file);
wav_file_sample_rate = readLE32(file);
readLE32(file);
readLE16(file);
wav_bits_per_sample = readLE16(file);
if (audio_format != 1) {
Serial.println(F("Поддерживается только PCM WAV"));
return false;
}
if (wav_channels != 1 && wav_channels != 2) {
Serial.println(F("Поддерживается только mono/stereo WAV"));
return false;
}
if (wav_bits_per_sample != 16) {
Serial.println(F("Поддерживается только 16-bit WAV"));
return false;
}
if (wav_file_sample_rate != WAV_SAMPLE_RATE) {
Serial.printf("Требуется %d Hz, в файле %lu Hz\\n", WAV_SAMPLE_RATE,
static_cast<unsigned long>(wav_file_sample_rate));
return false;
}
fmt_found = true;
} else if (strcmp(chunk_id, "data") == 0) {
data_size = chunk_size;
data_found = true;
break;
}
file.seek(chunk_start + chunk_size);
if (chunk_size & 1) {
file.seek(file.position() + 1);
}
}
if (!fmt_found || !data_found) {
Serial.println(F("В WAV не найден fmt или data"));
return false;
}
Serial.printf(“WAV: %lu Hz, %u каналов, %u бит, %lu байт данных\n”,
static_cast<unsigned long>(wav_file_sample_rate), wav_channels,
wav_bits_per_sample, static_cast<unsigned long>(data_size));
return true;
}
// =====================================================
// ЗАПИСЬ В I2S
// =====================================================
bool write_i2s_blocking(const uint8_t* data, size_t data_size) {
if (speaker_tx_handle == nullptr) {
Serial.println(F("I2S TX не инициализирован"));
return false;
}
size_t written = 0;
while (written < data_size) {
size_t remaining = data_size - written;
size_t chunk_size =
min(remaining, static_cast<size_t>(AUDIO_WRITE_CHUNK_SIZE));
size_t bytes_written = 0;
esp_err_t err = i2s_channel_write(speaker_tx_handle, data + written,
chunk_size, &bytes_written,
pdMS_TO_TICKS(AUDIO_WRITE_TIMEOUT_MS));
if (err != ESP_OK) {
Serial.printf("Ошибка i2s_channel_write: %s (%d)\\n", esp_err_to_name(err),
err);
return false;
}
if (bytes_written == 0) {
Serial.println(F("I2S записал 0 байт"));
return false;
}
written += bytes_written;
}
return true;
}
// =====================================================
// СИНХРОННОЕ ВОСПРОИЗВЕДЕНИЕ WAV
// =====================================================
bool play_wav_blocking(const char* path) {
if (!SD.exists(path)) {
Serial.printf("WAV файл не найден: %s\\n", path);
return false;
}
File file = SD.open(path, FILE_READ);
if (!file) {
Serial.printf("Не удалось открыть WAV: %s\\n", path);
return false;
}
// Отключаем микрофон на время воспроизведения
if (mic_rx_handle) {
i2s_channel_disable(mic_rx_handle);
}
uint32_t data_size = 0;
if (!prepare_wav(file, data_size)) {
Serial.println(F("Ошибка заголовка WAV"));
file.close();
if (mic_rx_handle) i2s_channel_enable(mic_rx_handle);
return false;
}
uint32_t bytes_left = data_size;
while (bytes_left > 0) {
size_t bytes_to_read =
min(static_cast<uint32_t>(sizeof(wav_input_buffer)), bytes_left);
bytes_to_read &= \~1u;
if (bytes_to_read == 0) {
break;
}
size_t bytes_read = file.read(wav_input_buffer, bytes_to_read);
if (bytes_read == 0) {
Serial.println(F("Ошибка чтения данных WAV"));
file.close();
if (mic_rx_handle) i2s_channel_enable(mic_rx_handle);
return false;
}
bytes_read &= \~1u;
bytes_left -= bytes_read;
const uint8_t\* output_buffer = nullptr;
size_t output_bytes = 0;
if (wav_channels == 1) {
size_t mono_samples = bytes_read / 2;
for (size_t i = 0; i < mono_samples; i++) {
uint16_t low = static_cast<uint16_t>(wav_input_buffer\[i \* 2\]);
uint16_t high = static_cast<uint16_t>(wav_input_buffer\[i \* 2 + 1\]) << 8;
int16_t sample = static_cast<int16_t>(low | high);
wav_stereo_buffer\[i \* 2\] = sample;
wav_stereo_buffer\[i \* 2 + 1\] = sample;
}
output_buffer = reinterpret_cast<const uint8_t\*>(wav_stereo_buffer);
output_bytes = mono_samples \* 2 \* sizeof(int16_t);
} else if (wav_channels == 2) {
output_buffer = wav_input_buffer;
output_bytes = bytes_read;
} else {
Serial.println(F("Неподдерживаемое число каналов"));
file.close();
if (mic_rx_handle) i2s_channel_enable(mic_rx_handle);
return false;
}
if (!write_i2s_blocking(output_buffer, output_bytes)) {
Serial.println(F("Ошибка вывода WAV в I2S"));
file.close();
if (mic_rx_handle) i2s_channel_enable(mic_rx_handle);
return false;
}
}
file.close();
// Снова включаем микрофон
if (mic_rx_handle) {
i2s_channel_enable(mic_rx_handle);
reset_microphone_slice();
}
return true;
}
bool play_number_wav(int number) {
number = abs(number);
String path = “/voice/” + String(number) + “.wav”;
return play_wav_blocking(path.c_str());
}
// =====================================================
// EDGE IMPULSE
// =====================================================
static int get_signal_data(size_t offset, size_t length, float* out_ptr) {
numpy::int16_to_float(&audio_buffer[offset], out_ptr, length);
return 0;
}
void reset_microphone_slice() { microphone_samples_filled = 0; }
bool microphone_slice_ready() {
return microphone_samples_filled >= EI_CLASSIFIER_SLICE_SIZE;
}
bool read_microphone_nonblocking() {
if (mic_rx_handle == nullptr) {
return false;
}
size_t bytes_read = 0;
esp_err_t err =
i2s_channel_read(mic_rx_handle, i2s_samples, sizeof(i2s_samples),
&bytes_read, pdMS_TO_TICKS(10));
if (err != ESP_OK) {
return false;
}
if (bytes_read == 0) {
return false;
}
size_t received_samples = bytes_read / sizeof(int32_t);
for (size_t i = 0; i < received_samples; i++) {
if (microphone_samples_filled >= EI_CLASSIFIER_SLICE_SIZE) {
break;
}
audio_buffer\[microphone_samples_filled++\] =
static_cast<int16_t>(i2s_samples\[i\] >> 16);
}
return true;
}
uint32_t get_audio_level() {
int64_t sum = 0;
for (size_t i = 0; i < EI_CLASSIFIER_SLICE_SIZE; i++) {
sum += abs(static_cast<int32_t>(audio_buffer\[i\]));
}
return static_cast<uint32_t>(sum / EI_CLASSIFIER_SLICE_SIZE);
}
// =====================================================
// ПОДТВЕРЖДЕНИЯ
// =====================================================
void reset_command_confirmations() {
weather_confirmations = 0;
coordinates_confirmations = 0;
}
void reset_all_confirmations() {
kant_confirmations = 0;
reset_command_confirmations();
}
// =====================================================
// ПОГОДА
// =====================================================
void execute_weather() {
Serial.println(F(“>>> КОМАНДА: WEATHER”));
if (!bmp_available) {
Serial.println(F("BMP280 недоступен"));
play_wav_blocking("/voice/sensor_error.wav");
return;
}
float temperature_value = bmp.readTemperature();
float pressure_value = bmp.readPressure() / 100.0f;
float humidity_value = NAN;
if (aht_available) {
humidity_value = aht.getHumidity();
}
Serial.printf(“Температура: %.1f C\n”, temperature_value);
Serial.printf(“Давление: %.1f hPa\n”, pressure_value);
Serial.printf(“Влажность: %.1f %%\n”, humidity_value);
if (isnan(temperature_value)) {
play_wav_blocking("/voice/sensor_error.wav");
return;
}
int temperature = static_cast(round(temperature_value));
play_wav_blocking(“/voice/temperature.wav”);
if (temperature < 0) {
play_wav_blocking("/voice/minus.wav");
}
int absolute_temperature = abs(temperature);
if (absolute_temperature < 11) {
play_number_wav(absolute_temperature);
} else {
play_number_wav(absolute_temperature / 10);
play_number_wav(absolute_temperature % 10);
}
play_wav_blocking(“/voice/degrees.wav”);
if (temperature > 0) {
play_wav_blocking("/voice/heat.wav");
} else if (temperature < 0) {
play_wav_blocking("/voice/cold.wav");
} else {
play_wav_blocking("/voice/zero.wav");
}
}
// =====================================================
// GPS
// =====================================================
void execute_coordinates() {
Serial.println(F(“>>> КОМАНДА: COORDINATES”));
if (gps.location.isValid() && gps.satellites.isValid() &&
gps.satellites.value() >= 3 && gps.hdop.isValid() &&
gps.hdop.value() < 15) {
Serial.print("Широта: ");
Serial.println(gps.location.lat(), 6);
Serial.print("Долгота: ");
Serial.println(gps.location.lng(), 6);
Serial.print("Высота: ");
Serial.println(gps.altitude.meters());
Serial.print("Спутники: ");
Serial.println(gps.satellites.value());
Serial.print("HDOP: ");
Serial.println(gps.hdop.value());
play_wav_blocking("/voice/coordinates_ready.wav");
} else {
Serial.println(F("Недостаточно спутников"));
play_wav_blocking("/voice/no_satellites.wav");
}
}
// =====================================================
// WAIT_KANT
// =====================================================
void return_to_wait_kant() {
assistant_state = WAIT_KANT;
reset_all_confirmations();
Serial.println(F(“Снова ожидаю: KANT”));
}
// =====================================================
// ОБРАБОТКА РЕЗУЛЬТАТА
// =====================================================
void process_result(ei_impulse_result_t* inference_result) {
float best_value = 0.0f;
float second_best_value = 0.0f;
const char* best_label = “noise”;
for (size_t i = 0; i < EI_CLASSIFIER_LABEL_COUNT; i++) {
const char\* label = inference_result->classification\[i\].label;
float value = inference_result->classification\[i\].value;
if (value > best_value) {
second_best_value = best_value;
best_value = value;
best_label = label;
} else if (value > second_best_value) {
second_best_value = value;
}
}
bool confident_result =
(best_value - second_best_value) >= MIN_CONFIDENCE_GAP;
if (assistant_state == WAIT_KANT) {
if (strcmp(best_label, "kant") == 0 && best_value >= WAKE_THRESHOLD &&
confident_result) {
kant_confirmations++;
Serial.printf("KANT: %.1f%% | gap: %.1f%% | %d/%d\\n", best_value \* 100.0f,
(best_value - second_best_value) \* 100.0f,
kant_confirmations, REQUIRED_CONFIRMATIONS);
if (kant_confirmations >= REQUIRED_CONFIRMATIONS) {
Serial.println(F(">>> KANT РАСПОЗНАН"));
Serial.println(F(">>> Скажи WEATHER или COORDINATES"));
assistant_state = WAIT_COMMAND;
command_wait_started = millis();
reset_all_confirmations();
}
} else {
kant_confirmations = 0;
}
return;
}
if (assistant_state != WAIT_COMMAND) {
return;
}
if (strcmp(best_label, “weather”) == 0 && best_value >= COMMAND_THRESHOLD &&
confident_result) {
weather_confirmations++;
coordinates_confirmations = 0;
Serial.printf("WEATHER: %.1f%% | gap: %.1f%% | %d/%d\\n",
best_value \* 100.0f,
(best_value - second_best_value) \* 100.0f,
weather_confirmations, REQUIRED_CONFIRMATIONS);
if (weather_confirmations >= REQUIRED_CONFIRMATIONS) {
execute_weather();
return_to_wait_kant();
}
return;
}
if (strcmp(best_label, “coordinates”) == 0 &&
best_value >= COMMAND_THRESHOLD && confident_result) {
coordinates_confirmations++;
weather_confirmations = 0;
Serial.printf("COORDINATES: %.1f%% | gap: %.1f%% | %d/%d\\n",
best_value \* 100.0f,
(best_value - second_best_value) \* 100.0f,
coordinates_confirmations, REQUIRED_CONFIRMATIONS);
if (coordinates_confirmations >= REQUIRED_CONFIRMATIONS) {
execute_coordinates();
return_to_wait_kant();
}
return;
}
reset_command_confirmations();
}
// =====================================================
// SETUP
// =====================================================
void setup() {
Serial.begin(115200);
delay(2000);
// Отключаем Task Watchdog Timer
esp_task_wdt_deinit();
Serial.println();
Serial.println(F(“================================”));
Serial.println(F(“Запуск голосового ассистента”));
Serial.println(F(“================================”));
// ---------------------------------------------------
// I2C
// ---------------------------------------------------
Wire.begin(8, 9);
bmp_available = bmp.begin(0x77);
if (bmp_available) {
Serial.println(F("BMP280 найден"));
} else {
Serial.println(F("BMP280 не найден"));
}
aht_available = aht.begin();
if (aht_available) {
Serial.println(F("AHT20 найден"));
} else {
Serial.println(F("AHT20 не найден"));
}
// ---------------------------------------------------
// SD
// ---------------------------------------------------
SPI.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);
delay(50);
if (!SD.begin(SD_CS, SPI, SD_FREQUENCY)) {
Serial.println(F("SD mount failed"));
while (true) {
delay(1000);
}
}
Serial.println(F(“SD mounted”));
// Проверка WAV-файлов
if (SD.exists(“/voice/hello.wav”)) {
Serial.println(F("hello.wav найден"));
} else {
Serial.println(F("WARNING: hello.wav не найден!"));
}
delay(100);
// ---------------------------------------------------
// GPS
// ---------------------------------------------------
gpsSerial.begin(9600, SERIAL_8N1, GPS_RX_PIN, -1);
// ---------------------------------------------------
// Микрофон
// ---------------------------------------------------
if (!setup_i2s_microphone()) {
Serial.println(F("Микрофон не запустился"));
while (true) {
delay(1000);
}
}
Serial.println(F(“ESP32-S3 + INMP441 + Edge Impulse”));
Serial.printf(“Частота модели: %d Hz\n”, EI_CLASSIFIER_FREQUENCY);
Serial.printf(“Сэмплов в slice: %d\n”, EI_CLASSIFIER_SLICE_SIZE);
Serial.printf(
"Длительность slice: %.3f сек\\n",
static_cast<float>(EI_CLASSIFIER_SLICE_SIZE) / EI_CLASSIFIER_FREQUENCY);
// ---------------------------------------------------
// MAX98357A
// ---------------------------------------------------
if (!setup_i2s_speaker()) {
Serial.println(F("MAX98357A не запустился"));
while (true) {
delay(1000);
}
}
Serial.println(F(“MAX98357A готов”));
// ---------------------------------------------------
// Edge Impulse
// ---------------------------------------------------
run_classifier_init();
Serial.println();
Serial.println(F(“Микрофон слушает непрерывно”));
Serial.println(F(“Скажи: KANT”));
// Тестовое воспроизведение
play_wav_blocking(“/voice/hello.wav”);
}
// =====================================================
// LOOP
// =====================================================
void loop() {
// ---------------------------------------------------
// GPS
// ---------------------------------------------------
while (gpsSerial.available()) {
gps.encode(gpsSerial.read());
}
// ---------------------------------------------------
// Тайм-аут команды
// ---------------------------------------------------
if (assistant_state == WAIT_COMMAND &&
millis() - command_wait_started > COMMAND_TIMEOUT_MS) {
Serial.println(F("Время ожидания команды закончилось"));
return_to_wait_kant();
}
// ---------------------------------------------------
// Микрофон
// ---------------------------------------------------
read_microphone_nonblocking();
if (!microphone_slice_ready()) {
delay(1);
return;
}
uint32_t audio_level = get_audio_level();
my_signal.total_length = EI_CLASSIFIER_SLICE_SIZE;
my_signal.get_data = get_signal_data;
EI_IMPULSE_ERROR error =
run_classifier_continuous(&my_signal, &result, false);
reset_microphone_slice();
if (error != EI_IMPULSE_OK) {
Serial.printf("Ошибка Edge Impulse: %d\\n", error);
return;
}
if (audio_level < MIN_AUDIO_LEVEL) {
reset_all_confirmations();
return;
}
process_result(&result);
}

