Community project

Drone Light Safety Controller (copy)

Raspberry Pi Pico
Photo of Drone Light Safety Controller (copy)
Generated with AI

ianlex.wip

Published September 26, 2026

This project builds a safety-focused light controller for racing drones that integrates with Betaflight flight controllers. The system uses a Raspberry Pi Pico to monitor two radio switches and a physical safety plug, controlling a 9 V drone light through a MOSFET transistor. The controller enforces a 30-second arming delay after the safety plug is removed and requires both radio permission and an explicit light request before the lamp activates.

The guide provides a complete wiring diagram, parts list, and firmware that communicates with the flight controller via MSP protocol to read RC channel values. Assembly covers building the fused power supply, wiring the lamp switching circuit, configuring the two Betaflight radio switches, and bench-testing the system before flight. This design prioritizes safety by preventing accidental light activation during critical flight phases.

Wiring diagram

Wiring diagram for Drone Light Safety Controller (copy)

Gather all the parts

QtyComponent
1

SpeedyBee F405 V5 flight controller

The drone flight controller that provides the selected radio-switch state to the XIAO over its unused UART2 pads.

1

6S LiPo or LiHV flight battery

The drone battery that supplies the lamp converter; it reaches about 25.2 V when a normal 6S LiPo is fully charged and up to 26.1 V when a 6S LiHV pack is fully charged.

1

2 A inline blade fuse and holder

A replaceable fuse that disconnects the lamp branch if a fault makes it draw too much current.

1

LM2596HVS adjustable buck converter

A 60 V-input adjustable converter set to a measured 9.0 V output for a lamp load up to 1 A.

1

9 V drone light (maximum 1 A for this build)

The external 9 V light that turns on after the safety delay when either permitted trigger requests it.

1

IRLZ44N Logic-Level N-Channel MOSFET

Logic-level N-channel MOSFET commonly used as a low-side switch for DC loads from 3.3V or 5V microcontroller GPIO. Use a gate resistor, a gate pulldown, and a flyback diode for inductive loads.

1

Micro Switch - Premium Zippy 3-Terminal

Higher-quality 3-terminal bump-actuator microswitch for arcade controls and light mechatronics, rated 30 VDC / 250 mA max. Common, normally-open, and normally-closed contacts.

1

Removable safety arming plug

A two-pin removable link that holds the controller safely disarmed while fitted; removing it begins the 30-second countdown.

1

Resistor

100 Ω

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

100 kΩ

Through-hole resistor (current-limiting in series with an LED)

Assemble it in 6 steps

1. Keep the flight controller ports free

Leave the receiver on UART6, GPS on UART4, ESC telemetry on UART5, and UART1 exactly as they are. On the flight controller, use only the unused UART2 pads: solder FC T2 to XIAO D7/RX, FC R2 to XIAO D6/TX, FC 5V to XIAO 5V/VBUS, and FC GND to XIAO GND. Each wire’s job is: T2 → D7/RX (data), R2 → D6/TX (data), 5V → 5V/VBUS (power), and GND → GND (ground).

  • Do not connect any XIAO pin directly to the 6S battery — a fully charged 6S LiHV battery can reach 26.1 V and will damage the XIAO.
  • Do not use a motor pad for this project; motor pads are needed to keep the drone flying safely.

2. Build the 9 V fused lamp supply

Make the lamp power branch away from the battery connector: battery positive goes to the fuse holder IN, fuse holder OUT goes to buck converter IN+, and battery negative goes to buck converter IN-. Each wire’s job is: BAT+ → fuse IN (fault protection), fuse OUT → buck IN+ (high-voltage power), and BAT− → buck IN− (ground). Before connecting the light, use a meter and turn the converter adjustment until OUT+ to OUT− reads 9.0 V.

  • Use an LM2596HVS or another converter explicitly rated for at least 30 V input. Ordinary LM2596 boards are often rated only to 40 V in theory and may not tolerate the electrical spikes on a drone.
  • Never adjust or test the converter with the propellers fitted.

3. Wire the lamp switching transistor

Connect buck OUT+ to the lamp positive lead. Connect the lamp negative lead to the IRLZ44N DRAIN pin. Connect IRLZ44N SOURCE to buck OUT− and the shared ground. Solder the 100 Ω resistor between XIAO D0 and IRLZ44N GATE, then the 100 kΩ resistor from IRLZ44N GATE to ground. Each wire’s job is: OUT+ → LAMP+ (9 V power), LAMP− → DRAIN (switched return), SOURCE → OUT− (ground return), D0 → 100 Ω → GATE (on/off signal), and GATE → 100 kΩ → GND (keeps light off while starting).

  • Check the exact pin order in the data sheet for the MOSFET you buy; a reversed MOSFET can short the lamp circuit or leave the lamp permanently on.
  • This proof-of-concept fuse, MOSFET, and converter arrangement is limited to a 9 V lamp that draws no more than 1 A. Do not fit a larger lamp until its watts or amps are known and the power parts are redesigned.

4. Fit the bumper and removable safety plug

Use the limit switch COM and NO terminals, not NC. Connect COM to XIAO ground and NO to XIAO D2. Make the safety plug as a removable two-pin shorting link: one contact goes to XIAO ground and the other to XIAO D3. Each wire’s job is: switch COM → GND (ground), switch NO → D2 (collision signal), safety plug GND → GND (ground), and safety plug SAFE → D3 (arming signal). With the plug fitted, the light is forced off. Removing it begins the 30-second wait.

  • Mount the limit switch where a frontal impact reliably presses it but ordinary vibration does not.
  • Use a keyed connector or a conspicuous pull-tab for the safety plug so it cannot be confused with a battery lead.
  • Keep the safety plug reachable from outside the frame. You must be able to make the lamp safe without plugging in a computer or touching a live battery lead.

5. Set the two radio switches in Betaflight

In Betaflight, enable MSP on UART2 at 115200 baud and do not enable Serial RX on UART2. Arrange your radio so AUX1 is the master permission switch and AUX2 is the intentional light switch. The XIAO reads those two values from the flight controller: it will ignore both switches for 30 seconds after the safety plug is removed, then it permits the lamp only when AUX1 is on. AUX2 turns on the light intentionally; the front switch also turns it on after a collision only while AUX1 is on.

  • Confirm AUX1 and AUX2 move in Betaflight’s Receiver tab before flying. If either channel is assigned differently, the programmed channel numbers must be changed before flight.

6. Bench-test before fitting propellers

With propellers removed, insert the safety plug and power the flight controller and lamp supply. The lamp must stay off. Remove the plug and wait at least 30 seconds: the lamp must still stay off until AUX1 is on. With AUX1 on, test AUX2 and then press the limit switch; each should turn on the lamp. Turn AUX1 off or reinsert the safety plug; the lamp must turn off immediately.

  • Do not test this system with propellers installed. An unexpected arming event or wiring fault can cause serious injury.
  • Stop immediately if the converter, fuse holder, MOSFET, or wires become hot during the test; heat means the lamp current is higher than this proof-of-concept design allows.

Review all connections

1. Connections between "speedybee_f405_v5" and "Raspberry Pi Pico"

Functionspeedybee_f405_v5Raspberry Pi Pico
power5V5V
groundGNDGND
uartT2GPIO 1
uartR2GPIO 0

2. Connections between "lipo_6s" and "Raspberry Pi Pico"

Functionlipo_6sRaspberry Pi Pico
powerBAT+ → 2 A inline blade fuse and holder INEXT
groundBAT-GND

3. Connections between "fuse_2a" and "Raspberry Pi Pico"

Functionfuse_2aRaspberry Pi Pico
powerOUT → LM2596HVS adjustable buck converter IN+EXT

4. Connections between "buck_6s_to_9v" and "Raspberry Pi Pico"

Functionbuck_6s_to_9vRaspberry Pi Pico
groundIN-GND
powerOUT+ → 9 V drone light (maximum 1 A for this build) LAMP+EXT
groundOUT- → IRLZ44N Logic-Level N-Channel MOSFET SOURCEEXT

5. Connections between "lamp_9v" and "Raspberry Pi Pico"

Functionlamp_9vRaspberry Pi Pico
groundLAMP- → IRLZ44N Logic-Level N-Channel MOSFET DRAINEXT

6. Connections between "limit_switch" and "Raspberry Pi Pico"

Functionlimit_switchRaspberry Pi Pico
groundCOMGND
digitalNOGPIO 28

7. Connections between "safety_plug" and "Raspberry Pi Pico"

Functionsafety_plugRaspberry Pi Pico
groundGNDGND
digitalSAFEGPIO 29

8. Connections between "gate_resistor" and "Raspberry Pi Pico"

Functiongate_resistorRaspberry Pi Pico
digitalP1GPIO 26
digitalP2 → IRLZ44N Logic-Level N-Channel MOSFET GATEEXT

9. Connections between "gate_pulldown" and "Raspberry Pi Pico"

Functiongate_pulldownRaspberry Pi Pico
digitalP1 → IRLZ44N Logic-Level N-Channel MOSFET GATEEXT
groundP2GND

Deploy the firmware

#include <Arduino.h>

// XIAO D6/RP2040 GPIO0 is TX and D7/RP2040 GPIO1 is RX.

// Forward declarations
uint8_t mspChecksum(uint8_t size, uint8_t command, const uint8_t *payload);
void requestRcChannels();
void readMsp();
bool channelOn(uint8_t channel);

constexpr uint8_t FC_TX_PIN = 0;
constexpr uint8_t FC_RX_PIN = 1;
constexpr uint8_t LAMP_GATE_PIN = 26;  // D0, through 100 ohm to MOSFET gate
constexpr uint8_t LIMIT_PIN = 28;      // D2, limit switch to ground when hit
constexpr uint8_t SAFETY_PIN = 29;     // D3, safety plug to ground when fitted

constexpr uint32_t MSP_BAUD = 115200;
constexpr uint32_t ARM_DELAY_MS = 30000;
constexpr uint32_t MSP_POLL_MS = 100;
constexpr uint32_t RC_TIMEOUT_MS = 500;

// Betaflight channel indexes: roll, pitch, throttle, yaw, AUX1, AUX2...
constexpr uint8_t PERMIT_CHANNEL = 4;       // AUX1: master permission
constexpr uint8_t REMOTE_LIGHT_CHANNEL = 5; // AUX2: intentional light request
constexpr uint16_t SWITCH_ON_US = 1600;

uint16_t rcChannels[16] = {0};
uint8_t rcCount = 0;
uint32_t lastRcMs = 0;
uint32_t lastPollMs = 0;
bool lastSafetyInserted = true;
uint32_t safetyRemovedMs = 0;

uint8_t mspChecksum(uint8_t size, uint8_t command, const uint8_t *payload) {
  uint8_t checksum = size ^ command;
  for (uint8_t i = 0; i < size; ++i) checksum ^= payload[i];
  return checksum;
}

void requestRcChannels() {
  const uint8_t size = 0;
  const uint8_t command = 105; // MSP_RC
  Serial1.write('$');
  Serial1.write('M');
  Serial1.write('<');
  Serial1.write(size);
  Serial1.write(command);
  Serial1.write(mspChecksum(size, command, nullptr));
}

void readMsp() {
  enum ParseState { WAIT_DOLLAR, WAIT_M, WAIT_DIR, WAIT_SIZE, WAIT_COMMAND, WAIT_PAYLOAD, WAIT_CHECKSUM };
  static ParseState state = WAIT_DOLLAR;
  static uint8_t payloadSize = 0;
  static uint8_t command = 0;
  static uint8_t payload[64];
  static uint8_t payloadIndex = 0;
  static uint8_t checksum = 0;

  while (Serial1.available()) {
    const uint8_t byteIn = Serial1.read();
    switch (state) {
      case WAIT_DOLLAR:
        if (byteIn == '$') state = WAIT_M;
        break;
      case WAIT_M:
        state = (byteIn == 'M') ? WAIT_DIR : WAIT_DOLLAR;
        break;
      case WAIT_DIR:
        state = (byteIn == '>') ? WAIT_SIZE : WAIT_DOLLAR;
        break;
      case WAIT_SIZE:
        payloadSize = byteIn;
        if (payloadSize > sizeof(payload)) { state = WAIT_DOLLAR; break; }
        checksum = byteIn;
        payloadIndex = 0;
        state = WAIT_COMMAND;
        break;
      case WAIT_COMMAND:
        command = byteIn;
        checksum ^= byteIn;
        state = (payloadSize == 0) ? WAIT_CHECKSUM : WAIT_PAYLOAD;
        break;
      case WAIT_PAYLOAD:
        payload[payloadIndex++] = byteIn;
        checksum ^= byteIn;
        if (payloadIndex >= payloadSize) state = WAIT_CHECKSUM;
        break;
      case WAIT_CHECKSUM:
        if (checksum == byteIn && command == 105 && payloadSize >= 16 && (payloadSize % 2 == 0)) {
          rcCount = min<uint8_t>(payloadSize / 2, 16);
          for (uint8_t i = 0; i < rcCount; ++i) {
            rcChannels[i] = uint16_t(payload[i * 2]) | (uint16_t(payload[i * 2 + 1]) << 8);
          }
          lastRcMs = millis();
        }
        state = WAIT_DOLLAR;
        break;
    }
  }
}

bool channelOn(uint8_t channel) {
  return channel < rcCount && rcChannels[channel] >= SWITCH_ON_US;
}

void setup() {
  pinMode(LAMP_GATE_PIN, OUTPUT);
  digitalWrite(LAMP_GATE_PIN, LOW); // Fail off before any serial traffic is accepted.
  pinMode(LIMIT_PIN, INPUT_PULLUP);
  pinMode(SAFETY_PIN, INPUT_PULLUP);
  Serial1.setTX(FC_TX_PIN);
  Serial1.setRX(FC_RX_PIN);
  Serial1.begin(MSP_BAUD);
}

void loop() {
  const uint32_t now = millis();
  readMsp();

  if (now - lastPollMs >= MSP_POLL_MS) {
    lastPollMs = now;
    requestRcChannels();
  }

  const bool safetyInserted = digitalRead(SAFETY_PIN) == LOW;
  if (safetyInserted) {
    safetyRemovedMs = 0;
  } else if (lastSafetyInserted) {
    safetyRemovedMs = now;
  }
  lastSafetyInserted = safetyInserted;

  const bool delayComplete = !safetyInserted && safetyRemovedMs != 0 && (now - safetyRemovedMs >= ARM_DELAY_MS);
  const bool rcFresh = (now - lastRcMs) <= RC_TIMEOUT_MS;
  const bool permitted = rcFresh && channelOn(PERMIT_CHANNEL);
  const bool remoteRequested = rcFresh && channelOn(REMOTE_LIGHT_CHANNEL);
  const bool collisionHit = digitalRead(LIMIT_PIN) == LOW;

  // The lamp can only run after the removed-plug delay, with fresh FC data,
  // and with AUX1 permission. AUX2 requests light manually; the bumper requests it on impact.
  const bool lampOn = delayComplete && permitted && (remoteRequested || collisionHit);
  digitalWrite(LAMP_GATE_PIN, lampOn ? HIGH : LOW);
}

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