Close

Firmware

A project log for Power Monitoring

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

strangerandstrange.rand 01/02/2022 at 07:550 Comments

How to upload firmware

WT32-ETH01 requires a standalone USB to UART adapter for firmware uploading. Wiring:

Pins IO0 and EN should be connected to GND only to start uploading firmware and then disconnected to allow the normal boot.

To configure wired ethernet, we have to add a few defines. It can be done in the code (in that case, the defines should go before "#include <ETH.h>") or in the platformio.ini using build_flags.

[env:ESP32-PM1]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
build_flags = 
    -D ETH_PHY_TYPE=ETH_PHY_LAN8720
    -D ETH_PHY_ADDR=1
    -D ETH_PHY_POWER=16
    -D ETH_CLK_MODE=ETH_CLOCK_GPIO0_IN

Code structure

I like keeping different things in different files instead of having a bulky "main.cpp". Let's see what functionality can be separated:

So the current structure is:

For example, Mqtt.h describes everything related to MQTT. If you want to use a variable that is defined in main.cpp, you have to define it as an external in the Mqtt.h (e.g., extern EventGroupHandle_t eg).

FreeRTOS

As you probably know, the Arduino core for ESP32 uses FreeRTOS, which gives us a lot of possibilities. Let's analyze our needs and check what tasks we have:

The first task is repeatable, and in my case, it should be executed once per second. The next three tasks should be executed only if we have new data to process, so they are event-based.

First of all, we have to define a few things:

EventGroupHandle_t eg;
QueueHandle_t qMqtt;
TimerHandle_t tRequestData;
SemaphoreHandle_t sema_PZEM;

Then in the setup function:

eg = xEventGroupCreate();
qMqtt = xQueueCreate(4, sizeof(MqttMessage));
sema_PZEM = xSemaphoreCreateMutex();

xTaskCreatePinnedToCore(taskRetrieveData, "RetrieveData", TaskStack10K, NULL, Priority3, NULL, Core1);
xTaskCreatePinnedToCore(taskUpdateDisplay, "UpdateDisplay", TaskStack15K, NULL, Priority3, NULL, Core1);
xTaskCreatePinnedToCore(taskUpdateWebClients, "UpdateWebClients", TaskStack10K, NULL, Priority3, NULL, Core1);
xTaskCreatePinnedToCore(taskSendMqttMessages, "tMqtt", TaskStack10K, NULL, Priority2, NULL, Core1);
tRequestData = xTimerCreate("RequestData", pdMS_TO_TICKS(Cfg::requestDataInterval), pdTRUE, (void *)0, reinterpret_cast<TimerCallbackFunction_t>(requestData));

The request data method is just one line of code

void requestData()
{
  xEventGroupSetBits(eg, EVENT_RETRIEVE_DATA);
}

It fires an event using the event group (eg) defined above. Theoretically, we can do real work here, but without blocking, so better to have a separate task.

EVENT_RETRIEVE_DATA is defined in the Pzem.h like this

#define EVENT_RETRIEVE_DATA (1 << 1)

Below you can see simplified code for retrieving the data. It waits for the event, takes a semaphore and retrieves the data, then gives the semaphore back, fires events to update display and web clients, and finally, composes MQTT message and pushes it to a queue. All other tasks are based on the same principles.

void taskRetrieveData(void *pvParameters)
{
    for (;;)
    {
        xEventGroupWaitBits(eg, EVENT_RETRIEVE_DATA, pdTRUE, pdTRUE, portMAX_DELAY);

        if (xSemaphoreTake(sema_PZEM, TICKS_TO_WAIT12) == pdTRUE)
        {
            // read data from PZEM here

            xSemaphoreGive(sema_PZEM);
            xEventGroupSetBits(eg, EVENT_UPDATE_DISPLAY | EVENT_UPDATE_WEB_CLIENTS);

            MqttMessage msg = composeMessage(data);
            if (xQueueSendToBack(qMqtt, &msg, 10) != pdPASS)
            {
                debugPrint("Failed to add to the mqtt queue");
            }
        }
    }
}

 As a result

WebSockets

One of the project's features is the web app that shows real-time data. The simplest and fastest way to implement this functionality is using WebSockets.

The WebSocket API is an advanced technology that makes it possible to open a two-way interactive communication session between the user's browser and a server. With this API, you can send messages to a server and receive event-driven responses without having to poll the server for a reply.

The code we have to write is minimal as all sophisticated logic is implemented inside the ESPAsyncWebServer library. All we need to do is to put the required data inside a buffer and then send it using

ws.textAll(buffer);

The web client is a simple HTML page with additional CSS styles and JS code. I'm thinking about rewriting it in a bit more fancy way but without using frameworks (pure JavaScript/"Vanilla.js" only).

All code can be found on GitHub.

Discussions