{"id":25282,"date":"2025-12-14T11:48:09","date_gmt":"2025-12-14T11:48:09","guid":{"rendered":"https:\/\/wolles-elektronikkiste.de\/?p=25282"},"modified":"2025-12-14T11:48:16","modified_gmt":"2025-12-14T11:48:16","slug":"async-webserver-with-the-esp32","status":"publish","type":"post","link":"https:\/\/wolles-elektronikkiste.de\/en\/async-webserver-with-the-esp32","title":{"rendered":"Async WebServer with the ESP32"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\">About this Post<\/h2>\n<p data-start=\"599\" data-end=\"1016\">In this article you will learn how to implement an asynchronous web server (Async WebServer) using ESP32 boards. The first steps are straightforward, but as soon as you want to use the server to control or query states, new challenges arise. Things get particularly interesting when several ESP32s need to communicate with each other and the user interface needs to be attractive at the same time.  <\/p>\n<p>This is what you can expect:<\/p>\n<ul>\n<li><a href=\"#asyncwebserver_vs_webserver\">Async WebServer vs. classic WebServer<\/a><\/li>\n<li><a href=\"#prep\">Preparations<\/a><\/li>\n<li><a href=\"#hello_world\">Introduction: &#8220;Hello World&#8221;<\/a>\n<ul>\n<li><a href=\"#bare_minimum\">Bare minimum<\/a><\/li>\n<li><a href=\"#extended_hello_world\">Extended Hello World sketch<\/a><\/li>\n<\/ul>\n<\/li>\n<li><a href=\"#one_led\">Switching one LED via browser<\/a><\/li>\n<li><a href=\"#leds_pwm_analogread\">Three LEDs + PWM + analogRead<\/a>\n<ul>\n<li><a href=\"#nicer_w_css\">More beautiful with CSS<\/a><\/li>\n<li><a href=\"#convenient_w_script\">More convenient with JavaScript<\/a><\/li>\n<\/ul>\n<\/li>\n<li><a href=\"#multiple_esp32\">Controlling multiple ESP32s<\/a>\n<ul>\n<li><a href=\"#multiple_esp32_asw\">Option 1: Everything via HTTP (Async WebServer)<\/a><\/li>\n<li><a href=\"#multiple_esp32_asw_espnow_sta\">Option 2a: Async WebServer and ESP-NOW Serial (&#8220;STA-only&#8221;)<\/a><\/li>\n<li><a href=\"#multiple_esp32_asw_espnow_sta_ap\">Option 2b: Async WebServer and ESP-NOW Serial (STA\/AP)<\/a><\/li>\n<li><a href=\"#multiple_esp32_asw_espnow_sta_complete\">Complete version ESP-NOW Serial (&#8220;STA-only&#8221;)<\/a><\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n<h2 class=\"wp-block-heading\" id=\"asyncwebserver_vs_webserver\">Async WebServer vs. classic WebServer<\/h2>\n<p>Perhaps some of you have read my article <a href=\"https:\/\/wolles-elektronikkiste.de\/en\/using-wifi-with-the-esp8266-and-esp32\" target=\"_blank\" rel=\"noopener\">WLAN with ESP8266 and ESP32<\/a>. There I used a simple, classic web server to control ESP8266 and ESP32 boards. So why another post about the Async WebServer?   <\/p>\n<p>A significant difference between the two web server variants is that the classic web server works in a blocking manner, unlike the Async WebServer. This is illustrated in the following diagram:  <\/p>\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/async_webserver_vs_webserver-1024x425.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"425\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/async_webserver_vs_webserver-1024x425.png\" alt=\"Classic WebServer vs. Async WebServer\" class=\"wp-image-25153\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/async_webserver_vs_webserver-1024x425.png 1024w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/async_webserver_vs_webserver-300x125.png 300w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/async_webserver_vs_webserver-768x319.png 768w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/async_webserver_vs_webserver.png 1265w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">Classic WebServer vs. Async WebServer<\/figcaption><\/figure>\n<p>With the classic web server, the server is handled in <code>loop()<\/code> and is continuously queried with <code>server.handleClient()<\/code>. In the case of a client request (e.g., the PC browser accesses the web server), the request is processed and then the response is sent. Meanwhile, the program is blocked, which can lead to problems if other time-dependent tasks need to be completed.   <\/p>\n<p>The Async WebServer, on the other hand, works event-based. If it receives a request, it is only very briefly interrupted from its current tasks and initiates the processing of the request (callback function) in a separate task.  <\/p>\n<p>For simple applications, e.g. when there is only one customer and only a few non-time-critical tasks that need to be completed per unit of time, there is nothing to be said against the classic web server. Otherwise, you should rather use the Async WebServer.  <\/p>\n\n<h2 class=\"wp-block-heading\" id=\"prep\">Preparations<\/h2>\n<p>I use <a href=\"https:\/\/github.com\/espressif\/arduino-esp32\" target=\"_blank\" rel=\"noopener\">&#8220;esp32<\/a>&#8221; from Espressif as the board package for the ESP32. Click <a href=\"https:\/\/docs.espressif.com\/projects\/arduino-esp32\/en\/latest\/installing.html\" target=\"_blank\" rel=\"noopener\">here<\/a> for installation instructions. The boards used were ESP-WROOM-32-based development boards. In principle, the examples in this article should work with all ESP32 boards.    <\/p>\n<p>For the Async WebServer, I use the <a href=\"https:\/\/github.com\/ESP32Async\/ESPAsyncWebServer\" target=\"_blank\" rel=\"noopener\">ESP Async WebServer<\/a> and <a href=\"https:\/\/github.com\/ESP32Async\/AsyncTCP\" target=\"_blank\" rel=\"noopener\">Async TCP<\/a> libraries, both from ESP32Async. You can easily install them in the Arduino IDE using the library manager. <\/p>\n\n<h2 class=\"wp-block-heading\" id=\"hello_world\">Introduction: &#8220;Hello World&#8221;<\/h2>\n\n<h3 class=\"wp-block-heading\" id=\"bare_minimum\">Bare minimum<\/h3>\n\n<p>We start with a simple sketch that displays &#8220;Hello World&#8221; in your browser. Customize it by entering the name of your WiFi network and the password (look for the comments: &#8220;TODO&#8221;). <\/p>\n<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"async_webserver_hello_world_bare_minimum.ino\" data-enlighter-title=\"async_webserver_hello_world_bare_minimum.ino\">#include &lt;WiFi.h&gt;\n#include &lt;AsyncTCP.h&gt;\n#include &lt;ESPAsyncWebServer.h&gt;\n\n\/\/ insert your credentials\nconstexpr char WIFI_SSID[] = \"Your SSID\";      \/\/ TODO\nconstexpr char WIFI_PASS[] = \"Your password\";  \/\/ TODO\n\nAsyncWebServer server(80);\n\nvoid connectWiFiSTA() {\n  WiFi.mode(WIFI_STA);\n  WiFi.begin(WIFI_SSID, WIFI_PASS);\n\n  Serial.print(\"Verbinde mit WLAN \");\n  Serial.print(WIFI_SSID);\n  Serial.print(\" ... \");\n\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(250);\n    Serial.print(\".\");\n  }\n  Serial.println();\n  \n  Serial.print(\"Connected, IP: \");\n  Serial.println(WiFi.localIP()); \n}\n\nvoid setup() {\n  Serial.begin(115200);\n  delay(200);\n\n  connectWiFiSTA();\n\n  server.on(\"\/\", HTTP_GET, [](AsyncWebServerRequest *request) {\n    request-&gt;send(200, \"text\/html\", \"Hello World\");\n  });\n\n  server.begin();\n  Serial.println(\"Server started.\");\n}\n\nvoid loop() {}<\/pre>\n<p>\n<p>The IP address assigned by the router is displayed in the serial monitor. If nothing appears in your serial monitor, press the reset button on the board or insert <code>delay()<\/code> (few seconds) at the beginning of <code>setup()<\/code>.  <\/p>\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/11\/output-async_webserver_hello_world_bare_minimum.png\"><img loading=\"lazy\" decoding=\"async\" width=\"541\" height=\"63\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/11\/output-async_webserver_hello_world_bare_minimum.png\" alt=\"Output of async_webserver_hello_world_bare_minimum.ino\" class=\"wp-image-25091\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/11\/output-async_webserver_hello_world_bare_minimum.png 541w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/11\/output-async_webserver_hello_world_bare_minimum-300x35.png 300w\" sizes=\"auto, (max-width: 541px) 100vw, 541px\" \/><\/a><figcaption class=\"wp-element-caption\">Output of async_webserver_hello_world_bare_minimum.ino<\/figcaption><\/figure>\n\n<p>Then go to the browser of your choice and type the IP into the address bar. A simple &#8220;Hello World&#8221; should appear. &nbsp;<\/p>\n\n<h3 class=\"wp-block-heading\">Explanations of the &#8220;Bare Minimum Sketch&#8221;<\/h3>\n<ul>\n<li><code>AsyncWebServer server(80)<\/code> creates the web server, which is accessible via port 80.<\/li>\n<li>Use <code>WiFi.mode(WIFI_STA)<\/code> to set the station mode. In this mode, the ESP32 connects to a WiFi network and receives its IP address from it. We will come to the AP mode (= Access Point) later, in which the ESP32 itself provides the WiFi network.  <\/li>\n<li><code>WiFi.begin()<\/code> &#8211; enter the name of the network and the password.<\/li>\n<li><code>WiFi.status() != WL_CONNECTED<\/code> is false as long as there is no connection to the network. <\/li>\n<\/ul>\n\n<h4 class=\"wp-block-heading\">Explanations of server.on()<\/h4>\n<p>The way server.on() is called may be a little unfamiliar: <\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">server.on(\"\/\", HTTP_GET, [](AsyncWebServerRequest *request) {\n request-&gt;send(200, \"text\/html\", \"Hello World\");\n});<\/pre>\n<p>This is &#8211; in principle &#8211; the short form of:<\/p>\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">void handleRoot(AsyncWebServerRequest *request) {\n  request-&gt;send(200, \"text\/html\", \"Hello World\");\n}\n\nvoid setup() {\n  ...\n server.on(\"\/\", HTTP_GET, handleRoot);\n}\n<\/pre>\n\n<p>In the short form, the third parameter of <code>server.on()<\/code>, i.e. <code>[](AsyncWebServerRequest *request) {...}<\/code> is a lambda function. It is also referred to as an anonymous function as it has no name. Lambda functions are written directly at the point where they are used instead of defining them separately. This is above all short and flexible. More on this would go too far at this point.    &nbsp;<\/p>\n<p><strong><em>That&#8217;s actually all you need to know about the Async WebServer! Everything else is mainly about replacing &#8220;Hello World&#8221; with text, HTML, CSS and JavaScript code. <\/em><\/strong><\/p>\n\n<h3 class=\"wp-block-heading\" id=\"extended_hello_world\">Extended Hello World &#8211; Sketch<\/h3>\n\n<p>Before we continue with the actual topic, I would like to introduce some useful extensions to the &#8220;Hello World&#8221; sketch.<\/p>\n<ol>\n<li>With <code>WiFi.config(ip, gateway, subnet);<\/code> you specify the IP address. I do this in most sketches. The function must be called before <code>WiFi.begin()<\/code>.  &nbsp;<\/li>\n<li>If calling up the IP address is too cryptic for you, you can use <code>MDNS.begin(HOSTNAME)<\/code> to assign a nicer name. To do this, you must include the ESPmDNS library. You can then call up the website with <em>http:\/\/HOSTNAME.local<\/em>.  &nbsp;<\/li>\n<li><code>server.onNotFound()<\/code> defines what happens if the path is not found. You can pass the 404 here. <\/li>\n<li>If the ESP32 does not connect to the network, it may make sense to restart it. This is exactly what the sketch does. &nbsp;<\/li>\n<\/ol>\n<p>I think everything else is self-explanatory. This is the sketch:<\/p>\n<\/p>\n<div class=\"scroll-paragraph\">\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"hello_world_extended.ino\" data-enlighter-title=\"hello_world_extended.ino\">#include &lt;WiFi.h&gt;\n#include &lt;AsyncTCP.h&gt;\n#include &lt;ESPAsyncWebServer.h&gt;\n#include &lt;ESPmDNS.h&gt;\n\nIPAddress ip(192,168,178,112);  \/\/TODO: IP address of your choice\nIPAddress gateway(192,168,178,1); \/\/TODO: Router address\nIPAddress subnet(255,255,255,0); \/\/TODO: subnet mask, probably OK as is.\n\n\/\/ insert your credentials\nconstexpr char WIFI_SSID[] = \"Your SSID\";  \/\/TODO\nconstexpr char WIFI_PASS[] = \"Your password\";  \/\/TODO\nconstexpr char HOSTNAME[] = \"MyESP32-Server\";  \/\/TODO: Server name of your choice\n\nAsyncWebServer server(80);\n\nvoid connectWiFiSTA() {\n  WiFi.mode(WIFI_STA);\n  WiFi.config(ip, gateway, subnet);\n  WiFi.begin(WIFI_SSID, WIFI_PASS);\n\n  Serial.print(\"Verbinde mit WLAN \");\n  Serial.print(WIFI_SSID);\n  Serial.print(\" ... \");\n\n  \/\/ wait max 10 seconds\n  uint32_t start = millis();\n  while (WiFi.status() != WL_CONNECTED &amp;&amp; millis() - start &lt; 10000) {\n    delay(250);\n    Serial.print(\".\");\n  }\n  Serial.println();\n\n  if (WiFi.status() == WL_CONNECTED) {\n    Serial.print(\"Connected, IP: \");\n    Serial.println(WiFi.localIP());\n  } else {\n    Serial.println(\"WiFi Connection failed (timeout). Restart in 5s \u2026\");\n    delay(5000);\n    ESP.restart();\n  }\n\n  if (!MDNS.begin(HOSTNAME)) {\n    Serial.println(\"Error starting mDNS\");\n    return;\n  }\n  \/\/ MDNS.addService(\"http\", \"tcp\", 80); \/\/ may be needed\n  Serial.print(\"MDNS responder started at http:\/\/\");\n  Serial.print(HOSTNAME);\n  Serial.println(\".local\");\n}\n\nvoid setup() {\n  Serial.begin(115200);\n  delay(200);\n\n  connectWiFiSTA();\n\n  \/\/ Route: GET \/\n  server.on(\"\/\", HTTP_GET, [](AsyncWebServerRequest *request) {\n    request-&gt;send(200, \"text\/html\", \"Hello World\");\n  });\n\n  \/\/ Optional: 404\n  server.onNotFound([](AsyncWebServerRequest *request) {\n    request-&gt;send(404, \"text\/html\", \"Not found\");\n  });\n\n  server.begin();\n  Serial.println(\"Server started.\");\n}\n\nvoid loop() {}\n<\/pre>\n<p>\u00a0<\/p>\n<\/div>\n<p>\n\n<p>You will see an output like this on the serial monitor:<\/p>\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/11\/output_aws_hello_world_extended.png\"><img loading=\"lazy\" decoding=\"async\" width=\"658\" height=\"78\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/11\/output_aws_hello_world_extended.png\" alt=\"Async WebServer output from hello_world_extended.ino\" class=\"wp-image-25093\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/11\/output_aws_hello_world_extended.png 658w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/11\/output_aws_hello_world_extended-300x36.png 300w\" sizes=\"auto, (max-width: 658px) 100vw, 658px\" \/><\/a><figcaption class=\"wp-element-caption\">Output of hello_world_extended.ino<\/figcaption><\/figure>\n\n<h2 class=\"wp-block-heading\" id=\"one_led\">Switching one LED via browser<\/h2>\n<p>In the next step, we will switch one LED attached to GPIO2 of the ESP32 on and off via the browser. I&#8217;m deliberately only using one LED and no CSS to illustrate the principle with this simple sketch.  Here is the sketch:<\/p>\n<\/p>\n<div class=\"scroll-paragraph\">\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"switch_one_led.ino\" data-enlighter-title=\"switch_one_led.ino\">#include &lt;WiFi.h&gt;\n#include &lt;AsyncTCP.h&gt;\n#include &lt;ESPAsyncWebServer.h&gt;\n\nIPAddress ip(192,168,178,112);  \/\/TODO\nIPAddress gateway(192,168,178,1);  \/\/TODO\nIPAddress subnet(255,255,255,0);  \/\/TODO\n\nconstexpr char WIFI_SSID[] = \"Your SSID\"; \/\/TODO\nconstexpr char WIFI_PASS[] = \"Your Password\";  \/\/TODO\n\nconst int ledPin = 2;\nbool ledOn = false;\n\nAsyncWebServer server(80);\n\nvoid setLed(bool on) {\n  ledOn = on;\n  digitalWrite(ledPin, on);\n}\n\nString page() {\n  if (ledOn) {\n    return String(F(\"&lt;a href=\\\"\/off\\\"&gt;Switch LED off&lt;\/a&gt;\"));\n  } else {\n    return String(F(\"&lt;a href=\\\"\/on\\\"&gt;Switch LED on&lt;\/a&gt;\"));\n  }\n}\n\nvoid connectWiFi() {\n  WiFi.mode(WIFI_STA);\n  WiFi.config(ip, gateway, subnet);\n  WiFi.begin(WIFI_SSID, WIFI_PASS);\n  Serial.printf(\"Connecting to %s\", WIFI_SSID);\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(250); Serial.print(\".\");\n  }\n  Serial.println();\n  Serial.print(\"IP: \"); Serial.println(WiFi.localIP());\n}\n\nvoid setup() {\n  Serial.begin(115200);\n  pinMode(ledPin, OUTPUT);\n  setLed(false);\n\n  connectWiFi();\n\n  server.on(\"\/\", HTTP_GET, [](AsyncWebServerRequest *req){\n    req-&gt;send(200, \"text\/html\", page());\n  });\n\n  server.on(\"\/on\", HTTP_GET, [](AsyncWebServerRequest *req){\n    setLed(true);\n    req-&gt;send(200, \"text\/html\", page());\n    \/\/ req-&gt;redirect(\"\/\"); \/\/ alternative to the line above\n  });\n\n  server.on(\"\/off\", HTTP_GET, [](AsyncWebServerRequest *req){\n    setLed(false);\n    req-&gt;send(200, \"text\/html\", page());\n    \/\/ req-&gt;redirect(\"\/\"); \/\/ alternative to the line above\n  });\n\n  server.begin();\n  Serial.println(\"Server ready.\");\n}\n\nvoid loop() {}<\/pre>\n<\/div>\n<p>\n<p>And this is what the output looks like in my browser:<\/p>\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/11\/firefox_one_led.png\"><img loading=\"lazy\" decoding=\"async\" width=\"673\" height=\"122\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/11\/firefox_one_led.png\" alt=\"Async WebServer - Browser output of switch_one_led.ino\" class=\"wp-image-25117\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/11\/firefox_one_led.png 673w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/11\/firefox_one_led-300x54.png 300w\" sizes=\"auto, (max-width: 673px) 100vw, 673px\" \/><\/a><figcaption class=\"wp-element-caption\">Browser output of switch_one_led.ino<\/figcaption><\/figure>\n\n<h3 class=\"wp-block-heading\">Explanations of switch_one_led.ino<\/h3>\n<p>Please understand that this article is not a lesson in HTML, CSS and JavaScript. It would simply go too far if I were to go into detail. I will focus on the Arduino code.  <\/p>\n<p>The variable <code>ledOn<\/code> saves the status of the LED, namely <code>false<\/code> for off and <code>true<\/code> for on. In the default state, the LED is off.  <\/p>\n<p>In the browser, the LED is controlled via links. The first call to the website via the IP address leads to the main path &#8220;\/&#8221;. Therefore, <code>server.on(\"\/\", ...<\/code> is used. The send function <code>req-&gt;send()<\/code> transmits the following to the client:   <\/p>\n<ul>\n<li>The HTTP status code 200 \u2192 everything OK.<\/li>\n<li>&#8220;text\/html&#8221; \u2192 this means that the message is interpreted as HTML.<\/li>\n<li>A string returned by the <code>page()<\/code> function. As <code>ledOn<\/code> is true, the link to &#8220;\/off&#8221; is sent. <\/li>\n<\/ul>\n<p>You will see the link text &#8220;Switch LED Off&#8221; in the browser. If you now click on this link, <code>server.on(\"\/off\", ...)<\/code> takes effect. The Lambda function first calls the function <code>setLed(false)<\/code>, which sets <code>ledOn<\/code> to <code>false<\/code> and switches the LED off. <code>req-&gt;send()<\/code> calls the function <code>page()<\/code> again. As <code>ledOn<\/code> is <code>false<\/code> this time, the link to &#8220;\/on&#8221; is returned. The link text &#8220;Switch LED on&#8221; appears in the browser. And so you can now switch back and forth.      <\/p>\n<p>As an alternative to <code>req-&gt;send(200, \"text\/html\", page())<\/code>, you could return to the main path with <code>req-&gt;redirect(\"\/\")<\/code>, which in turn calls up <code>req-&gt;send()<\/code>. <\/p>\n\n<h2 class=\"wp-block-heading\" id=\"leds_pwm_analogread\">Three LEDs + PWM + analogRead<\/h2>\n<p>We set the bar higher by switching three LEDs, dimming one LED via PWM and query the voltage on one pin. This still leaves us with (almost) pure HTML code. We&#8217;ll make it nicer and more user-friendly later.   <\/p>\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/async_webserver_circuit-1024x441.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"441\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/async_webserver_circuit-1024x441.png\" alt=\"Circuit for the following sketches\" class=\"wp-image-25264\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/async_webserver_circuit-1024x441.png 1024w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/async_webserver_circuit-300x129.png 300w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/async_webserver_circuit-768x331.png 768w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/async_webserver_circuit.png 1305w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">Circuit for the following sketches<\/figcaption><\/figure>\n\n<h4 class=\"wp-block-heading\">Switching \/ controlling the LEDs <\/h4>\n<p>Switching three LEDs is actually just a bit more paperwork. The principle remains the same, except that we handle the LED pins and the LED status (ledOn) as an array. As before, each switching operation has its own directory, i.e. \/led0_on, \/led0_off, \/led1_on etc.  <\/p>\n<p>We pass the PWM value using a form (<code>&lt;form&gt;...&lt;\/form&gt;<\/code>). As I have set the PWM resolution to 255, a <code>analogWrite()<\/code> would also have been sufficient here. However, this way you can also apply the code to other PWM applications.  <\/p>\n\n<h4 class=\"wp-block-heading\">Querying and displaying the analogueRead() value<\/h4>\n<p>Determining and outputting the voltage of just one pin is simple. However, this raises the question of how to update the voltage automatically. In principle, you can reload the whole page periodically by inserting <code>&lt;html&gt;&lt;head&gt;&lt;meta http-equiv='refresh' content='5'&gt;&lt;\/head&gt;<\/code> (for 5 seconds) at the beginning of the web page. However, this clashes with the PWM input. If the refresh occurs while you are inputting, your input field will be empty again.    <\/p>\n<p>One solution is using an iframe (= inline frame). This is another website embedded in the website, so to speak. If you only apply the refresh to the embedded web page, it will not interfere with the rest.   <\/p>\n<p>But here&#8217;s the sketch first:<\/p>\n<\/p>\n<div class=\"scroll-paragraph\">\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"switch_three_leds_plus_pwm_plus voltage.ino\" data-enlighter-title=\"switch_three_leds_plus_pwm_plus voltage.ino\">#include &lt;WiFi.h&gt;\n#include &lt;AsyncTCP.h&gt;\n#include &lt;ESPAsyncWebServer.h&gt;\n\nIPAddress ip(192,168,178,112);  \/\/TODO\nIPAddress gateway(192,168,178,1);  \/\/TODO\nIPAddress subnet(255,255,255,0);  \/\/TODO\n\nconstexpr char WIFI_SSID[] = \"Your SSID\";  \/\/TODO\nconstexpr char WIFI_PASS[] = \"Your password\";  \/\/TODO\n\nconst int ledPin[] = {2,4,18};\nconst int analogReadPin = 34;\nconst int pwmLedPin = 5;    \nint pwmValue = 0;\nbool ledOn[] = {false, false, false};\n\nAsyncWebServer server(80);\n\nvoid setLed(int num, bool on) {\n  ledOn[num] = on;\n  digitalWrite(ledPin[num], on);\n}\n\nString page() {\n  String myPage = \"\";\n  for(int i=0; i&lt;3; i++){\n    if (ledOn[i]) {\n      myPage += String(F(\"&lt;a href=\\\"\/led\"));\n      myPage += String(i);\n      myPage += String(F(\"_off\\\"&gt;Switch LED \"));\n      myPage += String(i);\n      myPage += String(F(\" off&lt;\/a&gt;&lt;\/BR&gt;&lt;\/BR&gt;\"));\n    } else {\n      myPage += String(F(\"&lt;a href=\\\"\/led\"));\n      myPage += String(i);\n      myPage += String(F(\"_on\\\"&gt;Switch LED \"));\n      myPage += String(i);\n      myPage += String(F(\" on&lt;\/a&gt;&lt;\/BR&gt;&lt;\/BR&gt;\"));\n    }\n  }\n  \n  myPage += \"&lt;\/BR&gt;&lt;form action='\/set'&gt;\";\n  myPage += \"PWM: &lt;input name='value' type='number' min='0' max='255'&gt;\";\n  myPage += \"&lt;input type='submit' value='Submit'&gt;\";\n  myPage += \"&lt;\/form&gt;\";\n  myPage += \"Current PWM value: \";\n  myPage += String(pwmValue); \n\n  myPage += \"&lt;\/BR&gt;&lt;\/BR&gt;&lt;\/BR&gt;\";\n  myPage += \"&lt;iframe src='\/value' width='200' height='40' frameborder='0'&gt;&lt;\/iframe&gt;&lt;\/BR&gt;&lt;\/BR&gt;\";\n  \n  return myPage;\n}\n\nvoid connectWiFi() {\n  WiFi.mode(WIFI_STA);\n  WiFi.config(ip, gateway, subnet);\n  WiFi.begin(WIFI_SSID, WIFI_PASS);\n  Serial.printf(\"Connecting to %s\", WIFI_SSID);\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(250); Serial.print(\".\");\n  }\n  Serial.println();\n  Serial.print(\"IP: \"); Serial.println(WiFi.localIP());\n}\n\nvoid setup() {\n  Serial.begin(115200);\n  \n  for(int i=0; i&lt;3; i++) {\n    pinMode(ledPin[i], OUTPUT);\n    setLed(i, false);\n  }\n  pinMode(analogReadPin, INPUT);\n  ledcAttach(pwmLedPin, 5000, 8);\n\n  connectWiFi();\n\n  server.on(\"\/\", HTTP_GET, [](AsyncWebServerRequest *req){\n    req-&gt;send(200, \"text\/html\", page());\n  });\n\n  server.on(\"\/led0_on\", HTTP_GET, [](AsyncWebServerRequest *req){\n    setLed(0, true);\n    req-&gt;send(200, \"text\/html\", page());\n    \/\/ req-&gt;redirect(\"\/\"); \/\/ alternative to the line above\n  });\n\n  server.on(\"\/led0_off\", HTTP_GET, [](AsyncWebServerRequest *req){\n    setLed(0, false);\n    req-&gt;send(200, \"text\/html\", page());\n    \/\/ req-&gt;redirect(\"\/\"); \/\/ alternative to the line above\n  });\n\n  server.on(\"\/led1_on\", HTTP_GET, [](AsyncWebServerRequest *req){\n    setLed(1, true);\n    req-&gt;send(200, \"text\/html\", page());\n    \/\/ req-&gt;redirect(\"\/\"); \/\/ alternative to the line above\n  });\n\n  server.on(\"\/led1_off\", HTTP_GET, [](AsyncWebServerRequest *req){\n    setLed(1, false);\n    req-&gt;send(200, \"text\/html\", page());\n    \/\/ req-&gt;redirect(\"\/\"); \/\/ alternative to the line above\n  });\n\n  server.on(\"\/led2_on\", HTTP_GET, [](AsyncWebServerRequest *req){\n    setLed(2, true);\n    req-&gt;send(200, \"text\/html\", page());\n    \/\/ req-&gt;redirect(\"\/\"); \/\/ alternative to the line above\n  });\n\n  server.on(\"\/led2_off\", HTTP_GET, [](AsyncWebServerRequest *req){\n    setLed(2, false);\n    req-&gt;send(200, \"text\/html\", page());\n    \/\/ req-&gt;redirect(\"\/\"); \/\/ alternative to the line above\n  });\n\n  server.on(\"\/set\", HTTP_GET, [](AsyncWebServerRequest *request){\n    if (request-&gt;hasParam(\"value\")) {\n      pwmValue = request-&gt;getParam(\"value\")-&gt;value().toInt();\n      pwmValue = constrain(pwmValue, 0, 255);\n      ledcWrite(pwmLedPin, pwmValue);\n      request-&gt;send(200, \"text\/html\", page());\n      \/\/ req-&gt;redirect(\"\/\"); \/\/ alternative to the line above\n    }\n  });\n  \n  server.on(\"\/value\", HTTP_GET, [](AsyncWebServerRequest *req){\n    float voltage = 3.3 * analogRead(analogReadPin) \/ 4096.0;\n    String html = \"\";\n    html += \"&lt;html&gt;&lt;head&gt;&lt;meta http-equiv='refresh' content='5'&gt;&lt;\/head&gt;\";\n    html += \"&lt;body style='margin:0; padding:0; text-align:left;'&gt;\";\n    html += \"Voltage [V]: \";\n    html += String(voltage, 2);\n    html += \"&lt;\/body&gt;&lt;\/html&gt;\";\n\n  req-&gt;send(200, \"text\/html\", html);\n  });\n    \n  server.begin();\n  Serial.println(\"Server ready.\");\n}\n\nvoid loop() {}<\/pre>\n<\/div>\n<p>\n<p>However, I should explain one more point, namely how the PWM value makes it from the form into the request.<\/p>\n<p>With <code data-start=\"95\" data-end=\"123\">request-&gt;hasParam(\"value\")<\/code>, the web server checks whether the HTTP request contains a parameter with the name <code>value<\/code>, which comes from the input field of the HTML form. <code data-start=\"268\" data-end=\"305\">request-&gt;getParam(\"value\")-&gt;value()<\/code> reads the text entered by the user from this form field. <code data-start=\"380\" data-end=\"389\">toInt()<\/code> then converts this text into an integer.<\/p>\n\n<p>And this is what the output looks like in my browser:<\/p>\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/11\/firefox_3leds_pwm_analogread.png\"><img loading=\"lazy\" decoding=\"async\" width=\"938\" height=\"343\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/11\/firefox_3leds_pwm_analogread.png\" alt=\"Async WebServer - Browser output of switch_three_leds_plus_pwm_plus voltage.ino\" class=\"wp-image-25119\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/11\/firefox_3leds_pwm_analogread.png 938w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/11\/firefox_3leds_pwm_analogread-300x110.png 300w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/11\/firefox_3leds_pwm_analogread-768x281.png 768w\" sizes=\"auto, (max-width: 938px) 100vw, 938px\" \/><\/a><figcaption class=\"wp-element-caption\">Browser output of switch_three_leds_plus_pwm_plus voltage.ino<\/figcaption><\/figure>\n\n<h3 class=\"wp-block-heading\" id=\"nicer_w_css\">More beautiful with CSS<\/h3>\n<p>HTML offers many design options. However, this can quickly make the code confusing. The solution to this is CSS (Cascading Style Sheets).  <\/p>\n<p>CSS definitions are stored separately from the actual page content (<code>&lt;body&gt;...&lt;\/body&gt;<\/code>) in the header area of the web document (<code>&lt;head&gt;...&lt;\/head&gt;<\/code>) within a separate section (<code>&lt;style&gt;...&lt;\/style&gt;<\/code>) or in an external file.<\/p>\n<p>Basically, there are two main things you can do with CSS:<\/p>\n<ol>\n<li>Make global settings for existing HTML elements:\n<ul>\n<li>e.g. for the h1 heading: <code>h1 { font-size: 24px; margin: 0 0 10px 0; text-align: center; }<\/code><br \/>The advantage: The definition only needs to be made once and then applies to all corresponding elements.<\/li>\n<\/ul>\n<\/li>\n<li>Define your own classes:\n<ul>\n<li>e.g. a subheading: <code>.subtitle { text-align: center; font-size: 13px; color: #666; margin-bottom: 20px; }<\/code><\/li>\n<\/ul>\n<\/li>\n<\/ol>\n<p>The classes can then be used in HTML elements, e.g.: <code>&lt;div class=\"subtitle\"&gt;...&lt;\/div&gt;<\/code>. It is also possible to combine several classes. <\/p>\n<p><em>Not familiar with CSS and don&#8217;t have the time or inclination to familiarize yourself with it? No problem in times of artificial intelligence. I also used ChatGPT for the visual design. ChatGPT may have one or two gaps in its knowledge, but my experience with this AI for programming HTML, CSS and JavaScript is extremely good. Nevertheless, it is, of course, always better to have at least basic knowledge.     <\/em><\/p>\n<\/p>\n<div class=\"scroll-paragraph\">\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"nicer_with_css.ino\" data-enlighter-title=\"nicer_with_css.ino\">#include &lt;WiFi.h&gt;\n#include &lt;AsyncTCP.h&gt;\n#include &lt;ESPAsyncWebServer.h&gt;\n\nIPAddress ip(192,168,178,112);  \/\/TODO\nIPAddress gateway(192,168,178,1);  \/\/TODO\nIPAddress subnet(255,255,255,0);  \/\/TODO\n\nconstexpr char WIFI_SSID[] = \"Your SSID\";  \/\/TODO\nconstexpr char WIFI_PASS[] = \"Your Password\";  \/\/TODO\n\nconst int ledPin[] = {2,4,18};\nconst int analogReadPin = 34;\nconst int pwmLedPin = 5;    \nint pwmValue = 0;\nbool ledOn[] = {false, false, false};\n\nAsyncWebServer server(80);\n\nvoid setLed(int num, bool on) {\n  ledOn[num] = on;\n  digitalWrite(ledPin[num], on);\n}\n\nString page() {\n  String myPage = \"\";\n\n  \/\/ head + CSS + container start\n  myPage += F(\n    \"&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;&lt;meta charset='UTF-8'&gt;\"\n    \"&lt;meta name='viewport' content='width=device-width, initial-scale=1.0'&gt;\"\n    \"&lt;title&gt;ESP32 LED &amp;amp; PWM Control&lt;\/title&gt;\"\n    \"&lt;style&gt;\"\n    \"body{font-family:Arial,Helvetica,sans-serif;background:#f0f2f5;margin:0;padding:0;}\"\n    \".container{max-width:600px;margin:40px auto;padding:20px;background:#ffffff;\"\n    \"border-radius:12px;box-shadow:0 4px 12px rgba(0,0,0,0.08);}\"\n    \"h1{font-size:24px;margin:0 0 10px 0;text-align:center;}\"\n    \".subtitle{text-align:center;font-size:13px;color:#666;margin-bottom:20px;}\"\n    \".section{margin:20px 0;}\"\n    \".section-title{font-weight:bold;margin-bottom:8px;font-size:15px;color:#333;}\"\n    \".led-grid{display:flex;gap:12px;justify-content:center;}\"\n    \".led-tile{flex:1;padding:18px 10px;text-align:center;border-radius:10px;\"\n    \"font-weight:bold;text-decoration:none;color:#ffffff;\"\n    \"transition:background-color 0.2s,transform 0.1s,box-shadow 0.1s;\"\n    \"box-shadow:0 2px 4px rgba(0,0,0,0.1);font-size:14px;}\"\n    \".led-on{background:#e53935;}\"\n    \".led-on:hover{background:#b71c1c;}\"\n    \".led-off{background:#43a047;}\"\n    \".led-off:hover{background:#1b5e20;}\"\n    \".led-tile:active{transform:translateY(1px);box-shadow:0 1px 2px rgba(0,0,0,0.2);}\"\n    \".pwm-form{display:flex;flex-direction:column;gap:8px;}\"\n    \".pwm-form label{font-size:14px;color:#333;}\"\n    \".pwm-form input[type='number']{width:100%;padding:8px 10px;border-radius:6px;\"\n    \"border:1px solid #ccc;box-sizing:border-box;font-size:14px;}\"\n    \".pwm-form input[type='submit']{align-self:flex-start;padding:8px 14px;border:none;\"\n    \"border-radius:6px;cursor:pointer;background:#1976d2;color:#ffffff;font-weight:bold;font-size:14px;\"\n    \"transition:background-color 0.2s;}\"\n    \".pwm-form input[type='submit']:hover{background:#0d47a1;}\"\n    \".pwm-value{margin-top:6px;font-size:14px;color:#333;}\"\n    \".voltage-box{background:#1565c0;border-radius:10px;padding:8px 10px;}\"\n    \".voltage-box iframe{border:0;width:100%;height:50px;}\"\n    \"&lt;\/style&gt;\"\n    \"&lt;\/head&gt;&lt;body&gt;&lt;div class='container'&gt;\"\n    \"&lt;h1&gt;ESP32 LED &amp;amp; PWM Control&lt;\/h1&gt;\"\n    \"&lt;div class='subtitle'&gt;Control LEDs, set LED brightness and display voltage&lt;\/div&gt;\"\n    \"&lt;div class='section'&gt;&lt;div class='section-title'&gt;LEDs&lt;\/div&gt;\"\n    \"&lt;div class='led-grid'&gt;\"\n  );\n\n  \/\/ LED icons\n  for(int i=0; i&lt;3; i++){\n    if (ledOn[i]) {\n      myPage += String(F(\"&lt;a href=\\\"\/led\"));\n      myPage += String(i);\n      myPage += String(F(\"_off\\\" class='led-tile led-on'&gt;LED \"));\n      myPage += String(i);\n      myPage += String(F(\" is ON&lt;\/a&gt;\"));\n    } else {\n      myPage += String(F(\"&lt;a href=\\\"\/led\"));\n      myPage += String(i);\n      myPage += String(F(\"_on\\\" class='led-tile led-off'&gt;LED \"));\n      myPage += String(i);\n      myPage += String(F(\" is OFF&lt;\/a&gt;\"));\n    }\n  }\n\n  myPage += F(\"&lt;\/div&gt;&lt;\/div&gt;\"); \/\/ Ende LED-Section\n\n  \/\/ PWM section\n  myPage += F(\n\"&lt;div class='section'&gt;&lt;div class='section-title'&gt;PWM for LED (Pin 5)&lt;\/div&gt;\"\n\"&lt;form class='pwm-form' action='\/set'&gt;\"\n\"&lt;label for='pwmInput'&gt;PWM value (0 \u2013 255):&lt;\/label&gt;\"\n\"&lt;input id='pwmInput' name='value' type='number' min='0' max='255' placeholder='e.g. 128'&gt;\"\n\"&lt;input type='submit' value='Set PWM'&gt;\"\n\"&lt;\/form&gt;\"\n  );\n  myPage += F(\"&lt;div class='pwm-value'&gt;Current PWM value: \");\n  myPage += String(pwmValue);\n  myPage += F(\"&lt;\/div&gt;&lt;\/div&gt;\");\n\n  \/\/ voltage section\n  myPage += F(\n\"&lt;div class='section'&gt;&lt;div class='section-title'&gt;Voltage at Pin 34&lt;\/div&gt;\"\n\"&lt;div class='voltage-box'&gt;\"\n\"&lt;iframe src='\/value'&gt;&lt;\/iframe&gt;\"\n\"&lt;\/div&gt;&lt;\/div&gt;\"\n  );\n\n  \/\/ page end\n  myPage += F(\"&lt;\/div&gt;&lt;\/body&gt;&lt;\/html&gt;\");\n\n  return myPage;\n}\n\nvoid connectWiFi() {\n  WiFi.mode(WIFI_STA);\n  WiFi.config(ip, gateway, subnet);\n  WiFi.begin(WIFI_SSID, WIFI_PASS);\n  Serial.printf(\"Connecting to %s\", WIFI_SSID);\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(250); Serial.print(\".\");\n  }\n  Serial.println();\n  Serial.print(\"IP: \"); Serial.println(WiFi.localIP());\n}\n\nvoid setup() {\n  Serial.begin(115200);\n  \n  for(int i=0; i&lt;3; i++) {\n    pinMode(ledPin[i], OUTPUT);\n    setLed(i, false);\n  }\n  pinMode(analogReadPin, INPUT);\n  ledcAttach(pwmLedPin, 5000, 8);\n\n  connectWiFi();\n\n  server.on(\"\/\", HTTP_GET, [](AsyncWebServerRequest *req){\n    req-&gt;send(200, \"text\/html\", page());\n  });\n\n  server.on(\"\/led0_on\", HTTP_GET, [](AsyncWebServerRequest *req){\n    setLed(0, true);\n    req-&gt;send(200, \"text\/html\", page());\n    \/\/ req-&gt;redirect(\"\/\"); \/\/ alternative to the line above\n  });\n\n  server.on(\"\/led0_off\", HTTP_GET, [](AsyncWebServerRequest *req){\n    setLed(0, false);\n    req-&gt;send(200, \"text\/html\", page());\n    \/\/ req-&gt;redirect(\"\/\"); \/\/ alternative to the line above\n  });\n\n  server.on(\"\/led1_on\", HTTP_GET, [](AsyncWebServerRequest *req){\n    setLed(1, true);\n    req-&gt;send(200, \"text\/html\", page());\n    \/\/ req-&gt;redirect(\"\/\"); \/\/ alternative to the line above\n  });\n\n  server.on(\"\/led1_off\", HTTP_GET, [](AsyncWebServerRequest *req){\n    setLed(1, false);\n    req-&gt;send(200, \"text\/html\", page());\n    \/\/ req-&gt;redirect(\"\/\"); \/\/ alternative to the line above\n  });\n\n  server.on(\"\/led2_on\", HTTP_GET, [](AsyncWebServerRequest *req){\n    setLed(2, true);\n    req-&gt;send(200, \"text\/html\", page());\n    \/\/ req-&gt;redirect(\"\/\"); \/\/ alternative to the line above\n  });\n\n  server.on(\"\/led2_off\", HTTP_GET, [](AsyncWebServerRequest *req){\n    setLed(2, false);\n    req-&gt;send(200, \"text\/html\", page());\n    \/\/ req-&gt;redirect(\"\/\"); \/\/ alternative to the line above\n  });\n\n  server.on(\"\/set\", HTTP_GET, [](AsyncWebServerRequest *request){\n    if (request-&gt;hasParam(\"value\")) {\n      pwmValue = request-&gt;getParam(\"value\")-&gt;value().toInt();\n      pwmValue = constrain(pwmValue, 0, 255);\n      ledcWrite(pwmLedPin, pwmValue);\n      request-&gt;send(200, \"text\/html\", page());\n      \/\/ req-&gt;redirect(\"\/\"); \/\/ alternative to the line above\n    }\n  });\n  \n  server.on(\"\/value\", HTTP_GET, [](AsyncWebServerRequest *req){\n    float voltage = 3.3 * analogRead(analogReadPin) \/ 4096.0;\n    String html = \"\";\n    html += \"&lt;html&gt;&lt;head&gt;&lt;meta http-equiv='refresh' content='5'&gt;\";\n    html += \"&lt;meta charset='UTF-8'&gt;&lt;style&gt;\";\n    html += \"body{margin:0;font-family:Arial,Helvetica,sans-serif;}\";\n    html += \".voltage-box{background:#1565c0;color:#ffffff;padding:8px 10px;\";\n    html += \"display:flex;align-items:center;justify-content:space-between;\";\n    html += \"height:100%;box-sizing:border-box;font-size:14px;}\";\n    html += \".label{font-weight:bold;margin-right:8px;}\";\n    html += \"&lt;\/style&gt;&lt;\/head&gt;\";\n    html += \"&lt;body&gt;&lt;div class='voltage-box'&gt;&lt;span class='label'&gt;Voltage [V]:&lt;\/span&gt;&lt;span&gt;\";\n    html += String(voltage, 2);\n    html += \"&lt;\/span&gt;&lt;\/div&gt;&lt;\/body&gt;&lt;\/html&gt;\";\n\n    req-&gt;send(200, \"text\/html\", html);\n  });\n    \n  server.begin();\n  Serial.println(\"Server ready.\");\n}\n\nvoid loop() {}<\/pre>\n<\/div>\n<p>\n<p>Here is the result:<\/p>\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/nicer_with_css-1024x429.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"429\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/nicer_with_css-1024x429.png\" alt=\"Async WebServer - Browser output of nicer_with_css.ino\" class=\"wp-image-25127\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/nicer_with_css-1024x429.png 1024w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/nicer_with_css-300x126.png 300w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/nicer_with_css-768x321.png 768w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/nicer_with_css.png 1228w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">Browser output of nicer_with_css.ino<\/figcaption><\/figure>\n\n<h3 class=\"wp-block-heading\" id=\"convenient_w_script\">More convenient with JavaScript<\/h3>\n<p>In addition to HTML and CSS, there is also JavaScript for website design. As the name suggests, this is a scripting language. JavaScript makes interacting with the website even more convenient and effective. I use JavaScript in the following sketch for the LED buttons and replace the input of the PWM value with a slider. JavaScript code is inserted within a <code>&lt;script&gt;...&lt;\/script&gt;<\/code> element.    <\/p>\n<p>A nice effect of the JavaScript code is that in this version the page does not have to be rebuilt again and again. Where previously the whole page was returned: <x id=\"gid_0\"><\/x>, only an OK is sent in this version: <x id=\"gid_1\"><\/x>. JavaScript takes care of updating the sliders and buttons.  <\/p>\n<p>Again, no problem if you are not familiar with JavaScript. ChatGPT can do that for you. I also only have basic knowledge of JavaScript.   <\/p>\n<\/p>\n<div class=\"scroll-paragraph\">\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"more_convenient_with_script.ino\" data-enlighter-title=\"more_convenient_with_script.ino\">#include &lt;WiFi.h&gt;\n#include &lt;AsyncTCP.h&gt;\n#include &lt;ESPAsyncWebServer.h&gt;\n\nIPAddress ip(192,168,178,112);  \/\/TODO\nIPAddress gateway(192,168,178,1);  \/\/TODO\nIPAddress subnet(255,255,255,0);  \/\/TODO\n\nconstexpr char WIFI_SSID[] = \"Your SSID\";  \/\/TODO\nconstexpr char WIFI_PASS[] = \"Your password\";  \/\/TODO\n\nconst int ledPin[] = {2, 4, 18};\nconst int analogReadPin = 34;\nconst int pwmLedPin = 5;\nint pwmValue = 0;\nbool ledOn[] = {false, false, false};\n\nAsyncWebServer server(80);\n\nvoid setLed(int num, bool on) {\n  ledOn[num] = on;\n  digitalWrite(ledPin[num], on);\n}\n\nString page() {\n  String myPage;\n\n  myPage += F(\n    \"&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;&lt;meta charset='UTF-8'&gt;\"\n    \"&lt;meta name='viewport' content='width=device-width, initial-scale=1.0'&gt;\"\n    \"&lt;title&gt;ESP32 LED &amp;amp; PWM Control&lt;\/title&gt;\"\n    \"&lt;style&gt;\"\n    \"body{font-family:Arial,Helvetica,sans-serif;background:#f0f2f5;margin:0;padding:0;}\"\n    \".container{max-width:600px;margin:40px auto;padding:20px;background:#ffffff;\"\n      \"border-radius:12px;box-shadow:0 4px 12px rgba(0,0,0,0.08);}\"\n    \"h1{font-size:24px;margin:0 0 10px 0;text-align:center;}\"\n    \".subtitle{text-align:center;font-size:13px;color:#666;margin-bottom:20px;}\"\n    \".section{margin:20px 0;}\"\n    \".section-title{font-weight:bold;margin-bottom:8px;font-size:15px;color:#333;}\"\n    \".led-grid{display:flex;gap:12px;justify-content:center;}\"\n    \".led-button{flex:1;padding:18px 10px;text-align:center;border-radius:10px;\"\n      \"font-weight:bold;border:none;cursor:pointer;color:#ffffff;\"\n      \"transition:background-color 0.2s,transform 0.1s,box-shadow 0.1s;\"\n      \"box-shadow:0 2px 4px rgba(0,0,0,0.1);font-size:14px;}\"\n    \".led-on{background:#e53935;}\"\n    \".led-on:hover{background:#b71c1c;}\"\n    \".led-off{background:#43a047;}\"\n    \".led-off:hover{background:#1b5e20;}\"\n    \".led-button:active{transform:translateY(1px);box-shadow:0 1px 2px rgba(0,0,0,0.2);}\"\n    \".pwm-form{display:flex;flex-direction:column;gap:8px;}\"\n    \".pwm-form label{font-size:14px;color:#333;}\"\n    \".pwm-form input[type='range']{width:100%;}\"\n    \".voltage-box{background:#1565c0;border-radius:10px;padding:8px 10px;}\"\n    \".voltage-box iframe{border:0;width:100%;height:50px;}\"\n    \"&lt;\/style&gt;\"\n    \"&lt;\/head&gt;&lt;body&gt;&lt;div class='container'&gt;\"\n    \"&lt;h1&gt;ESP32 LED &amp;amp; PWM Control&lt;\/h1&gt;\"\n    \"&lt;div class='subtitle'&gt;Control LEDs, set LED brightness and display voltage&lt;\/div&gt;\"\n    \"&lt;div class='section'&gt;&lt;div class='section-title'&gt;LEDs&lt;\/div&gt;\"\n    \"&lt;div class='led-grid'&gt;\"\n  );\n\n  \/\/ LED Buttons\n  for (int i = 0; i &lt; 3; i++) {\n    myPage += \"&lt;button type='button' class='led-button \";\n    if (ledOn[i]) {\n      myPage += \"led-on\";\n    } else {\n      myPage += \"led-off\";\n    }\n    myPage += \"' data-index='\";\n    myPage += String(i);\n    myPage += \"' data-state='\";\n    myPage += (ledOn[i] ? \"1\" : \"0\");\n    myPage += \"'&gt;\";\n    myPage += \"LED \";\n    myPage += String(i);\n    myPage += (ledOn[i] ? \" is ON\" : \" is OFF\");\n    myPage += \"&lt;\/button&gt;\";\n  }\n\n  myPage += F(\"&lt;\/div&gt;&lt;\/div&gt;\"); \/\/ Ende LED-Section\n\n  \/\/ PWM section (Slider)\n  myPage += F(\n    \"&lt;div class='section'&gt;&lt;div class='section-title'&gt;PWM for LED (Pin 5)&lt;\/div&gt;\"\n    \"&lt;div class='pwm-form'&gt;\"\n    \"&lt;label for='pwmSlider'&gt;PWM value (0 \u2013 255): &lt;span id='pwmValueLabel'&gt;&lt;\/span&gt;&lt;\/label&gt;\"\n  );\n\n  myPage += \"&lt;input id='pwmSlider' type='range' min='0' max='255' value='\";\n  myPage += String(pwmValue);\n  myPage += \"'&gt;\";\n\n  myPage += F(\"&lt;\/div&gt;&lt;\/div&gt;\");\n\n  \/\/ Voltage section (iframe)\n  myPage += F(\n    \"&lt;div class='section'&gt;&lt;div class='section-title'&gt;Voltage at Pin 34&lt;\/div&gt;\"\n    \"&lt;div class='voltage-box'&gt;\"\n    \"&lt;iframe src='\/value'&gt;&lt;\/iframe&gt;\"\n    \"&lt;\/div&gt;&lt;\/div&gt;\"\n  );\n\n  \/\/ JavaScript for buttons &amp; slider\n  myPage += F(\n    \"&lt;script&gt;\"\n    \"document.addEventListener('DOMContentLoaded', function(){\"\n      \"\/\/ LED Buttons\\n\"\n      \"var ledButtons = document.querySelectorAll('.led-button');\"\n      \"ledButtons.forEach(function(btn){\"\n        \"btn.addEventListener('click', function(){\"\n          \"var index = btn.getAttribute('data-index');\"\n          \"var state = btn.getAttribute('data-state') === '1';\"\n          \"var url = '\/led' + index + '_' + (state ? 'off' : 'on');\"\n          \"fetch(url)\"\n            \".then(function(){\"\n              \"var newState = !state;\"\n              \"btn.setAttribute('data-state', newState ? '1' : '0');\"\n              \"btn.classList.toggle('led-on', newState);\"\n              \"btn.classList.toggle('led-off', !newState);\"\n              \"btn.textContent = 'LED ' + index + (newState ? ' is ON' : ' is OFF');\"\n            \"})\"\n            \".catch(function(err){console.error(err);});\"\n        \"});\"\n      \"});\"\n\n      \"\/\/ PWM Slider\\n\"\n      \"var slider = document.getElementById('pwmSlider');\"\n      \"var valueLabel = document.getElementById('pwmValueLabel');\"\n      \"if(slider &amp;&amp; valueLabel){\"\n        \"var updateLabel = function(v){\"\n          \"valueLabel.textContent = v;\"\n        \"};\"\n        \"updateLabel(slider.value);\"\n        \"slider.addEventListener('input', function(e){\"\n          \"updateLabel(e.target.value);\"\n        \"});\"\n        \"slider.addEventListener('change', function(e){\"\n          \"var v = e.target.value;\"\n          \"fetch('\/set?value=' + v)\"\n            \".catch(function(err){console.error(err);});\"\n        \"});\"\n      \"}\"\n    \"});\"\n    \"&lt;\/script&gt;\"\n  );\n\n  myPage += F(\"&lt;\/div&gt;&lt;\/body&gt;&lt;\/html&gt;\");\n\n  return myPage;\n}\n\nvoid connectWiFi() {\n  WiFi.mode(WIFI_STA);\n  WiFi.config(ip, gateway, subnet);\n  WiFi.begin(WIFI_SSID, WIFI_PASS);\n  Serial.printf(\"Connecting to %s\", WIFI_SSID);\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(250);\n    Serial.print(\".\");\n  }\n  Serial.println();\n  Serial.print(\"IP: \");\n  Serial.println(WiFi.localIP());\n}\n\nvoid setup() {\n  Serial.begin(115200);\n\n  for (int i = 0; i &lt; 3; i++) {\n    pinMode(ledPin[i], OUTPUT);\n    setLed(i, false);\n  }\n  pinMode(analogReadPin, INPUT);\n\n  \/\/ PWM initialization\n  ledcAttach(pwmLedPin, 5000, 8);   \n\n  connectWiFi();\n\n  server.on(\"\/\", HTTP_GET, [](AsyncWebServerRequest *req){\n    req-&gt;send(200, \"text\/html\", page());\n  });\n\n  \/\/ LED endpoint: only \"OK\" is send, no complete refresh\n  server.on(\"\/led0_on\", HTTP_GET, [](AsyncWebServerRequest *req){\n    setLed(0, true);\n    req-&gt;send(200, \"text\/plain\", \"OK\");\n  });\n\n  server.on(\"\/led0_off\", HTTP_GET, [](AsyncWebServerRequest *req){\n    setLed(0, false);\n    req-&gt;send(200, \"text\/plain\", \"OK\");\n  });\n\n  server.on(\"\/led1_on\", HTTP_GET, [](AsyncWebServerRequest *req){\n    setLed(1, true);\n    req-&gt;send(200, \"text\/plain\", \"OK\");\n  });\n\n  server.on(\"\/led1_off\", HTTP_GET, [](AsyncWebServerRequest *req){\n    setLed(1, false);\n    req-&gt;send(200, \"text\/plain\", \"OK\");\n  });\n\n  server.on(\"\/led2_on\", HTTP_GET, [](AsyncWebServerRequest *req){\n    setLed(2, true);\n    req-&gt;send(200, \"text\/plain\", \"OK\");\n  });\n\n  server.on(\"\/led2_off\", HTTP_GET, [](AsyncWebServerRequest *req){\n    setLed(2, false);\n    req-&gt;send(200, \"text\/plain\", \"OK\");\n  });\n\n  \/\/ PWM endpoint, now for the slider\n  server.on(\"\/set\", HTTP_GET, [](AsyncWebServerRequest *request){\n    if (request-&gt;hasParam(\"value\")) {\n      pwmValue = request-&gt;getParam(\"value\")-&gt;value().toInt();\n      pwmValue = constrain(pwmValue, 0, 255);\n      ledcWrite(pwmLedPin, pwmValue);\n      request-&gt;send(200, \"text\/plain\", \"OK\");\n    } else {\n      request-&gt;send(400, \"text\/plain\", \"Missing value\");\n    }\n  });\n\n  \/\/ Voltage page, kept iframe\n  server.on(\"\/value\", HTTP_GET, [](AsyncWebServerRequest *req){\n    float voltage = 3.3 * analogRead(analogReadPin) \/ 4096.0;\n    String html = \"\";\n    html += \"&lt;html&gt;&lt;head&gt;&lt;meta http-equiv='refresh' content='5'&gt;\";\n    html += \"&lt;meta charset='UTF-8'&gt;&lt;style&gt;\";\n    html += \"body{margin:0;font-family:Arial,Helvetica,sans-serif;}\";\n    html += \".voltage-box{background:#1565c0;color:#ffffff;padding:8px 10px;\";\n    html += \"display:flex;align-items:center;justify-content:space-between;\";\n    html += \"height:100%;box-sizing:border-box;font-size:14px;}\";\n    html += \".label{font-weight:bold;margin-right:8px;}\";\n    html += \"&lt;\/style&gt;&lt;\/head&gt;\";\n    html += \"&lt;body&gt;&lt;div class='voltage-box'&gt;&lt;span class='label'&gt;Voltage [V]:&lt;\/span&gt;&lt;span&gt;\";\n    html += String(voltage, 2);\n    html += \"&lt;\/span&gt;&lt;\/div&gt;&lt;\/body&gt;&lt;\/html&gt;\";\n\n    req-&gt;send(200, \"text\/html\", html);\n  });\n\n  server.begin();\n  Serial.println(\"Server ready.\");\n}\n\nvoid loop() {}<\/pre>\n<\/div>\n<p>\n<p>Here is the result:<\/p>\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/convenient_with_script-1024x384.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"384\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/convenient_with_script-1024x384.png\" alt=\"Browser output of more_convenient_with_script.ino\" class=\"wp-image-25131\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/convenient_with_script-1024x384.png 1024w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/convenient_with_script-300x112.png 300w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/convenient_with_script-768x288.png 768w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/convenient_with_script.png 1166w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">Browser output of more_convenient_with_script.ino<\/figcaption><\/figure>\n\n<h4 class=\"wp-block-heading\">Alternative version of the sketch with JavaScript<\/h4>\n<p>Or would you like a different style for the LED buttons? That&#8217;s in the next sketch. I have also changed the method of the voltage update here. Instead of the iframe, JavaScript with a &#8220;fetch&#8221; function is used here. The update interval is set with <code>setInterval(updateVoltage, 5000);<\/code>.     <\/p>\n<\/p>\n<div class=\"scroll-paragraph\">\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"alternative_sketch_with_script.ino\" data-enlighter-title=\"alternative_sketch_with_script.ino\">#include &lt;WiFi.h&gt;\n#include &lt;AsyncTCP.h&gt;\n#include &lt;ESPAsyncWebServer.h&gt;\n\nIPAddress ip(192,168,178,112);  \/\/TODO\nIPAddress gateway(192,168,178,1);  \/\/TODO\nIPAddress subnet(255,255,255,0);  \/\/TODO\n\nconstexpr char WIFI_SSID[] = \"Your SSID\";  \/\/TODO\nconstexpr char WIFI_PASS[] = \"Your Password\";  \/\/TODO\n\nconst int ledPin[] = {2, 4, 18};\nconst int analogReadPin = 34;\nconst int pwmLedPin = 5;\nint pwmValue = 0;\nbool ledOn[] = {false, false, false};\n\nAsyncWebServer server(80);\n\nvoid setLed(int num, bool on) {\n  ledOn[num] = on;\n  digitalWrite(ledPin[num], on);\n}\n\nString page() {\n  String myPage;\n\n  myPage += F(\n    \"&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;&lt;meta charset='UTF-8'&gt;\"\n    \"&lt;meta name='viewport' content='width=device-width, initial-scale=1.0'&gt;\"\n    \"&lt;title&gt;ESP32 LED &amp;amp; PWM Control&lt;\/title&gt;\"\n    \"&lt;style&gt;\"\n    \"body{font-family:Arial,Helvetica,sans-serif;background:#f0f2f5;margin:0;padding:0;}\"\n    \".container{max-width:600px;margin:40px auto;padding:20px;background:#ffffff;\"\n      \"border-radius:12px;box-shadow:0 4px 12px rgba(0,0,0,0.08);}\"\n    \"h1{font-size:24px;margin:0 0 10px 0;text-align:center;}\"\n    \".subtitle{text-align:center;font-size:13px;color:#666;margin-bottom:20px;}\"\n    \".section{margin:20px 0;}\"\n    \".section-title{font-weight:bold;margin-bottom:8px;font-size:15px;color:#333;}\"\n\n    \/* LED rows + switches *\/\n    \".led-list{display:flex;flex-direction:column;gap:10px;}\"\n    \".led-row{display:flex;align-items:center;justify-content:space-between;\"\n      \"padding:10px 12px;border-radius:8px;background:#f7f7f7;}\"\n    \".led-label{font-size:14px;font-weight:bold;color:#333;}\"\n\n    \/* LED switch toggle *\/\n    \".switch{position:relative;display:inline-block;width:46px;height:24px;}\"\n    \".switch input{opacity:0;width:0;height:0;}\"\n    \".slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;\"\n      \"background-color:#ccc;transition:.2s;border-radius:24px;}\"\n    \".slider:before{position:absolute;content:'';height:18px;width:18px;\"\n      \"left:3px;bottom:3px;background-color:white;transition:.2s;border-radius:50%;}\"\n    \"input:checked + .slider{background-color:#43a047;}\"      \/* ON color *\/\n    \"input:checked + .slider:before{transform:translateX(22px);}\"\n\n    \".pwm-form{display:flex;flex-direction:column;gap:8px;}\"\n    \".pwm-form label{font-size:14px;color:#333;}\"\n    \".pwm-form input[type='range']{width:100%;}\"\n\n    \".voltage-box{background:#1565c0;border-radius:10px;padding:8px 10px;\"\n      \"color:#ffffff;display:flex;align-items:center;justify-content:space-between;}\"\n    \".label{font-weight:bold;margin-right:8px;}\"\n    \"&lt;\/style&gt;\"\n    \"&lt;\/head&gt;&lt;body&gt;&lt;div class='container'&gt;\"\n    \"&lt;h1&gt;ESP32 LED &amp;amp; PWM Control&lt;\/h1&gt;\"\n    \"&lt;div class='subtitle'&gt;Control LEDs, set LED brightness and display voltage&lt;\/div&gt;\"\n    \"&lt;div class='section'&gt;&lt;div class='section-title'&gt;LEDs&lt;\/div&gt;\"\n    \"&lt;div class='led-list'&gt;\"\n  );\n\n  for (int i = 0; i &lt; 3; i++) {\n    myPage += \"&lt;div class='led-row'&gt;\";\n    myPage += \"&lt;span class='led-label'&gt;LED \";\n    myPage += String(i);\n    myPage += \"&lt;\/span&gt;\";\n    myPage += \"&lt;label class='switch'&gt;\";\n    myPage += \"&lt;input type='checkbox' class='led-toggle' data-index='\";\n    myPage += String(i);\n    myPage += \"'\";\n    if (ledOn[i]) {\n      myPage += \" checked\";\n    }\n    myPage += \"&gt;\";\n    myPage += \"&lt;span class='slider'&gt;&lt;\/span&gt;\";\n    myPage += \"&lt;\/label&gt;\";\n    myPage += \"&lt;\/div&gt;\";\n  }\n\n  myPage += F(\"&lt;\/div&gt;&lt;\/div&gt;\"); \/\/ end LED switches\n\n  \/\/ PWM section (Slider)\n  myPage += F(\n    \"&lt;div class='section'&gt;&lt;div class='section-title'&gt;PWM for LED (Pin 5)&lt;\/div&gt;\"\n    \"&lt;div class='pwm-form'&gt;\"\n    \"&lt;label for='pwmSlider'&gt;PWM value (0 \u2013 255): &lt;span id='pwmValueLabel'&gt;&lt;\/span&gt;&lt;\/label&gt;\"\n  );\n\n  myPage += \"&lt;input id='pwmSlider' type='range' min='0' max='255' value='\";\n  myPage += String(pwmValue);\n  myPage += \"'&gt;\";\n\n  myPage += F(\"&lt;\/div&gt;&lt;\/div&gt;\");\n\n  \/\/ Voltage section (JSON + fetch)\n  myPage += F(\n    \"&lt;div class='section'&gt;&lt;div class='section-title'&gt;Voltage at Pin 34&lt;\/div&gt;\"\n    \"&lt;div class='voltage-box'&gt;\"\n      \"&lt;span class='label'&gt;Voltage [V]:&lt;\/span&gt;\"\n      \"&lt;span id='voltageValue'&gt;--.--&lt;\/span&gt;\"\n    \"&lt;\/div&gt;&lt;\/div&gt;\"\n  );\n\n  \/\/ JavaScript for LEDs, PWM slider &amp; voltage update\n  myPage += F(\n    \"&lt;script&gt;\"\n    \"document.addEventListener('DOMContentLoaded', function(){\"\n      \"\/\/ LED Toggles\\n\"\n      \"var toggles = document.querySelectorAll('.led-toggle');\"\n      \"toggles.forEach(function(cb){\"\n        \"cb.addEventListener('change', function(){\"\n          \"var index = cb.getAttribute('data-index');\"\n          \"var state = cb.checked ? 1 : 0;\"\n          \"fetch('\/api\/led?index=' + index + '&amp;state=' + state)\"\n            \".then(function(res){\"\n              \"if(!res.ok) throw new Error('LED request failed');\"\n            \"})\"\n            \".catch(function(err){\"\n              \"console.error(err);\"\n              \"cb.checked = !cb.checked;\"  \n            \"});\"\n        \"});\"\n      \"});\"\n\n      \"\/\/ PWM Slider\\n\"\n      \"var slider = document.getElementById('pwmSlider');\"\n      \"var valueLabel = document.getElementById('pwmValueLabel');\"\n      \"if(slider &amp;&amp; valueLabel){\"\n        \"var updateLabel = function(v){\"\n          \"valueLabel.textContent = v;\"\n        \"};\"\n        \"updateLabel(slider.value);\"\n        \"slider.addEventListener('input', function(e){\"\n          \"updateLabel(e.target.value);\"\n        \"});\"\n        \"slider.addEventListener('change', function(e){\"\n          \"var v = e.target.value;\"\n          \"fetch('\/set?value=' + v)\"\n            \".catch(function(err){console.error(err);});\"\n        \"});\"\n      \"}\"\n\n      \"\/\/ Voltage via JSON + fetch\\n\"\n      \"var voltageSpan = document.getElementById('voltageValue');\"\n      \"function updateVoltage(){\"\n        \"fetch('\/api\/voltage')\"\n          \".then(function(res){\"\n            \"if(!res.ok) throw new Error('Voltage request failed');\"\n            \"return res.json();\"\n          \"})\"\n          \".then(function(data){\"\n            \"if(voltageSpan &amp;&amp; typeof data.voltage === 'number'){\"\n              \"voltageSpan.textContent = data.voltage.toFixed(2);\"\n            \"}\"\n          \"})\"\n          \".catch(function(err){console.error(err);});\"\n      \"}\"\n      \"updateVoltage();\"\n      \"setInterval(updateVoltage, 5000);\"\n    \"});\"\n    \"&lt;\/script&gt;\"\n  );\n\n  myPage += F(\"&lt;\/div&gt;&lt;\/body&gt;&lt;\/html&gt;\");\n\n  return myPage;\n}\n\nvoid connectWiFi() {\n  WiFi.mode(WIFI_STA);\n  WiFi.config(ip, gateway, subnet);\n  WiFi.begin(WIFI_SSID, WIFI_PASS);\n  Serial.printf(\"Connecting to %s\", WIFI_SSID);\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(250);\n    Serial.print(\".\");\n  }\n  Serial.println();\n  Serial.print(\"IP: \");\n  Serial.println(WiFi.localIP());\n}\n\nvoid setup() {\n  Serial.begin(115200);\n\n  for (int i = 0; i &lt; 3; i++) {\n    pinMode(ledPin[i], OUTPUT);\n    setLed(i, false);\n  }\n  pinMode(analogReadPin, INPUT);\n\n  ledcAttach(pwmLedPin, 5000, 8);\n\n  connectWiFi();\n\n  server.on(\"\/\", HTTP_GET, [](AsyncWebServerRequest *req){\n    req-&gt;send(200, \"text\/html\", page());\n  });\n\n  \/\/ generic LED endpoint: \/api\/led?index=0&amp;state=1\n  server.on(\"\/api\/led\", HTTP_GET, [](AsyncWebServerRequest *req){\n    if (req-&gt;hasParam(\"index\") &amp;&amp; req-&gt;hasParam(\"state\")) {\n      int index = req-&gt;getParam(\"index\")-&gt;value().toInt();\n      int state = req-&gt;getParam(\"state\")-&gt;value().toInt();\n      if (index &gt;= 0 &amp;&amp; index &lt; 3) {\n        setLed(index, state != 0);\n        req-&gt;send(200, \"text\/plain\", \"OK\");\n      } else {\n        req-&gt;send(400, \"text\/plain\", \"Invalid index\");\n      }\n    } else {\n      req-&gt;send(400, \"text\/plain\", \"Missing index or state\");\n    }\n  });\n\n  \/\/ PWM endpoint \n  server.on(\"\/set\", HTTP_GET, [](AsyncWebServerRequest *request){\n    if (request-&gt;hasParam(\"value\")) {\n      pwmValue = request-&gt;getParam(\"value\")-&gt;value().toInt();\n      pwmValue = constrain(pwmValue, 0, 255);\n      ledcWrite(pwmLedPin, pwmValue);\n      request-&gt;send(200, \"text\/plain\", \"OK\");\n    } else {\n      request-&gt;send(400, \"text\/plain\", \"Missing value\");\n    }\n  });\n\n  \/\/ JSON endpoint for voltage\n  server.on(\"\/api\/voltage\", HTTP_GET, [](AsyncWebServerRequest *req){\n    float voltage = 3.3 * analogRead(analogReadPin) \/ 4096.0;\n    String json = \"{\\\"voltage\\\":\";\n    json += String(voltage, 3);\n    json += \"}\";\n    req-&gt;send(200, \"application\/json\", json);\n  });\n\n  server.begin();\n  Serial.println(\"Server ready.\");\n}\n\nvoid loop() {}\n<\/pre>\n<\/div>\n<p>\n<p>Here is the output in the browser:<\/p>\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/script_advanced-1024x449.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"449\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/script_advanced-1024x449.png\" alt=\"Async WebServer - Browser output of alternative_sketch_with_script.ino\" class=\"wp-image-25133\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/script_advanced-1024x449.png 1024w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/script_advanced-300x132.png 300w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/script_advanced-768x337.png 768w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/script_advanced.png 1135w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">Browser output of alternative_sketch_with_script.ino<\/figcaption><\/figure>\n<p>As you can see, the whole thing no longer has much to do with the actual topic, the Async WebServer. The complexity of the last sketches is rather due to their HTML, CSS and JavaScript content. <\/p>\n\n<h2 class=\"wp-block-heading\" id=\"multiple_esp32\">Controlling multiple ESP32s<\/h2>\n<p>If you want to control several ESP32s, you could create a web page for each of them. But that would be rather inconvenient. To combine everything on one website, use one of the ESP32 boards as the master. The other ESP32 boards (the &#8220;workers&#8221;) serve the master. However, the master also takes on worker tasks and therefore has a dual role. In relation to the device that displays the website, the master ESP32 is the server.      <\/p>\n<p>There are various ways in which the workers and the master communicate with each other. I would like to introduce the following: <\/p>\n<ol>\n<li>The workers are Async WebServers. Accordingly, the master is a client in relation to the workers. <\/li>\n<li>Communication between master and client via ESP-NOW Serial<a href=\"https:\/\/wolles-elektronikkiste.de\/en\/esp-now-serial\" target=\"_blank\" rel=\"noopener\">(see article<\/a>). ESP-NOW<a href=\"https:\/\/wolles-elektronikkiste.de\/en\/esp-now\" target=\"_blank\" rel=\"noopener\">(see article<\/a>) works in the same way, but ESP-NOW Serial is somewhat easier to use and is sufficient for this purpose.\n<ol style=\"list-style-type: lower-alpha;\">\n<li>All communication is via the router. The ESP32 boards work in STA mode and are part of the router&#8217;s network. <\/li>\n<li>The master ESP32 works in combined STA and AP mode. It connects to the router&#8217;s network, but also provides a network itself into which the workers can connect. The workers only &#8220;see&#8221; the master ESP32.  <\/li>\n<\/ol>\n<\/li>\n<\/ol>\n<p>To focus on the essential aspects in this section, I will not use CSS and JavaScript in the following examples. You can do this yourself according to your wishes &#8211; or consult an AI. We also only switch one LED per board. PWM control and voltage measurement are retained. We have dispensed with automatic updating of the voltage value here.    <\/p>\n<p>At the very <a href=\"#multiple_esp32_asw_espnow_sta_complete\">end of the article<\/a> there is a more stylish version with CSS, JavaScript and automatic refresh. <\/p>\n\n<h3 class=\"wp-block-heading multiple_esp32_asw\" id=\"multiple_esp32_asw\">Option 1: Everything via HTTP (Async WebServer)<\/h3>\n<p>In this variant, Master-ESP32 is a server for the PC and a client for the workers. All ESP32 boards receive their IP address from the router.  <\/p>\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/PC_Router_3xESP32-1024x550.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"550\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/PC_Router_3xESP32-1024x550.png\" alt=\"Three ESP32, communication via HTTP (using the Async WebServer)  \" class=\"wp-image-25258\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/PC_Router_3xESP32-1024x550.png 1024w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/PC_Router_3xESP32-300x161.png 300w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/PC_Router_3xESP32-768x413.png 768w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/PC_Router_3xESP32-1320x709.png 1320w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/PC_Router_3xESP32.png 1500w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">Three ESP32, communication via HTTP (using the Async WebServer)  <\/figcaption><\/figure>\n<p>Now we need three sketches, namely one for the master\/worker 1 and one each for worker 2 and worker 3, whereby the latter only differ slightly. <\/p>\n\n<h4 class=\"wp-block-heading\">Concept of the sketch for the Master\/Worker 1<\/h4>\n<p>Switching and dimming the LEDs and determining the voltage on the master\/worker 1 are nothing new in principle. Clicking on the corresponding links and sending the PWM value triggers the functions <code>setLocalLed()<\/code>, <code>setLocalPwm()<\/code> and <code>readLocalVoltage()<\/code>.  <\/p>\n<p>Things get more interesting when controlling workers 2 and 3. Clicking on the links to switch the LEDs and sending a PWM value leads to the helper function <code>callWorkerSimple(url)<\/code>. The URL is made up of the IP of the called worker, the relevant directory and the value. <\/p>\n<p>The voltage values are queried for two seconds in <code>loop()<\/code>. The function<br \/>is executed for this purpose. Within this function, the voltages of the workers are determined via the helper function <code>readWorker(url)<\/code>. Here, the URL consists of the IP of the worker and the voltage directory. However, the measured values in the browser are only updated when you click on the &#8220;Refresh page&#8221; link.    <\/p>\n<p>What our browser does automatically when we call up a website, we now have to do &#8220;manually&#8221;, namely send a GET request. This is done by the helper functions. However, for the master to be able to send a GET request at all, it must become a client. An HTTP client object is created for this purpose. This is also done by the helper functions.    <\/p>\n\n<h4 class=\"wp-block-heading\">Concept of the sketch for the Worker 2 and Worker 3<\/h4>\n<p>The sketches for Worker 2 and Worker 3 contain nothing new. They generate an Async WebServer that responds to the client&#8217;s requests. They themselves do not take care of the visual display in the browser, but switch and dim the LED and return the voltage value.   <\/p>\n<\/p>\n<div class=\"scroll-paragraph\">\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"three_esp32_asyncwebserver_only\" data-enlighter-title=\"three_esp32_asyncwebserver_only_master.ino\">#include &lt;WiFi.h&gt;\n#include &lt;AsyncTCP.h&gt;\n#include &lt;ESPAsyncWebServer.h&gt;\n#include &lt;HTTPClient.h&gt;\n\nIPAddress ip(192,168,178,112);  \/\/TODO\nIPAddress gateway(192,168,178,1);  \/\/TODO\nIPAddress subnet(255,255,255,0);  \/\/TODO\n\nconstexpr char WIFI_SSID[] = \"Your SSID\";  \/\/TODO\nconstexpr char WIFI_PASS[] = \"Your password\";  \/\/TODO\n\n\/\/ IP addresses of the two worker ESP32 boards\nconst char* ESP2_IP = \"192.168.178.113\";  \/\/TODO\nconst char* ESP3_IP = \"192.168.178.114\";  \/\/TODO\n\n\/\/ Local pins on the master (also acts as worker)\nconst int LED_PIN     = 2;\nconst int PWM_PIN     = 5;\nconst int ANALOG_PIN  = 34;\n\nint    localPwmValue   = 0;\nString masterVoltage   = \"\";\nString worker1Voltage  = \"\";\nString worker2Voltage  = \"\"; \n\nAsyncWebServer server(80);\n\n\/\/ Interval for voltage updates (in ms)\nconst unsigned long VOLTAGE_UPDATE_INTERVAL = 2000;\nunsigned long lastVoltageUpdate = 0;\n\n\/\/ ---------------- WiFi ----------------\n\nvoid connectWiFi() {\n  WiFi.mode(WIFI_STA);\n  WiFi.config(ip, gateway, subnet);\n  WiFi.begin(WIFI_SSID, WIFI_PASS);\n  Serial.print(\"Connecting to WiFi\");\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(500);\n    Serial.print(\".\");\n  }\n  Serial.println();\n  Serial.print(\"Connected. IP address: \");\n  Serial.println(WiFi.localIP());\n}\n\n\/\/ ---------------- HTTP helpers ----------------\n\n\/\/ Helper: call a worker URL (we ignore the response)\nvoid callWorkerSimple(const String&amp; url) {\n  HTTPClient http;\n  if (http.begin(url)) {\n    int code = http.GET();\n    Serial.print(\"Request to \");\n    Serial.print(url);\n    Serial.print(\" -&gt; HTTP code \");\n    Serial.println(code);\n    http.end();\n  } else {\n    Serial.print(\"Could not connect to \");\n    Serial.println(url);\n  }\n}\n\n\/\/ Helper: read a worker URL and return its response as String\nString readWorker(const String&amp; url) {\n  HTTPClient http;\n  String body = \"\";\n  if (http.begin(url)) {\n    int code = http.GET();\n    if (code &gt; 0) {\n      body = http.getString();\n    }\n    http.end();\n  } else {\n    Serial.print(\"Could not connect to \");\n    Serial.println(url);\n  }\n  return body;\n}\n\n\/\/ ---------------- Local helpers (master as worker) ----------------\n\nvoid setLocalLed(int state) {\n  digitalWrite(LED_PIN, state ? HIGH : LOW);\n}\n\nvoid setLocalPwm(int value) {\n  if (value &lt; 0) value = 0;\n  if (value &gt; 255) value = 255;\n  localPwmValue = value;\n  ledcWrite(PWM_PIN, localPwmValue);\n}\n\nfloat readLocalVoltage() {\n  int raw = analogRead(ANALOG_PIN);\n  float voltage = 3.3f * raw \/ 4096.0f;\n  return voltage;\n}\n\n\/\/ ---------------- Voltage update (periodic) ----------------\n\nvoid readAllVoltages() {\n  masterVoltage  = String(readLocalVoltage(), 2);\n\n  String worker1Url = String(\"http:\/\/\") + ESP2_IP + \"\/voltage\";\n  worker1Voltage = readWorker(worker1Url);\n  worker1Voltage.trim();\n\n  String worker2Url = String(\"http:\/\/\") + ESP3_IP + \"\/voltage\";\n  worker2Voltage = readWorker(worker2Url);\n  worker2Voltage.trim();\n\n  Serial.print(\"Voltages updated: master=\");\n  Serial.print(masterVoltage);\n  Serial.print(\" V, worker1=\");\n  Serial.print(worker1Voltage);\n  Serial.print(\" V, worker2=\");\n  Serial.print(worker2Voltage);\n  Serial.println(\" V\");\n}\n\n\/\/ ---------------- HTML page ----------------\n\nString buildMainPage() {\n  String html;\n  html += \"&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;&lt;meta charset='UTF-8'&gt;\";\n  html += \"&lt;title&gt;ESP32 Master Control&lt;\/title&gt;&lt;\/head&gt;&lt;body&gt;\";\n  html += \"&lt;h1&gt;ESP32 Master Control&lt;\/h1&gt;\";\n  html += \"&lt;p&gt;&lt;a href='\/'&gt;Refresh page&lt;\/a&gt;&lt;\/p&gt;\";\n\n  \/\/ Device 1: Master itself\n  html += \"&lt;h2&gt;Device 1 (Master ESP32)&lt;\/h2&gt;\";\n  html += \"&lt;p&gt;&lt;a href='\/esp1\/led?state=1'&gt;Turn LED ON&lt;\/a&gt;&lt;br&gt;\";\n  html += \"&lt;a href='\/esp1\/led?state=0'&gt;Turn LED OFF&lt;\/a&gt;&lt;\/p&gt;\";\n  html += \"&lt;form action='\/esp1\/pwm' method='get'&gt;\";\n  html += \"PWM value (0-255): &lt;input type='text' name='value'&gt;\";\n  html += \"&lt;input type='submit' value='Set PWM'&gt;\";\n  html += \"&lt;\/form&gt;\";\n  html += \"&lt;p&gt;Voltage [V]: \";\n  html += masterVoltage;\n  html += \"&lt;\/p&gt;&lt;hr&gt;\";\n\n  \/\/ Device 2: Worker 1\n  html += \"&lt;h2&gt;Device 2 (Worker ESP32 #2)&lt;\/h2&gt;\";\n  html += \"&lt;p&gt;&lt;a href='\/esp2\/led?state=1'&gt;Turn LED ON&lt;\/a&gt;&lt;br&gt;\";\n  html += \"&lt;a href='\/esp2\/led?state=0'&gt;Turn LED OFF&lt;\/a&gt;&lt;\/p&gt;\";\n  html += \"&lt;form action='\/esp2\/pwm' method='get'&gt;\";\n  html += \"PWM value (0-255): &lt;input type='text' name='value'&gt;\";\n  html += \"&lt;input type='submit' value='Set PWM'&gt;\";\n  html += \"&lt;\/form&gt;\";\n  html += \"&lt;p&gt;Voltage [V]: \"; \n  html += worker1Voltage;\n  html += \"&lt;\/p&gt;&lt;hr&gt;\";\n\n  \/\/ Device 3: Worker 2\n  html += \"&lt;h2&gt;Device 3 (Worker ESP32 #3)&lt;\/h2&gt;\";\n  html += \"&lt;p&gt;&lt;a href='\/esp3\/led?state=1'&gt;Turn LED ON&lt;\/a&gt;&lt;br&gt;\";\n  html += \"&lt;a href='\/esp3\/led?state=0'&gt;Turn LED OFF&lt;\/a&gt;&lt;\/p&gt;\";\n  html += \"&lt;form action='\/esp3\/pwm' method='get'&gt;\";\n  html += \"PWM value (0-255): &lt;input type='text' name='value'&gt;\";\n  html += \"&lt;input type='submit' value='Set PWM'&gt;\";\n  html += \"&lt;\/form&gt;\";\n  html += \"&lt;p&gt;Voltage [V]: \"; \n  html += worker2Voltage;\n  html += \"&lt;\/p&gt;&lt;hr&gt;\";\n\n  html += \"&lt;\/body&gt;&lt;\/html&gt;\";\n  return html;\n}\n\n\/\/ ---------------- Setup &amp; routes ----------------\n\nvoid setup() {\n  Serial.begin(115200);\n\n  pinMode(LED_PIN, OUTPUT);\n  digitalWrite(LED_PIN, LOW);\n  pinMode(ANALOG_PIN, INPUT);\n\n  ledcAttach(PWM_PIN, 5000, 8);   \/\/ 5 kHz, 8-bit (0..255)\n  ledcWrite(PWM_PIN, 0);\n\n  connectWiFi();\n\n  \/\/ initial voltage read to not produce an empty first page\n  readAllVoltages();\n  lastVoltageUpdate = millis();\n\n  \/\/ Main page\n  server.on(\"\/\", HTTP_GET, [](AsyncWebServerRequest* request) {\n    request-&gt;send(200, \"text\/html\", buildMainPage());\n  });\n\n  \/\/ ----------- Device 1 (Master itself) routes -----------\n\n  server.on(\"\/esp1\/led\", HTTP_GET, [](AsyncWebServerRequest* request) {\n    if (!request-&gt;hasParam(\"state\")) {\n      request-&gt;send(400, \"text\/plain\", \"Missing state parameter\");\n      return;\n    }\n    int state = request-&gt;getParam(\"state\")-&gt;value().toInt();\n    setLocalLed(state);\n    request-&gt;redirect(\"\/\");\n  });\n\n  server.on(\"\/esp1\/pwm\", HTTP_GET, [](AsyncWebServerRequest* request) {\n    if (!request-&gt;hasParam(\"value\")) {\n      request-&gt;send(400, \"text\/plain\", \"Missing value parameter\");\n      return;\n    }\n    int value = request-&gt;getParam(\"value\")-&gt;value().toInt();\n    setLocalPwm(value);\n    request-&gt;redirect(\"\/\");\n  });\n\n  \/\/ ----------- Device 2 (Worker ESP2) routes -----------\n\n  server.on(\"\/esp2\/led\", HTTP_GET, [](AsyncWebServerRequest* request) {\n    if (!request-&gt;hasParam(\"state\")) {\n      request-&gt;send(400, \"text\/plain\", \"Missing state parameter\");\n      return;\n    }\n    String state = request-&gt;getParam(\"state\")-&gt;value();\n    String url = String(\"http:\/\/\") + ESP2_IP + \"\/led?state=\" + state;\n    callWorkerSimple(url);\n    request-&gt;redirect(\"\/\");\n  });\n\n  server.on(\"\/esp2\/pwm\", HTTP_GET, [](AsyncWebServerRequest* request) {\n    if (!request-&gt;hasParam(\"value\")) {\n      request-&gt;send(400, \"text\/plain\", \"Missing value parameter\");\n      return;\n    }\n    String value = request-&gt;getParam(\"value\")-&gt;value();\n    String url = String(\"http:\/\/\") + ESP2_IP + \"\/pwm?value=\" + value;\n    callWorkerSimple(url);\n    request-&gt;redirect(\"\/\");\n  });\n\n  \/\/ ----------- Device 3 (Worker ESP3) routes -----------\n\n  server.on(\"\/esp3\/led\", HTTP_GET, [](AsyncWebServerRequest* request) {\n    if (!request-&gt;hasParam(\"state\")) {\n      request-&gt;send(400, \"text\/plain\", \"Missing state parameter\");\n      return;\n    }\n    String state = request-&gt;getParam(\"state\")-&gt;value();\n    String url = String(\"http:\/\/\") + ESP3_IP + \"\/led?state=\" + state;\n    callWorkerSimple(url);\n    request-&gt;redirect(\"\/\");\n  });\n\n  server.on(\"\/esp3\/pwm\", HTTP_GET, [](AsyncWebServerRequest* request) {\n    if (!request-&gt;hasParam(\"value\")) {\n      request-&gt;send(400, \"text\/plain\", \"Missing value parameter\");\n      return;\n    }\n    String value = request-&gt;getParam(\"value\")-&gt;value();\n    String url = String(\"http:\/\/\") + ESP3_IP + \"\/pwm?value=\" + value;\n    callWorkerSimple(url);\n    request-&gt;redirect(\"\/\");\n  });\n\n  server.begin();\n  Serial.println(\"Master server ready.\");\n}\n\n\/\/ ---------------- Loop ----------------\n\nvoid loop() {\n  unsigned long now = millis();\n  if (now - lastVoltageUpdate &gt;= VOLTAGE_UPDATE_INTERVAL) {\n    lastVoltageUpdate = now;\n    readAllVoltages();\n  }\n\n  delay(1);\n}\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"three_esp32_asyncwebserver_only\" data-enlighter-title=\"three_esp32_asyncwebserver_only_worker2.ino\">#include &lt;WiFi.h&gt;\n#include &lt;AsyncTCP.h&gt;\n#include &lt;ESPAsyncWebServer.h&gt;\n\nIPAddress ip(192,168,178,113);  \/\/TODO\nIPAddress gateway(192,168,178,1);  \/\/TODO\nIPAddress subnet(255,255,255,0);  \/\/TODO\n\nconstexpr char WIFI_SSID[] = \"Your SSID\";  \/\/TODO\nconstexpr char WIFI_PASS[] = \"Your password\";  \/\/TODO\n\nconst int LED_PIN     = 2;\nconst int PWM_PIN     = 5;\nconst int ANALOG_PIN  = 34;\n\nint pwmValue = 0;\n\nAsyncWebServer server(80);\n\nvoid connectWiFi() {\n  WiFi.mode(WIFI_STA);\n  WiFi.config(ip, gateway, subnet);\n  WiFi.begin(WIFI_SSID, WIFI_PASS);\n  Serial.print(\"Connecting to WiFi\");\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(500);\n    Serial.print(\".\");\n  }\n  Serial.println();\n  Serial.print(\"Connected. IP address: \");\n  Serial.println(WiFi.localIP());\n}\n\nvoid setup() {\n  Serial.begin(115200);\n\n  pinMode(LED_PIN, OUTPUT);\n  digitalWrite(LED_PIN, LOW);\n\n  pinMode(ANALOG_PIN, INPUT);\n\n  \/\/ Simple PWM setup\n  ledcAttach(PWM_PIN, 5000, 8);  \/\/ 5 kHz, 8-bit resolution\n  ledcWrite(PWM_PIN, 0);\n\n  connectWiFi();\n\n  \/\/ Optional simple root page (for debugging)\n  server.on(\"\/\", HTTP_GET, [](AsyncWebServerRequest* request) {\n    String html = \"&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;&lt;meta charset='UTF-8'&gt;&lt;title&gt;ESP32 Worker&lt;\/title&gt;&lt;\/head&gt;&lt;body&gt;\";\n    html += \"&lt;h1&gt;ESP32 Worker&lt;\/h1&gt;\";\n    html += \"&lt;p&gt;This device is controlled by a master ESP32.&lt;\/p&gt;\";\n    html += \"&lt;p&gt;Endpoints:&lt;\/p&gt;\";\n    html += \"&lt;ul&gt;\";\n    html += \"&lt;li&gt;\/led?state=0|1&lt;\/li&gt;\";\n    html += \"&lt;li&gt;\/pwm?value=0..255&lt;\/li&gt;\";\n    html += \"&lt;li&gt;\/voltage&lt;\/li&gt;\";\n    html += \"&lt;\/ul&gt;\";\n    html += \"&lt;\/body&gt;&lt;\/html&gt;\";\n    request-&gt;send(200, \"text\/html\", html);\n  });\n\n  \/\/ \/led?state=0|1\n  server.on(\"\/led\", HTTP_GET, [](AsyncWebServerRequest* request) {\n    if (request-&gt;hasParam(\"state\")) {\n      int state = request-&gt;getParam(\"state\")-&gt;value().toInt();\n      digitalWrite(LED_PIN, state ? HIGH : LOW);\n      String msg = \"LED state set to \";\n      msg += (state ? \"1\" : \"0\");\n      request-&gt;send(200, \"text\/plain\", msg);\n    } else {\n      request-&gt;send(400, \"text\/plain\", \"Missing state parameter\");\n    }\n  });\n\n  \/\/ \/pwm?value=0..255\n  server.on(\"\/pwm\", HTTP_GET, [](AsyncWebServerRequest* request) {\n    if (request-&gt;hasParam(\"value\")) {\n      pwmValue = request-&gt;getParam(\"value\")-&gt;value().toInt();\n      if (pwmValue &lt; 0) pwmValue = 0;\n      if (pwmValue &gt; 255) pwmValue = 255;\n      ledcWrite(PWM_PIN, pwmValue);\n      String msg = \"PWM value set to \";\n      msg += String(pwmValue);\n      request-&gt;send(200, \"text\/plain\", msg);\n    } else {\n      request-&gt;send(400, \"text\/plain\", \"Missing value parameter\");\n    }\n  });\n\n  \/\/ \/voltage -&gt; return voltage as plain text, e.g. \"1.23\"\n  server.on(\"\/voltage\", HTTP_GET, [](AsyncWebServerRequest* request) {\n    int raw = analogRead(ANALOG_PIN);\n    float voltage = 3.3f * raw \/ 4096.0f;\n    String msg = String(voltage, 2);  \/\/ e.g. \"1.23\"\n    request-&gt;send(200, \"text\/plain\", msg);\n  });\n\n  server.begin();\n  Serial.println(\"Worker server ready.\");\n}\n\nvoid loop() {}\n<\/pre>\n<\/div>\n<p>\n<p>Here is the output:<\/p>\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3esp32_asyncwebserver_only-1024x513.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"513\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3esp32_asyncwebserver_only-1024x513.png\" alt=\"Async WebServer - Browser output of three_esp32_asyncwebserver_only_master.ino\" class=\"wp-image-25182\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3esp32_asyncwebserver_only-1024x513.png 1024w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3esp32_asyncwebserver_only-300x150.png 300w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3esp32_asyncwebserver_only-768x385.png 768w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3esp32_asyncwebserver_only-860x430.png 860w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3esp32_asyncwebserver_only.png 1314w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">Browser output of three_esp32_asyncwebserver_only_master.ino<\/figcaption><\/figure>\n<p>As mentioned before, this is a step backwards in terms of appearance and comfort, but my intention was to illustrate the principle.<\/p>\n\n<h3 class=\"wp-block-heading\" id=\"multiple_esp32_asw_espnow_sta\">Option 2a: Async WebServer and ESP-NOW Serial (&#8220;STA-only&#8221;)<\/h3>\n<p>In this constellation, too, the master\/worker 1 is a server in relation to the device that displays the website in the browser. ESP-NOW communication between master\/worker 1 and worker 2 or 3 takes place in the router&#8217;s network. Workers 2 and 3 must also be assigned IP addresses, but the MAC address is decisive for ESP-NOW communication. I have described how to determine the MAC address <a href=\"https:\/\/wolles-elektronikkiste.de\/en\/esp-now-serial#get_mac_address\" target=\"_blank\" rel=\"noopener\">here<\/a>.   <\/p>\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3_ESP32_STA-1024x550.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"550\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3_ESP32_STA-1024x550.webp\" alt=\"\" class=\"wp-image-25262\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3_ESP32_STA-1024x550.webp 1024w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3_ESP32_STA-300x161.webp 300w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3_ESP32_STA-768x413.webp 768w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3_ESP32_STA-1320x709.webp 1320w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3_ESP32_STA.webp 1500w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">Three ESP32, communication via Async WebServer and ESP-NOW Serial, &#8220;STA-only&#8221;<\/figcaption><\/figure>\n\n<h4 class=\"wp-block-heading\">Sketch for the Master\/Worker 1<\/h4>\n<p>I will not go into the details of ESP-NOW Serial. You can read about that <a href=\"https:\/\/wolles-elektronikkiste.de\/en\/esp-now-serial\" target=\"_blank\" rel=\"noopener\">here<\/a> if you need to. In principle, the sketch is structured in the same way as the previous master sketch. Only the communication between Master\/Worker 1 and Worker 2 or 3 is &#8220;translated&#8221; to ESP-NOW Serial. The data to and from the workers is transmitted as structures.     <\/p>\n<p>And so that the output is not too boring, we present everything here in table form.  <\/p>\n<\/p>\n<div class=\"scroll-paragraph\">\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"three_esp32_espnow_sta\" data-enlighter-title=\"three_esp32_espnow_sta_master.ino\">#include &lt;WiFi.h&gt;\n#include &lt;AsyncTCP.h&gt;\n#include &lt;ESPAsyncWebServer.h&gt;\n\n#include \"ESP32_NOW_Serial.h\"\n#include \"MacAddress.h\"\n\n\n#define ESPNOW_WIFI_CHANNEL 1        \/\/ TODO: router channel\n#define VOLTAGE_UPDATE_PERIOD 2000   \/\/ local voltage update every 2s\n\n\/\/ ---------- Pins ----------\nconst int LED_PIN    = 2;\nconst int PWM_PIN    = 5;\nconst int ANALOG_PIN = 34;\n\nIPAddress ip(192,168,178,112);  \/\/TODO\nIPAddress gateway(192,168,178,1);  \/\/TODO\nIPAddress subnet(255,255,255,0);  \/\/TODO\n\n\/\/ ---------- WiFi STA config ----------\nconstexpr char WIFI_SSID[] = \"Your SSID\";  \/\/TODO\nconstexpr char WIFI_PASS[] = \"Your password\";  \/\/TODO\n\n\/\/ ---------- Worker MAC addresses ----------\nconst MacAddress worker2_mac({0x94, 0x3C, 0xC6, 0x34, 0xCF, 0xA4});  \/\/ TODO\nconst MacAddress worker3_mac({0xC8, 0xC9, 0xA3, 0xCA, 0x22, 0x70});  \/\/ TODO\n\n\/\/ ---------- ESP-NOW Serial ----------\nESP_NOW_Serial_Class NowSerial2(worker2_mac, ESPNOW_WIFI_CHANNEL, WIFI_IF_STA);\nESP_NOW_Serial_Class NowSerial3(worker3_mac, ESPNOW_WIFI_CHANNEL, WIFI_IF_STA);\n\n\/\/ ---------- Packet structures ----------\nstruct WorkerStatusPacket {\n  float   voltage;\n  uint8_t pwm;\n  uint8_t ledOn;\n};\n\nstruct WorkerCommandPacket {\n  uint8_t pwm;\n  uint8_t ledOn;\n};\n\n\/\/ ---------- Node data for UI ----------\nstruct NodeData {\n  bool   ledOn;\n  int    pwm;\n  float  voltage;\n};\n\n\/\/ index: 0=master, 1=worker2, 2=worker3\nNodeData nodes[3];\n\nunsigned long lastLocalVoltageMillis = 0;\n\nAsyncWebServer server(80);\n\n\n\/\/ ========== Helpers ==========\n\nvoid connectWiFi() {\n  WiFi.mode(WIFI_STA);\n  WiFi.config(ip, gateway, subnet);\n  WiFi.begin(WIFI_SSID, WIFI_PASS, ESPNOW_WIFI_CHANNEL);\n  Serial.print(\"Connecting to WiFi\");\n  while (WiFi.status() != WL_CONNECTED) {\n    Serial.print(\".\");\n    delay(400);\n  }\n  Serial.println();\n  Serial.println(WiFi.localIP());\n}\n\nfloat readLocalVoltage() {\n  int raw = analogRead(ANALOG_PIN);\n  return 3.3f * raw \/ 4096.0f;\n}\n\nvoid processWorkerSerial(ESP_NOW_Serial_Class &amp;serial, NodeData &amp;data) {\n  while (serial.available() &gt;= (int)sizeof(WorkerStatusPacket)) {\n    WorkerStatusPacket pkt;\n    serial.readBytes((uint8_t*)&amp;pkt, sizeof(pkt));\n    data.voltage = pkt.voltage;\n    data.pwm     = pkt.pwm;\n    data.ledOn   = (pkt.ledOn != 0);\n  }\n}\n\nvoid sendCommandToWorker(ESP_NOW_Serial_Class&amp; serial, NodeData&amp; node) {\n  WorkerCommandPacket cmd;\n  cmd.pwm = node.pwm;\n  cmd.ledOn = node.ledOn ? 1 : 0;\n  serial.write((uint8_t*)&amp;cmd, sizeof(cmd));\n}\n\n\n\/\/ ========== Simple HTML page (NO auto-refresh) ==========\n\nString buildMainPage() {\n  String html;\n  html += \"&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;&lt;meta charset='UTF-8'&gt;&lt;title&gt;ESP32 Master Control&lt;\/title&gt;&lt;\/head&gt;&lt;body&gt;\";\n\n  html += \"&lt;h1&gt;ESP32 Master Control (ESP-NOW Serial)&lt;\/h1&gt;\";\n  html += \"&lt;p&gt;&lt;a href='\/'&gt;Refresh page&lt;\/a&gt;&lt;\/p&gt;\";\n\n  html += \"&lt;table border='1' cellpadding='4' cellspacing='0'&gt;\";\n  html += \"&lt;tr&gt;&lt;th&gt;Device&lt;\/th&gt;&lt;th&gt;LED&lt;\/th&gt;&lt;th&gt;PWM&lt;\/th&gt;&lt;th&gt;Voltage [V]&lt;\/th&gt;&lt;\/tr&gt;\";\n\n  \/\/ -------- Master (index 0) --------\n  html += \"&lt;tr&gt;&lt;td&gt;ESP1 (Master)&lt;\/td&gt;&lt;td&gt;\";\n  html += (nodes[0].ledOn ? \"ON\" : \"OFF\");\n  html += \" (\";\n  html += \"&lt;a href='\/esp1\/led?state=1'&gt;ON&lt;\/a&gt; | \";\n  html += \"&lt;a href='\/esp1\/led?state=0'&gt;OFF&lt;\/a&gt;)\";\n  html += \"&lt;\/td&gt;&lt;td&gt;\";\n  html += String(nodes[0].pwm);\n  html += \"&lt;br&gt;&lt;form action='\/esp1\/pwm' method='get'&gt;Set PWM: \";\n  html += \"&lt;input type='text' name='value' size='4'&gt;&lt;input type='submit' value='Set'&gt;&lt;\/form&gt;\";\n  html += \"&lt;\/td&gt;&lt;td&gt;\";\n  html += String(nodes[0].voltage, 2);\n  html += \"&lt;\/td&gt;&lt;\/tr&gt;\";\n\n  \/\/ -------- Worker 2 (index 1) --------\n  html += \"&lt;tr&gt;&lt;td&gt;ESP2 (Worker)&lt;\/td&gt;&lt;td&gt;\";\n  html += (nodes[1].ledOn ? \"ON\" : \"OFF\");\n  html += \" (\";\n  html += \"&lt;a href='\/esp2\/led?state=1'&gt;ON&lt;\/a&gt; | \";\n  html += \"&lt;a href='\/esp2\/led?state=0'&gt;OFF&lt;\/a&gt;)\";\n  html += \"&lt;\/td&gt;&lt;td&gt;\";\n  html += String(nodes[1].pwm);\n  html += \"&lt;br&gt;&lt;form action='\/esp2\/pwm' method='get'&gt;Set PWM: \";\n  html += \"&lt;input type='text' name='value' size='4'&gt;&lt;input type='submit' value='Set'&gt;&lt;\/form&gt;\";\n  html += \"&lt;\/td&gt;&lt;td&gt;\";\n  html += String(nodes[1].voltage, 2);\n  html += \"&lt;\/td&gt;&lt;\/tr&gt;\";\n\n  \/\/ -------- Worker 3 (index 2) --------\n  html += \"&lt;tr&gt;&lt;td&gt;ESP3 (Worker)&lt;\/td&gt;&lt;td&gt;\";\n  html += (nodes[2].ledOn ? \"ON\" : \"OFF\");\n  html += \" (\";\n  html += \"&lt;a href='\/esp3\/led?state=1'&gt;ON&lt;\/a&gt; | \";\n  html += \"&lt;a href='\/esp3\/led?state=0'&gt;OFF&lt;\/a&gt;)\";\n  html += \"&lt;\/td&gt;&lt;td&gt;\";\n  html += String(nodes[2].pwm);\n  html += \"&lt;br&gt;&lt;form action='\/esp3\/pwm' method='get'&gt;Set PWM: \";\n  html += \"&lt;input type='text' name='value' size='4'&gt;&lt;input type='submit' value='Set'&gt;&lt;\/form&gt;\";\n  html += \"&lt;\/td&gt;&lt;td&gt;\";\n  html += String(nodes[2].voltage, 2);\n  html += \"&lt;\/td&gt;&lt;\/tr&gt;\";\n\n  html += \"&lt;\/table&gt;&lt;\/body&gt;&lt;\/html&gt;\";\n  return html;\n}\n\n\n\/\/ ========== Setup ==========\n\nvoid setupWiFiAndEspNow() {\n  connectWiFi();\n  NowSerial2.begin(115200);\n  NowSerial3.begin(115200);\n}\n\nvoid setup() {\n  Serial.begin(115200);\n\n  pinMode(LED_PIN, OUTPUT);\n  digitalWrite(LED_PIN, LOW);\n\n  pinMode(ANALOG_PIN, INPUT);\n\n  ledcAttach(PWM_PIN, 5000, 8);\n  ledcWrite(PWM_PIN, 0);\n\n  for (int i = 0; i &lt; 3; i++) {\n    nodes[i].ledOn   = false;\n    nodes[i].pwm     = 0;\n    nodes[i].voltage = 0.0f;\n  }\n\n  setupWiFiAndEspNow();\n\n  \/\/ -------- Web routes --------\n  server.on(\"\/\", HTTP_GET, [](AsyncWebServerRequest* request) {\n    request-&gt;send(200, \"text\/html\", buildMainPage());\n  });\n\n  \/\/ Master LED\n  server.on(\"\/esp1\/led\", HTTP_GET, [](AsyncWebServerRequest* request) {\n    int s = request-&gt;getParam(\"state\")-&gt;value().toInt();\n    nodes[0].ledOn = (s != 0);\n    digitalWrite(LED_PIN, nodes[0].ledOn ? HIGH : LOW);\n    request-&gt;redirect(\"\/\");\n  });\n\n  \/\/ Master PWM\n  server.on(\"\/esp1\/pwm\", HTTP_GET, [](AsyncWebServerRequest* request) {\n    int val = request-&gt;getParam(\"value\")-&gt;value().toInt();\n    if (val &lt; 0) val = 0;\n    if (val &gt; 255) val = 255;\n    nodes[0].pwm = val;\n    ledcWrite(PWM_PIN, nodes[0].pwm);\n    request-&gt;redirect(\"\/\");\n  });\n\n  \/\/ Worker 2 LED\n  server.on(\"\/esp2\/led\", HTTP_GET, [](AsyncWebServerRequest* request) {\n    int s = request-&gt;getParam(\"state\")-&gt;value().toInt();\n    nodes[1].ledOn = (s != 0);\n    sendCommandToWorker(NowSerial2, nodes[1]);\n    request-&gt;redirect(\"\/\");\n  });\n\n  \/\/ Worker 2 PWM\n  server.on(\"\/esp2\/pwm\", HTTP_GET, [](AsyncWebServerRequest* request) {\n    int val = request-&gt;getParam(\"value\")-&gt;value().toInt();\n    if (val &lt; 0) val = 0;\n    if (val &gt; 255) val = 255;\n    nodes[1].pwm = val;\n    sendCommandToWorker(NowSerial2, nodes[1]);\n    request-&gt;redirect(\"\/\");\n  });\n\n  \/\/ Worker 3 LED\n  server.on(\"\/esp3\/led\", HTTP_GET, [](AsyncWebServerRequest* request) {\n    int s = request-&gt;getParam(\"state\")-&gt;value().toInt();\n    nodes[2].ledOn = (s != 0);\n    sendCommandToWorker(NowSerial3, nodes[2]);\n    request-&gt;redirect(\"\/\");\n  });\n\n  \/\/ Worker 3 PWM\n  server.on(\"\/esp3\/pwm\", HTTP_GET, [](AsyncWebServerRequest* request) {\n    int val = request-&gt;getParam(\"value\")-&gt;value().toInt();\n    if (val &lt; 0) val = 0;\n    if (val &gt; 255) val = 255;\n    nodes[2].pwm = val;\n    sendCommandToWorker(NowSerial3, nodes[2]);\n    request-&gt;redirect(\"\/\");\n  });\n\n  server.begin();\n  Serial.println(\"Master ready.\");\n}\n\n\n\/\/ ========== Loop ==========\n\nvoid loop() {\n  unsigned long now = millis();\n\n  \/\/ Update master's own voltage\n  if (now - lastLocalVoltageMillis &gt; VOLTAGE_UPDATE_PERIOD) {\n    lastLocalVoltageMillis = now;\n    nodes[0].voltage = readLocalVoltage();\n  }\n\n  \/\/ Process worker packets\n  processWorkerSerial(NowSerial2, nodes[1]);\n  processWorkerSerial(NowSerial3, nodes[2]);\n\n  delay(1);\n}\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"three_esp32_espnow_sta\" data-enlighter-title=\"three_esp32_espnow_sta_worker.ino\">#include &lt;WiFi.h&gt;\n#include \"ESP32_NOW_Serial.h\"\n#include \"MacAddress.h\"\n\n#define ESPNOW_WIFI_CHANNEL 1        \/\/ same as master \/ router\n#define STATUS_SEND_PERIOD 2000      \/\/ [ms]\n\n\/\/ same pins as on master\nconst int LED_PIN    = 2;\nconst int PWM_PIN    = 5;\nconst int ANALOG_PIN = 34;\n\nIPAddress ip(192,168,178,113);  \/\/TODO\nIPAddress gateway(192,168,178,1);  \/\/TODO\nIPAddress subnet(255,255,255,0);  \/\/TODO\n\n\/\/ ---------- WiFi STA config ----------\nconstexpr char WIFI_SSID[] = \"Your SSID\";  \/\/TODO\nconstexpr char WIFI_PASS[] = \"Your password\";  \/\/TODO\n\n\/\/ ---------- MAC of master (STA interface) ----------\nconst MacAddress master_mac({0xC8, 0xC9, 0xA3, 0xC6, 0xFE, 0x54});  \/\/ TODO\n\nESP_NOW_Serial_Class NowSerial(master_mac, ESPNOW_WIFI_CHANNEL, WIFI_IF_STA);\n\n\/\/ data packets\nstruct WorkerStatusPacket {\n  float   voltage;\n  uint8_t pwm;\n  uint8_t ledOn;\n};\n\nstruct WorkerCommandPacket {\n  uint8_t pwm;\n  uint8_t ledOn;\n};\n\n\/\/ local state\nuint8_t currentPwm   = 0;\nbool    currentLedOn = false;\n\nunsigned long lastStatusSent = 0;\n\n\/\/ ---------- helpers ----------\nvoid connectWiFi() {\n  WiFi.mode(WIFI_STA);\n  WiFi.config(ip, gateway, subnet);\n  WiFi.begin(WIFI_SSID, WIFI_PASS, ESPNOW_WIFI_CHANNEL);\n  Serial.print(\"Connecting to WiFi\");\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(500);\n    Serial.print(\".\");\n  }\n  Serial.println();\n  Serial.print(\"Connected. IP address: \");\n  Serial.println(WiFi.localIP());\n}\n\nfloat readVoltage() {\n  int raw = analogRead(ANALOG_PIN);\n  float voltage = 3.3f * raw \/ 4096.0f;\n  return voltage;\n}\n\nvoid applyCommand(const WorkerCommandPacket&amp; cmd) {\n  currentPwm   = cmd.pwm;\n  currentLedOn = (cmd.ledOn != 0);\n\n  ledcWrite(PWM_PIN, currentPwm);\n  digitalWrite(LED_PIN, currentLedOn ? HIGH : LOW);\n}\n\nvoid sendStatus() {\n  WorkerStatusPacket pkt;\n  pkt.voltage = readVoltage();\n  pkt.pwm     = currentPwm;\n  pkt.ledOn   = currentLedOn ? 1 : 0;\n\n  int ok = NowSerial.write((uint8_t*)&amp;pkt, sizeof(pkt));\n  if (!ok) {\n    Serial.println(\"Status send failed\");\n  }\n}\n\n\/\/ ---------- setup() ----------\nvoid setup() {\n  Serial.begin(115200);\n\n  pinMode(LED_PIN, OUTPUT);\n  digitalWrite(LED_PIN, LOW);\n\n  pinMode(ANALOG_PIN, INPUT);\n\n  ledcAttach(PWM_PIN, 5000, 8);\n  ledcWrite(PWM_PIN, 0);\n\n  connectWiFi();\n\n  \/\/ ESP-NOW Serial on STA interface\n  NowSerial.begin(115200);\n\n  Serial.println(\"Worker ready.\");\n}\n\n\/\/ ---------- loop() ----------\nvoid loop() {\n  unsigned long now = millis();\n\n  \/\/ periodic status to master\n  if (now - lastStatusSent &gt; STATUS_SEND_PERIOD) {\n    lastStatusSent = now;\n    sendStatus();\n  }\n\n  \/\/ incoming commands\n  while (NowSerial.available() &gt;= (int)sizeof(WorkerCommandPacket)) {\n    WorkerCommandPacket cmd;\n    NowSerial.readBytes((uint8_t*)&amp;cmd, sizeof(cmd));\n    applyCommand(cmd);\n  }\n\n  delay(1);\n}\n<\/pre>\n<\/div>\n<p>\n\n<p>And this is the output:<\/p>\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3esp32_esp_now_serial.png\"><img loading=\"lazy\" decoding=\"async\" width=\"820\" height=\"285\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3esp32_esp_now_serial.png\" alt=\"Async WebServer - Browser output of three_esp32_espnow_sta_master.ino\" class=\"wp-image-25174\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3esp32_esp_now_serial.png 820w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3esp32_esp_now_serial-300x104.png 300w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3esp32_esp_now_serial-768x267.png 768w\" sizes=\"auto, (max-width: 820px) 100vw, 820px\" \/><\/a><figcaption class=\"wp-element-caption\">Browser output of three_esp32_espnow_sta_master.ino<\/figcaption><\/figure>\n\n<h3 class=\"wp-block-heading\" id=\"multiple_esp32_asw_espnow_sta_ap\">Option 2b: Async WebServer and ESP-NOW Serial (STA\/AP)<\/h3>\n<p>In this constellation, we have to set up an Async WebServer on the master ESP32 as usual. In addition, the master ESP32 provides a network to which Worker 2 and Worker 3 can connect. You assign a name and password of your choice for this network.  <\/p>\n<p>An important point is that the SoftAP MAC address of the master ESP32 is used for ESP-NOW serial communication. I have described how to determine it <a href=\"https:\/\/wolles-elektronikkiste.de\/en\/esp-now-serial#get_mac_address\" target=\"_blank\" rel=\"noopener\">here<\/a>. Normally you only have to add 1 to the last number of the MAC address.  <\/p>\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3_ESP32_ESPNOW_Serial_STA_AP-1024x512.webp\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"512\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3_ESP32_ESPNOW_Serial_STA_AP-1024x512.webp\" alt=\"\" class=\"wp-image-25263\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3_ESP32_ESPNOW_Serial_STA_AP-1024x512.webp 1024w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3_ESP32_ESPNOW_Serial_STA_AP-300x150.webp 300w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3_ESP32_ESPNOW_Serial_STA_AP-768x384.webp 768w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3_ESP32_ESPNOW_Serial_STA_AP-1536x768.webp 1536w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3_ESP32_ESPNOW_Serial_STA_AP-860x430.webp 860w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3_ESP32_ESPNOW_Serial_STA_AP-1320x660.webp 1320w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3_ESP32_ESPNOW_Serial_STA_AP.webp 1649w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">Three ESP32, communication via Async WebServer and ESP-NOW Serial, &#8220;AP_STA-Mode&#8221;<\/figcaption><\/figure>\n<p>The network is set up in the <code>startAPForWorkers()<\/code> function. This should be fairly self-explanatory. What you may notice is that in this version we specify the ESPNOW_WIFI_CHANNEL for all participants. This is necessary because we have two networks and the Wi-Fi channels of the two could differ when automatically selected.    <\/p>\n<p>Not much changes on the worker side, except that the SSID and password of the network of the master ESP32 are used. <\/p>\n<\/p>\n<div class=\"scroll-paragraph\">\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"three_esp32_espnow_sta_api\" data-enlighter-title=\"three_esp32_espnow_sta_api_master.ino\">#include &lt;WiFi.h&gt;\n#include &lt;AsyncTCP.h&gt;\n#include &lt;ESPAsyncWebServer.h&gt;\n\n#include \"ESP32_NOW_Serial.h\"\n#include \"MacAddress.h\"\n#include \"esp_wifi.h\"\n\n#define ESPNOW_WIFI_CHANNEL 1        \/\/ All ESP32 and the master's AP use this channel\n#define VOLTAGE_UPDATE_PERIOD 2000   \/\/ [ms] local voltage update period\n\n\/\/ ---------- Pins ----------\nconst int LED_PIN    = 2;\nconst int PWM_PIN    = 5;\nconst int ANALOG_PIN = 34;\n\n\/\/ ---------- WiFi STA (home network) ----------\nIPAddress ip(192,168,178,112);     \/\/TODO\nIPAddress gateway(192,168,178,1);  \/\/TODO\nIPAddress subnet(255,255,255,0);   \/\/TODO\n\nconstexpr char WIFI_SSID[] = \"Your SSID\";  \/\/TODO\nconstexpr char WIFI_PASS[] = \"Your password\";  \/\/TODO\n\n\/\/ ---------- WiFi AP (for the workers) ----------\nconst char* AP_SSID = \"ESP32-Master\";        \/\/ TODO: choose AP SSID for workers\nconst char* AP_PASS = \"12345678\";            \/\/ TODO: choose AP password (&gt;= 8 chars)\n\n\/\/ ---------- Worker MAC addresses (STA interface of ESP2 and ESP3) ----------\nconst MacAddress worker2_mac({0x94, 0x3C, 0xC6, 0x34, 0xCF, 0xA4});  \/\/ TODO\nconst MacAddress worker3_mac({0xC8, 0xC9, 0xA3, 0xCA, 0x22, 0x70});  \/\/ TODO\n\n\/\/ ---------- ESP-NOW Serial on MASTER: use AP interface ----------\nESP_NOW_Serial_Class NowSerial2(worker2_mac, ESPNOW_WIFI_CHANNEL, WIFI_IF_AP);\nESP_NOW_Serial_Class NowSerial3(worker3_mac, ESPNOW_WIFI_CHANNEL, WIFI_IF_AP);\n\n\/\/ ---------- Packet structures ----------\nstruct WorkerStatusPacket {\n  float   voltage;\n  uint8_t pwm;\n  uint8_t ledOn;\n};\n\nstruct WorkerCommandPacket {\n  uint8_t pwm;\n  uint8_t ledOn;\n};\n\n\/\/ ---------- Node data for UI ----------\nstruct NodeData {\n  bool   ledOn;\n  int    pwm;\n  float  voltage;\n};\n\n\/\/ index: 0 = master, 1 = worker2, 2 = worker3\nNodeData nodes[3];\n\nunsigned long lastLocalVoltageMillis = 0;\n\nAsyncWebServer server(80);\n\n\n\/\/ ================== Helper functions ==================\n\nvoid connectWiFiSTA() {\n  \/\/ Master connects as STA to your home router\n  WiFi.mode(WIFI_AP_STA);\n  WiFi.config(ip, gateway, subnet);\n  WiFi.begin(WIFI_SSID, WIFI_PASS, ESPNOW_WIFI_CHANNEL);\n  Serial.print(\"Connecting to home WiFi (STA)\");\n  while (WiFi.status() != WL_CONNECTED) {\n    Serial.print(\".\");\n    delay(400);\n  }\n  Serial.println();\n  Serial.print(\"STA IP address (use this in browser): \");\n  Serial.println(WiFi.localIP());\n}\n\nvoid startAPForWorkers() {\n  \/\/ Master starts an AP for the worker ESP32 boards\n  bool ok = WiFi.softAP(AP_SSID, AP_PASS, ESPNOW_WIFI_CHANNEL);\n  if (!ok) {\n    Serial.println(\"Error starting AP. Restarting...\");\n    delay(2000);\n    ESP.restart();\n  }\n  Serial.print(\"AP started. SSID: \");\n  Serial.println(AP_SSID);\n  Serial.print(\"AP IP: \");\n  Serial.println(WiFi.softAPIP());\n}\n\nfloat readLocalVoltage() {\n  int raw = analogRead(ANALOG_PIN);\n  return 3.3f * raw \/ 4096.0f;\n}\n\nvoid processWorkerSerial(ESP_NOW_Serial_Class &amp;serial, NodeData &amp;data) {\n  while (serial.available() &gt;= (int)sizeof(WorkerStatusPacket)) {\n    WorkerStatusPacket pkt;\n    serial.readBytes((uint8_t*)&amp;pkt, sizeof(pkt));\n    data.voltage = pkt.voltage;\n    data.pwm     = pkt.pwm;\n    data.ledOn   = (pkt.ledOn != 0);\n  }\n}\n\nvoid sendCommandToWorker(ESP_NOW_Serial_Class&amp; serial, NodeData&amp; node) {\n  WorkerCommandPacket cmd;\n  cmd.pwm   = (uint8_t)node.pwm;\n  cmd.ledOn = node.ledOn ? 1 : 0;\n  serial.write((uint8_t*)&amp;cmd, sizeof(cmd));\n}\n\n\n\/\/ ================== HTML page (no auto-refresh, no last-update) ==================\n\nString buildMainPage() {\n  String html;\n  html += \"&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;&lt;meta charset='UTF-8'&gt;\";\n  html += \"&lt;title&gt;ESP32 Master Control&lt;\/title&gt;&lt;\/head&gt;&lt;body&gt;\";\n\n  html += \"&lt;h1&gt;ESP32 Master Control (ESP-NOW Serial, AP+STA)&lt;\/h1&gt;\";\n  html += \"&lt;p&gt;STA IP address: \";\n  html += WiFi.localIP().toString();\n  html += \"&lt;\/p&gt;\";\n  html += \"&lt;p&gt;&lt;a href='\/'&gt;Refresh page&lt;\/a&gt;&lt;\/p&gt;\";\n\n  html += \"&lt;table border='1' cellpadding='4' cellspacing='0'&gt;\";\n  html += \"&lt;tr&gt;&lt;th&gt;Device&lt;\/th&gt;&lt;th&gt;LED&lt;\/th&gt;&lt;th&gt;PWM&lt;\/th&gt;&lt;th&gt;Voltage [V]&lt;\/th&gt;&lt;\/tr&gt;\";\n\n  \/\/ ----- ESP1 (Master \/ Worker #1) -----\n  html += \"&lt;tr&gt;&lt;td&gt;ESP1 (Master)&lt;\/td&gt;&lt;td&gt;\";\n  html += (nodes[0].ledOn ? \"ON\" : \"OFF\");\n  html += \" (\";\n  html += \"&lt;a href='\/esp1\/led?state=1'&gt;ON&lt;\/a&gt; | \";\n  html += \"&lt;a href='\/esp1\/led?state=0'&gt;OFF&lt;\/a&gt;)\";\n  html += \"&lt;\/td&gt;&lt;td&gt;\";\n  html += String(nodes[0].pwm);\n  html += \"&lt;br&gt;&lt;form action='\/esp1\/pwm' method='get'&gt;Set PWM: \";\n  html += \"&lt;input type='text' name='value' size='4'&gt;\";\n  html += \"&lt;input type='submit' value='Set'&gt;\";\n  html += \"&lt;\/form&gt;\";\n  html += \"&lt;\/td&gt;&lt;td&gt;\";\n  html += String(nodes[0].voltage, 2);\n  html += \"&lt;\/td&gt;&lt;\/tr&gt;\";\n\n  \/\/ ----- ESP2 (Worker) -----\n  html += \"&lt;tr&gt;&lt;td&gt;ESP2 (Worker)&lt;\/td&gt;&lt;td&gt;\";\n  html += (nodes[1].ledOn ? \"ON\" : \"OFF\");\n  html += \" (\";\n  html += \"&lt;a href='\/esp2\/led?state=1'&gt;ON&lt;\/a&gt; | \";\n  html += \"&lt;a href='\/esp2\/led?state=0'&gt;OFF&lt;\/a&gt;)\";\n  html += \"&lt;\/td&gt;&lt;td&gt;\";\n  html += String(nodes[1].pwm);\n  html += \"&lt;br&gt;&lt;form action='\/esp2\/pwm' method='get'&gt;Set PWM: \";\n  html += \"&lt;input type='text' name='value' size='4'&gt;\";\n  html += \"&lt;input type='submit' value='Set'&gt;\";\n  html += \"&lt;\/form&gt;\";\n  html += \"&lt;\/td&gt;&lt;td&gt;\";\n  html += String(nodes[1].voltage, 2);\n  html += \"&lt;\/td&gt;&lt;\/tr&gt;\";\n\n  \/\/ ----- ESP3 (Worker) -----\n  html += \"&lt;tr&gt;&lt;td&gt;ESP3 (Worker)&lt;\/td&gt;&lt;td&gt;\";\n  html += (nodes[2].ledOn ? \"ON\" : \"OFF\");\n  html += \" (\";\n  html += \"&lt;a href='\/esp3\/led?state=1'&gt;ON&lt;\/a&gt; | \";\n  html += \"&lt;a href='\/esp3\/led?state=0'&gt;OFF&lt;\/a&gt;)\";\n  html += \"&lt;\/td&gt;&lt;td&gt;\";\n  html += String(nodes[2].pwm);\n  html += \"&lt;br&gt;&lt;form action='\/esp3\/pwm' method='get'&gt;Set PWM: \";\n  html += \"&lt;input type='text' name='value' size='4'&gt;\";\n  html += \"&lt;input type='submit' value='Set'&gt;\";\n  html += \"&lt;\/form&gt;\";\n  html += \"&lt;\/td&gt;&lt;td&gt;\";\n  html += String(nodes[2].voltage, 2);\n  html += \"&lt;\/td&gt;&lt;\/tr&gt;\";\n\n  html += \"&lt;\/table&gt;&lt;\/body&gt;&lt;\/html&gt;\";\n  return html;\n}\n\n\n\/\/ ================== Setup ==================\n\nvoid setup() {\n  Serial.begin(115200);\n\n  pinMode(LED_PIN, OUTPUT);\n  digitalWrite(LED_PIN, LOW);\n  pinMode(ANALOG_PIN, INPUT);\n\n  ledcAttach(PWM_PIN, 5000, 8);\n  ledcWrite(PWM_PIN, 0);\n\n  \/\/ init node data\n  for (int i = 0; i &lt; 3; i++) {\n    nodes[i].ledOn   = false;\n    nodes[i].pwm     = 0;\n    nodes[i].voltage = 0.0f;\n  }\n\n  \/\/ combined STA (home WiFi) + AP (for workers)\n  connectWiFiSTA();\n  startAPForWorkers();\n\n  \/\/ ESP-NOW Serial uses AP interface on the master\n  NowSerial2.begin(115200);\n  NowSerial3.begin(115200);\n\n  \/\/ ---------- Web routes ----------\n\n  \/\/ main page\n  server.on(\"\/\", HTTP_GET, [](AsyncWebServerRequest* request) {\n    request-&gt;send(200, \"text\/html\", buildMainPage());\n  });\n\n  \/\/ ---- Master LED ----\n  server.on(\"\/esp1\/led\", HTTP_GET, [](AsyncWebServerRequest* request) {\n    if (!request-&gt;hasParam(\"state\")) {\n      request-&gt;send(400, \"text\/plain\", \"Missing state parameter\");\n      return;\n    }\n    int s = request-&gt;getParam(\"state\")-&gt;value().toInt();\n    nodes[0].ledOn = (s != 0);\n    digitalWrite(LED_PIN, nodes[0].ledOn ? HIGH : LOW);\n    request-&gt;redirect(\"\/\");\n  });\n\n  \/\/ ---- Master PWM ----\n  server.on(\"\/esp1\/pwm\", HTTP_GET, [](AsyncWebServerRequest* request) {\n    if (!request-&gt;hasParam(\"value\")) {\n      request-&gt;send(400, \"text\/plain\", \"Missing value parameter\");\n      return;\n    }\n    int val = request-&gt;getParam(\"value\")-&gt;value().toInt();\n    if (val &lt; 0)   val = 0;\n    if (val &gt; 255) val = 255;\n    nodes[0].pwm = val;\n    ledcWrite(PWM_PIN, nodes[0].pwm);\n    request-&gt;redirect(\"\/\");\n  });\n\n  \/\/ ---- Worker 2 LED ----\n  server.on(\"\/esp2\/led\", HTTP_GET, [](AsyncWebServerRequest* request) {\n    if (!request-&gt;hasParam(\"state\")) {\n      request-&gt;send(400, \"text\/plain\", \"Missing state parameter\");\n      return;\n    }\n    int s = request-&gt;getParam(\"state\")-&gt;value().toInt();\n    nodes[1].ledOn = (s != 0);\n    sendCommandToWorker(NowSerial2, nodes[1]);\n    request-&gt;redirect(\"\/\");\n  });\n\n  \/\/ ---- Worker 2 PWM ----\n  server.on(\"\/esp2\/pwm\", HTTP_GET, [](AsyncWebServerRequest* request) {\n    if (!request-&gt;hasParam(\"value\")) {\n      request-&gt;send(400, \"text\/plain\", \"Missing value parameter\");\n      return;\n    }\n    int val = request-&gt;getParam(\"value\")-&gt;value().toInt();\n    if (val &lt; 0)   val = 0;\n    if (val &gt; 255) val = 255;\n    nodes[1].pwm = val;\n    sendCommandToWorker(NowSerial2, nodes[1]);\n    request-&gt;redirect(\"\/\");\n  });\n\n  \/\/ ---- Worker 3 LED ----\n  server.on(\"\/esp3\/led\", HTTP_GET, [](AsyncWebServerRequest* request) {\n    if (!request-&gt;hasParam(\"state\")) {\n      request-&gt;send(400, \"text\/plain\", \"Missing state parameter\");\n      return;\n    }\n    int s = request-&gt;getParam(\"state\")-&gt;value().toInt();\n    nodes[2].ledOn = (s != 0);\n    sendCommandToWorker(NowSerial3, nodes[2]);\n    request-&gt;redirect(\"\/\");\n  });\n\n  \/\/ ---- Worker 3 PWM ----\n  server.on(\"\/esp3\/pwm\", HTTP_GET, [](AsyncWebServerRequest* request) {\n    if (!request-&gt;hasParam(\"value\")) {\n      request-&gt;send(400, \"text\/plain\", \"Missing value parameter\");\n      return;\n    }\n    int val = request-&gt;getParam(\"value\")-&gt;value().toInt();\n    if (val &lt; 0)   val = 0;\n    if (val &gt; 255) val = 255;\n    nodes[2].pwm = val;\n    sendCommandToWorker(NowSerial3, nodes[2]);\n    request-&gt;redirect(\"\/\");\n  });\n\n  server.begin();\n  Serial.println(\"Master (AP+STA) ready.\");\n\n  Serial.print(\"Soft AP MAC address: \");\n  Serial.println(WiFi.softAPmacAddress());   \/\/ Master-AP-MAC f\u00fcr master_ap_mac\n}\n\n\n\/\/ ================== Loop ==================\n\nvoid loop() {\n  unsigned long now = millis();\n\n  \/\/ Update master's own voltage every 2 s\n  if (now - lastLocalVoltageMillis &gt; VOLTAGE_UPDATE_PERIOD) {\n    lastLocalVoltageMillis = now;\n    nodes[0].voltage = readLocalVoltage();\n  }\n\n  \/\/ Process worker packets\n  processWorkerSerial(NowSerial2, nodes[1]);\n  processWorkerSerial(NowSerial3, nodes[2]);\n\n  delay(1);\n}\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"three_esp32_espnow_sta_api\" data-enlighter-title=\"three_esp32_espnow_sta_api_worker.ino\">#include &lt;WiFi.h&gt;\n#include \"ESP32_NOW_Serial.h\"\n#include \"MacAddress.h\"\n#include \"esp_wifi.h\"\n\n#define ESPNOW_WIFI_CHANNEL 1\n#define STATUS_SEND_PERIOD 2000  \/\/ [ms]\n\n\/\/ Same pins as on master\nconst int LED_PIN    = 2;\nconst int PWM_PIN    = 5;\nconst int ANALOG_PIN = 34;\n\n\/\/ -------- WiFi STA: connect to master's AP --------\nconst char* AP_SSID = \"ESP32-Master\";   \/\/ must match master's AP_SSID\nconst char* AP_PASS = \"12345678\";       \/\/ must match master's AP_PASS\n\nconst MacAddress master_ap_mac({0xC8, 0xC9, 0xA3, 0xC6, 0xFE, 0x55});  \/\/ TODO: adjust\n\n\/\/ ESP-NOW Serial on worker: use STA interface\nESP_NOW_Serial_Class NowSerial(master_ap_mac, ESPNOW_WIFI_CHANNEL, WIFI_IF_STA);\n\n\/\/ ---------- Data packets ----------\nstruct WorkerStatusPacket {\n  float   voltage;\n  uint8_t pwm;\n  uint8_t ledOn;\n};\n\nstruct WorkerCommandPacket {\n  uint8_t pwm;\n  uint8_t ledOn;\n};\n\nuint8_t currentPwm   = 0;\nbool    currentLedOn = false;\n\nunsigned long lastStatusSent = 0;\n\n\n\/\/ ================== Helper functions ==================\n\nvoid connectToMasterAP() {\n  WiFi.mode(WIFI_STA);\n  WiFi.begin(AP_SSID, AP_PASS, ESPNOW_WIFI_CHANNEL);\n  Serial.print(\"Connecting to master AP\");\n  while (WiFi.status() != WL_CONNECTED) {\n    Serial.print(\".\");\n    delay(400);\n  }\n  Serial.println();\n  Serial.print(\"Worker STA IP (not really needed for ESP-NOW): \");\n  Serial.println(WiFi.localIP());\n}\n\nfloat readVoltage() {\n  int raw = analogRead(ANALOG_PIN);\n  return 3.3f * raw \/ 4096.0f;\n}\n\nvoid applyCommand(const WorkerCommandPacket&amp; cmd) {\n  currentPwm   = cmd.pwm;\n  currentLedOn = (cmd.ledOn != 0);\n\n  ledcWrite(PWM_PIN, currentPwm);\n  digitalWrite(LED_PIN, currentLedOn ? HIGH : LOW);\n}\n\nvoid sendStatus() {\n  WorkerStatusPacket pkt;\n  pkt.voltage = readVoltage();\n  pkt.pwm     = currentPwm;\n  pkt.ledOn   = currentLedOn ? 1 : 0;\n\n  int ok = NowSerial.write((uint8_t*)&amp;pkt, sizeof(pkt));\n  if (!ok) {\n    Serial.println(\"Status send failed\");\n  }\n}\n\n\n\/\/ ================== Setup ==================\n\nvoid setup() {\n  Serial.begin(115200);\n\n  pinMode(LED_PIN, OUTPUT);\n  digitalWrite(LED_PIN, LOW);\n\n  pinMode(ANALOG_PIN, INPUT);\n\n  ledcAttach(PWM_PIN, 5000, 8);\n  ledcWrite(PWM_PIN, 0);\n\n  connectToMasterAP();\n\n  NowSerial.begin(115200);\n\n  Serial.println(\"Worker ready (STA \u2192 master AP).\");\n}\n\n\n\/\/ ================== Loop ==================\n\nvoid loop() {\n  unsigned long now = millis();\n\n  \/\/ periodically send status to master\n  if (now - lastStatusSent &gt; STATUS_SEND_PERIOD) {\n    lastStatusSent = now;\n    sendStatus();\n  }\n\n  \/\/ handle incoming commands\n  while (NowSerial.available() &gt;= (int)sizeof(WorkerCommandPacket)) {\n    WorkerCommandPacket cmd;\n    NowSerial.readBytes((uint8_t*)&amp;cmd, sizeof(cmd));\n    applyCommand(cmd);\n  }\n\n  delay(1);\n}\n<\/pre>\n<\/div>\n<p>\n\n<h3 class=\"wp-block-heading\" id=\"multiple_esp32_asw_espnow_sta_complete\">Complete version ESP-NOW Serial (&#8220;STA-only&#8221;) <\/h3>\n\n<p>I would like to provide a complete version, i.e. with sliders and switches for the &#8220;STA-only&#8221; version with ESP-NOW Serial.&nbsp;&nbsp;<\/p>\n<\/p>\n<div class=\"scroll-paragraph\">\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"three_esp32_espnow_sta_complete\" data-enlighter-title=\"three_esp32_espnow_sta_complete_master.ino\">#include &lt;WiFi.h&gt;\n#include &lt;AsyncTCP.h&gt;\n#include &lt;ESPAsyncWebServer.h&gt;\n\n#include \"ESP32_NOW_Serial.h\"\n#include \"MacAddress.h\"\n\n\/\/ ================== CONFIG ==================\n\n#define ESPNOW_WIFI_CHANNEL     1\n#define VOLTAGE_UPDATE_PERIOD  2000\n\n\/\/ ---------- Pins ----------\nconst int LED_PIN    = 2;\nconst int PWM_PIN    = 5;\nconst int ANALOG_PIN = 34;\n\n\/\/ ---------- Network ----------\nIPAddress ip(192,168,178,112);  \/\/TODO\nIPAddress gateway(192,168,178,1);  \/\/TODO\nIPAddress subnet(255,255,255,0);  \/\/TODO\n\nconstexpr char WIFI_SSID[] = \"Your SSID\";  \/\/TODO\nconstexpr char WIFI_PASS[] = \"Your password\";  \/\/TODO\n\n\/\/ ---------- Worker MACs ----------\nconst MacAddress worker2_mac({0x94,0x3C,0xC6,0x34,0xCF,0xA4});  \/\/TODO\nconst MacAddress worker3_mac({0xC8,0xC9,0xA3,0xCA,0x22,0x70});  \/\/TODO\n\n\/\/ ---------- ESP-NOW Serial ----------\nESP_NOW_Serial_Class NowSerial2(worker2_mac, ESPNOW_WIFI_CHANNEL, WIFI_IF_STA);\nESP_NOW_Serial_Class NowSerial3(worker3_mac, ESPNOW_WIFI_CHANNEL, WIFI_IF_STA);\n\n\/\/ ================== DATA ==================\n\nstruct WorkerStatusPacket {\n  float   voltage;\n  uint8_t pwm;\n  uint8_t ledOn;\n};\n\nstruct WorkerCommandPacket {\n  uint8_t pwm;\n  uint8_t ledOn;\n};\n\nstruct NodeData {\n  bool  ledOn;\n  int   pwm;\n  float voltage;\n};\n\n\/\/ index: 0 = master, 1 = worker2, 2 = worker3\nNodeData nodes[3];\n\nunsigned long lastLocalVoltageMillis = 0;\n\nAsyncWebServer server(80);\n\n\/\/ ================== HELPERS ==================\n\nvoid connectWiFi() {\n  WiFi.mode(WIFI_STA);\n  WiFi.config(ip, gateway, subnet);\n  WiFi.begin(WIFI_SSID, WIFI_PASS, ESPNOW_WIFI_CHANNEL);\n  Serial.print(\"Connecting to WiFi\");\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(400);\n    Serial.print(\".\");\n  }\n  Serial.println();\n  Serial.print(\"IP: \");\n  Serial.println(WiFi.localIP());\n}\n\nfloat readLocalVoltage() {\n  int raw = analogRead(ANALOG_PIN);\n  return 3.3f * raw \/ 4096.0f;\n}\n\nvoid processWorkerSerial(ESP_NOW_Serial_Class &amp;serial, NodeData &amp;data) {\n  while (serial.available() &gt;= (int)sizeof(WorkerStatusPacket)) {\n    WorkerStatusPacket pkt;\n    serial.readBytes((uint8_t*)&amp;pkt, sizeof(pkt));\n    data.voltage = pkt.voltage;\n    data.pwm     = pkt.pwm;\n    data.ledOn   = pkt.ledOn != 0;\n  }\n}\n\nvoid sendCommandToWorker(ESP_NOW_Serial_Class &amp;serial, NodeData &amp;node) {\n  WorkerCommandPacket cmd;\n  cmd.pwm   = node.pwm;\n  cmd.ledOn = node.ledOn ? 1 : 0;\n  serial.write((uint8_t*)&amp;cmd, sizeof(cmd));\n}\n\n\/\/ ================== HTML UI ==================\n\nString buildMainPage() {\n  String html;\n  html += F(R\"rawliteral(\n&lt;!DOCTYPE html&gt;\n&lt;html&gt;\n&lt;head&gt;\n&lt;meta charset=\"UTF-8\"&gt;\n&lt;meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"&gt;\n&lt;title&gt;ESP32 ESP-NOW Control&lt;\/title&gt;\n&lt;style&gt;\nbody{font-family:Arial,Helvetica,sans-serif;background:#f0f2f5;margin:0}\n.container{max-width:900px;margin:30px auto;background:#fff;\npadding:20px;border-radius:14px;box-shadow:0 6px 18px rgba(0,0,0,.1)}\nh1{text-align:center;margin-bottom:10px}\ntable{width:100%;border-collapse:collapse}\nth,td{padding:10px;text-align:center;border-bottom:1px solid #ddd}\n\n.switch{position:relative;display:inline-block;width:50px;height:26px}\n.switch input{display:none}\n.slider{position:absolute;cursor:pointer;inset:0;background:#ccc;border-radius:34px}\n.slider:before{content:\"\";position:absolute;height:20px;width:20px;left:3px;bottom:3px;\nbackground:white;border-radius:50%;transition:.2s}\ninput:checked+.slider{background:#4CAF50}\ninput:checked+.slider:before{transform:translateX(24px)}\n\n.pwm-box{display:inline-flex;align-items:center;gap:8px;width:260px}\ninput[type=range]{width:180px}\n\n.value{\n  font-variant-numeric: tabular-nums;\n  display:inline-block;\n  width:3ch;\n  text-align:right;\n}\n&lt;\/style&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n&lt;div class=\"container\"&gt;\n&lt;h1&gt;ESP32 ESP-NOW Control&lt;\/h1&gt;\n\n&lt;table&gt;\n&lt;tr&gt;&lt;th&gt;Device&lt;\/th&gt;&lt;th&gt;LED&lt;\/th&gt;&lt;th&gt;PWM&lt;\/th&gt;&lt;th&gt;Voltage [V]&lt;\/th&gt;&lt;\/tr&gt;\n\n&lt;script&gt;\nasync function refreshState(){\n  const r = await fetch('\/api\/state',{cache:'no-store'});\n  const s = await r.json();\n  [1,2,3].forEach(d=&gt;{\n    const n=s['dev'+d];\n    document.getElementById('led'+d).checked=n.ledOn;\n    document.getElementById('pwm'+d).value=n.pwm;\n    document.getElementById('pwmt'+d).textContent=n.pwm;\n    document.getElementById('v'+d).textContent=n.voltage.toFixed(2);\n  });\n}\nasync function setLed(dev,val){\n  await fetch(`\/api\/set?dev=${dev}&amp;led=${val}`);\n}\nasync function setPwm(dev,val){\n  await fetch(`\/api\/set?dev=${dev}&amp;pwm=${val}`);\n}\nsetInterval(refreshState,5000);\nwindow.onload=refreshState;\n&lt;\/script&gt;\n)rawliteral\");\n\n  const char* names[]={\"ESP1 (Master)\",\"ESP2 (Worker)\",\"ESP3 (Worker)\"};\n\n  for(int i=1;i&lt;=3;i++){\n    html += \"&lt;tr&gt;&lt;td&gt;\"+String(names[i-1])+\"&lt;\/td&gt;\";\n\n    html += \"&lt;td&gt;&lt;label class='switch'&gt;&lt;input type='checkbox' id='led\"+String(i)+\"' \";\n    html += \"onchange='setLed(\"+String(i)+\",this.checked?1:0)'&gt;\";\n    html += \"&lt;span class='slider'&gt;&lt;\/span&gt;&lt;\/label&gt;&lt;\/td&gt;\";\n\n    html += \"&lt;td&gt;&lt;div class='pwm-box'&gt;\";\n    html += \"&lt;input type='range' min='0' max='255' id='pwm\"+String(i)+\"' \";\n    html += \"oninput='pwmt\"+String(i)+\".textContent=this.value' \";\n    html += \"onchange='setPwm(\"+String(i)+\",this.value)'&gt;\";\n    html += \"&lt;span class='value' id='pwmt\"+String(i)+\"'&gt;0&lt;\/span&gt;\";\n    html += \"&lt;\/div&gt;&lt;\/td&gt;\";\n\n    html += \"&lt;td&gt;&lt;span class='value' id='v\"+String(i)+\"'&gt;0.00&lt;\/span&gt;&lt;\/td&gt;&lt;\/tr&gt;\";\n  }\n\n  html += \"&lt;\/table&gt;&lt;\/div&gt;&lt;\/body&gt;&lt;\/html&gt;\";\n  return html;\n}\n\n\/\/ ================== JSON API ==================\n\nString jsonState() {\n  String j=\"{\";\n  for(int i=0;i&lt;3;i++){\n    j+=\"\\\"dev\"+String(i+1)+\"\\\":{\";\n    j+=\"\\\"ledOn\\\":\"+String(nodes[i].ledOn?1:0)+\",\";\n    j+=\"\\\"pwm\\\":\"+String(nodes[i].pwm)+\",\";\n    j+=\"\\\"voltage\\\":\"+String(nodes[i].voltage,3)+\"}\";\n    if(i&lt;2) j+=\",\";\n  }\n  j+=\"}\";\n  return j;\n}\n\n\/\/ ================== SETUP ==================\n\nvoid setup() {\n  Serial.begin(115200);\n\n  pinMode(LED_PIN, OUTPUT);\n  pinMode(ANALOG_PIN, INPUT);\n\n  ledcAttach(PWM_PIN, 5000, 8);\n  ledcWrite(PWM_PIN, 0);\n\n  for(int i=0;i&lt;3;i++){\n    nodes[i]={false,0,0.0f};\n  }\n\n  connectWiFi();\n  NowSerial2.begin(115200);\n  NowSerial3.begin(115200);\n\n  server.on(\"\/\", HTTP_GET, [](AsyncWebServerRequest* r){\n    r-&gt;send(200,\"text\/html\",buildMainPage());\n  });\n\n  server.on(\"\/api\/state\", HTTP_GET, [](AsyncWebServerRequest* r){\n    r-&gt;send(200,\"application\/json\",jsonState());\n  });\n\n  server.on(\"\/api\/set\", HTTP_GET, [](AsyncWebServerRequest* r){\n    int dev=r-&gt;getParam(\"dev\")-&gt;value().toInt();\n    bool hasLed=r-&gt;hasParam(\"led\");\n    bool hasPwm=r-&gt;hasParam(\"pwm\");\n\n    if(dev==1){\n      if(hasLed){ nodes[0].ledOn=r-&gt;getParam(\"led\")-&gt;value().toInt(); digitalWrite(LED_PIN,nodes[0].ledOn); }\n      if(hasPwm){ nodes[0].pwm=r-&gt;getParam(\"pwm\")-&gt;value().toInt(); ledcWrite(PWM_PIN,nodes[0].pwm); }\n    }\n    if(dev==2){\n      if(hasLed) nodes[1].ledOn=r-&gt;getParam(\"led\")-&gt;value().toInt();\n      if(hasPwm) nodes[1].pwm=r-&gt;getParam(\"pwm\")-&gt;value().toInt();\n      sendCommandToWorker(NowSerial2,nodes[1]);\n    }\n    if(dev==3){\n      if(hasLed) nodes[2].ledOn=r-&gt;getParam(\"led\")-&gt;value().toInt();\n      if(hasPwm) nodes[2].pwm=r-&gt;getParam(\"pwm\")-&gt;value().toInt();\n      sendCommandToWorker(NowSerial3,nodes[2]);\n    }\n    r-&gt;send(200,\"text\/plain\",\"OK\");\n  });\n\n  server.begin();\n  Serial.println(\"Master ready.\");\n}\n\n\/\/ ================== LOOP ==================\n\nvoid loop() {\n  unsigned long now=millis();\n\n  if(now-lastLocalVoltageMillis&gt;VOLTAGE_UPDATE_PERIOD){\n    lastLocalVoltageMillis=now;\n    nodes[0].voltage=readLocalVoltage();\n  }\n\n  processWorkerSerial(NowSerial2,nodes[1]);\n  processWorkerSerial(NowSerial3,nodes[2]);\n\n  delay(1);\n}\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-group=\"three_esp32_espnow_sta_complete\" data-enlighter-title=\"three_esp32_espnow_sta_complete_worker1.ino\">#include &lt;WiFi.h&gt;\n#include \"ESP32_NOW_Serial.h\"\n#include \"MacAddress.h\"\n\n#define ESPNOW_WIFI_CHANNEL 1        \/\/ same as master \/ router\n#define STATUS_SEND_PERIOD 2000      \/\/ [ms]\n\n\/\/ same pins as on master\nconst int LED_PIN    = 2;\nconst int PWM_PIN    = 5;\nconst int ANALOG_PIN = 34;\n\nIPAddress ip(192,168,178,113);  \/\/TODO\nIPAddress gateway(192,168,178,1);  \/\/TODO\nIPAddress subnet(255,255,255,0);  \/\/TODO\n\n\/\/ ---------- WiFi STA config ----------\nconstexpr char WIFI_SSID[] = \"Your SSID\";  \/\/TODO\nconstexpr char WIFI_PASS[] = \"Your password\";  \/\/TODO\n\/\/ ---------- MAC of master (STA interface) ----------\n\nconst MacAddress master_mac({0xC8, 0xC9, 0xA3, 0xC6, 0xFE, 0x54});  \/\/ TODO\n\nESP_NOW_Serial_Class NowSerial(master_mac, ESPNOW_WIFI_CHANNEL, WIFI_IF_STA);\n\n\/\/ data packets\nstruct WorkerStatusPacket {\n  float   voltage;\n  uint8_t pwm;\n  uint8_t ledOn;\n};\n\nstruct WorkerCommandPacket {\n  uint8_t pwm;\n  uint8_t ledOn;\n};\n\n\/\/ local state\nuint8_t currentPwm   = 0;\nbool    currentLedOn = false;\n\nunsigned long lastStatusSent = 0;\n\n\/\/ ---------- helpers ----------\nvoid connectWiFi() {\n  WiFi.mode(WIFI_STA);\n  WiFi.config(ip, gateway, subnet);\n  WiFi.begin(WIFI_SSID, WIFI_PASS, ESPNOW_WIFI_CHANNEL);\n  Serial.print(\"Connecting to WiFi\");\n  while (WiFi.status() != WL_CONNECTED) {\n    delay(500);\n    Serial.print(\".\");\n  }\n  Serial.println();\n  Serial.print(\"Connected. IP address: \");\n  Serial.println(WiFi.localIP());\n}\n\nfloat readVoltage() {\n  int raw = analogRead(ANALOG_PIN);\n  float voltage = 3.3f * raw \/ 4096.0f;\n  return voltage;\n}\n\nvoid applyCommand(const WorkerCommandPacket&amp; cmd) {\n  currentPwm   = cmd.pwm;\n  currentLedOn = (cmd.ledOn != 0);\n\n  ledcWrite(PWM_PIN, currentPwm);\n  digitalWrite(LED_PIN, currentLedOn ? HIGH : LOW);\n}\n\nvoid sendStatus() {\n  WorkerStatusPacket pkt;\n  pkt.voltage = readVoltage();\n  pkt.pwm     = currentPwm;\n  pkt.ledOn   = currentLedOn ? 1 : 0;\n\n  int ok = NowSerial.write((uint8_t*)&amp;pkt, sizeof(pkt));\n  if (!ok) {\n    Serial.println(\"Status send failed\");\n  }\n}\n\n\/\/ ---------- setup() ----------\nvoid setup() {\n  Serial.begin(115200);\n\n  pinMode(LED_PIN, OUTPUT);\n  digitalWrite(LED_PIN, LOW);\n\n  pinMode(ANALOG_PIN, INPUT);\n\n  ledcAttach(PWM_PIN, 5000, 8);\n  ledcWrite(PWM_PIN, 0);\n\n  connectWiFi();\n\n  \/\/ ESP-NOW Serial on STA interface\n  NowSerial.begin(115200);\n\n  Serial.println(\"Worker ready.\");\n}\n\n\/\/ ---------- loop() ----------\nvoid loop() {\n  unsigned long now = millis();\n\n  \/\/ periodic status to master\n  if (now - lastStatusSent &gt; STATUS_SEND_PERIOD) {\n    lastStatusSent = now;\n    sendStatus();\n  }\n\n  \/\/ incoming commands\n  while (NowSerial.available() &gt;= (int)sizeof(WorkerCommandPacket)) {\n    WorkerCommandPacket cmd;\n    NowSerial.readBytes((uint8_t*)&amp;cmd, sizeof(cmd));\n    applyCommand(cmd);\n  }\n\n  delay(1);\n}<\/pre>\n<p>\u00a0<\/p>\n<\/div>\n<p>\n\n<p>And here is the final result in the browser:<\/p>\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3esp32_asw_espnow_st_only_complete.png\"><img loading=\"lazy\" decoding=\"async\" width=\"982\" height=\"333\" src=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3esp32_asw_espnow_st_only_complete.png\" alt=\"Async WebServer - Browser output of three_esp32_espnow_sta_complete_master.ino\" class=\"wp-image-25201\" srcset=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3esp32_asw_espnow_st_only_complete.png 982w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3esp32_asw_espnow_st_only_complete-300x102.png 300w, https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/3esp32_asw_espnow_st_only_complete-768x260.png 768w\" sizes=\"auto, (max-width: 982px) 100vw, 982px\" \/><\/a><figcaption class=\"wp-element-caption\">Browser output of three_esp32_espnow_sta_complete_master.ino<\/figcaption><\/figure>\n","protected":false},"excerpt":{"rendered":"<p>I will show you step-by-step how you can control one or more ESP32s via your browser using the Async WebServer. We will switch LEDs, dim LEDs and read out values. <\/p>\n","protected":false},"author":1,"featured_media":25281,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[543,548,1320],"tags":[556,2812,2813,2821,2828,2819,2815,2817,2035,2546,1044,2827,1631,2825,2826,2816,2822,2818,2823,2824,2820,2814],"class_list":["post-25282","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-boards-and-microcontrollers","category-wireless","category-wireless-en-2","tag-arduino-en-2","tag-async-webserver","tag-asyncwebserver","tag-asyncwebserverrequest","tag-asyntcp","tag-browser","tag-control-several-eps32","tag-css","tag-esp-now-en","tag-esp-now-serial-en","tag-esp32-en","tag-espmdns","tag-get-request-en-2","tag-host","tag-hostname","tag-html","tag-iframe","tag-javascript","tag-json","tag-mac-address-2","tag-server-on","tag-webserver"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.4 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Async WebServer with the ESP32 &#8226; Wolles Elektronikkiste<\/title>\n<meta name=\"description\" content=\"I will show you step-by-step how you can control one or more ESP32s via your browser using the Async WebServer.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/wolles-elektronikkiste.de\/en\/async-webserver-with-the-esp32\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Async WebServer with the ESP32 &#8226; Wolles Elektronikkiste\" \/>\n<meta property=\"og:description\" content=\"I will show you step-by-step how you can control one or more ESP32s via your browser using the Async WebServer.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/wolles-elektronikkiste.de\/en\/async-webserver-with-the-esp32\" \/>\n<meta property=\"og:site_name\" content=\"Wolles Elektronikkiste\" \/>\n<meta property=\"article:published_time\" content=\"2025-12-14T11:48:09+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2025-12-14T11:48:16+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/postimage_asyncwebserver.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1024\" \/>\n\t<meta property=\"og:image:height\" content=\"1024\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"Wolfgang Ewald\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Wolfgang Ewald\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"67 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\\\/async-webserver-with-the-esp32#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\\\/async-webserver-with-the-esp32\"},\"author\":{\"name\":\"Wolfgang Ewald\",\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en#\\\/schema\\\/person\\\/b774e4d64b4766889a2f7c6e5ec85b46\"},\"headline\":\"Async WebServer with the ESP32\",\"datePublished\":\"2025-12-14T11:48:09+00:00\",\"dateModified\":\"2025-12-14T11:48:16+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\\\/async-webserver-with-the-esp32\"},\"wordCount\":3037,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en#\\\/schema\\\/person\\\/b774e4d64b4766889a2f7c6e5ec85b46\"},\"image\":{\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\\\/async-webserver-with-the-esp32#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/wp-content\\\/uploads\\\/2025\\\/12\\\/postimage_asyncwebserver.jpg\",\"keywords\":[\"Arduino\",\"Async WebServer\",\"AsyncWebServer\",\"AsyncWebServerRequest\",\"AsynTCP\",\"browser\",\"Control several EPS32\",\"CSS\",\"ESP-NOW\",\"ESP-NOW Serial\",\"ESP32\",\"ESPmDNS\",\"GET request\",\"host\",\"hostname\",\"HTML\",\"iframe\",\"JavaScript\",\"json\",\"MAC address\",\"server.on\",\"WebServer\"],\"articleSection\":[\"Boards and Microcontrollers\",\"Wireless\",\"wireless\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\\\/async-webserver-with-the-esp32#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\\\/async-webserver-with-the-esp32\",\"url\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\\\/async-webserver-with-the-esp32\",\"name\":\"Async WebServer with the ESP32 &#8226; Wolles Elektronikkiste\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\\\/async-webserver-with-the-esp32#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\\\/async-webserver-with-the-esp32#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/wp-content\\\/uploads\\\/2025\\\/12\\\/postimage_asyncwebserver.jpg\",\"datePublished\":\"2025-12-14T11:48:09+00:00\",\"dateModified\":\"2025-12-14T11:48:16+00:00\",\"description\":\"I will show you step-by-step how you can control one or more ESP32s via your browser using the Async WebServer.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\\\/async-webserver-with-the-esp32#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\\\/async-webserver-with-the-esp32\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\\\/async-webserver-with-the-esp32#primaryimage\",\"url\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/wp-content\\\/uploads\\\/2025\\\/12\\\/postimage_asyncwebserver.jpg\",\"contentUrl\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/wp-content\\\/uploads\\\/2025\\\/12\\\/postimage_asyncwebserver.jpg\",\"width\":1024,\"height\":1024},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\\\/async-webserver-with-the-esp32#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Startseite\",\"item\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Async WebServer with the ESP32\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en#website\",\"url\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en\",\"name\":\"Wolles Elektronikkiste\",\"description\":\"Die wunderbare Welt der Elektronik\",\"publisher\":{\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en#\\\/schema\\\/person\\\/b774e4d64b4766889a2f7c6e5ec85b46\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/en#\\\/schema\\\/person\\\/b774e4d64b4766889a2f7c6e5ec85b46\",\"name\":\"Wolfgang Ewald\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/wp-content\\\/uploads\\\/2019\\\/03\\\/cropped-Logo-1.png\",\"url\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/wp-content\\\/uploads\\\/2019\\\/03\\\/cropped-Logo-1.png\",\"contentUrl\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/wp-content\\\/uploads\\\/2019\\\/03\\\/cropped-Logo-1.png\",\"width\":512,\"height\":512,\"caption\":\"Wolfgang Ewald\"},\"logo\":{\"@id\":\"https:\\\/\\\/wolles-elektronikkiste.de\\\/wp-content\\\/uploads\\\/2019\\\/03\\\/cropped-Logo-1.png\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Async WebServer with the ESP32 &#8226; Wolles Elektronikkiste","description":"I will show you step-by-step how you can control one or more ESP32s via your browser using the Async WebServer.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/wolles-elektronikkiste.de\/en\/async-webserver-with-the-esp32","og_locale":"en_US","og_type":"article","og_title":"Async WebServer with the ESP32 &#8226; Wolles Elektronikkiste","og_description":"I will show you step-by-step how you can control one or more ESP32s via your browser using the Async WebServer.","og_url":"https:\/\/wolles-elektronikkiste.de\/en\/async-webserver-with-the-esp32","og_site_name":"Wolles Elektronikkiste","article_published_time":"2025-12-14T11:48:09+00:00","article_modified_time":"2025-12-14T11:48:16+00:00","og_image":[{"width":1024,"height":1024,"url":"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/postimage_asyncwebserver.jpg","type":"image\/jpeg"}],"author":"Wolfgang Ewald","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Wolfgang Ewald","Est. reading time":"67 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/wolles-elektronikkiste.de\/en\/async-webserver-with-the-esp32#article","isPartOf":{"@id":"https:\/\/wolles-elektronikkiste.de\/en\/async-webserver-with-the-esp32"},"author":{"name":"Wolfgang Ewald","@id":"https:\/\/wolles-elektronikkiste.de\/en#\/schema\/person\/b774e4d64b4766889a2f7c6e5ec85b46"},"headline":"Async WebServer with the ESP32","datePublished":"2025-12-14T11:48:09+00:00","dateModified":"2025-12-14T11:48:16+00:00","mainEntityOfPage":{"@id":"https:\/\/wolles-elektronikkiste.de\/en\/async-webserver-with-the-esp32"},"wordCount":3037,"commentCount":0,"publisher":{"@id":"https:\/\/wolles-elektronikkiste.de\/en#\/schema\/person\/b774e4d64b4766889a2f7c6e5ec85b46"},"image":{"@id":"https:\/\/wolles-elektronikkiste.de\/en\/async-webserver-with-the-esp32#primaryimage"},"thumbnailUrl":"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/postimage_asyncwebserver.jpg","keywords":["Arduino","Async WebServer","AsyncWebServer","AsyncWebServerRequest","AsynTCP","browser","Control several EPS32","CSS","ESP-NOW","ESP-NOW Serial","ESP32","ESPmDNS","GET request","host","hostname","HTML","iframe","JavaScript","json","MAC address","server.on","WebServer"],"articleSection":["Boards and Microcontrollers","Wireless","wireless"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/wolles-elektronikkiste.de\/en\/async-webserver-with-the-esp32#respond"]}]},{"@type":"WebPage","@id":"https:\/\/wolles-elektronikkiste.de\/en\/async-webserver-with-the-esp32","url":"https:\/\/wolles-elektronikkiste.de\/en\/async-webserver-with-the-esp32","name":"Async WebServer with the ESP32 &#8226; Wolles Elektronikkiste","isPartOf":{"@id":"https:\/\/wolles-elektronikkiste.de\/en#website"},"primaryImageOfPage":{"@id":"https:\/\/wolles-elektronikkiste.de\/en\/async-webserver-with-the-esp32#primaryimage"},"image":{"@id":"https:\/\/wolles-elektronikkiste.de\/en\/async-webserver-with-the-esp32#primaryimage"},"thumbnailUrl":"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/postimage_asyncwebserver.jpg","datePublished":"2025-12-14T11:48:09+00:00","dateModified":"2025-12-14T11:48:16+00:00","description":"I will show you step-by-step how you can control one or more ESP32s via your browser using the Async WebServer.","breadcrumb":{"@id":"https:\/\/wolles-elektronikkiste.de\/en\/async-webserver-with-the-esp32#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/wolles-elektronikkiste.de\/en\/async-webserver-with-the-esp32"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/wolles-elektronikkiste.de\/en\/async-webserver-with-the-esp32#primaryimage","url":"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/postimage_asyncwebserver.jpg","contentUrl":"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2025\/12\/postimage_asyncwebserver.jpg","width":1024,"height":1024},{"@type":"BreadcrumbList","@id":"https:\/\/wolles-elektronikkiste.de\/en\/async-webserver-with-the-esp32#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Startseite","item":"https:\/\/wolles-elektronikkiste.de\/en"},{"@type":"ListItem","position":2,"name":"Async WebServer with the ESP32"}]},{"@type":"WebSite","@id":"https:\/\/wolles-elektronikkiste.de\/en#website","url":"https:\/\/wolles-elektronikkiste.de\/en","name":"Wolles Elektronikkiste","description":"Die wunderbare Welt der Elektronik","publisher":{"@id":"https:\/\/wolles-elektronikkiste.de\/en#\/schema\/person\/b774e4d64b4766889a2f7c6e5ec85b46"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/wolles-elektronikkiste.de\/en?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/wolles-elektronikkiste.de\/en#\/schema\/person\/b774e4d64b4766889a2f7c6e5ec85b46","name":"Wolfgang Ewald","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2019\/03\/cropped-Logo-1.png","url":"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2019\/03\/cropped-Logo-1.png","contentUrl":"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2019\/03\/cropped-Logo-1.png","width":512,"height":512,"caption":"Wolfgang Ewald"},"logo":{"@id":"https:\/\/wolles-elektronikkiste.de\/wp-content\/uploads\/2019\/03\/cropped-Logo-1.png"}}]}},"_links":{"self":[{"href":"https:\/\/wolles-elektronikkiste.de\/en\/wp-json\/wp\/v2\/posts\/25282","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wolles-elektronikkiste.de\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/wolles-elektronikkiste.de\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/wolles-elektronikkiste.de\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/wolles-elektronikkiste.de\/en\/wp-json\/wp\/v2\/comments?post=25282"}],"version-history":[{"count":3,"href":"https:\/\/wolles-elektronikkiste.de\/en\/wp-json\/wp\/v2\/posts\/25282\/revisions"}],"predecessor-version":[{"id":25286,"href":"https:\/\/wolles-elektronikkiste.de\/en\/wp-json\/wp\/v2\/posts\/25282\/revisions\/25286"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/wolles-elektronikkiste.de\/en\/wp-json\/wp\/v2\/media\/25281"}],"wp:attachment":[{"href":"https:\/\/wolles-elektronikkiste.de\/en\/wp-json\/wp\/v2\/media?parent=25282"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/wolles-elektronikkiste.de\/en\/wp-json\/wp\/v2\/categories?post=25282"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/wolles-elektronikkiste.de\/en\/wp-json\/wp\/v2\/tags?post=25282"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}