Привет, Хабр!
Сегодня я поделюсь опытом работы с протоколом UDP вместе с микроконтроллером ESP8266, где я управлял светодиодом, а также получал температуру с датчика DHT11. Всё управление будет происходить из Android-приложения, написание логики которого также будет рассмотрено.

Почему UDP?

Немного теории о природе протокола UDP.UDP (User Datagram Protocol) — это один из основных протоколов транспортного уровня в стеке TCP/IP. Он используется для отправки и получения данных между устройствами в сети без установления соединения, что позволяет обмениваться данными с большей скоростью, однако при этом целостность данных может быть повреждена, но для наших целей он будет оптимальным, поскольку наши пакеты не будут содержать критичных данных, но их обмен будет проходить быстрее.

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

Схема устройства

Основным управляющим устройством выступает микроконтроллер ESP8266, снабжённый модулем Wi-Fi, который будет работать в роли сервера. К микроконтроллеру также подключён светодиод с резистором на 20 кОм (у меня не было под рукой резистора меньшего номинала, поэтому светодиод может светиться тускло), а также температурный сенсор DHT11. Общая схема представлена ниже.

Схема подключения
Схема подключения

Настраиваем ESP8266

Чтобы настроить работу по UDP, воспользуемся библиотекой WiFiUdp.h, которая добавит в наш проект поддержку сетевого протокола. Также потребуется библиотека ESP8266WiFi.h — с её помощью настроим работу микроконтроллера в режиме точки доступа. Ну и, наконец, добавим библиотеку DHT.h для работы с температурным сенсором.

#include <DHT.h>

#include <ESP8266WiFi.h>

#include <WiFiUdp.h>

Для начала подключим все библиотеки и несколько переменных, а также экземпляры нескольких классов: DHT и WiFiUDP.

#define DHT_PIN         13

const char* ssid = "ESP8266";

const int port = 4210;

char incomingBytes[20];

DHT dht(DHT_PIN, DHT11);

WiFiUDP udp;

Далее, внутри функции setup() произведём настройку всей периферии.

void setup() {

  WiFi.mode(WIFI_AP);

  WiFi.softAP(ssid);

  dht.begin();

  udp.begin(port);

  Serial.begin(9600);

  pinMode(15, OUTPUT);

}

В loop() производится отслеживание входящих UDP-пакетов. За парсинг полученных данных отвечает функция udpHandler, которой передаётся принятое сообщение.

void loop() {

  int packetSize = udp.parsePacket();

  if (packetSize){

    int len = udp.read(incomingBytes, 20);

    incomingBytes[len] = '\0';

    if (len){

      char* str = (char*)incomingBytes;

      udpHandler(str);

    }

  }

}


Если поступает сообщение "LedOn" — светодиод загорается, если "LedOff" — соответственно, выключается. Функция strcmp сравнивает две строки и возвращает 0, если они совпадают — поэтому используется именно такая конструкция.
Если в UDP-пакете приходит "Temperature" — считывается текущая температура с сенсора и отправляется обратно по IP и порту.

void udpHandler(char* message){

  Serial.println(message);

  if (!strcmp(message,"LedOn")){digitalWrite(15, HIGH);}

  else if (!strcmp(message,"LedOff")){digitalWrite(15, LOW);}

  else if (!strcmp(message, "Temperature")){

    float temp = dht.readTemperature();

    char replyPacket[10];

    snprintf(replyPacket, sizeof(replyPacket), "%.2f", temp);

    udp.beginPacket(udp.remoteIP(), udp.remotePort());

    udp.write(replyPacket, strlen(replyPacket));

    udp.endPacket();

  }

На этом работа с ESP8266 завершена — переходим к Android-приложению на Java.

#include <DHT.h>

#include <ESP8266WiFi.h>

#include <WiFiUdp.h>

#define DHT_PIN         13

const char* ssid = "ESP8266";

const int port = 4210;

char incomingBytes[20];

DHT dht(DHT_PIN, DHT11);

WiFiUDP udp;

void setup() {

  WiFi.mode(WIFI_AP);

  WiFi.softAP(ssid);

  dht.begin();

  udp.begin(port);

  pinMode(15, OUTPUT);

}

void loop() {

  int packetSize = udp.parsePacket();

  if (packetSize){

    int len = udp.read(incomingBytes, 20);

    incomingBytes[len] = '\0';

    if (len){

      char* str = (char*)incomingBytes;

      udpHandler(str);

    }

  }

}

void udpHandler(char* message){

  if (!strcmp(message,"LedOn")){digitalWrite(15, HIGH);}

  else if (!strcmp(message,"LedOff")){digitalWrite(15, LOW);}

  else if (!strcmp(message, "Temperature")){

    float temp = dht.readTemperature();

    char replyPacket[10];

    snprintf(replyPacket, sizeof(replyPacket), "%.2f", temp);

    udp.beginPacket(udp.remoteIP(), udp.remotePort());

    udp.write(replyPacket, strlen(replyPacket));

    udp.endPacket();

  }

}

Пишем "приложение"

Для управления светодиодом и получения температуры создадим три кнопки Button, а также один TextView, куда будет выводиться полученная температура. Весь код XML-файла приведён ниже.

<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:id="@+id/Temp_btn" android:layout_width="181dp" android:layout_height="51dp" android:layout_marginTop="50dp" android:text="Temperature" android:textColor="@color/black" android:textStyle="bold" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/led_off_btn" /> <Button android:id="@+id/led_on_btn" android:layout_width="181dp" android:layout_height="51dp" android:layout_marginTop="50dp" android:text="Led On" android:textColor="@color/black" android:textStyle="bold" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.498" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/temp_view" /> <Button android:id="@+id/led_off_btn" android:layout_width="181dp" android:layout_height="51dp" android:layout_marginTop="50dp" android:text="Led Off" android:textColor="@color/black" android:textStyle="bold" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.498" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/led_on_btn" /> <TextView android:id="@+id/temp_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="50dp" android:textColor="@color/black" android:textSize="34sp" android:textStyle="bold" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>

Общий вид UI
Общий вид UI

Также, для работы с сетью, в приложение нужно добавить разрешение на доступ к интернету.

<uses-permission android:name="android.permission.INTERNET" />

Далее напишем логику приложения в классе MainActivity. Инициализируем несколько переменных для работы с UI соответствующих типов. Присваиваем им значения по их ID из XML.
Один момент: добавим несколько строк, чтобы задать фоновый цвет приложения белым (на некоторых устройствах по умолчанию он может быть другим), а также уберём фиолетовую полосу сверху, которая добавляется при создании новой активности.

Создадим отдельную функцию udpPost, принимающую в качестве аргумента сообщение для отправки по UDP. Создаём новый поток, поскольку все сетевые операции должны выполняться во вторичном потоке, чтобы не повесить интерфейс.
Для создания UDP-соединения создаём экземпляр класса DatagramSocket. Затем создаём два буфера: для отправки и получения сообщения. Далее создаём два экземпляра DatagramPacket — для формирования UDP-пакетов. В первый передаём наше сообщение, IP-адрес микроконтроллера (он статичен, так как микроконтроллер работает как точка доступа) и номер порта.

Далее отправляем сообщение и получаем ответ. После этого переходим в UI-поток и отображаем полученное значение в TextView — но только если это сообщение о температуре. Если мы управляем светодиодом — ответ не приходит, и выводить ничего не нужно.

void udpPost(String message){

new Thread(new Runnable() {

@Override

public void run() {

try {

DatagramSocket datagramSocket = new DatagramSocket();

byte sendBuffer[] = message.getBytes();

byte[] receiveBuffer = new byte[1024];

DatagramPacket packet = new DatagramPacket(sendBuffer, sendBuffer.length, InetAddress.getByName(ip), port);

DatagramPacket receivePacket = new DatagramPacket(receiveBuffer, receiveBuffer.length);

datagramSocket.send(packet); datagramSocket.receive(receivePacket);

String message = new String(receivePacket.getData(), 0, receivePacket.getLength());

runOnUiThread(new Runnable() {

@Override

public void run() {

if (message != ""){

tempView.setText(message + "°C");

}

}

});

} catch (SocketException | UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }).start(); }

Последним шагом возвращаемся в функцию onCreate(), где каждой кнопке добавляем обработчик нажатия. Внутри него вызываем созданную нами функцию, передавая соответствующее сообщение.

package com.example.udpclient;import android.annotation.SuppressLint;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.TextView;import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;public class MainActivity extends AppCompatActivity {
ConstraintLayout constraintLayout;
Button ledOn, ledOff, temp;
TextView tempView;
int port = 4210;
String ip = "192.168.4.1";
@SuppressLint("MissingInflatedId")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
);
constraintLayout = findViewById(R.id.main);
constraintLayout.setBackgroundColor(Color.WHITE);
ledOn = findViewById(R.id.led_on_btn);
ledOff = findViewById(R.id.led_off_btn);
temp = findViewById(R.id.Temp_btn);
tempView = findViewById(R.id.temp_view); ledOn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
udpPost("LedOn");
}
});
ledOff.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
udpPost("LedOff");
}
});
temp.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
udpPost("Temperature");
}
});
}
void udpPost(String message){
new Thread(new Runnable() {
@Override
public void run() {
try {
DatagramSocket datagramSocket = new DatagramSocket();
byte sendBuffer[] = message.getBytes();
byte[] receiveBuffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(sendBuffer, sendBuffer.length, InetAddress.getByName(ip), port);
DatagramPacket receivePacket = new DatagramPacket(receiveBuffer, receiveBuffer.length);
datagramSocket.send(packet);
datagramSocket.receive(receivePacket);
String message = new String(receivePacket.getData(), 0, receivePacket.getLength());
runOnUiThread(new Runnable() {
@Override
public void run() {
if (message != ""){
tempView.setText(message + "°C");
}
}
}); } catch (SocketException | UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}

Смотрим результат

Настала пора взглянуть на результат. Устанавливаем приложение на Android-устройство и загружаем прошивку в ESP8266. Подключаемся со смартфона к точке доступа микроконтроллера. Открываем приложение, где управляем светодиодом и получаем данные о температуре — таким образом можно отслеживать температурные показатели (или любые другие значения) прямо через собственное приложение по wifi сети.

Включение светодиода
Включение светодиода
Выключение светодиода
Выключение светодиода
Получение температуры
Получение температуры

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


  1. aladkoi
    08.07.2025 04:36

    Esp серия обычно программируется через среду esp-idf в vscode. Еще можно использовать спец. графическую среду без "ручного" такого написания кода. Да использование устаревшего 8266 не самое удачное решение. Есть мини платы esp32c6 , например, с поддержкой зигби. Это более интересное решение.


    1. randomsimplenumber
      08.07.2025 04:36

      Ну это же демка.

      Но тема необычно высоких скоростей передачи по UDP не раскрыта. Где бенчмарки? Где сравнение с TCP? Может, в этой конфигурации (esp8266 + Android) и нет никакой разницы?


    1. Danchkin_Sab Автор
      08.07.2025 04:36

      Возможности esp32c6 я ещё не изучал, хотя идея хорошая, под рукой оказалась только esp8266 с которой уже имел опыт работы


      1. Danchkin_Sab Автор
        08.07.2025 04:36

        Это идея для следующий статьи, поскольку опыт работы с tcp и даже http я имею - сделаю сравнение


  1. dmitryrf
    08.07.2025 04:36

    Хорошее демо!

    Мне в этой схеме вот что кажется неудобным: IP адрес жестко прошит и никак не настраивается. Плюс нужно отключаться от сети с доступом в интернет чтобы воспользоваться устройством. Было бы здорово, если бы устройство подключалось к домашней сети как клиент, а приложение искало бы его через mdns


    1. Danchkin_Sab Автор
      08.07.2025 04:36

      Можно и так, никаких проблем: настраиваем esp на работу в роли клиента, задаём ей статичный ip и также обмениваемся данными. Нюанс просто в том что не везде может быть собственная wifi сеть