Saturday, 3 January 2026

Captive WiFi Portal 2

 The Captive WiFi Portal has been updated.

I've got the LED pulsing continuously, and the HTML of the web page has an embedded SVG file.

I can't work out how to include the ON/OFF buttons yet, as the HTML in the Portal is presented before the setup inside a block like this


const char index_html[] PROGMEM = R"rawliteral(...

...</body></html>)rawliteral";

I need to work out how to move that code back into the void loop() part.


Embedding SVG

Embedding an SVG image was easier than I thought. All you have to do is put the XML code from the svg file between <svg> ... </svg> tags.

I was able to open the SVG of a file I had in Visual Studio Code and copy the raw XML.

I pasted that into https://www.w3schools.com/graphics/svg_inhtml.asp where you can play around with embedded SVG. This meant I could strip out a lot of unnecessary lines that Inkscape left behind.

The Code

#include <DNSServer.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include "ESPAsyncWebServer.h"
#include <Adafruit_NeoPixel.h>


DNSServer dnsServer;
AsyncWebServer server(80);

String user_name;
String proficiency;
bool name_received = false;
bool proficiency_received = false;

//Sed up the LED
int led = 18; // the PWM pin the LED is attached to
int brightness = 0;  // how bright the LED is
int fadeAmount = 5;  // how many points to fade the LED by

// How many internal neopixels do we have? some boards have more than one!
#define NUMPIXELS        1

Adafruit_NeoPixel pixels(NUMPIXELS, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800);

// Auxiliar variables to store the current output state
String NeoPixelColor = "red";
String output26State = "off";

// Assign output variables to GPIO pins
const int output26 = 26;

// Current time
unsigned long currentTime = millis();
// Previous time
unsigned long previousTime = 0;
// Define timeout time in milliseconds (example: 2000ms = 2s)
const long timeoutTime = 2000;



const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html><head>
  <title>Captive Portal Demo</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}
  .button { background-color: #4CAF50; border: none; color: white; padding: 16px 40px;
  text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}
  .button2 {background-color: #AF4C50;}</style>
  </head><body>
  <h1>Welcome to The Internet</h1>
  <br><br>
  <p>The Internet is online</p>
  <br><br>
  <p><B>DO NOT DROP THE INTERNET</B></p>
<svg xmlns:dc="http://purl.org/dc/elements/1.1/"
   width="162.6481"
   height="131.60365"
   id="svg2"
   sodipodi:version="0.32"
   inkscape:version="0.48.4 r9939"
   version="1.0"
   sodipodi:docname="specman-2thumbs.svg"
   inkscape:export-xdpi="90"
   inkscape:export-ydpi="90">
  <defs
     id="defs4" />
  <sodipodi:namedview
     id="base"
     pagecolor="#ffffff"
     bordercolor="#666666"
     borderopacity="1.0"
     gridtolerance="10000"
     guidetolerance="10"
     objecttolerance="10"
     inkscape:pageopacity="0.0"
     inkscape:pageshadow="2"
     inkscape:zoom="1.4142136"
     inkscape:cx="66.493527"
     inkscape:cy="82.80098"
     inkscape:document-units="px"
     inkscape:current-layer="layer2"
     width="228.89px"
     height="228.89008px"
     inkscape:window-width="1366"
     inkscape:window-height="745"
     inkscape:window-x="-8"
     inkscape:window-y="-8"
     showgrid="false"
     inkscape:window-maximized="1"
     fit-margin-top="0"
     fit-margin-left="0"
     fit-margin-right="0"
     fit-margin-bottom="0" />

  <g
     inkscape:label="Layer 1"
     inkscape:groupmode="layer"
     id="layer1"
     transform="translate(-522.96998,-264.37834)">
    <path
       inkscape:connector-curvature="0"
       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
       d="m 536.17537,313.47567 23.72461,5.26226"
       id="path3152" />
    <path
       inkscape:connector-curvature="0"
       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
       d="m 548.78306,296.71428 17.46696,11.43071"
       id="path3154" />
    <path
       inkscape:connector-curvature="0"
       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
       d="m 563.51063,284.42356 15.16233,15.80899"
       id="path3156" />
    <path
       inkscape:connector-curvature="0"
       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
       d="m 581.81535,272.03727 3.6329,21.69714"
       id="path3158" />
    <path
       inkscape:connector-curvature="0"
       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
       d="m 601.01676,265.62834 -1.21365,25.5116"
       id="path3160" />
    <path
       inkscape:connector-curvature="0"
       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
       d="m 619.70372,267.55072 -5.6875,19.62983"
       id="path3162" />
    <path
       inkscape:connector-curvature="0"
       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
       d="m 637.82687,272.01504 -11.94834,17.79663"
       id="path3164" />
    <path
       sodipodi:type="arc"
       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:5.6008954;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
       id="path2160"
       sodipodi:cx="490.47076"
       sodipodi:cy="244.74196"
       sodipodi:rx="107.56804"
       sodipodi:ry="86.843918"
       d="m 598.0388,244.74196 c 0,47.96257 -48.15985,86.84392 -107.56804,86.84392 -59.40818,0 -107.56803,-38.88135 -107.56803,-86.84392 0,-47.96257 48.15985,-86.84392 107.56803,-86.84392 59.40819,0 107.56804,38.88135 107.56804,86.84392 z"
       transform="matrix(0.3573021,-0.2275734,0.2526576,0.3966856,362.81748,335.45959)" />
    <path
       sodipodi:type="arc"
       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:3.15332937;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
       id="path3134"
       sodipodi:cx="557"
       sodipodi:cy="311.59448"
       sodipodi:rx="16"
       sodipodi:ry="16.5"
       d="m 573,311.59448 c 0,9.1127 -7.16344,16.5 -16,16.5 -8.83656,0 -16,-7.3873 -16,-16.5 0,-9.1127 7.16344,-16.5 16,-16.5 8.83656,0 16,7.3873 16,16.5 z"
       transform="matrix(0.66195777,-0.43024315,0.42161517,0.67550415,84.022503,343.51634)" />
    <path
       sodipodi:type="arc"
       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
       id="path3136"
       sodipodi:cx="593"
       sodipodi:cy="316.09448"
       sodipodi:rx="9"
       sodipodi:ry="11"
       d="m 602,316.09448 c 0,6.07513 -4.02944,11 -9,11 -4.97056,0 -9,-4.92487 -9,-11 0,-6.07513 4.02944,-11 9,-11 4.97056,0 9,4.92487 9,11 z"
       transform="matrix(0.8434483,-0.5372104,0.5372104,0.8434483,-56.561173,354.02408)" />
    <path
       sodipodi:type="arc"
       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
       id="path3138"
       sodipodi:cx="559"
       sodipodi:cy="317.09448"
       sodipodi:rx="6"
       sodipodi:ry="6"
       d="m 565,317.09448 c 0,3.31371 -2.68629,6 -6,6 -3.31371,0 -6,-2.68629 -6,-6 0,-3.31371 2.68629,-6 6,-6 3.31371,0 6,2.68629 6,6 z"
       transform="matrix(0.8434483,-0.5372104,0.5372104,0.8434483,-52.504963,346.67495)" />
    <path
       sodipodi:type="arc"
       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
       id="path3140"
       sodipodi:cx="594"
       sodipodi:cy="320.09448"
       sodipodi:rx="4"
       sodipodi:ry="4"
       d="m 598,320.09448 c 0,2.20914 -1.79086,4 -4,4 -2.20914,0 -4,-1.79086 -4,-4 0,-2.20914 1.79086,-4 4,-4 2.20914,0 4,1.79086 4,4 z"
       transform="matrix(0.8434483,-0.5372104,0.5372104,0.8434483,-63.544403,354.50048)" />
    <path
       inkscape:connector-curvature="0"
       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
       d="m 587.27637,344.798 c 7.5499,18.99764 53.32665,-8.88788 41.02273,-24.94265"
       id="path3142"
       sodipodi:nodetypes="cc" />
  </g>
  <g
     inkscape:groupmode="layer"
     id="layer2"
     inkscape:label="spex"
     transform="translate(-42.467034,0)">
    <g
       id="g3834"
       inkscape:transform-center-y="6.9492243"
       transform="translate(-231.63385,-325.61837)">
      <path
         sodipodi:nodetypes="csssssscsc"
         inkscape:connector-curvature="0"
         id="rect3016"
         d="m 309.92368,377.34669 c 0.16434,0.77209 0.38786,1.52249 0.67664,2.28343 l 0.84264,2.22043 c 3.11216,8.20078 12.38701,12.2339 20.79562,9.04286 l 3.79449,-1.43999 c 8.40861,-3.19103 12.67254,-12.36206 9.56038,-20.56284 l -0.84264,-2.22043 c -0.28878,-0.76094 -0.61942,-1.47068 -1.00866,-2.15743 -0.91368,-3.32758 -10.60562,-1.75272 -16.10825,0.24197 -5.79886,2.10208 -19.56865,9.37964 -17.71022,12.592 z"
         style="fill:#f9f9f9;fill-opacity:0.21637424;stroke:#000000;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
      <rect
         transform="matrix(0.93494003,-0.35480579,0.35480578,0.93494003,0,0)"
         y="461.66455"
         x="149.74968"
         height="6.1890011"
         width="5.6407523"
         id="rect3021"
         style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
      <path
         style="fill:#f9f9f9;fill-opacity:0.21637424;stroke:#000000;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
         d="m 382.3489,349.86163 c 0.38927,0.68673 0.71988,1.39648 1.00865,2.15743 l 0.84264,2.22042 c 3.11216,8.20078 -1.15176,17.37181 -9.56037,20.56284 l -3.7945,1.44 c -8.40861,3.19103 -17.68346,-0.84209 -20.79562,-9.04287 l -0.84264,-2.22043 c -0.28877,-0.76094 -0.51226,-1.51135 -0.67663,-2.28343 -1.52403,-3.09595 6.77256,-8.34767 12.21314,-10.50588 5.73347,-2.27439 20.86463,-5.96462 21.60533,-2.32808 z"
         id="path3023"
         inkscape:connector-curvature="0"
         sodipodi:nodetypes="csssssscsc" />
      <rect
         transform="matrix(-0.93494003,0.35480579,0.35480578,0.93494003,0,0)"
         style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
         id="rect3025"
         width="5.6407523"
         height="6.1890011"
         x="-239.46594"
         y="461.66455" />
      <path
         sodipodi:nodetypes="ccccc"
         inkscape:connector-curvature="0"
         id="rect3027"
         d="m 343.60543,363.39377 c 1.86816,0.61701 3.43149,0.43077 4.28493,-1.62611 l 1.34193,3.53609 c -1.88821,-0.66983 -3.2085,0.15684 -4.28493,1.62612 z"
         style="fill:#ffffff;fill-opacity:0;stroke:#000000;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1" />
    </g>
    <path
       style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2.5;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
       d="m 176.64891,31.409261 c -0.47333,4.885951 -1.70961,10.946289 6.99683,25.202804 -8.51638,-1.547864 -25.23345,-0.992063 -31.71459,1.041623 -4.36201,1.368736 -7.79963,4.556089 -7.30104,9.100539 0.33296,3.03471 2.25343,5.484505 4.84639,6.636034 l 0.33819,2.509335 c -1.8196,1.381125 -2.90285,3.628399 -2.63458,6.073557 0.27861,2.539412 1.92847,4.613589 4.12667,5.520371 0.008,0.0031 -0.91905,1.711823 0.031,-0.0034 l 0.17749,1.943254 c -0.0477,0.467471 -2.79292,0.362862 -2.27114,4.570887 0.52175,4.208058 5.37926,4.577658 6.06928,5.506318 -5.06742,14.144447 29.29326,6.411587 36.45201,6.021847 6.8945,0.0293 12.27852,-5.680577 12.09517,-12.110087 L 202.972,62.11375 c -0.12297,-4.312764 -2.73403,-7.961615 -6.51573,-9.816697 -3.51452,-3.189714 -11.89122,-9.684811 -0.51149,-26.635494 1.71884,-2.560293 0.19925,-7.25678 -6.69822,-7.125152 -8.03931,0.153418 -11.84741,5.128463 -12.59766,12.872856 z"
       id="rect3057"
       inkscape:connector-curvature="0"
       sodipodi:nodetypes="scssccsccczcccccsss" />
    <path
       sodipodi:nodetypes="scssccsccczcccccsss"
       inkscape:connector-curvature="0"
       id="path3838"
       d="m 65.394507,54.717635 c 2.075804,4.448325 5.262557,9.749333 1.809502,26.093345 7.512502,-4.299643 23.457763,-9.35119 30.246082,-9.595555 4.568759,-0.164464 8.872599,1.693863 9.918239,6.144398 0.69824,2.971999 -0.2952,5.922044 -2.35563,7.87245 l 0.51809,2.478449 c 2.17605,0.695167 3.9468,2.452481 4.5094,4.84711 0.5843,2.486932 -0.27931,4.992605 -2.04921,6.580624 -0.007,0.0055 1.43735,1.307284 -0.0302,0.0075 l 0.48079,1.891184 c 0.20088,0.42479 2.75403,-0.58942 3.6656,3.5517 0.9116,4.14113 -3.5445,6.10965 -3.88529,7.21528 9.49477,11.64447 -25.477573,15.81444 -32.356415,17.83462 -6.489963,2.3271 -13.47008,-1.26015 -15.441616,-7.38267 L 50.819401,92.443774 c -1.322476,-4.106837 -0.0779,-8.41761 2.868561,-11.427757 2.249443,-4.17925 7.980249,-13.096271 -8.40134,-25.280994 -2.474339,-1.840423 -2.608143,-6.774817 3.938293,-8.951189 7.630165,-2.536659 12.879512,0.883437 16.169735,7.934176 z"
       style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:2.5;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
       inkscape:transform-center-x="17.070774"
       inkscape:transform-center-y="-17.110181" />
  </g>
</svg>
 
</body></html>)rawliteral";

class CaptiveRequestHandler : public AsyncWebHandler {
public:
  CaptiveRequestHandler() {}
  virtual ~CaptiveRequestHandler() {}

  bool canHandle(AsyncWebServerRequest *request){
    //request->addInterestingHeader("ANY");
    return true;
  }

  void handleRequest(AsyncWebServerRequest *request) {
    request->send_P(200, "text/html", index_html);
  }
};

void setupServer(){
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
      request->send_P(200, "text/html", index_html);
      Serial.println("Client Connected");
  });
   
  server.on("/get", HTTP_GET, [] (AsyncWebServerRequest *request) {
      String inputMessage;
      String inputParam;
 
      if (request->hasParam("name")) {
        inputMessage = request->getParam("name")->value();
        inputParam = "name";
        user_name = inputMessage;
        Serial.println(inputMessage);
        name_received = true;
      }

      if (request->hasParam("proficiency")) {
        inputMessage = request->getParam("proficiency")->value();
        inputParam = "proficiency";
        proficiency = inputMessage;
        Serial.println(inputMessage);
        proficiency_received = true;
      }
      request->send(200, "text/html", "The values entered by you have been successfully sent to the device <br><a href=\"/\">Return to Home Page</a>");
  });
}

void setup() {
  // put your setup code here, to run once:
  //your other setup stuff...
    Serial.begin(115200);
    Serial.println();
    Serial.println("Setting up AP Mode");
    WiFi.mode(WIFI_AP);
    WiFi.softAP("The_Internet");
    Serial.print("AP IP address: ");Serial.println(WiFi.softAPIP());
    Serial.println("Setting up Async WebServer");
    setupServer();
    Serial.println("Starting DNS Server");
    dnsServer.start(53, "*", WiFi.softAPIP());
    server.addHandler(new CaptiveRequestHandler()).setFilter(ON_AP_FILTER);//only when requested from AP
    //more handlers...
    server.begin();
    Serial.println("All Done!");

    // declare pin 18 to be an output for the LED:
  pinMode(led, OUTPUT);
 
 #if defined(NEOPIXEL_POWER)
  // If this board has a power control pin, we must set it to output and high
  // in order to enable the NeoPixels. We put this in an #if defined so it can
  // be reused for other boards without compilation errors
  pinMode(NEOPIXEL_POWER, OUTPUT);
  digitalWrite(NEOPIXEL_POWER, HIGH);
#endif

  pixels.begin(); // INITIALIZE NeoPixel strip object (REQUIRED)
  pixels.setBrightness(20); // not so bright
  // set color red
   pixels.fill(0xFF0000);
   pixels.show();

   // set the brightness of pin 9:
  analogWrite(led, brightness);
}

void loop(){
  analogWrite(led, brightness);
  dnsServer.processNextRequest();
  if(name_received && proficiency_received){
      Serial.print("Hello ");Serial.println(user_name);
      Serial.print("You have stated your proficiency to be ");Serial.println(proficiency);
      name_received = false;
      proficiency_received = false;
      Serial.println("We'll wait for the next client now");
    }

  //do the fade
    // change the brightness for next time through the loop:
    brightness = brightness + fadeAmount;
    delay(50);  

    // reverse the direction of the fading at the ends of the fade:
    if (brightness <= 0 || brightness >= 255) {
      fadeAmount = -fadeAmount;
      delay(50);
    }
}

CH32V203 Big Round Button

Having successfully uploaded the Blink code to the  CH32V203 , the next task is to try the HID button demo. The circuit I’m using has two bu...