Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions firmware/inc/Communication/WebFrontend/WebFrontend.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,26 @@
******************************************************************************************************************************************************/
class WebFrontend
{
public:
/* One file a clock hands out unchanged: the two pages, the manifest and the two icons.
Everything a backend needs in order to answer for it, and nothing about how - which is
why this is a table and not five handlers. It was five handlers, twice, and the ten
differed only in a blob, a content type and whether a header went out with it.

The bytes live in the generated WebPage.h, which only WebFrontend.cpp includes: the
generated arrays have internal linkage, so a second includer would put a second copy
of both pages in flash. */
struct AssetType {
const char* Path;
const uint8_t* Bytes;
size_t Size;
const char* ContentType;
/* Stored compressed and sent compressed, so the backend has to say so in a header
and the browser unpacks it. False for the manifest, which is 485 bytes, and for
the icons, a PNG being deflated already. */
bool IsGzipped;
};

/******************************************************************************************************************************************************
* P R I V A T E D A T A A N D F U N C T I O N S
******************************************************************************************************************************************************/
Expand Down Expand Up @@ -107,6 +127,17 @@ class WebFrontend
}

// methods
/* What a backend may size a static array of routes with, since the count itself is only
known in the source. A static_assert there holds the two together, so a file added to
the table without raising this fails the build rather than one registration. */
static constexpr byte MaxAssets{5u};

/* The files a clock serves unchanged, for a backend to register a route per entry and
answer it from the table. Walked rather than named one by one, so a file added to
web/ reaches both backends by appearing here. */
static byte getNumberOfAssets();
static const AssetType& getAsset(byte Index);

/* The command catalog as JSON, so a page can build its own form from it, and the panel's
shape and letters. Both take the body a backend's route handler opened, which is the
only thing about them that is a backend's business. */
Expand Down
58 changes: 58 additions & 0 deletions firmware/src/Communication/WebFrontend/WebFrontend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
#include "DisplayCharacters.h"
#include "MessageCatalog.h"
#include "Pixels.h"
/* The build generates this from web/. Included here and nowhere else: its arrays have
internal linkage, so every further includer would be a second copy of both pages in
flash. */
#include "WebPage.h"
#include "WordclockSerial.h"

#include <stdio.h>
Expand Down Expand Up @@ -130,10 +134,64 @@ class ChunkWriter

} // namespace

/******************************************************************************************************************************************************
* L O C A L D A T A
******************************************************************************************************************************************************/
namespace {

/* Every file a clock hands out unchanged, at the path it hands it out at.
*
* "/" is the panel and "/console" the console, which is the whole arrangement rather than a
* detail: nobody types a path, they type the clock's address and take what comes, so what
* comes is the page for the things somebody changes often, with a link to the other one in
* its header.
*
* The three that follow are neither page but the home screen icon - a manifest and two PNGs -
* which is what lets the console be installed with an icon to tap and a start without an
* address bar.
*/
constexpr WebFrontend::AssetType Assets[]{
{"/", WebAppGzip, WebAppGzipSize, "text/html", true},
{"/console", WebPageGzip, WebPageGzipSize, "text/html", true},
{"/manifest.webmanifest", WebManifest, WebManifestSize, "application/manifest+json", false},
{"/icon-192.png", WebIcon192, WebIcon192Size, "image/png", false},
{"/icon-512.png", WebIcon512, WebIcon512Size, "image/png", false},
};

/* A backend sizes its route array with MaxAssets, which it can see and this count it cannot.
Adding a file here without raising that fails the build, rather than quietly registering
the first five of six. */
static_assert((sizeof(Assets) / sizeof(Assets[0])) <= WebFrontend::MaxAssets,
"WebFrontend::MaxAssets has to grow with the asset table");

} // namespace

/******************************************************************************************************************************************************
* P U B L I C F U N C T I O N S
******************************************************************************************************************************************************/

/******************************************************************************************************************************************************
getNumberOfAssets() / getAsset()
******************************************************************************************************************************************************/
/*! \brief The files a clock serves unchanged, for a backend to register routes from
*
* \details A table rather than a handler per file, because that is what the ten
* handlers this replaces actually were: each one set a content type, set a
* Content-Encoding header or did not, and pushed a blob. The difference
* between them was data, and the sameness between the two backends was
* complete.
******************************************************************************************************************************************************/
byte WebFrontend::getNumberOfAssets()
{
return static_cast<byte>(sizeof(Assets) / sizeof(Assets[0]));
}

const WebFrontend::AssetType& WebFrontend::getAsset(byte Index)
{
return Assets[Index];
}


/******************************************************************************************************************************************************
writeCommands()
******************************************************************************************************************************************************/
Expand Down
127 changes: 32 additions & 95 deletions platform/esp32/src/WebInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
#include "System.h"
#include "WebFrontend.h"
#include "WebInterface.h"
#include "WebPage.h"
#include "WebTransport.h"
#include "WordclockSerial.h"

Expand Down Expand Up @@ -150,71 +149,30 @@ bool isRequestAuthorised(httpd_req_t* Request)


/******************************************************************************************************************************************************
sendPage()
handleAsset()
******************************************************************************************************************************************************/
/*! \brief Serves one of the two pages straight out of flash
* \details Sent compressed, which is how it is stored: the pages were gzipped at build
* time, so this only pushes the bytes and the browser unpacks them.
/*! \brief Serves one of the files the clock hands out unchanged
*
* \details One handler for all five - the two pages, the manifest and the two icons -
* because what separates them is data and the data is WebFrontend's table.
* Which entry this request is for arrives in the server's own user context,
* which is what that field exists for.
*
* A gzipped asset is sent as it is stored and the browser unpacks it; that
* is the whole of what the header does here, and the table says which.
*
* \return ESP_OK
******************************************************************************************************************************************************/
esp_err_t sendPage(httpd_req_t* Request, const uint8_t* Page, size_t Size)
esp_err_t handleAsset(httpd_req_t* Request)
{
if(!isRequestAuthorised(Request)) { return ESP_OK; }

httpd_resp_set_type(Request, "text/html");
httpd_resp_set_hdr(Request, "Content-Encoding", "gzip");

return httpd_resp_send(Request, reinterpret_cast<const char*>(Page), Size);
}
const WebFrontend::AssetType& Asset = *static_cast<const WebFrontend::AssetType*>(Request->user_ctx);

httpd_resp_set_type(Request, Asset.ContentType);
if(Asset.IsGzipped) { httpd_resp_set_hdr(Request, "Content-Encoding", "gzip"); }

/******************************************************************************************************************************************************
handleRoot()
******************************************************************************************************************************************************/
/*! \brief Serves the page a clock hands out at "/"
* \details The one made for what somebody changes: a colour, a brightness, an
* animation. Nobody types a path - they type the clock's address and take
* what comes - so what comes is the page for the frequent things, and the
* console is one link away.
******************************************************************************************************************************************************/
esp_err_t handleRoot(httpd_req_t* Request)
{
return sendPage(Request, WebAppGzip, WebAppGzipSize);
}


/******************************************************************************************************************************************************
handleConsole()
******************************************************************************************************************************************************/
/*! \brief Serves the console at "/console"
* \details Everything the page at "/" does not cover, which is most of the command
* set: it draws a group per command out of the catalog, so a command added to
* the firmware appears there without a page being touched. That is why it
* moved rather than being replaced.
******************************************************************************************************************************************************/
esp_err_t handleConsole(httpd_req_t* Request)
{
return sendPage(Request, WebPageGzip, WebPageGzipSize);
}


/******************************************************************************************************************************************************
handleManifest()
******************************************************************************************************************************************************/
/*! \brief Serves the web app manifest, which is what makes the console installable
* \details Nothing on the clock reads it - a browser does, to put the console on a
* home screen with an icon and start it without an address bar. Sent as it
* is, unlike the page: it is 485 bytes, and compressing it would save
* fewer than 250 of them for a header on the wire and an inflate in
* anything that wants to read it - curl and this backend's own test
* included.
******************************************************************************************************************************************************/
esp_err_t handleManifest(httpd_req_t* Request)
{
if(!isRequestAuthorised(Request)) { return ESP_OK; }

httpd_resp_set_type(Request, "application/manifest+json");

return httpd_resp_send(Request, reinterpret_cast<const char*>(WebManifest), WebManifestSize);
return httpd_resp_send(Request, reinterpret_cast<const char*>(Asset.Bytes), Asset.Size);
}


Expand Down Expand Up @@ -324,32 +282,6 @@ esp_err_t handleUpdate(httpd_req_t* Request)
}


/******************************************************************************************************************************************************
handleIcon192() / handleIcon512()
******************************************************************************************************************************************************/
/*! \brief Serves the home screen icons
* \details Two sizes because that is what a manifest is asked for: the small one is
* the icon itself and what iOS takes, the large one is what Android draws
* the splash screen from. Sent as they are - a PNG is already deflated, so
* gzipping one would add a header and save nothing.
******************************************************************************************************************************************************/
esp_err_t handleIcon192(httpd_req_t* Request)
{
if(!isRequestAuthorised(Request)) { return ESP_OK; }

httpd_resp_set_type(Request, "image/png");

return httpd_resp_send(Request, reinterpret_cast<const char*>(WebIcon192), WebIcon192Size);
}

esp_err_t handleIcon512(httpd_req_t* Request)
{
if(!isRequestAuthorised(Request)) { return ESP_OK; }

httpd_resp_set_type(Request, "image/png");

return httpd_resp_send(Request, reinterpret_cast<const char*>(WebIcon512), WebIcon512Size);
}


/******************************************************************************************************************************************************
Expand Down Expand Up @@ -491,23 +423,28 @@ StdReturnType WebInterface::begin()
return E_NOT_OK;
}

static const httpd_uri_t RootUri{"/", HTTP_GET, handleRoot, nullptr, false, false, nullptr};
static const httpd_uri_t ConsoleUri{"/console", HTTP_GET, handleConsole, nullptr, false, false, nullptr};
/* The files served unchanged, one route per entry of WebFrontend's table. The array is
static because httpd keeps the pointer it is registered with rather than a copy, and
sized from the table so that a file added to web/ needs no edit here - only
max_uri_handlers above has to still be large enough, which is why that number is said
out loud rather than left at its default. */
static httpd_uri_t AssetUris[WebFrontend::MaxAssets]{};

for(byte Index = 0u; Index < WebFrontend::getNumberOfAssets(); Index++) {
const WebFrontend::AssetType& Asset = WebFrontend::getAsset(Index);

AssetUris[Index] = httpd_uri_t{Asset.Path, HTTP_GET, handleAsset, const_cast<WebFrontend::AssetType*>(&Asset),
false, false, nullptr};
httpd_register_uri_handler(HttpServer, &AssetUris[Index]);
}

static const httpd_uri_t CommandsUri{"/commands", HTTP_GET, handleCommands, nullptr, false, false, nullptr};
static const httpd_uri_t DisplayUri{"/display", HTTP_GET, handleDisplay, nullptr, false, false, nullptr};
static const httpd_uri_t ManifestUri{"/manifest.webmanifest", HTTP_GET, handleManifest, nullptr, false, false, nullptr};
static const httpd_uri_t Icon192Uri{"/icon-192.png", HTTP_GET, handleIcon192, nullptr, false, false, nullptr};
static const httpd_uri_t Icon512Uri{"/icon-512.png", HTTP_GET, handleIcon512, nullptr, false, false, nullptr};
static const httpd_uri_t UpdateUri{"/update", HTTP_POST, handleUpdate, nullptr, false, false, nullptr};
static const httpd_uri_t SocketUri{"/ws", HTTP_GET, handleSocket, nullptr, true, false, nullptr};

httpd_register_uri_handler(HttpServer, &RootUri);
httpd_register_uri_handler(HttpServer, &ConsoleUri);
httpd_register_uri_handler(HttpServer, &CommandsUri);
httpd_register_uri_handler(HttpServer, &DisplayUri);
httpd_register_uri_handler(HttpServer, &ManifestUri);
httpd_register_uri_handler(HttpServer, &Icon192Uri);
httpd_register_uri_handler(HttpServer, &Icon512Uri);
httpd_register_uri_handler(HttpServer, &UpdateUri);
httpd_register_uri_handler(HttpServer, &SocketUri);

Expand Down
57 changes: 39 additions & 18 deletions platform/esp32/test/cases/web_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,34 @@
#include <vector>


static httpd_handler_t SocketHandler = nullptr;
static httpd_handler_t RootHandler = nullptr;
static httpd_handler_t CommandsHandler = nullptr;
static httpd_handler_t DisplayHandler = nullptr;
static httpd_handler_t ManifestHandler = nullptr;
static httpd_handler_t Icon192Handler = nullptr;
static httpd_handler_t Icon512Handler = nullptr;
static httpd_handler_t UpdateHandler = nullptr;
static httpd_handler_t ConsoleHandler = nullptr;
/* A registered route as the server keeps it: the handler and the context handed back with
it. The context matters since the five files served unchanged share one handler and tell
each other apart by it - so a route is called through here rather than as a bare function
pointer, or every asset would answer as whichever was registered last. */
struct RouteType {
httpd_handler_t Handler = nullptr;
void* Context = nullptr;

esp_err_t operator()(httpd_req_t* Request) const {
Request->user_ctx = Context;
return Handler(Request);
}

/* So a case can still ask whether a route was registered at all, which is what the
nullptr comparisons below are for. */
bool operator!=(std::nullptr_t) const { return Handler != nullptr; }
bool operator==(std::nullptr_t) const { return Handler == nullptr; }
};

static RouteType SocketHandler;
static RouteType RootHandler;
static RouteType CommandsHandler;
static RouteType DisplayHandler;
static RouteType ManifestHandler;
static RouteType Icon192Handler;
static RouteType Icon512Handler;
static RouteType UpdateHandler;
static RouteType ConsoleHandler;
static std::string SentType;
static std::string SentStatus;
/* Every header a handler set, by name - the gate's WWW-Authenticate is read out of here. */
Expand All @@ -45,15 +64,17 @@ esp_err_t httpd_start(httpd_handle_t* h, const httpd_config_t*) { *h = (httpd_ha

esp_err_t httpd_register_uri_handler(httpd_handle_t, const httpd_uri_t* u)
{
if(strcmp(u->uri, "/ws") == 0) { SocketHandler = u->handler; }
else if(strcmp(u->uri, "/commands") == 0) { CommandsHandler = u->handler; }
else if(strcmp(u->uri, "/display") == 0) { DisplayHandler = u->handler; }
else if(strcmp(u->uri, "/manifest.webmanifest") == 0) { ManifestHandler = u->handler; }
else if(strcmp(u->uri, "/icon-192.png") == 0) { Icon192Handler = u->handler; }
else if(strcmp(u->uri, "/icon-512.png") == 0) { Icon512Handler = u->handler; }
else if(strcmp(u->uri, "/update") == 0) { UpdateHandler = u->handler; }
else if(strcmp(u->uri, "/console") == 0) { ConsoleHandler = u->handler; }
else { RootHandler = u->handler; }
const RouteType Route{u->handler, u->user_ctx};

if(strcmp(u->uri, "/ws") == 0) { SocketHandler = Route; }
else if(strcmp(u->uri, "/commands") == 0) { CommandsHandler = Route; }
else if(strcmp(u->uri, "/display") == 0) { DisplayHandler = Route; }
else if(strcmp(u->uri, "/manifest.webmanifest") == 0) { ManifestHandler = Route; }
else if(strcmp(u->uri, "/icon-192.png") == 0) { Icon192Handler = Route; }
else if(strcmp(u->uri, "/icon-512.png") == 0) { Icon512Handler = Route; }
else if(strcmp(u->uri, "/update") == 0) { UpdateHandler = Route; }
else if(strcmp(u->uri, "/console") == 0) { ConsoleHandler = Route; }
else { RootHandler = Route; }
return ESP_OK;
}

Expand Down
Loading