ESP32 AP server na socketach – przykładowy kod Arduino, TCP echo, LED, select()

Pytanie

ESP32 server AP na socketach przykładowy kod w Aruino

Odpowiedź Sztucznej Inteligencji


Bezpośrednia odpowiedź na pytanie

Tak. Jeśli chcesz zrobić ESP32 jako Access Point (AP) i jednocześnie uruchomić serwer TCP na surowych socketach w Arduino, to należy użyć:

  • WiFi.mode(WIFI_AP)
  • WiFi.softAP(...)
  • niskopoziomowych gniazd z lwip/sockets.h

Poniżej masz kompletny, działający przykład dla Arduino IDE.
ESP32 tworzy własną sieć Wi-Fi, a po połączeniu klient może otworzyć połączenie TCP i wysyłać komendy tekstowe.

Najważniejsze punkty:

  • ESP32 działa jako punkt dostępowy
  • serwer działa na porcie 3333
  • obsługa klientów jest nieblokująca przez select()
  • przykładowe komendy: PING, STATUS, LEDON, LEDOFF

Bezpośrednia odpowiedź na pytanie Szczegółowa analiza problemu

Poniżej masz wersję, którą polecam jako punkt wyjścia, jeśli przez „na socketach” rozumiesz prawdziwe BSD sockets / lwIP, a nie tylko WiFiServer.

Kompletny przykład: ESP32 AP + TCP server na socketach

#include <WiFi.h>
#include <lwip/sockets.h>
#include <lwip/inet.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <strings.h>
#include <string.h>
#ifndef LED_BUILTIN
#define LED_BUILTIN 2
#endif
const char* AP_SSID = "ESP32_SOCKET_AP";
const char* AP_PASS = "12345678";
constexpr uint16_t SERVER_PORT = 3333;
constexpr int MAX_CLIENTS = 4;
constexpr size_t RX_LINE_SIZE = 128;
const int LED_PIN = LED_BUILTIN;
int serverSock = -1;
int clientSocks[MAX_CLIENTS];
char lineBuf[MAX_CLIENTS][RX_LINE_SIZE];
size_t lineLen[MAX_CLIENTS];
bool setNonBlocking(int sock) {
  int flags = fcntl(sock, F_GETFL, 0);
  if (flags < 0) return false;
  return fcntl(sock, F_SETFL, flags | O_NONBLOCK) == 0;
}
void sendText(int sock, const char* text) {
  if (sock >= 0) {
    send(sock, text, strlen(text), 0);
  }
}
void closeClient(int idx) {
  if (idx < 0 || idx >= MAX_CLIENTS) return;
  if (clientSocks[idx] >= 0) {
    close(clientSocks[idx]);
    clientSocks[idx] = -1;
  }
  lineLen[idx] = 0;
  lineBuf[idx][0] = '\0';
}
void processCommand(int idx, const char* cmd) {
  int sock = clientSocks[idx];
  Serial.printf("[Klient %d] CMD: %s\n", idx, cmd);
  if (strcasecmp(cmd, "PING") == 0) {
    sendText(sock, "PONG\r\n");
  }
  else if (strcasecmp(cmd, "STATUS") == 0) {
    char out[128];
    snprintf(out, sizeof(out),
             "OK uptime_ms=%lu free_heap=%u stations=%d led=%d\r\n",
             millis(),
             ESP.getFreeHeap(),
             WiFi.softAPgetStationNum(),
             digitalRead(LED_PIN));
    sendText(sock, out);
  }
  else if (strcasecmp(cmd, "LEDON") == 0) {
    digitalWrite(LED_PIN, HIGH);
    sendText(sock, "LED=ON\r\n");
  }
  else if (strcasecmp(cmd, "LEDOFF") == 0) {
    digitalWrite(LED_PIN, LOW);
    sendText(sock, "LED=OFF\r\n");
  }
  else if (strcasecmp(cmd, "HELP") == 0) {
    sendText(sock, "Commands: PING, STATUS, LEDON, LEDOFF, HELP\r\n");
  }
  else if (strlen(cmd) == 0) {
    // ignoruj pustą linię
  }
  else {
    sendText(sock, "ERR unknown command\r\n");
  }
}
void feedRxData(int idx, const char* data, int len) {
  for (int i = 0; i < len; i++) {
    char c = data[i];
    if (c == '\r') continue;
    if (c == '\n') {
      lineBuf[idx][lineLen[idx]] = '\0';
      processCommand(idx, lineBuf[idx]);
      lineLen[idx] = 0;
      lineBuf[idx][0] = '\0';
      continue;
    }
    if (lineLen[idx] < RX_LINE_SIZE - 1) {
      lineBuf[idx][lineLen[idx]++] = c;
    } else {
      sendText(clientSocks[idx], "ERR line too long\r\n");
      lineLen[idx] = 0;
      lineBuf[idx][0] = '\0';
    }
  }
}
void acceptNewClient() {
  struct sockaddr_in clientAddr;
  socklen_t addrLen = sizeof(clientAddr);
  int newSock = accept(serverSock, (struct sockaddr*)&clientAddr, &addrLen);
  if (newSock < 0) {
    return;
  }
  setNonBlocking(newSock);
  int slot = -1;
  for (int i = 0; i < MAX_CLIENTS; i++) {
    if (clientSocks[i] < 0) {
      slot = i;
      break;
    }
  }
  char ip[16];
  inet_ntoa_r(clientAddr.sin_addr, ip, sizeof(ip));
  if (slot < 0) {
    Serial.printf("Brak wolnego slotu dla klienta %s\n", ip);
    sendText(newSock, "ERR server busy\r\n");
    close(newSock);
    return;
  }
  clientSocks[slot] = newSock;
  lineLen[slot] = 0;
  lineBuf[slot][0] = '\0';
  Serial.printf("Klient %s:%u -> slot %d\n", ip, ntohs(clientAddr.sin_port), slot);
  sendText(newSock, "ESP32 TCP server ready\r\nCommands: PING, STATUS, LEDON, LEDOFF, HELP\r\n");
}
void setupServerSocket() {
  serverSock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
  if (serverSock < 0) {
    Serial.println("Blad: socket()");
    return;
  }
  int opt = 1;
  setsockopt(serverSock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
  if (!setNonBlocking(serverSock)) {
    Serial.println("Uwaga: nie udalo sie ustawic trybu nieblokujacego");
  }
  struct sockaddr_in serverAddr;
  memset(&serverAddr, 0, sizeof(serverAddr));
  serverAddr.sin_family = AF_INET;
  serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
  serverAddr.sin_port = htons(SERVER_PORT);
  if (bind(serverSock, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) {
    Serial.printf("Blad: bind(), errno=%d\n", errno);
    close(serverSock);
    serverSock = -1;
    return;
  }
  if (listen(serverSock, MAX_CLIENTS) < 0) {
    Serial.printf("Blad: listen(), errno=%d\n", errno);
    close(serverSock);
    serverSock = -1;
    return;
  }
  Serial.printf("Serwer TCP slucha na porcie %u\n", SERVER_PORT);
}
void setup() {
  Serial.begin(115200);
  delay(1000);
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);
  for (int i = 0; i < MAX_CLIENTS; i++) {
    clientSocks[i] = -1;
    lineLen[i] = 0;
    lineBuf[i][0] = '\0';
  }
  WiFi.mode(WIFI_AP);
  if (!WiFi.softAP(AP_SSID, AP_PASS)) {
    Serial.println("Blad: WiFi.softAP()");
    return;
  }
  Serial.println();
  Serial.println("ESP32 uruchomiony w trybie AP");
  Serial.print("SSID: ");
  Serial.println(AP_SSID);
  Serial.print("Haslo: ");
  Serial.println(AP_PASS);
  Serial.print("IP AP: ");
  Serial.println(WiFi.softAPIP());
  setupServerSocket();
  Serial.println("Test z PC:");
  Serial.println("nc 192.168.4.1 3333");
}
void loop() {
  if (serverSock < 0) {
    delay(1000);
    return;
  }
  fd_set readSet;
  FD_ZERO(&readSet);
  FD_SET(serverSock, &readSet);
  int maxFd = serverSock;
  for (int i = 0; i < MAX_CLIENTS; i++) {
    if (clientSocks[i] >= 0) {
      FD_SET(clientSocks[i], &readSet);
      if (clientSocks[i] > maxFd) {
        maxFd = clientSocks[i];
      }
    }
  }
  struct timeval timeout;
  timeout.tv_sec = 0;
  timeout.tv_usec = 200000; // 200 ms
  int ready = select(maxFd + 1, &readSet, nullptr, nullptr, &timeout);
  if (ready < 0) {
    if (errno != EINTR) {
      Serial.printf("Blad: select(), errno=%d\n", errno);
    }
    delay(1);
    return;
  }
  if (ready == 0) {
    delay(1);
    return;
  }
  if (FD_ISSET(serverSock, &readSet)) {
    acceptNewClient();
  }
  for (int i = 0; i < MAX_CLIENTS; i++) {
    int sock = clientSocks[i];
    if (sock < 0) continue;
    if (FD_ISSET(sock, &readSet)) {
      char rx[64];
      int n = recv(sock, rx, sizeof(rx), 0);
      if (n == 0) {
        Serial.printf("Klient slot %d rozlaczyl sie\n", i);
        closeClient(i);
      }
      else if (n < 0) {
        if (errno != EWOULDBLOCK && errno != EAGAIN) {
          Serial.printf("recv() error dla slot %d, errno=%d\n", i, errno);
          closeClient(i);
        }
      }
      else {
        feedRxData(i, rx, n);
      }
    }
  }
  delay(1);
}

Jak to działa

  1. ESP32 uruchamia własną sieć Wi-Fi

    • WiFi.mode(WIFI_AP);
    • WiFi.softAP(AP_SSID, AP_PASS);
  2. Tworzony jest serwer TCP

    • socket(AF_INET, SOCK_STREAM, IPPROTO_IP)
    • bind()
    • listen()
  3. Obsługa wielu klientów

    • select() monitoruje:
      • socket serwera
      • sockety aktywnych klientów
  4. Dane są odbierane jako strumień TCP

    • To ważne: TCP nie przenosi „wiadomości”, tylko strumień bajtów
    • dlatego przykład składa komendę aż do znaku końca linii \n
  5. Przetwarzanie komend

    • PINGPONG
    • STATUS → czas działania, wolny heap, liczba stacji AP
    • LEDON / LEDOFF → sterowanie LED

Aktualne informacje i trendy

W praktyce dla ESP32 spotkasz dziś trzy główne podejścia:

  • WiFiServer

    • prostsze
    • dobre do podstawowego TCP
    • bardziej „arduino-we”
  • surowe sockety lwip/sockets.h

    • większa kontrola
    • możliwość użycia select(), recv(), send()
    • lepsze, jeśli chcesz pisać coś bardziej „systemowo”
  • HTTP / WebSocket

    • najlepsze, gdy klientem ma być przeglądarka
    • wygodne do paneli WWW i sterowania z telefonu

Technicznie:

  • jeśli chcesz połączenie z terminala / aplikacji PC / Delphi / Python / C#, to raw TCP sockets są bardzo sensowne
  • jeśli chcesz otwierać ESP32 w przeglądarce, lepszy będzie HTTP albo WebSocket

Istotna korekta względem niektórych przykładowych odpowiedzi:

  • WiFiServer to nadal serwer TCP, ale jest to warstwa wyższego poziomu
  • jeśli pytasz o „sockety”, to właściwsze jest użycie lwip/sockets.h
  • w jednej z przykładowych odpowiedzi pojawił się wątek Delphi i kolorów przycisku — to jest niezwiązane z pytaniem o ESP32 AP server

Wspierające wyjaśnienia i detale

Różnica między WiFiServer a lwip/sockets.h

Podejście Poziom abstrakcji Zastosowanie
WiFiServer wyższy szybki start, prosty TCP
lwip/sockets.h niższy większa kontrola, wiele klientów, select()

Dlaczego nie użyłem blokującego accept() w loop()

Blokujące podejście działa, ale ma wady:

  • może zatrzymywać resztę logiki programu
  • trudniej obsłużyć kilku klientów
  • pogarsza responsywność

Dlatego w przykładzie:

  • socket serwera jest ustawiony jako nieblokujący
  • wielokrotne połączenia są obsługiwane przez select()

Dlaczego nie użyłem String

W systemach embedded lepiej ograniczać:

  • dynamiczną alokację
  • fragmentację pamięci heap

Dlatego zastosowano:

  • statyczne bufory char[]
  • składanie komendy znak po znaku

To jest rozwiązanie bardziej inżynierskie i stabilniejsze.

Uwaga praktyczna: TCP to nie UART

Bardzo częsty błąd początkujących:

  • zakładają, że jedno send() po stronie klienta odpowiada jednemu recv() po stronie serwera

To nie jest gwarantowane.
Możesz dostać:

  • połowę komendy
  • kilka komend naraz
  • dane podzielone na losowe fragmenty

Dlatego właśnie przykład używa:

  • bufora per klient
  • końca linii \n jako znacznika końca ramki aplikacyjnej

Aspekty etyczne i prawne

W tym przypadku aspekt prawny jest ograniczony, ale warto pamiętać o kilku rzeczach:

  • Bezpieczeństwo sieci

    • nie używaj słabych haseł typu 12345678 w projekcie produkcyjnym
    • do testów jest akceptowalne, do wdrożeń — nie
  • Sterowanie urządzeniami

    • jeśli sockety sterują przekaźnikami, napędami lub zasilaniem, konieczne są:
      • autoryzacja
      • timeout bezpieczeństwa
      • stan bezpieczny po utracie łączności
  • Zakłócenia i kompatybilność radiowa

    • pracuj na legalnych kanałach i nie przeciążaj środowiska RF
    • w zastosowaniach przemysłowych rozważ diagnostykę RSSI i odporność na utratę pakietów

Praktyczne wskazówki

Jak testować

  1. Wgraj kod do ESP32
  2. Otwórz monitor portu szeregowego: 115200
  3. Połącz komputer lub telefon z Wi-Fi:
    • SSID: ESP32_SOCKET_AP
    • hasło: 12345678
  4. Otwórz klienta TCP

Na Linux/macOS:

nc 192.168.4.1 3333

Na Windows:

  • PuTTY w trybie Raw
  • albo ncat, jeśli masz zainstalowany Nmap/Ncat

Przykładowe komendy do wpisania

PING
STATUS
LEDON
LEDOFF
HELP

Jeśli chcesz połączyć się z przeglądarki

Ten kod jest raw TCP, więc przeglądarka nie jest idealnym klientem.
Przeglądarka oczekuje HTTP.

Jeżeli chcesz, aby po wejściu na adres ESP32 w przeglądarce wyświetlała się strona, musisz:

  • albo użyć WebServer
  • albo ręcznie wysłać odpowiedź HTTP przez send()

Minimalna odpowiedź HTTP wygląda tak:

const char* httpResponse =
  "HTTP/1.1 200 OK\r\n"
  "Content-Type: text/plain; charset=utf-8\r\n"
  "Connection: close\r\n"
  "\r\n"
  "ESP32 AP server dziala\r\n";

Dobre praktyki projektowe

  • unikaj blokujących pętli while(client.connected())
  • nie buduj protokołu na String
  • stosuj delimitery ramek, np. \n
  • dodaj timeout bezczynności klienta
  • ogranicz maksymalną długość komendy
  • loguj błędy errno

Zasilanie

ESP32 w trybie AP potrafi pobierać zauważalny prąd impulsowy.
Jeżeli masz resety lub niestabilność:

  • sprawdź zasilanie 3,3 V
  • dodaj kondensatory odsprzęgające
  • nie zasilaj z przypadkowego konwertera USB-UART o słabej wydajności

Ewentualne zastrzeżenia lub uwagi dodatkowe

  • LED_BUILTIN nie na każdej płytce oznacza fizycznie dostępną diodę
  • domyślny adres AP ESP32 to zwykle 192.168.4.1, ale najlepiej odczytać go z:
    • WiFi.softAPIP()
  • liczba klientów w AP jest ograniczona
  • ten przykład nie implementuje:
    • szyfrowania na poziomie aplikacji
    • uwierzytelniania komend
    • mechanizmu heartbeat / keepalive
    • retransmisji na poziomie aplikacyjnym

Jeśli projekt ma sterować czymś krytycznym, należy to dodać.


Sugestie dalszych badań

Jeżeli chcesz rozwinąć ten projekt, następne sensowne kroki to:

  • wersja HTTP na surowych socketach
  • wersja WebSocket do komunikacji z przeglądarką
  • JSON jako format komend
  • obsługa timeoutów klientów
  • FreeRTOS task do wydzielenia komunikacji sieciowej
  • tryb AP+STA jednocześnie
  • integracja z aplikacją PC
    • Python
    • C#
    • Delphi
    • Qt

Mogę też przygotować od razu jedną z tych wersji:

  1. najprostszy serwer TCP na WiFiServer
  2. HTTP server w AP
  3. WebSocket server w AP
  4. ESP32 AP + klient Delphi/Python

Krótkie podsumowanie

Jeśli pytasz o ESP32 server AP na socketach w Arduino, to najbardziej właściwe rozwiązanie to:

  • uruchomić ESP32 jako WIFI_AP
  • stworzyć serwer TCP przez lwip/sockets.h
  • obsługiwać klientów nieblokująco przez select()

Kod powyżej jest poprawnym, praktycznym punktem startowym i jest technicznie lepszy od bardzo prostych przykładów blokujących.

Jeśli chcesz, mogę w następnym kroku przygotować:

  • wersję minimalną, 30–40 linii
    albo
  • wersję HTTP do otwierania w przeglądarce.

Oceń odpowiedź:
Zastrzeżenie: Odpowiedzi udzielone przez sztuczną inteligencję (model językowy) mogą być niedokładne i wprowadzające w błąd. Elektroda nie ponosi odpowiedzialności za dokładność, rzetelność ani kompletność prezentowanych informacji. Wszystkie odpowiedzi powinny być zweryfikowane przez użytkownika.

Zadaj dodatkowe pytanie Sztucznej Inteligencji

Czekaj (2min)...