Community project

Drone-Mounted LoRa Loudspeaker

ESP32
Photo of Drone-Mounted LoRa Loudspeaker

Gabriel Bessa

Published October 3, 2026

This project builds a lightweight LoRa-connected speaker pod designed to mount on drones or deploy in remote locations. It receives compressed voice packets over 915 MHz LoRa radio, decodes them using Codec2, and plays audio through a compact 2 W amplified speaker. The guide includes a complete wiring diagram, bill of materials, firmware source code, and step-by-step assembly instructions for integrating the Seeed Studio Wio-SX1262 module, MAX98357A amplifier, and LiPo battery into a flight-ready audio payload.

The pod runs on an ESP32 microcontroller with no Wi-Fi or Bluetooth enabled, keeping power consumption minimal and radio interference low. Assembly covers preparing the amplifier and radio boards, building a protected power harness, soldering signal connections, installing the speaker and antenna, and strain-relieving all components for vibration tolerance. Firmware handles LoRa packet reception, Codec2 audio decoding, I2S playback, volume control, battery monitoring, and low-power sleep modes.

Wiring diagram

Wiring diagram for Drone-Mounted LoRa Loudspeaker

Gather all the parts

QtyComponent
1

Seeed Studio Wio-SX1262 for XIAO

The compact 862–930 MHz LoRa radio board that mounts directly on the XIAO's board-to-board connector.

1

Adafruit MAX98357A I2S Class-D mono amp breakout (3006)

Adafruit 3006

The compact digital audio amplifier that drives the 4 Ω speaker directly from the pod's switched LiPo supply.

1

28 mm 4 Ω 2 W shielded neodymium speaker

The upward-facing lightweight loudspeaker that turns the amplifier output into audible voice and siren sound.

1

402025 3.7 V 150 mAh protected LiPo

The lightweight rechargeable single-cell battery that independently powers the speaker pod.

1

MSK-12C02 micro SPDT slide switch

The physical storage switch that disconnects battery positive so the pod has no standby drain.

1

Green 0603 status LED

green 0603

The small light-pipe indicator that shows pod state and battery warnings.

1

3 x 3 mm top-actuated tactile switch

The momentary button under the light pipe for wake, sleep, and battery-level indication.

1

100 µF 6.3 V polymer or tantalum capacitor

The local energy reservoir that reduces battery-voltage dips when loud audio peaks demand current.

1

915 MHz flexible PCB antenna with U.FL pigtail

The lightweight 915 MHz antenna that carries the pod's LoRa messages while keeping clear of the battery and speaker.

1

40 mm USB-C male-to-female FPC extension

The short flexible USB-C extension that moves the XIAO charging and programming socket to the pod tail.

1

220 kΩ 0603 resistor (battery divider top)

220 kΩ

The upper resistor that reduces battery voltage before it reaches the XIAO reading pin.

1

220 kΩ 0603 resistor (battery divider bottom)

220 kΩ

The lower resistor that completes the battery-voltage divider to ground.

1

100 nF 0603 ceramic capacitor (battery sense filter)

100 nF

The small capacitor that smooths the divided battery-voltage reading.

1

330 Ω 0603 resistor (status LED)

330 Ω

The series resistor that keeps the status LED current at a safe level.

Assemble it in 5 steps

1. Prepare the amplifier and boards

Remove the headers from max98357a_1, clean its pads, and solder only short 30 AWG silicone wires. Press wio_sx1262_1 fully onto the XIAO board-to-board connector; do not fit any headers anywhere.

  • Do not pull on the SX1262 board after it is fitted; the small board-to-board connector can be damaged.

2. Make the protected power harness

Solder lipo_1 positive to the COM terminal of storage_switch_1 with a red wire, then solder the ON terminal to the XIAO BAT pad, max98357a_1 VIN, bulk_cap_1 positive, and the top 220 kΩ divider resistor. Solder lipo_1 negative to every GND point with black wire. Keep the red battery wire under 25 mm.

  • Make sure LiPo positive and negative are not swapped — swapped battery wires can destroy the boards or heat the cell.
  • Keep the 100 µF capacitor polarity correct: its marked negative side goes to GND.

3. Solder the signal wires

Solder blue BCLK from XIAO D1/GPIO2 to max98357a_1 BCLK, yellow WS from D4/GPIO5 to LRC, white DIN from D5/GPIO6 to DIN, and orange SD from D3/GPIO4 to SD. Solder button_1 between D2/GPIO3 and GND. Solder r_led_1 from D7/GPIO44 to the LED anode and the LED cathode to GND. Make the divider midpoint from both 220 kΩ resistors and c_divider_1 positive, then run a purple wire to D0/GPIO1.

  • Keep every signal wire 45 mm or shorter and twist the BCLK, WS, and DIN wires loosely with a ground wire where possible.
  • The LED only lights one way: its cathode mark goes to GND.

4. Install speaker and antenna

Solder the speaker leads directly to max98357a_1 SPK+ and SPK−, keeping the pair under 35 mm. Put speaker_1 above lipo_1 in the round front zone, facing upward. Use thin foam or hot glue only around the speaker rim to make an airtight seal to the baffle. Click antenna_1 onto the Wio U.FL socket and glue its flexible section vertically along the sealed tail-side wall, away from the LiPo and speaker magnet.

  • Never connect either speaker lead to GND; the amplifier has two driven speaker outputs.
  • Do not bend, pinch, or sharply crease the U.FL cable; it can break internally.

5. Pack and strain-relieve the pod

Lay lipo_1 flat on the front floor, place speaker_1 on top, and place the XIAO/Wio stack flat in the rear. Put max98357a_1 flat above the stack with insulating tape beneath it. Secure wires with small flexible adhesive spots, not hard glue on pads. Route usb_extension_1 from the XIAO USB-C connector to the tail opening and secure the FPC at both ends.

  • Keep the front speaker volume sealed from the rear electronics volume; an air leak around the speaker rim greatly reduces sound.
  • Do not cover the XIAO antenna area with the 915 MHz antenna or with metal tape.

Review all connections

1. Connections between "wio_sx1262_1" and "ESP32"

Functionwio_sx1262_1ESP32
power3V33V3
groundGNDGND
spiSCKGPIO 7
spiMISOGPIO 8
spiMOSIGPIO 9
digitalNSSGPIO 41
digitalDIO1GPIO 39
digitalRESETGPIO 42
digitalBUSYGPIO 40
digitalRF_SWGPIO 38
dataU.FL → 915 MHz flexible PCB antenna with U.FL pigtail U.FLEXT

2. Connections between "lipo_1" and "ESP32"

Functionlipo_1ESP32
powerBAT+ → MSK-12C02 micro SPDT slide switch COMEXT
groundBAT-GND

3. Connections between "max98357a_1" and "ESP32"

Functionmax98357a_1ESP32
powerVIN → MSK-12C02 micro SPDT slide switch ONEXT
groundGNDGND
dataBCLKGPIO 2
dataLRCGPIO 5
dataDINGPIO 6
digitalSDGPIO 4
dataSPK+ → 28 mm 4 Ω 2 W shielded neodymium speaker SPK+EXT
dataSPK- → 28 mm 4 Ω 2 W shielded neodymium speaker SPK-EXT

4. Connections between "button_1" and "ESP32"

Functionbutton_1ESP32
digitalSIGNALGPIO 3
groundGNDGND

5. Connections between "r_divider_top_1" and "ESP32"

Functionr_divider_top_1ESP32
powerA → MSK-12C02 micro SPDT slide switch ONEXT
analogBGPIO 1

6. Connections between "r_divider_bottom_1" and "ESP32"

Functionr_divider_bottom_1ESP32
groundBGND
analogA → 220 kΩ 0603 resistor (battery divider top) BEXT

7. Connections between "c_divider_1" and "ESP32"

Functionc_divider_1ESP32
groundBGND
analogA → 220 kΩ 0603 resistor (battery divider top) BEXT

8. Connections between "r_led_1" and "ESP32"

Functionr_led_1ESP32
digitalAGPIO 44
digitalB → Green 0603 status LED ANODEEXT

9. Connections between "status_led_1" and "ESP32"

Functionstatus_led_1ESP32
groundCATHODEGND

10. Connections between "bulk_cap_1" and "ESP32"

Functionbulk_cap_1ESP32
power+ → MSK-12C02 micro SPDT slide switch ONEXT
ground-GND

11. Connections between "usb_extension_1" and "ESP32"

Functionusb_extension_1ESP32
dataFEMALE_USB-C → tail charging/programming openingEXT
dataMALE_USB-C → main-controller USB-C receptacleEXT

12. Connections between "storage_switch_1" and "ESP32"

Functionstorage_switch_1ESP32
powerON → main-controller BAT solder padEXT

Deploy the firmware

#include <Arduino.h>
#include <SPI.h>
#include <RadioLib.h>
#include <codec2.h>
#include <WiFi.h>
#include "esp_bt.h"
#include "driver/i2s_std.h"
#include "driver/rtc_io.h"
#include "esp_sleep.h"
#include "esp_wifi.h"
#include "config.h"

// Neo2 Speaker Pod: 915–928 MHz Brazil LoRa receive pod. No Wi-Fi or Bluetooth is enabled.
static SX1262 radio = new Module(PIN_LORA_NSS, PIN_LORA_DIO1, PIN_LORA_RESET, PIN_LORA_BUSY);
static i2s_chan_handle_t txChan = nullptr;
static codec2* codec = nullptr;
static volatile bool radioIrq = false;
static uint16_t lastSequence = 0;
static uint8_t volumePct = 60;
static uint8_t stateCode = 0; // 0 idle, 1 stream, 2 siren
static uint32_t lastPacketMs = 0;
static int16_t previousFrame[160] = {0};
static bool ampEnabled = false;

constexpr uint8_t MAGIC = 0xA7;
constexpr uint8_t TYPE_VOICE = 0x01, TYPE_STOP = 0x03, TYPE_VOLUME = 0x04;
constexpr uint8_t TYPE_SIREN = 0x05, TYPE_PING = 0x06, TYPE_TELEMETRY = 0x10, TYPE_ACK = 0x11;

static uint16_t crc16(const uint8_t* p, size_t n) {
  uint16_t crc = 0xFFFF;
  while (n--) { crc ^= uint16_t(*p++) << 8; for (uint8_t i=0;i<8;i++) crc = (crc & 0x8000) ? (crc << 1) ^ 0x1021 : crc << 1; }
  return crc;
}
static void IRAM_ATTR onRadioIrq() { radioIrq = true; }
static void setAmp(bool on) {
  if (on == ampEnabled) return;
  digitalWrite(PIN_AMP_SD, on ? HIGH : LOW);
  ampEnabled = on;
  if (on) delay(20); // MAX98357A pre-roll
}
static float batteryVoltage() {
  uint32_t total = 0;
  for (int i=0;i<16;i++) { total += analogReadMilliVolts(PIN_VBAT); delay(1); }
  return (total / 16.0f) * 2.0f / 1000.0f; // 220k / 220k divider
}
static void writeMono16k(const int16_t* mono8k, size_t count) {
  // Duplicate each 8 kHz input sample; put it in LEFT I2S slot and silence RIGHT.
  int16_t out[320];
  for (size_t i=0;i<count;i++) {
    int32_t v = (int32_t)mono8k[i] * volumePct / 100;
    v = constrain(v, -29204, 29204); // -1 dBFS hard limiter
    out[i*2] = (int16_t)v;
    out[i*2+1] = 0;
  }
  size_t written = 0;
  i2s_channel_write(txChan, out, count * 2 * sizeof(int16_t), &written, portMAX_DELAY);
}
static void playCodecFrame(const uint8_t* bits) {
  int16_t pcm[160];
  codec2_decode(codec, pcm, const_cast<uint8_t*>(bits));
  memcpy(previousFrame, pcm, sizeof(pcm));
  setAmp(true);
  writeMono16k(pcm, 160);
}
static void stopAudio() { setAmp(false); stateCode = 0; }
static void sendTelemetry() {
  uint8_t packet[5 + 4 + 8 + 2];
  uint16_t mv = (uint16_t)(batteryVoltage() * 1000.0f);
  packet[0]=MAGIC; packet[1]=TYPE_TELEMETRY; packet[2]=lastSequence & 0xFF; packet[3]=lastSequence >> 8; packet[4]=12;
  packet[5]=POD_ID; packet[6]=POD_ID>>8; packet[7]=POD_ID>>16; packet[8]=POD_ID>>24;
  packet[9]=mv; packet[10]=mv>>8; packet[11]=(int8_t)radio.getRSSI(); packet[12]=(int8_t)(radio.getSNR()*4.0f);
  packet[13]=0; packet[14]=stateCode; packet[15]=volumePct;
  packet[16]=(batteryVoltage()<VBAT_WARN)?1:0;
  uint16_t c=crc16(packet,17); packet[17]=c; packet[18]=c>>8;
  digitalWrite(PIN_LORA_RF_SW, LOW);
  radio.transmit(packet, sizeof(packet));
  digitalWrite(PIN_LORA_RF_SW, HIGH);
  radio.startReceive();
}
static void handlePacket(uint8_t* p, size_t n) {
  if (n < 11 || p[0] != MAGIC || p[4] + 7 != n) return;
  uint16_t received = p[n-2] | (uint16_t(p[n-1]) << 8);
  if (crc16(p,n-2) != received) return;
  uint32_t id = uint32_t(p[5]) | uint32_t(p[6])<<8 | uint32_t(p[7])<<16 | uint32_t(p[8])<<24;
  if (id != POD_ID) return;
  lastSequence = p[2] | (uint16_t(p[3]) << 8);
  lastPacketMs = millis();
  const uint8_t* payload = p + 9; size_t length = p[4] - 4;
  if (p[1] == TYPE_VOICE && length >= 9 && batteryVoltage() >= VBAT_STOP) {
    // mode byte followed by Codec2 3200 frames, exactly 8 bytes/frame.
    for (size_t i=1; i+7<length; i+=8) playCodecFrame(payload+i);
    stateCode = 1;
  } else if (p[1] == TYPE_STOP) stopAudio();
  else if (p[1] == TYPE_VOLUME && length) volumePct = min<uint8_t>(MAX_VOLUME, payload[0]);
  else if (p[1] == TYPE_SIREN && length >= 2) stateCode = payload[0] ? 2 : 0;
  else if (p[1] == TYPE_PING) sendTelemetry();
}
static void serviceSiren() {
  static uint32_t phase = 0;
  if (stateCode != 2 || batteryVoltage() < VBAT_STOP) return;
  int16_t pcm[160];
  for (int i=0;i<160;i++) { float f = 1000.0f + 1000.0f * sinf(2.0f * PI * phase / 64000.0f); pcm[i]=(int16_t)(9000*sinf(2*PI*f*phase/16000.0f)); phase++; }
  setAmp(true); writeMono16k(pcm,160);
}
static void configureI2S() {
  i2s_chan_config_t chan = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
  i2s_new_channel(&chan, &txChan, nullptr);
  i2s_std_config_t cfg = {
    .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(16000),
    .slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
    .gpio_cfg = {.mclk=I2S_GPIO_UNUSED, .bclk=(gpio_num_t)PIN_I2S_BCLK, .ws=(gpio_num_t)PIN_I2S_WS, .dout=(gpio_num_t)PIN_I2S_DOUT, .din=I2S_GPIO_UNUSED, .invert_flags={false,false,false}}
  };
  i2s_channel_init_std_mode(txChan, &cfg); i2s_channel_enable(txChan);
}
void setup() {
  WiFi.mode(WIFI_OFF); esp_wifi_stop(); btStop(); esp_bt_controller_disable();
  Serial.begin(115200); delay(100);
  pinMode(PIN_AMP_SD, OUTPUT); pinMode(PIN_LED, OUTPUT); pinMode(PIN_BUTTON, INPUT_PULLUP); pinMode(PIN_LORA_RF_SW, OUTPUT);
  digitalWrite(PIN_LED, LOW); digitalWrite(PIN_LORA_RF_SW, HIGH); setAmp(false);
  analogReadResolution(12); analogSetPinAttenuation(PIN_VBAT, ADC_11db);
  configureI2S(); codec = codec2_create(CODEC2_MODE_3200);
  SPI.begin(PIN_LORA_SCK, PIN_LORA_MISO, PIN_LORA_MOSI, PIN_LORA_NSS);
  radio.setTCXO(1.8); radio.begin(LORA_MHZ, 500.0, 7, 5, 0x12, 14, 8, 1.8, false);
  radio.setDio2AsRfSwitch(true); radio.setCurrentLimit(140.0); radio.setDio1Action(onRadioIrq); radio.startReceive();
  esp_sleep_enable_ext0_wakeup((gpio_num_t)PIN_BUTTON, 0);
}
void loop() {
  float v = batteryVoltage();
  if (v < VBAT_SLEEP) { stopAudio(); rtc_gpio_hold_en((gpio_num_t)PIN_AMP_SD); esp_deep_sleep_start(); }
  if (radioIrq) { radioIrq=false; uint8_t raw[255]; size_t len=0; if (radio.readData(raw, sizeof(raw))==RADIOLIB_ERR_NONE) { len=raw[4]+7; if (len<=sizeof(raw)) handlePacket(raw,len); } radio.startReceive(); }
  if (stateCode == 1 && millis()-lastPacketMs > 1500) stopAudio();
  serviceSiren();
  if (stateCode == 0) { digitalWrite(PIN_LED, (millis()%2000)<80); }
  else digitalWrite(PIN_LED, HIGH);
  static uint32_t held=0; if (!digitalRead(PIN_BUTTON)) { if (!held) held=millis(); if (millis()-held>2000) { stopAudio(); rtc_gpio_hold_en((gpio_num_t)PIN_AMP_SD); esp_deep_sleep_start(); } } else held=0;
  static uint32_t telemetryAt=0; if (millis()-telemetryAt>1000) { telemetryAt=millis(); sendTelemetry(); }
  delay(2);
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik