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

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

Схема устройства
Основным управляющим устройством выступает микроконтроллер 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>

Также, для работы с сетью, в приложение нужно добавить разрешение на доступ к интернету.
<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 сети.


