How to Build a Local Voice Satellite for Hermes or OpenClaw

A Nest Mini-style ESP32-S3 mic and speaker puck for LAN agent chats

ESP32Smart HomeIntermediate60 minutes6 components

Updated

How to Build a Local Voice Satellite for Hermes or OpenClaw
For illustrative purposes only
On this page

What you'll build

Build a small voice satellite: an ESP32-S3 board wired to an INMP441 I2S microphone, a MAX98357A I2S amplifier, a compact speaker, a push-to-talk button, and a status LED. Press the button and the satellite records three seconds of 16 kHz mono audio, HTTP-POSTs it as a WAV file to a bridge URL you set, then plays the WAV reply through the amplifier. The bridge machine — your laptop or a Pi — handles speech recognition, calls your Hermes or OpenClaw agent, synthesises the reply, and returns it. Agent credentials stay on the bridge, not in the microcontroller.

Hermes exposes an OpenAI-compatible API server when its gateway is enabled on port 8642; OpenClaw's gateway is commonly on port 18789. The firmware only needs your bridge's LAN address and the endpoint path — edit BRIDGE_URL before flashing. If the bridge is unreachable, the firmware prints a clear diagnostic to Serial and blinks the status LED four times rather than hanging.

This is a practical base for a local voice interface. Start with push-to-talk, a trusted LAN or Tailnet, and a visible recording LED. Once the bridge is reliable you can extend it with wake-word detection, a nicer enclosure, acoustic echo handling, longer recordings, or alternative STT/TTS choices such as local faster-whisper, Piper, or whatever your bridge already supports.

The firmware has four jobs: record 3 s of 16 kHz mono WAV from the INMP441 while the PTT button is held; HTTP-POST the WAV to BRIDGE_URL and receive a WAV reply; play the reply WAV through the MAX98357A amplifier; and blink the status LED to signal recording, ready, and bridge-error states. Setting up the bridge server and writing the STT/TTS pipeline are deliberately out of scope — the satellite firmware is standalone and testable with any endpoint that accepts a WAV POST and returns a WAV body.

Upload and calibrate

Open the sketch in Schematik and edit three constants near the top before flashing:

  • WIFI_SSID and WIFI_PASSWORD — your network credentials.
  • BRIDGE_URL — the full URL of your bridge endpoint, for example http://192.168.1.50:8787/voice. Adjust the IP, port, and path to match your bridge machine.

Flash via USB at 115200 baud. Open Serial Monitor at 115200 to watch boot progress. Within a few seconds you should see [Hermes Satellite] booting… followed by a Wi-Fi connection message and finally Hermes Satellite ready. The status LED stays off while idle.

Hold the PTT button. The LED lights solid while the satellite records. Release the button to stop recording and trigger the HTTP POST. Watch Serial Monitor for the bridge response. When the bridge replies with a WAV, the LED turns off and the speaker plays the synthesised audio. If the bridge is unreachable within the 20-second timeout, Serial prints a diagnostic and the LED blinks four times.

Troubleshooting

  • LED lights but no audio plays after release. Serial will show whether the HTTP POST failed or returned a non-200 status. Check that BRIDGE_URL matches the running bridge address and that the bridge machine is reachable on the same network.
  • Mic records silence (all-zero WAV). Confirm L/R is tied to GND, check INMP441 SCK/WS/SD pin order, and verify the mic is on 3.3 V not 5 V.
  • Distorted or clipped audio playback. The bridge should return 16 kHz mono WAV; very high bit-rates can stall playback. Reduce bridge TTS output quality if needed.
  • ESP32-S3 fails to boot into normal mode. GPIO 0 strapping conflicts can occur if the PTT button is held at power-on. Release the button, press Reset, and re-power.
  • LED blinks four times after every PTT press. Bridge is unreachable. Check network connectivity, firewall rules on the bridge host, and that the bridge service is running on the expected port.
  • Low recording volume. Keep the INMP441 within 20 cm. Room echo can be reduced by placing the mic inside a small enclosure with sound-absorbing foam.

Going further

Once push-to-talk is reliable, the natural extension is wake-word detection: add a lightweight on-device model so you can say a trigger phrase instead of pressing a button. You can also make the satellite bidirectional — have the bridge push unsolicited audio notifications back to it over a persistent WebSocket. For multi-room setups, each satellite only needs a different BRIDGE_URL; all conversation logic stays on the bridge.

Wiring diagram

Loading diagram…
Interactive wiring diagram

Components needed

Supplier links, prices, and availability are shown as a guide and may change. Schematik may earn a commission from purchases made through affiliate links.

Assembly

1

Wire the INMP441 I2S microphone

Connect the INMP441 VCC to the ESP32-S3 3.3 V rail and GND to the GND rail. Wire SCK to GPIO4, WS to GPIO5, and SD to GPIO6. Tie the L/R pin on the INMP441 to GND to select the left channel.

2

Wire the MAX98357A I2S amplifier

Connect MAX98357A VIN to the 5 V rail (or 3.3 V — the module works at both) and GND to GND. Wire BCLK to GPIO7, LRC to GPIO15, and DIN to GPIO16. Leave GAIN and SD (shutdown) floating for default 9 dB gain and always-on operation.

3

Connect the speaker

Solder or clip the compact 8 Ω speaker leads to the MAX98357A OUT+ and OUT− terminals. Polarity affects phase, not volume — match the +/− markings on the speaker if present.

4

Wire the push-to-talk button and status LED

Connect one leg of the push-to-talk button to GPIO0 and the other to GND — the firmware enables the internal pull-up. Connect the status LED anode (long leg) to GPIO2 through a 220 Ω current-limiting resistor, and the cathode (short leg) to GND.

5

Edit firmware credentials and upload

Open the sketch and set WIFI_SSID, WIFI_PASSWORD, and BRIDGE_URL to match your network and bridge machine IP (for example http://192.168.1.50:8787/voice). Flash via USB at 115200 baud, then open Serial Monitor to confirm 'Hermes Satellite ready'.

6

Test push-to-talk recording and playback

With the bridge server running, hold the PTT button — the status LED lights while the satellite records 3 seconds of audio. Release the button to stop. Watch Serial Monitor for the HTTP POST result. When the bridge replies, the LED turns off and the speaker plays the synthesized response.

Pin assignments

PinConnectionType
3V3inmp441-mic VCCPOWER
GNDinmp441-mic GNDGROUND
GPIO 4inmp441-mic SCKDATA
GPIO 5inmp441-mic WSDATA
GPIO 6inmp441-mic SDDATA
5Vmax98357a-amp VINPOWER
GNDmax98357a-amp GNDGROUND
GPIO 7max98357a-amp BCLKDATA
GPIO 15max98357a-amp LRCDATA
GPIO 16max98357a-amp DINDATA
VCCspeaker SPEAKER_POS MAX98357A OUT+POWER
GNDspeaker SPEAKER_NEG MAX98357A OUT-POWER
GPIO 0push-button SIGNALDIGITAL
GNDpush-button GNDGROUND
GPIO 2status-led ANODEDIGITAL
GNDstatus-led CATHODEGROUND

Code

Arduino C++
/*
 * Hermes Voice Satellite
 * ESP32-S3 push-to-talk voice satellite
 *
 * Records 3 s of 16 kHz mono WAV via INMP441 I2S mic,
 * HTTP POSTs it to a local bridge server,
 * plays the returned WAV reply through a MAX98357A I2S amp.
 *
 * Edit WIFI_SSID, WIFI_PASSWORD, and BRIDGE_URL before flashing.
 */

#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <driver/i2s.h>

// ── User-editable settings ─────────────────────────────────────────────────

const char* WIFI_SSID     = "YOUR_WIFI_NAME";       // edit this
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";   // edit this

// Bridge endpoint — adjust IP and port to match your bridge machine.
// Example: Hermes gateway on port 8642, OpenClaw on port 18789.
const char* BRIDGE_URL = "http://192.168.1.50:8787/voice";

// ── Pin definitions (must match pin_definitions.json) ─────────────────────

#define MIC_BCLK_PIN    4
#define MIC_WS_PIN      5
#define MIC_DATA_PIN    6
#define AMP_BCLK_PIN    7
#define AMP_WS_PIN      15
#define AMP_DATA_PIN    16
#define PTT_PIN         0
#define STATUS_LED_PIN  2

// ── Audio constants ────────────────────────────────────────────────────────

#define SAMPLE_RATE     16000
#define RECORD_SECONDS  3
#define BITS_PER_SAMPLE 16
#define NUM_CHANNELS    1   // mono

// Total 16-bit samples for the recording
#define TOTAL_SAMPLES   (SAMPLE_RATE * RECORD_SECONDS * NUM_CHANNELS)
// Total PCM bytes
#define PCM_BYTES       (TOTAL_SAMPLES * (BITS_PER_SAMPLE / 8))
// Complete WAV file = 44-byte header + PCM data
#define WAV_HEADER_SIZE 44
#define WAV_TOTAL_BYTES (WAV_HEADER_SIZE + PCM_BYTES)

// I2S driver port assignments
#define I2S_MIC_PORT  I2S_NUM_0
#define I2S_AMP_PORT  I2S_NUM_1

// HTTP timeout in ms — give the bridge time to run STT + LLM + TTS
#define BRIDGE_TIMEOUT_MS 20000

// ── LED helper states ──────────────────────────────────────────────────────
// OFF  = idle / ready
// ON   = recording
// BLINK = bridge error

// ── Forward declarations ───────────────────────────────────────────────────

void setupWiFi();
bool setupMicI2S();
bool setupAmpI2S();
void recordWAV(uint8_t* buf, size_t bufSize);
void buildWAVHeader(uint8_t* header, uint32_t pcmBytes);
bool postAndReceiveWAV(const uint8_t* wavBuf, size_t wavLen,
                       uint8_t** replyBuf, size_t* replyLen);
void playWAV(const uint8_t* wavBuf, size_t wavLen);
void blinkError(int times);

// ── Setup ──────────────────────────────────────────────────────────────────

void setup() {
  Serial.begin(115200);
  delay(300);
  Serial.println("[Hermes Satellite] booting…");

  pinMode(STATUS_LED_PIN, OUTPUT);
  digitalWrite(STATUS_LED_PIN, LOW);

  // PTT button: internal pull-up; button should short to GND
  pinMode(PTT_PIN, INPUT_PULLUP);

  setupWiFi();

  if (!setupMicI2S()) {
    Serial.println("[ERROR] INMP441 I2S driver failed — check wiring on "
                   "GPIO4(BCLK) GPIO5(WS) GPIO6(DATA)");
    while (true) { blinkError(3); delay(2000); }
  }

  if (!setupAmpI2S()) {
    Serial.println("[ERROR] MAX98357A I2S driver failed — check wiring on "
                   "GPIO7(BCLK) GPIO15(LRC) GPIO16(DIN)");
    while (true) { blinkError(5); delay(2000); }
  }

  Serial.println("[Hermes Satellite] ready — hold PTT to record");
}

// ── Main loop ──────────────────────────────────────────────────────────────

void loop() {
  // Wait for PTT press (active LOW with internal pull-up)
  if (digitalRead(PTT_PIN) != LOW) return;

  // Debounce
  delay(30);
  if (digitalRead(PTT_PIN) != LOW) return;

  Serial.println("[Satellite] PTT pressed — recording…");

  // Allocate WAV buffer in PSRAM if available, heap otherwise
  uint8_t* wavBuf = (uint8_t*)ps_malloc(WAV_TOTAL_BYTES);
  if (!wavBuf) {
    wavBuf = (uint8_t*)malloc(WAV_TOTAL_BYTES);
  }
  if (!wavBuf) {
    Serial.printf("[ERROR] Cannot allocate %u bytes for WAV buffer\n",
                  (unsigned)WAV_TOTAL_BYTES);
    blinkError(6);
    return;
  }

  // LED ON = recording
  digitalWrite(STATUS_LED_PIN, HIGH);

  recordWAV(wavBuf, WAV_TOTAL_BYTES);

  // LED OFF while waiting on bridge
  digitalWrite(STATUS_LED_PIN, LOW);

  Serial.printf("[Satellite] sending %u-byte WAV to %s\n",
                (unsigned)WAV_TOTAL_BYTES, BRIDGE_URL);

  uint8_t* replyBuf = nullptr;
  size_t   replyLen = 0;

  bool ok = postAndReceiveWAV(wavBuf, WAV_TOTAL_BYTES, &replyBuf, &replyLen);
  free(wavBuf);

  if (!ok || replyLen < WAV_HEADER_SIZE) {
    Serial.println("[ERROR] Bridge unreachable or empty reply — check BRIDGE_URL "
                   "and that the bridge machine is on the same LAN");
    blinkError(4);
    if (replyBuf) free(replyBuf);
    return;
  }

  Serial.printf("[Satellite] playing %u-byte reply WAV\n", (unsigned)replyLen);
  playWAV(replyBuf, replyLen);
  free(replyBuf);

  Serial.println("[Satellite] done — ready for next PTT");

  // Wait for button release before re-arming
  while (digitalRead(PTT_PIN) == LOW) delay(10);
}

// ── Wi-Fi ──────────────────────────────────────────────────────────────────

void setupWiFi() {
  Serial.printf("[WiFi] connecting to \"%s\"…\n", WIFI_SSID);
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  unsigned long t = millis();
  while (WiFi.status() != WL_CONNECTED) {
    if (millis() - t > 15000) {
      Serial.println("[WiFi] connection timed out — continuing without network; "
                     "bridge calls will fail");
      return;
    }
    delay(500);
    Serial.print('.');
  }
  Serial.printf("\n[WiFi] connected, IP: %s\n", WiFi.localIP().toString().c_str());
}

// ── I2S microphone (INMP441) ───────────────────────────────────────────────

bool setupMicI2S() {
  i2s_config_t cfg = {
    .mode                 = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate          = SAMPLE_RATE,
    .bits_per_sample      = I2S_BITS_PER_SAMPLE_32BIT, // INMP441 delivers 32-bit frames
    .channel_format       = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_STAND_I2S,
    .intr_alloc_flags     = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count        = 8,
    .dma_buf_len          = 256,
    .use_apll             = false,
    .tx_desc_auto_clear   = false,
    .fixed_mclk           = 0,
  };

  i2s_pin_config_t pins = {
    .bck_io_num   = MIC_BCLK_PIN,
    .ws_io_num    = MIC_WS_PIN,
    .data_out_num = I2S_PIN_NO_CHANGE,
    .data_in_num  = MIC_DATA_PIN,
  };

  esp_err_t err = i2s_driver_install(I2S_MIC_PORT, &cfg, 0, nullptr);
  if (err != ESP_OK) {
    Serial.printf("[ERROR] i2s_driver_install (mic) failed: %d\n", err);
    return false;
  }
  err = i2s_set_pin(I2S_MIC_PORT, &pins);
  if (err != ESP_OK) {
    Serial.printf("[ERROR] i2s_set_pin (mic) failed: %d\n", err);
    return false;
  }
  i2s_zero_dma_buffer(I2S_MIC_PORT);
  return true;
}

// ── I2S amplifier (MAX98357A) ──────────────────────────────────────────────

bool setupAmpI2S() {
  i2s_config_t cfg = {
    .mode                 = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
    .sample_rate          = SAMPLE_RATE,
    .bits_per_sample      = I2S_BITS_PER_SAMPLE_16BIT,
    .channel_format       = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_STAND_I2S,
    .intr_alloc_flags     = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count        = 8,
    .dma_buf_len          = 256,
    .use_apll             = false,
    .tx_desc_auto_clear   = true,  // output silence on underrun
    .fixed_mclk           = 0,
  };

  i2s_pin_config_t pins = {
    .bck_io_num   = AMP_BCLK_PIN,
    .ws_io_num    = AMP_WS_PIN,
    .data_out_num = AMP_DATA_PIN,
    .data_in_num  = I2S_PIN_NO_CHANGE,
  };

  esp_err_t err = i2s_driver_install(I2S_AMP_PORT, &cfg, 0, nullptr);
  if (err != ESP_OK) {
    Serial.printf("[ERROR] i2s_driver_install (amp) failed: %d\n", err);
    return false;
  }
  err = i2s_set_pin(I2S_AMP_PORT, &pins);
  if (err != ESP_OK) {
    Serial.printf("[ERROR] i2s_set_pin (amp) failed: %d\n", err);
    return false;
  }
  i2s_zero_dma_buffer(I2S_AMP_PORT);
  return true;
}

// ── Recording ─────────────────────────────────────────────────────────────

void buildWAVHeader(uint8_t* h, uint32_t pcmBytes) {
  uint32_t byteRate    = SAMPLE_RATE * NUM_CHANNELS * (BITS_PER_SAMPLE / 8);
  uint16_t blockAlign  = NUM_CHANNELS * (BITS_PER_SAMPLE / 8);
  uint32_t chunkSize   = 36 + pcmBytes; // RIFF chunk size

  // RIFF header
  h[0]='R'; h[1]='I'; h[2]='F'; h[3]='F';
  h[4]=(chunkSize)&0xFF; h[5]=(chunkSize>>8)&0xFF;
  h[6]=(chunkSize>>16)&0xFF; h[7]=(chunkSize>>24)&0xFF;
  h[8]='W'; h[9]='A'; h[10]='V'; h[11]='E';

  // fmt sub-chunk
  h[12]='f'; h[13]='m'; h[14]='t'; h[15]=' ';
  h[16]=16; h[17]=0; h[18]=0; h[19]=0;   // sub-chunk size = 16
  h[20]=1;  h[21]=0;                      // PCM format
  h[22]=(uint8_t)NUM_CHANNELS; h[23]=0;
  h[24]=(SAMPLE_RATE)&0xFF; h[25]=(SAMPLE_RATE>>8)&0xFF;
  h[26]=(SAMPLE_RATE>>16)&0xFF; h[27]=(SAMPLE_RATE>>24)&0xFF;
  h[28]=(byteRate)&0xFF; h[29]=(byteRate>>8)&0xFF;
  h[30]=(byteRate>>16)&0xFF; h[31]=(byteRate>>24)&0xFF;
  h[32]=(blockAlign)&0xFF; h[33]=(blockAlign>>8)&0xFF;
  h[34]=(uint8_t)BITS_PER_SAMPLE; h[35]=0;

  // data sub-chunk
  h[36]='d'; h[37]='a'; h[38]='t'; h[39]='a';
  h[40]=(pcmBytes)&0xFF; h[41]=(pcmBytes>>8)&0xFF;
  h[42]=(pcmBytes>>16)&0xFF; h[43]=(pcmBytes>>24)&0xFF;
}

void recordWAV(uint8_t* buf, size_t bufSize) {
  // Write the 44-byte WAV header first
  buildWAVHeader(buf, PCM_BYTES);

  uint8_t* pcmDst = buf + WAV_HEADER_SIZE;

  // INMP441 gives 32-bit samples (18-bit data left-justified).
  // We read in 32-bit words and keep the top 16 bits for 16-bit PCM.
  const size_t DMA_CHUNK = 256;           // samples per read
  int32_t  raw32[DMA_CHUNK];
  int16_t* dst16 = (int16_t*)pcmDst;
  size_t   samplesLeft = TOTAL_SAMPLES;

  while (samplesLeft > 0) {
    size_t want  = min(samplesLeft, DMA_CHUNK);
    size_t bytes = want * sizeof(int32_t);
    size_t readBytes = 0;
    i2s_read(I2S_MIC_PORT, raw32, bytes, &readBytes, portMAX_DELAY);
    size_t got = readBytes / sizeof(int32_t);
    for (size_t i = 0; i < got; i++) {
      // Shift right 14 bits: INMP441 data is in bits 31..14 of the 32-bit frame
      dst16[TOTAL_SAMPLES - samplesLeft + i] = (int16_t)(raw32[i] >> 14);
    }
    samplesLeft -= got;
  }

  Serial.printf("[Rec] captured %u samples at %u Hz\n",
                (unsigned)TOTAL_SAMPLES, (unsigned)SAMPLE_RATE);
}

// ── HTTP POST + receive ────────────────────────────────────────────────────

bool postAndReceiveWAV(const uint8_t* wavBuf, size_t wavLen,
                       uint8_t** replyBuf, size_t* replyLen) {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("[HTTP] Wi-Fi not connected — cannot reach bridge");
    return false;
  }

  HTTPClient http;
  http.begin(BRIDGE_URL);
  http.setTimeout(BRIDGE_TIMEOUT_MS);
  http.addHeader("Content-Type", "audio/wav");

  int httpCode = http.POST((uint8_t*)wavBuf, wavLen);

  if (httpCode <= 0) {
    Serial.printf("[HTTP] bridge unreachable — error %d (%s)\n",
                  httpCode, http.errorToString(httpCode).c_str());
    Serial.printf("[HTTP] check BRIDGE_URL: %s\n", BRIDGE_URL);
    http.end();
    return false;
  }

  if (httpCode != HTTP_CODE_OK) {
    Serial.printf("[HTTP] bridge returned HTTP %d\n", httpCode);
    http.end();
    return false;
  }

  // Read the response WAV into a heap buffer
  size_t len = (size_t)http.getSize();
  if (len == 0) {
    // Chunked / unknown length — collect via stream
    WiFiClient* stream = http.getStreamPtr();
    size_t cap = 65536;
    uint8_t* rb = (uint8_t*)malloc(cap);
    if (!rb) { http.end(); return false; }
    size_t pos = 0;
    unsigned long deadline = millis() + BRIDGE_TIMEOUT_MS;
    while (millis() < deadline) {
      if (stream->available()) {
        if (pos + 1 > cap) {
          cap *= 2;
          uint8_t* tmp = (uint8_t*)realloc(rb, cap);
          if (!tmp) { free(rb); http.end(); return false; }
          rb = tmp;
        }
        rb[pos++] = (uint8_t)stream->read();
      } else if (!http.connected()) {
        break;
      }
    }
    *replyBuf = rb;
    *replyLen = pos;
  } else {
    uint8_t* rb = (uint8_t*)malloc(len);
    if (!rb) { http.end(); return false; }
    http.getStream().readBytes(rb, len);
    *replyBuf = rb;
    *replyLen = len;
  }

  http.end();
  return true;
}

// ── Playback ───────────────────────────────────────────────────────────────

void playWAV(const uint8_t* wavBuf, size_t wavLen) {
  if (wavLen <= WAV_HEADER_SIZE) {
    Serial.println("[Play] WAV too short — skipping");
    return;
  }

  // Parse sample rate and bits from WAV header so we adapt to the bridge reply
  uint32_t sr   = wavBuf[24] | ((uint32_t)wavBuf[25] << 8)
                | ((uint32_t)wavBuf[26] << 16) | ((uint32_t)wavBuf[27] << 24);
  uint16_t bits = wavBuf[34] | ((uint16_t)wavBuf[35] << 8);
  uint16_t ch   = wavBuf[22] | ((uint16_t)wavBuf[23] << 8);

  Serial.printf("[Play] WAV: %u Hz, %u bit, %u ch\n", sr, bits, ch);

  // Reconfigure amp sample rate if it differs from our recording rate
  if (sr != SAMPLE_RATE) {
    i2s_set_clk(I2S_AMP_PORT, sr,
                (i2s_bits_per_sample_t)bits,
                ch == 2 ? I2S_CHANNEL_STEREO : I2S_CHANNEL_MONO);
  }

  const uint8_t* pcm   = wavBuf + WAV_HEADER_SIZE;
  size_t         pcmLen = wavLen - WAV_HEADER_SIZE;

  const size_t CHUNK = 2048;
  size_t offset = 0;
  while (offset < pcmLen) {
    size_t toWrite = min(CHUNK, pcmLen - offset);
    size_t written = 0;
    i2s_write(I2S_AMP_PORT, pcm + offset, toWrite, &written, portMAX_DELAY);
    offset += written;
  }

  // Restore amp sample rate for potential next playback at original rate
  if (sr != SAMPLE_RATE) {
    i2s_set_clk(I2S_AMP_PORT, SAMPLE_RATE,
                I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);
  }
}

// ── LED error blink ───────────────────────────────────────────────────────

void blinkError(int times) {
  for (int i = 0; i < times; i++) {
    digitalWrite(STATUS_LED_PIN, HIGH);
    delay(120);
    digitalWrite(STATUS_LED_PIN, LOW);
    delay(120);
  }
}

// Run this and build other cool things at schematik.io

Ready to build this?

Open this project in Schematik to get the full wiring diagram, pin assignments, and deployable code for the Hermes Voice Satellite.

Open in Schematik →

Related guides