Multiple Web Pages with SoftAP [Edited]

Continuing the discussion from SoftAP with WebServer.h:

I'm hitting a problem when my webpage gets larger than about 9500 bytes (approx). I am unsure if it is related to timeout issues (the time it takes for the Photon to parse out the webpage to the browser) or if I've hit the ceiling on the size of a char array.

A followup question: how may I use @rickkas7 's code (linked topic) to add a second page (with links to each other) so that I may put size and/or timeout issues aside?

There are a couple lines of code missing from my example for use when not in SoftAP mode to support multiple pages. I’ll write and test those tonight or tomorrow morning. They’re where this code is:

			// Ignore the actual request and just return the index.html data
			responseCode = 200;
			responseStr = "OK";
			fileToSend = &fileInfo[0];

Is the web page not being returned properly when large over SoftAP or not SoftAP? They use different code paths.

Thanks for the response.

I’m using the SoftAP as-is verbatim. The SoftAP pages deliver well.

My issue is the page I’m delivering once the Photon is connected and a connection from a browser is attempted.

I really appreciate your help.

I fixed the bug where long responses were truncated on the local server. The timeout wasn’t reset on sending output.

And I made a modification so the local server actually returns the file you requested instead of always returning the index.html file.

Though I found a problem with SoftAP. I can’t get it to return large files. Or at least a 16K or 50K file, which works fine with the local server. Not sure what’s going on there, but smaller files are fine.

Also, I modified the code to allow a page to be either for SoftAP or local server, or for both.

Since there’s a JPEG file embedded in the source to test large files now, it’s too large to paste here.

The code is here:

I also noticed this issue.
Me it was around 5.5k. Arrays longer than that didn’t work so what I did is I splitted the array in 2 array and it worked well…

Maybe something related to the dynamically allocated ram in a stream when you push all the data. TCPStreams arnt using pointers, they are copying the data directly to the stream but if all the ram is used, this may be the issue…
It’s just an idea…

I also found few other issues with the softap like when you want to read more than 1152 bytes from a json request by example, you have to read it in chunk of 1152 bytes or some part of the data will be lost (dont do a read of 512 bytes twice, there will be a hole in the data between the 2 reads or the second read will read 0).

It looks like you only modified the readRequest() method, yes?

  • readRequest is changed dramatically
  • writeResponse sets lastUse when count > 0
  • FileInfo structure now has an options field that specifies which server the file is for
  • New constants FOR_SOFTAP_SERVER and FOR_LOCAL_SERVER
2 Likes

OK, thanks for doing all this.

I need to review it, figure it all out and build a working version on my own… then, on to refactoring my project to try to get this in.

:grinning:

2 Likes

So, a snowy day here gave me a little time between sledding and snowball fights…

An example of what I was looking for, using your base code follows. It works like this:

  1. uses the basic SoftAP code with WiFi discovery - I had to use that to get all of the Wi-Fi html out of the Photon.
  2. It has two web pages, once connected… a main and a setup with links to each other.

It works, but my coding is a bit of a kludge… any suggestions would be appreciated.

@rickkas7… thanks so much.

(image removed to allow posting, but it works with the image posted above)


#define PARTICLE_DEVICE_ID "<your id>"  //in parens

#define DEVICE_ACCESS_TOKEN "<your access token>"   //in parens

/******** SOFT AP CODE STARTS HERE ********/

INSERT SOFTAP CODE FROM NEXT POST HERE

/******** SOFT AP CODE ENDS HERE ********/

/* Function prototypes -------------------------------------------------------*/

int ledPower(String command);

/* Function prototypes end----------------------------------------------------*/

const int FOR_SOFTAP_SERVER = 0x0001;
const int FOR_LOCAL_SERVER = 0x0002;

// [start b4dea613-4f5a-47ac-b70e-e4d50d19e7f9]
// name=/image.jpg contentType=image/jpeg size=18516 modified=2017-01-07 08:46:11
const byte mainIMAGE[] = {};

const char mainHTML[] =
  "<?xml version='1.0' encoding='UTF-8' ?>"
  "<!DOCTYPE html>"
  "<html xmlns='http://www.w3.org/1999/xhtml'>"
  "<head>"
  "<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />"
  "<meta name='viewport' content='width=device-width,initial-scale=1.0,user-scalable=0'>"
  "<title>LightBulb</title>"
  "<script src='http://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>"
  "<script type='text/javascript' src='main.js'></script>   "
  "<link rel='stylesheet' type='text/css' href='main.css' />"
  ""
  "</head>"
  "<body align='center'>"
  "<div id='main'>"
  "<div>"
  "<h2>Example of Web Page</h2>"
  "<h2>Using Particle SoftAP</h2>"
  "</div>"
  "<hr />"
  "<div id = 'ledButton' onClick = 'toggleBulb()'><canvas id='lightBulb' width='100' height='75'></canvas></div>"
  "<div><h4>Click Me To Toggle Led</h4></div>"
  "<hr />"
  "<a href='./setup.html'>Setup Page</a>"
  "</div>"
  //	"<p><img src='image.jpg'/></p>"
  //	"<hr/>"
  "</body>"
  "</html>";

// name=/main.css contentType=text/css size=222 modified=2017-01-07 07:18:59
const char mainCSS[] =
  "@CHARSET 'UTF-8';"
  "width: 600px;"
  "body{align: center; background-color: yellow; font-family: Arial, Helvetica, sans-serif;} "
  "h2{color:maroon;} "
  ".hide{display:none;}";

// name=/main.js contentType=application/javascript size=23 modified=2017-01-07 08:05:33
const char mainJS[] =
  "$.ajaxSetup({timeout: 5 * 1000});"
  "var refreshAjax = function() {"
  "getParticleData();"
  "};"
  "$(document).ready(function(){"
  "getParticleData();"
  "});"
  "var timer = window.setInterval(refreshAjax, 2000);"
  "var accessToken = '"
  DEVICE_ACCESS_TOKEN
  "';"
  "var deviceID = '"
  PARTICLE_DEVICE_ID
  "';"
  "function getParticleData(){"
  "var deviceURL = 'https://api.spark.io/v1/devices/' + deviceID + '/' + 'ledState' + '/?access_token=' + accessToken;"
  "$.getJSON(deviceURL, function(data) {"
  "var ledState = JSON.parse(data.result);"
  "drawBulb(ledState);"
  "});"
  "};"
  "var cx=50;"
  "var cy=25;"
  "var bottom = 50;"
  "var radius=20;"
  "var startAngle=.65 * Math.PI;"
  "var endAngle= .35 * Math.PI;"
  "function drawBulb(powerState){"
  "var ctx = document.getElementById('lightBulb').getContext('2d');"
  "ctx.lineCap='round';"
  "ctx.lineWidth=3;"
  "ctx.lineJoin='round';"
  "ctx.beginPath();"
  "ctx.arc(cx,cy,radius,startAngle,endAngle);"
  "var start=xyOnArc(cx,cy,radius, startAngle);"
  "var end=xyOnArc(cx,cy,radius,endAngle);"
  "ctx.lineTo(end.x, bottom);"
  "ctx.lineTo(start.x, bottom);"
  "ctx.lineTo(start.x, end.y);"
  "if(powerState == 1)"
  "{ctx.fillStyle = 'yellow';"
  "ctx.fill();};"
  "if(powerState == 0)"
  "{ctx.fillStyle = 'white';"
  "ctx.fill();};"
  "ctx.lineCap='round';"
  "ctx.moveTo(end.x, bottom + 5);"
  "ctx.lineTo(start.x, bottom + 5);"
  "ctx.moveTo(end.x - 2, bottom + 10);"
  "ctx.lineTo(start.x + 2, bottom + 10);"
  "ctx.moveTo(end.x - 12, bottom + 13);"
  "ctx.lineTo(start.x + 12, bottom + 13);"
  "ctx.stroke();"
  "};"
  "function xyOnArc(cx,cy,radius,radianAngle){"
  "var x=cx+radius*Math.cos(radianAngle);"
  "var y=cy+radius*Math.sin(radianAngle);"
  "return({x:x,y:y});"
  "};"
  "function toggleBulb(){"
  "var requestURL = 'https://api.spark.io/v1/devices/' + deviceID + '/Toggle/';"
  "$.post(requestURL, {"
  "params: -1,"
  "access_token: accessToken"
  "});"
  "getParticleData();"
  "}";

// name=/index.html contentType=text/html size=464 modified=2017-01-07 07:28:00
const char setupHTML[] =
  "<?xml version='1.0' encoding='UTF-8' ?>"
  "<!DOCTYPE html>"
  "<html xmlns='http://www.w3.org/1999/xhtml'>"
  "<head>"
  "<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />"
  "<meta name='viewport' content='width=device-width,initial-scale=1.0,user-scalable=0'>"
  "<title>Setup Page</title>"
  "<script src='http://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>"
  "<script type='text/javascript' src='setup.js'></script>"
  "<link rel='stylesheet' type='text/css' href='setup.css' />"
  "</head>"
  "<body align='center'>"
  "<div id='main'>"
  "<h2>This is the Setup Page</h2>"
  "<hr/>"
  "<a href='.'>Main Page</a>"
  "<hr/>"
  //	"<p><img src='image.jpg'/></p>"
  //	"<hr/>"
  "</div> <!-- main -->"
  "</body>"
  "</html>";

const char setupCSS[] =
  "@CHARSET 'UTF-8';"
  "width: 600px;"
  "body{align: center; background-color: yellow; font-family: Arial, Helvetica, sans-serif;} "
  "h2{color:maroon;} "
  ".hide{display:none;}";

const char setupJS[] =
  ""
  "$(document).ready(function(){"
  "console.log('loaded');"
  "})"
  "";

typedef struct {
  const char *name;
  const char *mimeType;
  const uint8_t *data;
  size_t dataSize;
  unsigned long modified;
  bool isBinary;
  int options;
} FileInfo;

const FileInfo mainPage[] = {
  {"/image.jpg", "image/jpeg", mainIMAGE, sizeof(mainIMAGE), 1483796771, TRUE, FOR_SOFTAP_SERVER | FOR_LOCAL_SERVER},
  {"/index.html", "text/html", (const uint8_t *)mainHTML, sizeof(mainHTML) - 1, 1483792090, FALSE, FOR_LOCAL_SERVER},
  {"/main.css", "text/css", (const uint8_t *)mainCSS, sizeof(mainCSS) - 1, 1483791539, FALSE, FOR_SOFTAP_SERVER | FOR_LOCAL_SERVER},
  {"/main.js", "application/javascript", (const uint8_t *)mainJS, sizeof(mainJS) - 1, 1483794333, FALSE, FOR_SOFTAP_SERVER | FOR_LOCAL_SERVER},
  {NULL, NULL, 0, 0, FALSE, 0}
};

const FileInfo setupPage[] = {
  {"/image.jpg", "image/jpeg", mainIMAGE, sizeof(mainIMAGE), 1483796771, TRUE, FOR_SOFTAP_SERVER | FOR_LOCAL_SERVER},
  {"/setup.html", "text/html", (const uint8_t *)setupHTML, sizeof(setupHTML) - 1, 1483792090, FALSE, FOR_LOCAL_SERVER},
  {"/setup.css", "text/css", (const uint8_t *)setupCSS, sizeof(setupCSS) - 1, 1483791539, FALSE, FOR_SOFTAP_SERVER | FOR_LOCAL_SERVER},
  {"/setup.js", "application/javascript", (const uint8_t *)setupJS, sizeof(setupJS) - 1, 1483794333, FALSE, FOR_SOFTAP_SERVER | FOR_LOCAL_SERVER},
  {NULL, NULL, 0, 0, FALSE, 0}
};

static const char *dateFormatStr = "%a, %d %b %Y %T %z";

enum State {
  FREE_STATE,
  READ_REQUEST_STATE,
  WRITE_HEADER_STATE,
  WRITE_RESPONSE_STATE
};

const int MAX_CLIENTS = 5;
const int LISTEN_PORT = 80;
const int CLIENT_BUF_SIZE = 1024;
const int MAX_TO_WRITE = 1024;
const unsigned long INACTIVITY_TIMEOUT_MS = 30000;

class ClientConnection {
  public:
    ClientConnection();
    virtual ~ClientConnection();

    void loop();
    bool accept();

  protected:
    void clear();
    void readRequest();
    void generateResponseHeader();
    void writeResponse();

  private:
    uint8_t clientBuf[CLIENT_BUF_SIZE + 1];
    State state;
    int clientId;
    TCPClient client;
    int readOffset;
    int writeOffset;
    unsigned long lastUse;
    time_t startTime;

    // Response data
    int responseCode;
    String responseStr;
    const FileInfo *fileToSend;

    const uint8_t *sendBuf;
    size_t sendOffset;
    size_t sendLen;

};

String localIP;
TCPServer server(LISTEN_PORT);
ClientConnection clients[MAX_CLIENTS];
int nextClientId = 1;
bool wifiUp = false;
uint8_t led2 = D7;
int ledState = 0;

void setup()
{
  Serial.begin(9600);
  // From CLI, use something like:
  // particle get test5 localip
  // to get the IP address of the Photon (replace "test5" with your device name)
  pinMode(led2, OUTPUT);
  Particle.variable("localip", localIP);
  Particle.variable("ledState", ledState);
  Particle.function("Toggle", ledPower);
}

void loop()
{
  if (WiFi.listening()) // In listening mode, running SoftAP
  {
    wifiUp = false;
    return;
  }

  if (WiFi.ready())
  {
    if (!wifiUp)
    {
      Serial.println("wifi up");
      // WiFi.localIP() will return 0.0.0.0 sometimes immediately after WiFi.ready()
      // This shouldn't happen very often, so a 500 millisecond delay won't be a problem.
      delay(500);
      localIP = WiFi.localIP(); // localIP must be a global variable
      Serial.printlnf("server=%s:%d", localIP.c_str(), LISTEN_PORT);
      server.begin();
      wifiUp = true;
    }
  }
  else
  {
    if (wifiUp)
    {
      Serial.println("wifi down");
      wifiUp = false;
    }
  }
  // Handle any existing connections
  for (int ii = 0; ii < MAX_CLIENTS; ii++)
  {
    clients[ii].loop();
  }
  // Accept a new one if there is one waiting (and we have a free client)
  for (int ii = 0; ii < MAX_CLIENTS; ii++)
  {
    if (clients[ii].accept())
    {
      break;
    }
  }
  static uint32_t lastUpdateMillis = 0;
  if (millis() - lastUpdateMillis > 5000)
  {
    Serial.println(".");
    lastUpdateMillis = millis();
  }
}

int ledPower(String command)
{
  ledState = ledState == 0 ? 1 : 0;
  digitalWrite(D7, ledState);
  return ledState;
}


ClientConnection::ClientConnection() : state(FREE_STATE) {
  clear();
}

ClientConnection::~ClientConnection() {
}

void ClientConnection::loop() {
  if (state == FREE_STATE) {
    return;
  }

  if (client.connected()) {
    switch (state) {
      case READ_REQUEST_STATE:
        readRequest();
        break;

      case WRITE_HEADER_STATE:
      case WRITE_RESPONSE_STATE:
        writeResponse();
        break;
    }

    if (millis() - lastUse > INACTIVITY_TIMEOUT_MS) {
      Serial.printlnf("%d: inactivity timeout", clientId);
      client.stop();
      clear();
    }
  }
  else {
    Serial.printlnf("%d: client disconnected", clientId);
    client.stop();
    clear();
  }
}

bool ClientConnection::accept() {
  if (state != FREE_STATE) {
    return false;
  }

  client = server.available();
  if (client.connected()) {
    lastUse = millis();
    state = READ_REQUEST_STATE;
    clientId = nextClientId++;
    startTime = Time.now();
    Serial.printlnf("%d: connection accepted", clientId);
  }
  return true;
}

void ClientConnection::clear() {
  lastUse = 0;
  readOffset = 0;
  writeOffset = 0;
  state = FREE_STATE;
  fileToSend = 0;
}

void ClientConnection::readRequest() {
  // Note: client.read returns -1 if there is no data; there is no need to call available(),
  // which basically does the same check as the one inside read().
  size_t toRead = CLIENT_BUF_SIZE - readOffset;
  if (toRead == 0) {
    // Didn't get end of header
    Serial.printlnf("%d: didn't receive end-of-header", clientId);
    client.stop();
    return;
  }

  int count = client.read(&clientBuf[readOffset], toRead);
  if (count > 0) {
    readOffset += count;
    clientBuf[readOffset] = 0;

    if (strstr((const char *)clientBuf, "\015\012\015\012")) {
      // Parse request line

      char *requestMethodStr = strtok((char *)clientBuf, " ");
      if (requestMethodStr != NULL) {
        if (strcmp(requestMethodStr, "GET") == 0) {
          const char *url = strtok(NULL, " ");
          if (url != NULL)
          {
            // const char *vers = strtok(NULL, " ");
            Serial.print("URL =");
            Serial.println(url);
            const FileInfo* ptrToPage;
            if (strcmp(url, "/") == 0) {
              url = "/index.html";
              Serial.println("Load Main Page");
              ptrToPage = mainPage;
            }
            else if (strstr(url, "setup") != 0)
            {
              Serial.println("Load Setup Page");
              ptrToPage = setupPage;
            }
            else
            {
              Serial.println("Load Main Page");
              ptrToPage = mainPage;
            }
            Serial.printlnf("%d: request: %s", clientId, url);

            int index = -1;

            for (size_t ii = 0; ptrToPage[ii].name != NULL; ii++) {
              if ((strcmp(ptrToPage[ii].name, url) == 0) && ((ptrToPage[ii].options & FOR_LOCAL_SERVER) != 0)) {
                index = (int) ii;
                break;
              }
            }
            if (index >= 0) {
              responseCode = 200;
              responseStr = "OK";
              fileToSend = &ptrToPage[index];
              Serial.printlnf("%d: sending %s index=%d", clientId, fileToSend->name, index);
            }
            else {
              responseCode = 404;
              responseStr = "Not found";
              Serial.printlnf("%d: not found", clientId);
            }
          }
        }
        else {
          responseCode = 405;
          responseStr = "Unsupported method";
          Serial.printlnf("%d: unsupported method", clientId);
        }
      }

      generateResponseHeader();
    }
    lastUse = millis();
  }
}

void ClientConnection::generateResponseHeader() {
  char *dst = (char *)clientBuf;
  char *end = &dst[CLIENT_BUF_SIZE];
  // Generate HTTP response header
  // HTTP/1.0 200 OK
  dst += snprintf(dst, end - dst, "HTTP/1.0 %d %s\r\n", responseCode, responseStr.c_str());

  // Date
  String s = Time.format(Time.now(), dateFormatStr);
  dst += snprintf(dst, end - dst, "Date: %s\r\n", s.c_str());

  if (responseCode == 200 && fileToSend) {
    // Content-Type
    if (fileToSend->mimeType) {
      dst += snprintf(dst, end - dst, "Content-Type: %s\r\n", fileToSend->mimeType);
    }

    // Content-Length is the length if known. contentLength is initialized to -1 (not known)
    // but it's good to set it if you know, because not settings a content length means keepalive
    // cannot be used.
    // For HEAD, Content-Length is the length the body would be, not the actual length (0 for HEAD).
    if (fileToSend->dataSize >= 0) {
      dst += snprintf(dst, end - dst, "Content-Length: %d\r\n", fileToSend->dataSize);
    }
    // Last-Modified
    if (fileToSend->modified != 0) {
      s = Time.format(fileToSend->modified, dateFormatStr);
      dst += snprintf(dst, end - dst, "Last-Modified: %s\r\n", s.c_str());
    }
  }
  dst += snprintf(dst, end - dst, "\r\n");
  // Now send
  sendBuf = clientBuf;
  sendOffset = 0;
  sendLen = dst - (char *)clientBuf;
  state = WRITE_HEADER_STATE;
}

void ClientConnection::writeResponse() {
  if (sendOffset == sendLen) {
    if (state == WRITE_HEADER_STATE && fileToSend) {
      // Write body now
      sendOffset = 0;
      sendBuf = fileToSend->data;
      sendLen = fileToSend->dataSize;
    }
    else {
      // Done
      Serial.printlnf("%d: send complete", clientId);
      client.stop();
      return;
    }
  }
  size_t bytesToWrite = sendLen - sendOffset;
  if (bytesToWrite >= MAX_TO_WRITE) {
    bytesToWrite = MAX_TO_WRITE;
  }

  int count = client.write(&sendBuf[sendOffset], bytesToWrite);
  if (count == -16) {
    // Special case on Photon; buffer is full, retry later
  }
  else if (count > 0) {
    sendOffset += count;
    lastUse = millis();
  }
  else {
    Serial.printlnf("%d: error writing %d", clientId, count);
    client.stop();
  }
}

The softAP code is here:

#pragma SPARK_NO_PREPROCESSOR

#include "Particle.h"
#include "softap_http.h"

SYSTEM_THREAD(ENABLED)

struct Page
{
  const char* url;
  const char* mime_type;
  const char* data;
};


const char index_html[] = "<!DOCTYPE html><html><head><meta name='viewport' content='width=device-width, initial-scale=1'><title>Setup your device</title><link rel='stylesheet' type='text/css' href='style.css'></head><body><h1>Connect me to your WiFi!</h1><h3>My device ID:</h3><input type=text id='device-id' size='25' value='' disabled/><button type='button' class='input-helper' id='copy-button'>Copy</button><div id='scan-div'><h3>Scan for visible WiFi networks</h3><button id='scan-button' type='button'>Scan</button></div><div id='networks-div'></div><div id='connect-div' style='display: none'><p>Don't see your network? Move me closer to your router, then re-scan.</p><form id='connect-form'><input type='password' id='password' size='25' placeholder='password'/><button type='button' class='input-helper' id='show-button'>Show</button><button type='submit' id='connect-button'>Connect</button></form></div><script src='rsa-utils/jsbn_1.js'></script><script src='rsa-utils/jsbn_2.js'></script><script src='rsa-utils/prng4.js'></script><script src='rsa-utils/rng.js'></script><script src='rsa-utils/rsa.js'></script><script src='script.js'></script></body></html>";

const char rsa_js[] = "function parseBigInt(a,b){return new BigInteger(a,b);}function linebrk(a,b){var c='';var d=0;while(d+b<a.length){c+=a.substring(d,d+b)+'\\n';d+=b;}return c+a.substring(d,a.length);}function byte2Hex(a){if(a<0x10)return '0'+a.toString(16);else return a.toString(16);}function pkcs1pad2(a,b){if(b<a.length+11){alert('Message too long for RSA');return null;}var c=new Array();var d=a.length-1;while(d>=0&&b>0){var e=a.charCodeAt(d--);if(e<128)c[--b]=e;else if((e>127)&&(e<2048)){c[--b]=(e&63)|128;c[--b]=(e>>6)|192;}else{c[--b]=(e&63)|128;c[--b]=((e>>6)&63)|128;c[--b]=(e>>12)|224;}}c[--b]=0;var f=new SecureRandom();var g=new Array();while(b>2){g[0]=0;while(g[0]==0)f.nextBytes(g);c[--b]=g[0];}c[--b]=2;c[--b]=0;return new BigInteger(c);}function RSAKey(){this.n=null;this.e=0;this.d=null;this.p=null;this.q=null;this.dmp1=null;this.dmq1=null;this.coeff=null;}function RSASetPublic(a,b){if(a!=null&&b!=null&&a.length>0&&b.length>0){this.n=parseBigInt(a,16);this.e=parseInt(b,16);}else alert('Invalid RSA public key');}function RSADoPublic(a){return a.modPowInt(this.e,this.n);}function RSAEncrypt(a){var b=pkcs1pad2(a,(this.n.bitLength()+7)>>3);if(b==null)return null;var c=this.doPublic(b);if(c==null)return null;var d=c.toString(16);if((d.length&1)==0)return d;else return '0'+d;}RSAKey.prototype.doPublic=RSADoPublic;RSAKey.prototype.setPublic=RSASetPublic;RSAKey.prototype.encrypt=RSAEncrypt;";

const char style_css[] = "html{height:100%;margin:auto;background-color:white}body{box-sizing:border-box;min-height:100%;padding:20px;background-color:#1aabe0;font-family:'Lucida Sans Unicode','Lucida Grande',sans-serif;font-weight:normal;color:white;margin-top:0;margin-left:auto;margin-right:auto;margin-bottom:0;max-width:400px;text-align:center;border:1px solid #6e6e70;border-radius:4px}div{margin-top:25px;margin-bottom:25px}h1{margin-top:25px;margin-bottom:25px}button{border-color:#1c75be;background-color:#1c75be;color:white;border-radius:5px;height:30px;font-size:15px;font-weight:bold}button.input-helper{background-color:#bebebe;border-color:#bebebe;color:#6e6e70;margin-left:3px}button:disabled{background-color:#bebebe;border-color:#bebebe;color:white}input[type='text'],input[type='password']{background-color:white;color:#6e6e70;border-color:white;border-radius:5px;height:25px;text-align:center;font-size:15px}input:disabled{background-color:#bebebe;border-color:#bebebe}input[type='radio']{position:relative;bottom:-0.33em;margin:0;border:0;height:1.5em;width:15%}label{padding-top:7px;padding-bottom:7px;padding-left:5%;display:inline-block;width:80%;text-align:left}input[type='radio']:checked+label{font-weight:bold;color:#1c75be}.scanning-error{font-weight:bold;text-align:center}.radio-div{box-sizing:border-box;margin:2px;margin-left:auto;margin-right:auto;background-color:white;color:#6e6e70;border:1px solid #6e6e70;border-radius:3px;width:100%;padding:5px}#networks-div{margin-left:auto;margin-right:auto;text-align:left}#device-id{text-align:center}#scan-button{min-width:100px}#connect-button{display:block;min-width:100px;margin-top:10px;margin-left:auto;margin-right:auto;margin-bottom:20px}#password{margin-top:20px;margin-bottom:10px}";

const char rng_js[] = "var rng_state;var rng_pool;var rng_pptr;function rng_seed_int(a){rng_pool[rng_pptr++]^=a&255;rng_pool[rng_pptr++]^=(a>>8)&255;rng_pool[rng_pptr++]^=(a>>16)&255;rng_pool[rng_pptr++]^=(a>>24)&255;if(rng_pptr>=rng_psize)rng_pptr-=rng_psize;}function rng_seed_time(){rng_seed_int(new Date().getTime());}if(rng_pool==null){rng_pool=new Array();rng_pptr=0;var t;if(window.crypto&&window.crypto.getRandomValues){var ua=new Uint8Array(32);window.crypto.getRandomValues(ua);for(t=0;t<32;++t)rng_pool[rng_pptr++]=ua[t];}if(navigator.appName=='Netscape'&&navigator.appVersion<'5'&&window.crypto){var z=window.crypto.random(32);for(t=0;t<z.length;++t)rng_pool[rng_pptr++]=z.charCodeAt(t)&255;}while(rng_pptr<rng_psize){t=Math.floor(65536*Math.random());rng_pool[rng_pptr++]=t>>>8;rng_pool[rng_pptr++]=t&255;}rng_pptr=0;rng_seed_time();}function rng_get_byte(){if(rng_state==null){rng_seed_time();rng_state=prng_newstate();rng_state.init(rng_pool);for(rng_pptr=0;rng_pptr<rng_pool.length;++rng_pptr)rng_pool[rng_pptr]=0;rng_pptr=0;}return rng_state.next();}function rng_get_bytes(a){var b;for(b=0;b<a.length;++b)a[b]=rng_get_byte();}function SecureRandom(){}SecureRandom.prototype.nextBytes=rng_get_bytes;";

const char jsbn_2_js[] = "function bnpRShiftTo(a,b){b.s=this.s;var c=Math.floor(a/this.DB);if(c>=this.t){b.t=0;return;}var d=a%this.DB;var e=this.DB-d;var f=(1<<d)-1;b[0]=this[c]>>d;for(var g=c+1;g<this.t;++g){b[g-c-1]|=(this[g]&f)<<e;b[g-c]=this[g]>>d;}if(d>0)b[this.t-c-1]|=(this.s&f)<<e;b.t=this.t-c;b.clamp();}function bnpSubTo(a,b){var c=0,d=0,e=Math.min(a.t,this.t);while(c<e){d+=this[c]-a[c];b[c++]=d&this.DM;d>>=this.DB;}if(a.t<this.t){d-=a.s;while(c<this.t){d+=this[c];b[c++]=d&this.DM;d>>=this.DB;}d+=this.s;}else{d+=this.s;while(c<a.t){d-=a[c];b[c++]=d&this.DM;d>>=this.DB;}d-=a.s;}b.s=(d<0)?-1:0;if(d<-1)b[c++]=this.DV+d;else if(d>0)b[c++]=d;b.t=c;b.clamp();}function bnpMultiplyTo(a,b){var c=this.abs(),d=a.abs();var e=c.t;b.t=e+d.t;while(--e>=0)b[e]=0;for(e=0;e<d.t;++e)b[e+c.t]=c.am(0,d[e],b,e,0,c.t);b.s=0;b.clamp();if(this.s!=a.s)BigInteger.ZERO.subTo(b,b);}function bnpSquareTo(a){var b=this.abs();var c=a.t=2*b.t;while(--c>=0)a[c]=0;for(c=0;c<b.t-1;++c){var d=b.am(c,b[c],a,2*c,0,1);if((a[c+b.t]+=b.am(c+1,2*b[c],a,2*c+1,d,b.t-c-1))>=b.DV){a[c+b.t]-=b.DV;a[c+b.t+1]=1;}}if(a.t>0)a[a.t-1]+=b.am(c,b[c],a,2*c,0,1);a.s=0;a.clamp();}function bnpDivRemTo(a,b,c){var d=a.abs();if(d.t<=0)return;var e=this.abs();if(e.t<d.t){if(b!=null)b.fromInt(0);if(c!=null)this.copyTo(c);return;}if(c==null)c=nbi();var f=nbi(),g=this.s,h=a.s;var i=this.DB-nbits(d[d.t-1]);if(i>0){d.lShiftTo(i,f);e.lShiftTo(i,c);}else{d.copyTo(f);e.copyTo(c);}var j=f.t;var k=f[j-1];if(k==0)return;var l=k*(1<<this.F1)+((j>1)?f[j-2]>>this.F2:0);var m=this.FV/l,n=(1<<this.F1)/l,o=1<<this.F2;var p=c.t,q=p-j,r=(b==null)?nbi():b;f.dlShiftTo(q,r);if(c.compareTo(r)>=0){c[c.t++]=1;c.subTo(r,c);}BigInteger.ONE.dlShiftTo(j,r);r.subTo(f,f);while(f.t<j)f[f.t++]=0;while(--q>=0){var s=(c[--p]==k)?this.DM:Math.floor(c[p]*m+(c[p-1]+o)*n);if((c[p]+=f.am(0,s,c,q,0,j))<s){f.dlShiftTo(q,r);c.subTo(r,c);while(c[p]<--s)c.subTo(r,c);}}if(b!=null){c.drShiftTo(j,b);if(g!=h)BigInteger.ZERO.subTo(b,b);}c.t=j;c.clamp();if(i>0)c.rShiftTo(i,c);if(g<0)BigInteger.ZERO.subTo(c,c);}function bnMod(a){var b=nbi();this.abs().divRemTo(a,null,b);if(this.s<0&&b.compareTo(BigInteger.ZERO)>0)a.subTo(b,b);return b;}function Classic(a){this.m=a;}function cConvert(a){if(a.s<0||a.compareTo(this.m)>=0)return a.mod(this.m);else return a;}function cRevert(a){return a;}function cReduce(a){a.divRemTo(this.m,null,a);}function cMulTo(a,b,c){a.multiplyTo(b,c);this.reduce(c);}function cSqrTo(a,b){a.squareTo(b);this.reduce(b);}Classic.prototype.convert=cConvert;Classic.prototype.revert=cRevert;Classic.prototype.reduce=cReduce;Classic.prototype.mulTo=cMulTo;Classic.prototype.sqrTo=cSqrTo;function bnpInvDigit(){if(this.t<1)return 0;var a=this[0];if((a&1)==0)return 0;var b=a&3;b=(b*(2-(a&0xf)*b))&0xf;b=(b*(2-(a&0xff)*b))&0xff;b=(b*(2-(((a&0xffff)*b)&0xffff)))&0xffff;b=(b*(2-a*b%this.DV))%this.DV;return(b>0)?this.DV-b:-b;}function Montgomery(a){this.m=a;this.mp=a.invDigit();this.mpl=this.mp&0x7fff;this.mph=this.mp>>15;this.um=(1<<(a.DB-15))-1;this.mt2=2*a.t;}function montConvert(a){var b=nbi();a.abs().dlShiftTo(this.m.t,b);b.divRemTo(this.m,null,b);if(a.s<0&&b.compareTo(BigInteger.ZERO)>0)this.m.subTo(b,b);return b;}function montRevert(a){var b=nbi();a.copyTo(b);this.reduce(b);return b;}function montReduce(a){while(a.t<=this.mt2)a[a.t++]=0;for(var b=0;b<this.m.t;++b){var c=a[b]&0x7fff;var d=(c*this.mpl+(((c*this.mph+(a[b]>>15)*this.mpl)&this.um)<<15))&a.DM;c=b+this.m.t;a[c]+=this.m.am(0,d,a,b,0,this.m.t);while(a[c]>=a.DV){a[c]-=a.DV;a[++c]++;}}a.clamp();a.drShiftTo(this.m.t,a);if(a.compareTo(this.m)>=0)a.subTo(this.m,a);}function montSqrTo(a,b){a.squareTo(b);this.reduce(b);}function montMulTo(a,b,c){a.multiplyTo(b,c);this.reduce(c);}Montgomery.prototype.convert=montConvert;Montgomery.prototype.revert=montRevert;Montgomery.prototype.reduce=montReduce;Montgomery.prototype.mulTo=montMulTo;Montgomery.prototype.sqrTo=montSqrTo;function bnpIsEven(){return((this.t>0)?(this[0]&1):this.s)==0;}function bnpExp(a,b){if(a>0xffffffff||a<1)return BigInteger.ONE;var c=nbi(),d=nbi(),e=b.convert(this),f=nbits(a)-1;e.copyTo(c);while(--f>=0){b.sqrTo(c,d);if((a&(1<<f))>0)b.mulTo(d,e,c);else{var g=c;c=d;d=g;}}return b.revert(c);}function bnModPowInt(a,b){var c;if(a<256||b.isEven())c=new Classic(b);else c=new Montgomery(b);return this.exp(a,c);}BigInteger.prototype.copyTo=bnpCopyTo;BigInteger.prototype.fromInt=bnpFromInt;BigInteger.prototype.fromString=bnpFromString;BigInteger.prototype.clamp=bnpClamp;BigInteger.prototype.dlShiftTo=bnpDLShiftTo;BigInteger.prototype.drShiftTo=bnpDRShiftTo;BigInteger.prototype.lShiftTo=bnpLShiftTo;BigInteger.prototype.rShiftTo=bnpRShiftTo;BigInteger.prototype.subTo=bnpSubTo;BigInteger.prototype.multiplyTo=bnpMultiplyTo;BigInteger.prototype.squareTo=bnpSquareTo;BigInteger.prototype.divRemTo=bnpDivRemTo;BigInteger.prototype.invDigit=bnpInvDigit;BigInteger.prototype.isEven=bnpIsEven;BigInteger.prototype.exp=bnpExp;BigInteger.prototype.toString=bnToString;BigInteger.prototype.negate=bnNegate;BigInteger.prototype.abs=bnAbs;BigInteger.prototype.compareTo=bnCompareTo;BigInteger.prototype.bitLength=bnBitLength;BigInteger.prototype.mod=bnMod;BigInteger.prototype.modPowInt=bnModPowInt;BigInteger.ZERO=nbv(0);BigInteger.ONE=nbv(1);";

const char jsbn_1_js[] = "var dbits;var canary=0xdeadbeefcafe;var j_lm=((canary&0xffffff)==0xefcafe);function BigInteger(a,b,c){if(a!=null)if('number'==typeof a)this.fromNumber(a,b,c);else if(b==null&&'string'!=typeof a)this.fromString(a,256);else this.fromString(a,b);}function nbi(){return new BigInteger(null);}function am1(a,b,c,d,e,f){while(--f>=0){var g=b*this[a++]+c[d]+e;e=Math.floor(g/0x4000000);c[d++]=g&0x3ffffff;}return e;}function am2(a,b,c,d,e,f){var g=b&0x7fff,h=b>>15;while(--f>=0){var i=this[a]&0x7fff;var j=this[a++]>>15;var k=h*i+j*g;i=g*i+((k&0x7fff)<<15)+c[d]+(e&0x3fffffff);e=(i>>>30)+(k>>>15)+h*j+(e>>>30);c[d++]=i&0x3fffffff;}return e;}function am3(a,b,c,d,e,f){var g=b&0x3fff,h=b>>14;while(--f>=0){var i=this[a]&0x3fff;var j=this[a++]>>14;var k=h*i+j*g;i=g*i+((k&0x3fff)<<14)+c[d]+e;e=(i>>28)+(k>>14)+h*j;c[d++]=i&0xfffffff;}return e;}if(j_lm&&(navigator.appName=='Microsoft Internet Explorer')){BigInteger.prototype.am=am2;dbits=30;}else if(j_lm&&(navigator.appName!='Netscape')){BigInteger.prototype.am=am1;dbits=26;}else{BigInteger.prototype.am=am3;dbits=28;}BigInteger.prototype.DB=dbits;BigInteger.prototype.DM=((1<<dbits)-1);BigInteger.prototype.DV=(1<<dbits);var BI_FP=52;BigInteger.prototype.FV=Math.pow(2,BI_FP);BigInteger.prototype.F1=BI_FP-dbits;BigInteger.prototype.F2=2*dbits-BI_FP;var BI_RM='0123456789abcdefghijklmnopqrstuvwxyz';var BI_RC=new Array();var rr,vv;rr='0'.charCodeAt(0);for(vv=0;vv<=9;++vv)BI_RC[rr++]=vv;rr='a'.charCodeAt(0);for(vv=10;vv<36;++vv)BI_RC[rr++]=vv;rr='A'.charCodeAt(0);for(vv=10;vv<36;++vv)BI_RC[rr++]=vv;function int2char(a){return BI_RM.charAt(a);}function intAt(a,b){var c=BI_RC[a.charCodeAt(b)];return(c==null)?-1:c;}function bnpCopyTo(a){for(var b=this.t-1;b>=0;--b)a[b]=this[b];a.t=this.t;a.s=this.s;}function bnpFromInt(a){this.t=1;this.s=(a<0)?-1:0;if(a>0)this[0]=a;else if(a<-1)this[0]=a+this.DV;else this.t=0;}function nbv(a){var b=nbi();b.fromInt(a);return b;}function bnpFromString(a,b){var c;if(b==16)c=4;else if(b==8)c=3;else if(b==256)c=8;else if(b==2)c=1;else if(b==32)c=5;else if(b==4)c=2;else{this.fromRadix(a,b);return;}this.t=0;this.s=0;var d=a.length,e=false,f=0;while(--d>=0){var g=(c==8)?a[d]&0xff:intAt(a,d);if(g<0){if(a.charAt(d)=='-')e=true;continue;}e=false;if(f==0)this[this.t++]=g;else if(f+c>this.DB){this[this.t-1]|=(g&((1<<(this.DB-f))-1))<<f;this[this.t++]=(g>>(this.DB-f));}else this[this.t-1]|=g<<f;f+=c;if(f>=this.DB)f-=this.DB;}if(c==8&&(a[0]&0x80)!=0){this.s=-1;if(f>0)this[this.t-1]|=((1<<(this.DB-f))-1)<<f;}this.clamp();if(e)BigInteger.ZERO.subTo(this,this);}function bnpClamp(){var a=this.s&this.DM;while(this.t>0&&this[this.t-1]==a)--this.t;}function bnToString(a){if(this.s<0)return '-'+this.negate().toString(a);var b;if(a==16)b=4;else if(a==8)b=3;else if(a==2)b=1;else if(a==32)b=5;else if(a==4)b=2;else return this.toRadix(a);var c=(1<<b)-1,d,e=false,f='',g=this.t;var h=this.DB-(g*this.DB)%b;if(g-->0){if(h<this.DB&&(d=this[g]>>h)>0){e=true;f=int2char(d);}while(g>=0){if(h<b){d=(this[g]&((1<<h)-1))<<(b-h);d|=this[--g]>>(h+=this.DB-b);}else{d=(this[g]>>(h-=b))&c;if(h<=0){h+=this.DB;--g;}}if(d>0)e=true;if(e)f+=int2char(d);}}return e?f:'0';}function bnNegate(){var a=nbi();BigInteger.ZERO.subTo(this,a);return a;}function bnAbs(){return(this.s<0)?this.negate():this;}function bnCompareTo(a){var b=this.s-a.s;if(b!=0)return b;var c=this.t;b=c-a.t;if(b!=0)return(this.s<0)?-b:b;while(--c>=0)if((b=this[c]-a[c])!=0)return b;return 0;}function nbits(a){var b=1,c;if((c=a>>>16)!=0){a=c;b+=16;}if((c=a>>8)!=0){a=c;b+=8;}if((c=a>>4)!=0){a=c;b+=4;}if((c=a>>2)!=0){a=c;b+=2;}if((c=a>>1)!=0){a=c;b+=1;}return b;}function bnBitLength(){if(this.t<=0)return 0;return this.DB*(this.t-1)+nbits(this[this.t-1]^(this.s&this.DM));}function bnpDLShiftTo(a,b){var c;for(c=this.t-1;c>=0;--c)b[c+a]=this[c];for(c=a-1;c>=0;--c)b[c]=0;b.t=this.t+a;b.s=this.s;}function bnpDRShiftTo(a,b){for(var c=a;c<this.t;++c)b[c-a]=this[c];b.t=Math.max(this.t-a,0);b.s=this.s;}function bnpLShiftTo(a,b){var c=a%this.DB;var d=this.DB-c;var e=(1<<d)-1;var f=Math.floor(a/this.DB),g=(this.s<<c)&this.DM,h;for(h=this.t-1;h>=0;--h){b[h+f+1]=(this[h]>>d)|g;g=(this[h]&e)<<c;}for(h=f-1;h>=0;--h)b[h]=0;b[f]=g;b.t=this.t+f+1;b.s=this.s;b.clamp();}";

const char script_js[] = "var base_url='http://192.168.0.1/';var network_list;var public_key;var rsa=new RSAKey();var scanButton=document.getElementById('scan-button');var connectButton=document.getElementById('connect-button');var copyButton=document.getElementById('copy-button');var showButton=document.getElementById('show-button');var deviceID=document.getElementById('device-id');var connectForm=document.getElementById('connect-form');var public_key_callback={success:function(a){console.log('Public key: '+a.b);public_key=a.b;rsa.setPublic(public_key.substring(58,58+256),public_key.substring(318,318+6));},error:function(a,b){console.log(a);window.alert('There was a problem fetching important information from your device. Please verify your connection, then reload this page.');}};var device_id_callback={success:function(a){var b=a.id;deviceID.value=b;},error:function(a,b){console.log(a);var c='COMMUNICATION_ERROR';deviceID.value=c;}};var scan=function(){console.log('Scanning...!');disableButtons();scanButton.innerHTML='Scanning...';connectButton.innerHTML='Connect';document.getElementById('connect-div').style.display='none';document.getElementById('networks-div').style.display='none';getRequest(base_url+'scan-ap',scan_callback);};var scan_callback={success:function(a){network_list=a.scans;console.log('I found:');var b=document.getElementById('networks-div');b.innerHTML='';if(network_list.length>0)for(var c=0;c<network_list.length;c++){ssid=network_list[c].ssid;console.log(network_list[c]);add_wifi_option(b,ssid);document.getElementById('connect-div').style.display='block';}else b.innerHTML='<p class=\\'scanning-error\\'>No networks found.</p>';},error:function(a){console.log('Scanning error:'+a);document.getElementById('networks-div').innerHTML='<p class=\\'scanning-error\\'>Scanning error.</p>';},regardless:function(){scanButton.innerHTML='Re-Scan';enableButtons();document.getElementById('networks-div').style.display='block';}};var configure=function(a){a.preventDefault();var b=get_selected_network();var c=document.getElementById('password').value;if(!b){window.alert('Please select a network!');return false;}var d={idx:0,ssid:b.ssid,pwd:rsa.encrypt(c),sec:b.sec,ch:b.ch};connectButton.innerHTML='Sending credentials...';disableButtons();console.log('Sending credentials: '+JSON.stringify(d));postRequest(base_url+'configure-ap',d,configure_callback);};var configure_callback={success:function(a){console.log('Credentials received.');connectButton.innerHTML='Credentials received...';postRequest(base_url+'connect-ap',{idx:0},connect_callback);},error:function(a,b){console.log('Configure error: '+a);window.alert('The configuration command failed, check that you are still well connected to the device\\'s WiFi hotspot and retry.');connectButton.innerHTML='Retry';enableButtons();}};var connect_callback={success:function(a){console.log('Attempting to connect to the cloud.');connectButton.innerHTML='Attempting to connect...';window.alert('Your device should now start flashing green and attempt to connect to the cloud. This usually takes about 20 seconds, after which it will begin slowly blinking cyan. \\n\\n\\nIf this process fails because you entered the wrong password, the device will flash green indefinitely. In this case, hold the setup button for 6 seconds until the device starts blinking blue again. Then reconnect to the WiFi hotspot it generates and reload this page to try again.');},error:function(a,b){console.log('Connect error: '+a);window.alert('The connect command failed, check that you are still well connected to the device\\'s WiFi hotspot and retry.');connectButton.innerHTML='Retry';enableButtons();}};var disableButtons=function(){connectButton.disabled=true;scanButton.disabled=true;};var enableButtons=function(){connectButton.disabled=false;scanButton.disabled=false;};var add_wifi_option=function(a,b){var c=document.createElement('INPUT');c.type='radio';c.value=b;c.id=b;c.name='ssid';c.className='radio';var d=document.createElement('DIV');d.className='radio-div';d.appendChild(c);var e=document.createElement('label');e.htmlFor=b;e.innerHTML=b;d.appendChild(e);a.appendChild(d);};var get_selected_network=function(){for(var a=0;a<network_list.length;a++){ssid=network_list[a].ssid;if(document.getElementById(ssid).checked)return network_list[a];}};var copy=function(){window.prompt('Copy to clipboard: Ctrl + C, Enter',deviceID.value);};var toggleShow=function(){var a=document.getElementById('password');inputType=a.type;if(inputType==='password'){showButton.innerHTML='Hide';a.type='text';}else{showButton.innerHTML='Show';a.type='password';}};var getRequest=function(a,b){var c=new XMLHttpRequest();c.open('GET',a,true);c.timeout=8000;c.send();c.onreadystatechange=function(){if(c.readyState==4)if(b){if(c.status==200){if(b.success)b.success(JSON.parse(c.responseText));}else if(b.error)b.error(c.status,c.responseText);if(b.regardless)b.regardless();}};};var postRequest=function(a,b,c){var d=JSON.stringify(b);var e=new XMLHttpRequest();e.open('POST',a,true);e.timeout=4000;e.setRequestHeader('Content-Type','multipart/form-data');e.send(d);e.onreadystatechange=function(){if(e.readyState==4)if(c){if(e.status==200){if(c.success)c.success(JSON.parse(e.responseText));}else if(c.error)c.error(e.status,e.responseText);if(c.regardless)c.regardless();}};};if(scanButton.addEventListener){copyButton.addEventListener('click',copy);showButton.addEventListener('click',toggleShow);scanButton.addEventListener('click',scan);connectForm.addEventListener('submit',configure);}else if(scanButton.attachEvent){copyButton.attachEvent('onclick',copy);showButton.attachEvent('onclick',toggleShow);scanButton.attachEvent('onclick',scan);connectForm.attachEvent('onsubmit',configure);}getRequest(base_url+'device-id',device_id_callback);getRequest(base_url+'public-key',public_key_callback);";

const char prng4_js[] = "function Arcfour(){this.i=0;this.j=0;this.S=new Array();}function ARC4init(a){var b,c,d;for(b=0;b<256;++b)this.S[b]=b;c=0;for(b=0;b<256;++b){c=(c+this.S[b]+a[b%a.length])&255;d=this.S[b];this.S[b]=this.S[c];this.S[c]=d;}this.i=0;this.j=0;}function ARC4next(){var a;this.i=(this.i+1)&255;this.j=(this.j+this.S[this.i])&255;a=this.S[this.i];this.S[this.i]=this.S[this.j];this.S[this.j]=a;return this.S[(a+this.S[this.i])&255];}Arcfour.prototype.init=ARC4init;Arcfour.prototype.next=ARC4next;function prng_newstate(){return new Arcfour();}var rng_psize=256;";

Page myPages[] = {
  { "/index.html", "text/html", index_html },
  { "/rsa-utils/rsa.js", "application/javascript", rsa_js },
  { "/style.css", "text/css", style_css },
  { "/rsa-utils/rng.js", "application/javascript", rng_js },
  { "/rsa-utils/jsbn_2.js", "application/javascript", jsbn_2_js },
  { "/rsa-utils/jsbn_1.js", "application/javascript", jsbn_1_js },
  { "/script.js", "application/javascript", script_js },
  { "/rsa-utils/prng4.js", "application/javascript", prng4_js },
  { nullptr }
};

void myPage(const char* url, ResponseCallback* cb, void* cbArg, Reader* body, Writer* result, void* reserved)
{
  Serial.printlnf("handling page %s", url);

  if (strcmp(url, "/index") == 0) {
    Serial.println("sending redirect");
    Header h("Location: /index.html\r\n");
    cb(cbArg, 0, 301, "text/plain", &h);
    return;
  }

  int8_t idx = 0;
  for (;; idx++) {
    Page& p = myPages[idx];
    if (!p.url) {
      idx = -1;
      break;
    }
    else if (strcmp(url, p.url) == 0) {
      break;
    }
  }

  if (idx == -1) {
    cb(cbArg, 0, 404, nullptr, nullptr);
  }
  else {
    cb(cbArg, 0, 200, myPages[idx].mime_type, nullptr);
    result->write(myPages[idx].data);
  }
}

STARTUP(softap_set_application_page_handler(myPage, nullptr));
2 Likes

Hello @BulldogLowell,

This is some amazing work. It helped me a lot. I wanted to know that how can I assign a DNS to my webpage so that I shall need not to remember the IP all the time?

Thank You,
djs07

1 Like

I recall doing that, lemme see if I can find an example.

1 Like

@BulldogLowell, Have you found any?

Can I replicate the process with the Google web hosting service. I have managed service of Google’s GCE which is powered by a third party host named Cloudways. As, I am looking forward to host multiple web pages with SoftAP.
If you have any better suggestion for me then please go ahead.