Close

Settings and WebApp

A project log for Power Monitoring

A device based on PZEM-004T that monitors power, current, voltage, frequency, etc.

strangerandstrange.rand 01/07/2022 at 12:080 Comments

Many things were added/improved during the last few days:

Settings

Now we can configure:

There is no validation on the server, so I wouldn't say that everything is super secure and production-ready, but it works. To simplify server-side code, all fields are combined into a single structure so in that way, we can read/write everything at once.

The only tricky part was receiving a JSON to later parse it using the ArduinoJson library.

server.on("/power/api/settings", HTTP_PUT, [](AsyncWebServerRequest *request){}, NULL, _saveSettings);
... ... ...
void _saveSettings(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
{
    ... ... ...
}

Depending on the body size, a request callback can be executed once or a few times. In my case, I had two executions of the callback, and getting a single JSON result requires additional logic (plus an intermediate buffer). The current code looks like this:

void _saveSettings(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
{
    if (total > sizeof(dataBuffer))
    {
        request->send(500, CONTENT_TYPE_TEXT, "Content is to big");
        return;
    }

    memcpy(dataBuffer + index, data, len);

    if (len + index < total)
        return;

     // JSON parsing here
}

The dataBuffer defined in main.cpp. First of all, we check whether the total body size is bigger than our intermediate buffer if yes, we won't be able to receive data and have to report an error. Then we copy the data into the buffer starting from the particular index, and if we haven't received all data (len + index < total) stop execution. In that way, only the last executed callback will do real work (parsing and saving).

As you probably noticed, the current approach is not suitable for a multi-user scenario when two users simultaneously save settings. Maybe, I'll fix it in the future.

Static files and automated build process

Web server configuration was simplified a lot:

server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->redirect("/power/index.html"); });
server.on("/power/", HTTP_GET, [](AsyncWebServerRequest *request)
              { request->redirect("/power/index.html"); });

server.serveStatic("/power/", SPIFFS, "/");
server.serveStatic("/power/images", SPIFFS, "/images");

The two first directives redirect a user to index.html, next two serve all static files. Additionally, all static files are compressed by gzip (during the build) to use less space and speed up loading. All SVG icons used from CSS were embedded into the resulting CSS file to have fewer requests and simpler caching.

Playing with Webpack took me more time than the rest of things altogether. But it was interesting, and now it is easier to develop new changes.

Progressive web app

From the beginning, I wanted to have a progressive web application (PWA) that could be installed on the phone/desktop like a native app. I will not talk about how it works in detail because it is a broad topic, but more information can be found on web.dev/progressive-web-apps or any other resource.

The app uses the cache first strategy, so all files are loaded from the browser cache (even if the network is available). This approach has one pitfall: even when the app is updated on the device/server and the user opens it in a browser, it will see the cached version instead of the new one. To get a new version, the user has to:

The simplest way to handle such a case is to notify the user about using an outdated version. I don't think this functionality is truly needed for this particular app, but it was an excellent time to learn something new.

I've almost forgotten to add one more important note about PWA. To make it installable, the app should satisfy a few criteria:

  1. The web app is not already installed
  2. Meets a user engagement heuristic
  3. Be served over HTTPS
  4. Includes a web app manifest that includes:
    • short_name or name
    • icons - must include a 192px and a 512px icon
    • start_url
    • display - must be one of fullscreen, standalone, or minimal-ui
    • prefer_related_applications must not be present, or be false
  5. Registers a service worker with a fetch handler

All points except 3rd one are implemented in the application. I don't want to add SSL/TLS support to the device, though it is theoretically possible. In my case, a standalone server with a public IP and dedicated domain is proxying requests to the module. It already has a valid SSL certificate and auto-update functionality using Let's Encrypt. By the way, this is the reason why I configured everything to be served from /power/ instead of the root /.

Next steps

As for now, almost everything planned has already been implemented. The only important thing left is resetting the energy counter at the beginning of the month. I also have a few additional ideas that might be implemented too:

I already have all charts in the Grafana, but adding something to the device itself looks interesting and relatively simple, so why not?

Discussions