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