Предисловие

Данная статья это рассказ об отдельной части моего проекта по созданию системы доставки дронами. Система совсем в ранней стадии разработки. Но в принципе эта часть готова и если кому интересно может пользоваться в своих проектов. Если будет заинтересованность могу продолжить публиковать чертежи, схемы и программный код.

Введение

С ростом использования дронов в логистике и техническом обслуживании растет потребность в универсальных модулях для перевозки грузов и сменных аккумуляторов. Для решения этой задачи был разработан интеллектуальный бокс, управляемый удалённо через контроллер на базе ESP8266, который способен:

Общая схема. Флашки фиксации будут с обоих сторон и сервопривод будет посередине. Так же не показанны концевики.
Общая схема. Флашки фиксации будут с обоих сторон и сервопривод будет посередине. Так же не показанны концевики.
  • ? открывать и закрывать отсек для груза;

  • ? открывать и закрывать отсек для сменного аккумулятора;

  • ? передавать телеметрию о состоянии устройства;

  • ✅ принимать команды через WebSocket-сервер.

Аппаратная часть

Устройство построено на контроллере ESP8266 (например, ESP-01 или NodeMCU) и включает в себя:

  • 2 сервопривода:
    Servo1 — управление крышкой грузового отсека.
    Servo2 — управление крышкой отсека аккумулятора.

  • 2 концевых переключателя:
    Button1 — наличие груза в боксе.
    Button2 — наличие аккумулятора.

  • Светодиод: используется для индикации состояния соединения.

Сеть и связь

Контроллер подключается к заданной Wi-Fi-сети и устанавливает соединение с WebSocket-сервером. Это позволяет оператору дрона или логистической системе управлять боксом в режиме реального времени.

Состояния подключения визуализируются через встроенный светодиод:

  • ✴️ Быстро мигает — нет подключения к Wi-Fi.

  • ⏳ Медленно мигает — Wi-Fi подключен, WebSocket нет.

  • ✅ Постоянно горит — соединение WebSocket установлено.

Принцип работы

После включения устройство:

  1. Подключается к Wi-Fi.

  2. Периодически пытается установить WebSocket-соединение.

  3. После подключения отправляет данные об устройстве (идентификатор и пароль).

  4. Ожидает команды от сервера и реагирует на них.

Обработка команд

Поддерживаются следующие команды:

  • "servo1" — установить угол открытия грузового отсека.

  • "servo2" — установить угол открытия аккумуляторного отсека.

  • "reboot" — перезагрузка устройства.

Телеметрия

Устройство автоматически отправляет телеметрию по команде "PING":

  • Статус "работает",

  • Углы обоих сервоприводов,

  • Состояние обоих концевиков ("pressed" или "released").

Кнопки (концевики)

Концевики подключены с использованием программного класса Button. Система отслеживает их состояния и отправляет обновления при нажатии или по таймеру (раз в секунду), если WebSocket-соединение активно.

Программная архитектура

Ключевые технологии:

  • ESP8266WiFi — подключение к сети.

  • WebSocketsClient — двусторонняя связь с сервером.

  • ArduinoJson — обработка JSON-команд и телеметрии.

  • Servo — управление сервоприводами.

  • Кастомный класс Button — простая логика обработки состояний кнопок.

Применение

Такая система может использоваться в следующих случаях:

  • Доставка малых грузов: автоматически открыть отсек для выгрузки.

  • Обслуживание дронов: быстрое извлечение и замена аккумуляторов.

  • Мониторинг состояния: оператор всегда знает, находится ли груз или аккумулятор в боксе.

Так выглядит подключение на практике

Куда что подключать можно понять из прошивки.

Код для прошивки.

#include <ESP8266WiFi.h>
#include <WebSocketsClient.h>
#include <ArduinoJson.h>
#include <Servo.h>
#include "Button.h" // Подключаем наш класс Button

const char* ssid = "----";
const char* password = "-----";

WebSocketsClient webSocket;

const char* deviceName = "esp01";
const char* devicePassword = "1234";

const int LED_PIN = LED_BUILTIN;
const int BUTTON1_PIN = 14; // GPIO14 (D5 на NodeMCU)
const int BUTTON2_PIN = 4; // GPIO4 (D2 на NodeMCU)
const int SERVO1_PIN = 12; // Пин первого сервопривода (D6)
const int SERVO2_PIN = 13; // Пин второго сервопривода (D7)
Button button1(BUTTON1_PIN);
Button button2(BUTTON2_PIN);

Servo servo1;
Servo servo2;

unsigned long previousMillis = 0;
unsigned long lastConnectionAttempt = 0;
const long connectionInterval = 10000; // 10 секунд между попытками подключения

int ledState = LOW;
int connectionState = 0; // 0 - нет подключения, 1 - WiFi подключен, 2 - WebSocket подключен
int servo1Pos = 90; // Текущее положение сервопривода 1 (0-180)
int servo2Pos = 90; // Текущее положение сервопривода 2 (0-180)

void updateLed() {
unsigned long currentMillis = millis();
unsigned long interval;

switch(connectionState) {
case 0: // Нет подключения - быстрое мигание (500ms)
interval = 500;
break;
case 1: // WiFi подключен - медленное мигание (1000ms)
interval = 1000;
break;
case 2: // WebSocket подключен - постоянно включен
digitalWrite(LED_PIN, LOW);
return;
}

if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
ledState = (ledState == LOW) ? HIGH : LOW;
digitalWrite(LED_PIN, ledState);
}
}

void handleCommand(const char* cmd, const char* value) {
Serial.printf("Received command: %s, value: %s\n", cmd, value);

if (strcmp(cmd, "servo1") == 0) {
int pos = atoi(value);
if (pos >= 0 && pos <= 360) {
servo1Pos = pos;
servo1.write(servo1Pos);
Serial.printf("Servo1 set to %d\n", servo1Pos);
}
}
else if (strcmp(cmd, "servo2") == 0) {
int pos = atoi(value);
if (pos >= 0 && pos <= 360) {
servo2Pos = pos;
servo2.write(servo2Pos);
Serial.printf("Servo2 set to %d\n", servo2Pos);
}
}
else if (strcmp(cmd, "reboot") == 0) {
Serial.println("Rebooting...");
ESP.restart();
}
}

void webSocketEvent(WStype_t type, uint8_t* payload, size_t length) {
switch (type) {
case WStype_CONNECTED:
Serial.println("[WS] Connected");
connectionState = 2; // WebSocket подключен
updateLed();

// Отправляем информацию об устройстве при подключении
{
StaticJsonDocument<256> doc;
doc["type"] = "auth";
doc["name"] = deviceName;
doc["password"] = devicePassword;
String json;
serializeJson(doc, json);
webSocket.sendTXT(json);
}
break;

case WStype_DISCONNECTED:
Serial.println("[WS] Disconnected");
connectionState = (WiFi.status() == WL_CONNECTED) ? 1 : 0;
break;

case WStype_TEXT:
Serial.printf("[WS] Received: %s\n", payload);
//Serial.printf("-%s-\n", payload);

// Обработка PING
//if (strstr((char*)payload, "\"command\":\"PING\"") != NULL) {
if (strstr((char*)payload, "\"command\": \"PING\"") != NULL) {

Serial.println("Processing PING command");
StaticJsonDocument<512> doc;
doc["type"] = "telemetry";
doc["status"] = "работает";
doc["servo1"] = servo1Pos;
doc["servo2"] = servo2Pos;
doc["button1"] = button1.isPressed() ? "pressed" : "released";
doc["button2"] = button2.isPressed() ? "pressed" : "released";
String json;
serializeJson(doc, json);
webSocket.sendTXT(json);
Serial.println("Телеметрия отправлена");
}
// Обработка команд
else {
StaticJsonDocument<256> doc;
DeserializationError error = deserializeJson(doc, payload);

Serial.print("Deserialization error: ");
Serial.println(error.c_str()); // Выведет "Ok" если ошибок нет

// Выводим сырой payload для проверки
Serial.print("Raw payload: ");
Serial.println((char*)payload);

// Выводим содержимое doc в Serial
Serial.println("Parsed JSON content:");
serializeJsonPretty(doc, Serial); // Красивый вывод с отступами
Serial.println("\n---");

if (!error && doc.containsKey("command") && doc.containsKey("value")) {
const char* cmd = doc["command"];
const char* value = doc["value"];
handleCommand(cmd, value);

// Отправляем подтверждение
doc["type"] = "command_ack";
doc["status"] = "ok";
String json;
serializeJson(doc, json);
webSocket.sendTXT(json);
Serial.println("");
}
}
break;
}
}

void setup() {
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, HIGH);

pinMode(BUTTON1_PIN, INPUT_PULLUP);
pinMode(BUTTON2_PIN, INPUT_PULLUP);

servo1.attach(SERVO1_PIN);
servo2.attach(SERVO2_PIN);
servo1.write(servo1Pos);
servo2.write(servo2Pos);

Serial.begin(115200);
WiFi.begin(ssid, password);

// Начальное состояние - нет подключения
connectionState = 0;
}

void handleButtons() {
static unsigned long lastSend = 0;

// Проверяем нажатия
if (button1.click() button2.click() millis() - lastSend > 1000) {
if (webSocket.isConnected()) {
sendButtonStates();
lastSend = millis();
}
}
}
void sendButtonStates() {
StaticJsonDocument<200> doc;
doc["type"] = "button_state";
doc["button1"] = button1.isPressed() ? "pressed" : "released";
doc["button2"] = button2.isPressed() ? "pressed" : "released";

String json;
serializeJson(doc, json);
webSocket.sendTXT(json);
}

void loop() {
unsigned long currentMillis = millis();

// Обновляем состояние подключения WiFi
if (WiFi.status() != WL_CONNECTED) {
connectionState = 0;
if (currentMillis - lastConnectionAttempt >= connectionInterval) {
Serial.println("Connecting to WiFi...");
WiFi.begin(ssid, password);
lastConnectionAttempt = currentMillis;
}
}
else if (connectionState == 0) {
connectionState = 1;
Serial.println("\nWiFi connected");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}

updateLed();
handleButtons();
webSocket.loop();

// Если WiFi подключен, но WebSocket еще нет - пытаемся подключиться
if (WiFi.status() == WL_CONNECTED && !webSocket.isConnected() &&
currentMillis - lastConnectionAttempt >= connectionInterval) {
connectionState = 1;
Serial.println("Connecting to WebSocket server...");
webSocket.begin("192.168.1.10", 8765, "/");
webSocket.onEvent(webSocketEvent);
webSocket.setReconnectInterval(5000);
lastConnectionAttempt = currentMillis;
}

delay(10); // Небольшая задержка для стабильности

Библиотека button.h

#ifndef Button_h
#define Button_h
#include
class Button {
public:
Button(byte pin) : pin(pin), lastState(HIGH), lastDebounceTime(0), flag(false) {
pinMode(_pin, INPUT_PULLUP);
}
bool click() {
bool currentState = digitalRead(_pin);
bool result = false;
if (currentState != lastState) {
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) > DEBOUNCEDELAY) {
if (!currentState && !_flag) {
flag = true;
result = true;
}
if (currentState &&
flag) {
flag = false;
}
}
lastState = currentState;
return result;
}
bool isPressed() {
return (digitalRead(_pin) == LOW);
}
private:
static const uint32_t DEBOUNCE_DELAY = 50;
byte pin;
bool
lastState;
uint32_t lastDebounceTime;
bool
flag;
};

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

Заключение

Разработанный программный модуль представляет собой надёжную и гибкую платформу для удаленного управления дроновым оборудованием. Благодаря WebSocket-соединению обеспечивается высокая скорость отклика, а наличие телеметрии и визуальной индикации делает эксплуатацию безопасной и предсказуемой. Данный подход легко масштабируется и может быть адаптирован под различные типы дронов и задач.

Комментарии (8)


  1. itshnick88
    30.06.2025 06:41

    А чего код просто текстом вывалили, а не оформили? И кучи запятых по тексту не хватает... =(


    1. stilrambler Автор
      30.06.2025 06:41

      Поэтому уровень понимания я выбрал сложный. Запятые и оформление для слабоков.


  1. tas
    30.06.2025 06:41

    Разработанный программный модуль представляет собой надёжную и гибкую платформу для удаленного управления дроновым оборудованием.

    Надежность нужно подтверждать реальными тестами т.к. сейчас вы половины рисков просто не видите.

    А гибкость - понятие растяжимое и из статьи непонятное. Можно взять конкретный кейс доставки еды в парке (https://dtf.ru/food/3677852-dostavka-edy-dronami-v-kitae?ysclid=mciqh1c7a418379175) и еще парочку и на них показать - тогда станет понятнее.


    1. stilrambler Автор
      30.06.2025 06:41

      Система в ранней разработке. Представленная часть просто небольшой её кусок. Причем я даже не понимаю доведу я его хоть да какого то реального проекта. Но в любом случае я планирую ограничится максимум прототипом системы. И главная особенность моей системы это станция для дронов. Об этом я и хочу в последствии написать. Но пока там много чего не готово.


  1. randomsimplenumber
    30.06.2025 06:41

    Уровень реально сложный ;) Эту штуку дрон будет привозить в назначенное место и там оставлять? Или как оно работает?


    1. stilrambler Автор
      30.06.2025 06:41

      Эта штука крепиться к любому дрону для перевозки грузов. И с помощью дополнительных устройств и под управление сервера можно совершать полностью автоматическую погрузку разгрузку груза и замену батареи. Ещё в комплекте нужны посадочные опоры и направляющие для фиксации и правильной фиксации дрона на столе станции приёма дронов. Но там надо много чего показывать и рассказывать что в одну статью все не влезет. Если будет интерес продолжу.


  1. AndyLem
    30.06.2025 06:41

    А дрон знает что к нему по вебсокету стучатся? Работать будет только с одной моделью дронов, как я понимаю?


    1. stilrambler Автор
      30.06.2025 06:41

      Дрон в принципе может с боксом не общается. Да и скорее всего не будет. С боксом общается сервер системы который и занимается автоматической разгрузкой - загрузкой бокса. То есть у сервера есть подключённые устройства с помощью которых он осуществляет доставку. Дрон служить только в качестве тоскателя бокса по точкам. Сначала с помощью оператора. В последствии в автоматическом режиме когда создадут такой дрон который на это будет способен.