SoftAP with WebServer.h

I’m using softAP to allow users to configure their devices for their local Wi-Fi network, works a treat.

I’m using WebServer.h to allow access and control of devices, also great.

After connecting via softAP, I’d like to be able to serve up a simple webpage providing some functionality for the device I’m making. That is, I want a softAP while there is no Wi-Fi connection and if there is a Wi-Fi connection, switch over to be a web server once the device is connected.

I cannot find an example here… has anyone?

1 Like

I didn’t test with the WebServer library, but yes, this works. One catch is that you’ll probably need to use SYSTEM_THREAD(ENABLED) and check for WiFi.listening() in loop. When you get WiFi.ready() you’ll probably need to server.begin() again at that point to get the HTTP server listener to start.

Here’s how I tested it. I’m not recommending this as the best way to do it, just how I tested it.

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

SYSTEM_THREAD(ENABLED);


// [start d1872940-586d-44a9-b719-4a67e8a29213]
// name=/index.html contentType=text/html size=700 modified=2016-08-26 07:30:43
const char fileData0[] = 
"<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
"<!DOCTYPE html>\n"
"<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
"<head>\n"
"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n"
"\n"
"<title>SoftAP Sample 1</title>\n"
"\n"
"</head>\n"
"<body>\n"
"\n"
"<div id=\"main\">\n"
"\n"
"<h2>SoftAP Sample 1</h2>\n"
"<p>Testing 1, 2, 3, ...</p>"
"\n"
"</div> <!-- main -->\n"
"\n"
"</body>\n"
"</html>\n"
"";


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

const FileInfo fileInfo[] = {
	{"/index.html", "text/html", (const uint8_t *)fileData0, sizeof(fileData0) - 1, 1472211043, FALSE},
	{NULL, NULL, 0, 0, FALSE}
};
// [end d1872940-586d-44a9-b719-4a67e8a29213]

void pageHandler(const char* url, ResponseCallback* cb, void* cbArg, Reader* body, Writer* result, void* reserved); // forward declaration

STARTUP(softap_set_application_page_handler(pageHandler, nullptr));


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 = 7123;
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;

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)
	Particle.variable("localip", localIP);
}

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;
		}
	}

}


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")) {
			// Ignore the actual request and just return the index.html data
			responseCode = 200;
			responseStr = "OK";
			fileToSend = &fileInfo[0];

			Serial.printlnf("%d: sending %s", clientId, fileToSend->name);
			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());
		}
	}


	// End of header
	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;
	}
	else {
		Serial.printlnf("%d: error writing %d", clientId, count);
		client.stop();
	}

}

//
// SoftAP
//
void pageHandler(const char* url, ResponseCallback* cb, void* cbArg, Reader* body, Writer* result, void* reserved) {

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


	int index = -1;
	for(size_t ii = 0; fileInfo[ii].name != NULL; ii++) {
		if (strcmp(fileInfo[ii].name, url) == 0) {
			index = (int) ii;
			break;
		}
	}
	if (index >= 0) {
		cb(cbArg, 0, 200, fileInfo[index].mimeType, nullptr);
		result->write(fileInfo[index].data, fileInfo[index].dataSize);
		Serial.printlnf("returned url=%s dataSize=%d mimeType=%s", url, fileInfo[index].dataSize, fileInfo[index].mimeType);
	}
	else {
	    Serial.printlnf("404 url=%s", url);
		cb(cbArg, 0, 404, nullptr, nullptr);
	}
}

1 Like

@BulldogLowell These type of functions are good for anybody building a product so please do share when you get this working so I can get an idea of what is possible please :smiley:

@rickkas7,

Thanks for that. Works well, but it took me a few minutes to realize it was supposed to serve up the same page as the SoftAP as during program execution

@RWB,

Here is an example using the standard softAP with Wi-Fi credentials and capable of serving up a webpage once connected, using Rick’s example (no WebServer.h necessary):

Rats! limits on the length of code… here is a link, but it would be nice to be able to post this for the community.

2 Likes

I downloaded and flashed the code but I couldn’t figure out exactly how to access this screen:

The 192.168.1.31 address did not pull anything up.

After you connect the Wi-Fi, you can use Dev Show Cloud Variables and see a particle variable containing your local IP.

Remember to edit the Access Token and Device ID in the JavaScript, too!

Got it working after adding the token, Device ID, it works just fine on my cell phone & laptop browser.

Pretty cool to see this working :smiley: considering it can be used in so many different ways.

The code looks massive and complicated at first glance and I’m not sure how much room this code eats up, but I’m sure it’s workable in most situations.

I have a few questions.

  1. Is the light bulb image pulled from the internet or is it hard coded in the app?

  2. Is there a particular line of code that renders the HTML page and how it looks? I’m used to editing HTML pages using web page editors, so I’m curious if that would be an easy way to modify how the HTML page looks or not?

  3. How hard do you think it would be to radio or slider buttons to change the state of individual variables?

Please do share your progress on this, it’s pretty powerful in my opinion :smiley:

I used HTML canvas to 'draw' the lightbulb (it gets updated via AJAX calls back to the device) in these lines of code:

    "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});"
         "};"

The web page block is all within the const char fileData0[] variable and is in HTML/JavaScript with some CSS and jquery. It can easily be modified, I put the on/off led function in there so folks could see how to do that in this example. This is different than the WebServer.h examples, which call back to the Particle device for updates based on Document changes; it is all in one page and is not dynamic. But given the strengths of the Particle variables and functions, you can do a lot without the need for dynamic page drawing.

simple.... if you are familiar with HTML, a little tougher if you are not!

:smile:

EDIT:

attempting to compile firmware 
downloading binary from: /v1/binaries/584daf5be360913e25c709ae
saving to: photon_firmware_1481486169458.bin
Memory use: 
   text	   data	    bss	    dec	    hex	filename
  40476	    300	   8328	  49104	   bfd0	
Compile succeeded.

as is, it has lots of room for more cool stuff!!

2 Likes

Thanks for the replies.

Never heard of HTML canvas draw before. I see figuring this all out is going to take me some time but I like the end result of having a local hosted web page to view the product status & change variables.

Looking forward to see how you progress with this.

1 Like

Just to add: Apart from drawing with canvas, you could also use SVG images in code. Most modern browsers support them.