Interface software

The interface image makes the device an “OPNpool Interface”. Besides monitoring the pool controller and providing access to a web interface, it provides automatic updates, network discovery and supports post mortem debugging.

Architecture

The entry point of the code is app_main() in the file main/main.c.

app_main() starts by initializing devices and connecting to the WiFi network. The interface appplication is split over different tasks that communicate over message queues offered by the Free Real Time Operating System (Freitas).

  • factory_reset_task
  • pool_task
  • ota_update_task
  • http server
  • mqtt_task
  • hass_task

factory_reset_task

A factory reset erases the Wifi credentials and returns to the factory app. This allows the device to be provisioned from scratch. To perform a Factory Reset, pull GPIO#0 low for five seconds, while the interface app is running (not during boot up). The development board, has a boot button) that services the same function. I

                I (753) factory_reset_task: Waiting for 3 sec BOOT/RESET press ..
                W (26408) factory_reset_task: Factory reset ..
            
Device debug trace

The code base uses the git submodule ESP32_factory-reset-task.

pool_task

The pool_task forms the heart of OPNpool. It handles data received from the pool controller, and can send requests to the pool controller.

Its responsibilities are:

  • Initially, it queues a few messages that request thermostat and scheduling information from the pool controller.
  • If it receives a message in the to_pool_q, it creates a corresponding network message. It then calls datalink_tx_pkt_queue to add the preamble and tail, and queues it for transmission through the rs485 driver.
  • Reads bytes from the rs485 driver to create a data packet. It passes this up the stack create a network message. Based on that it updates the pool state information and publishes it using the MQTT task.

The protocol stack is shown in the illustration below.

Pool task layers

The following sections provide more information about the protocol stack.

RS-485 driver

Starting from the physical RS-485 bus, the signal passes through the MAX3485 transceiver that connects to the RX, TX and RTS pins of the ESP32 UART. This UART presents the signal as bytes to the driver.

The driver:

  • Initializes the UART.
  • Provides functions to read and write bytes.
  • Bytes to be transmitted are queued as as rs485_q_msg in rs485->tx_q. When a transmit opportunity arrises, the pool_task dequeues from rs485->tx_q, relies on the driver to transmits the bytes.

Transmitting on the RS-485 is a bit tricky because it operates in half-duplex and has multiple nodes on the same single twisted pair. In practice, the pool controller appears to only accept messages after it broadcasts its periodic status message, and before it communicates with the chlorinator. Therefore, outgoing bytes are queued, until the moment arrives where they can be transmitted

It communicates with the datalink layer using the rs485_handle_t device handle.

        typedef struct rs485_instance_t {
            rs485_available_fnc_t available;      // bytes available in rx buffer
            rs485_read_bytes_fnc_t read_bytes;    // read bytes from rx buffer
            rs485_write_bytes_fnc_t write_bytes;  // write bytes to tx buffer 
            rs485_flush_fnc_t flush;              // wait until all bytes are transmitted
            rx485_tx_mode_fnc_t tx_mode;          // controls RTS pin (for half-duplex)
            rs485_queue_fnc_t queue;              // queue to handle->tx_q
            rs485_dequeue_fnc_t dequeue;          // dequeue from handle->tx_q
            QueueHandle_t tx_q;                   // transmit queue
        } rs485_instance_t;
        typedef struct rs485_instance_t * rs485_handle_t;
    

Datalink layer

Provides framing and byte error detection. The interface with the network layers is through datalink_pkt_t.

It supports both the A5 and IC protocol.

It communicates with the network layer using the datalink_pkt_t structure.

        typedef struct datalink_pkt_t {
            datalink_prot_t    prot;      // datalink_prot as detected by `_read_head`
            uint8_t            prot_typ;  // from datalink_hdr_a5->typ
            uint8_t            src;       // from datalink_hdr_a5->src
            uint8_t            dst;       // from datalink_hdr_a5->dst
            datalink_data_t *  data;
            size_t             data_len;
            skb_handle_t       skb;
        } datalink_pkt_t;
    

Its functions are:

  • datalink_rx_pkt(), receives bytes from the RS-485 bus and makes a datalink_pkt from it. It is implemented as a state machine, using socket buffers to store the bytes received.
  • datalink_tx_pkt(), receives a datalink_pkt and wraps it with a header and tailer. It then queues it for transmission on the RS-485 bus. Once there is a transmit opportunity, the pool_task send it using the rs485 driver.

Network layer

This layer translates between datalink_pkt and the protocol-agnostic network_msg.

It communicates with the `poolstate` layer using the network_msg_t structure.

        typedef struct network_msg_t {
            network_msg_typ_t typ;
            union {
                network_msg_pump_reg_set_t * pump_reg_set;
                network_msg_pump_reg_resp_t * pump_reg_set_resp;
                network_msg_pump_ctrl_t * pump_ctrl;
                network_msg_pump_mode_t * pump_mode;
                network_msg_pump_run_t * pump_run;
                network_msg_pump_status_resp_t * pump_status_resp;
                network_msg_ctrl_set_ack_t * ctrl_set_ack;
                network_msg_ctrl_circuit_set_t * ctrl_circuit_set;
                network_msg_ctrl_sched_resp_t * ctrl_sched_resp;
                network_msg_ctrl_state_t * ctrl_state;
                network_msg_ctrl_time_t * ctrl_time;
                network_msg_ctrl_heat_t * ctrl_heat;
                network_msg_ctrl_heat_set_t * ctrl_heat_set;
                network_msg_ctrl_layout_t * ctrl_layout;
                network_msg_ctrl_layout_set_t * ctrl_layout_set;
                network_msg_chlor_ping_req_t * chlor_ping_req;
                network_msg_chlor_ping_resp_t * chlor_ping;
                network_msg_chlor_name_t * chlor_name;
                network_msg_chlor_level_set_t * chlor_level_set;
                network_msg_chlor_level_resp_t * chlor_level_resp;
                uint8_t * bytes;
            } u;
        } network_msg_t;
    

Its public functions are:

  • network_rx_msg(), translates a datalink_pkt writes it as network_msg.
  • network_create_msg(), creates a datalink_pkt from a network_msg.

Poolstate

The sysState_t structure stores the state of the pool system. It is updated based on status messages from the controller, and by querying the controller. Once the controller replied to these queries, pool() starts honoring commands from the Serial port connected to the computer.

ota_update_task

This is the same git submodule that we used for Factory application earlier.

Each time the device powers up, it checks to see if an new interface app is available. If so, it will download the image and switches to the new version. [doc] To trigger an OTA update, power cycle the device, or press the EN button.

When the new interface app has critical errors, a rollback mechanism keep the device working by automatically rolling back to the previous working application.

After the app is downloaded using OTA, it is marked as pending_verify. Once the application determines that it is working well, it marks it as valid. On the other hand, when the application marks it as invalid or the device resets, the bootloader will boot the previous working application. [doc]

http server

Albeit not technically a task, we’ll discuss it here.

The HTTP server provides an UI to see the pool state and control a few settings.

The main_task uses the git module wifi_connect to establish a connection to the WiFi access point. Once established, it calls httpd_register_handlers to register the URI handlers for incoming GET requests.

These handlers are:

  • httpd_root, replies to GET /
  • httpd_ico“, replies to GET /favicon.ico
  • httpd_json, replies to GET /json

httpd_root()

To save memory space on the ESP32, the device returns a minimum HTML page. The body part is replaced by the replace_body.js script.

httpd_ico()

The browser places the favicon next to the page’s title on the tab. This function simply sends an HTTP response containing the favicon.

httpd_json()

The Arduino replies to any command with the pool state information formatted as JSON. See illustration further down.

If query variables were passed, they will be send to the ipc->to_pool_q. The pool_task will create a corresponding message and send it to the pool controller.

The following query variables are supported:

        homeassistant/switch/opnpool/pool_circuit/set
        homeassistant/switch/opnpool/spa_circuit/set
        homeassistant/switch/opnpool/aux1_circuit/set
        homeassistant/switch/opnpool/aux2_circuit/set
        homeassistant/switch/opnpool/aux3_circuit/set
        homeassistant/switch/opnpool/ft1_circuit/set
        homeassistant/switch/opnpool/ft2_circuit/set
        homeassistant/switch/opnpool/ft3_circuit/set
        homeassistant/switch/opnpool/ft4_circuit/set
        homeassistant/climate/opnpool/pool_heater/set_mode
        homeassistant/climate/opnpool/pool_heater/set_tmp
        homeassistant/climate/opnpool/spa_heater/set_mode
        homeassistant/climate/opnpool/spa_heater/set_tmp
    

For the switches, it supports the values ON and OFF. For a thermostat (climate) mode, it supports the values off, heat, solar_pref and solar. For the thermostat temperature it accept a value in Fahrenheit.

Example

GET /json request
GET /json response

For details on web integration, refer to the Web UI section.

mqtt_task

IBM’s MQ telemetry transport (MQTT) is a lightweight, publish-subscribe network protocol that transports messages between devices. The keyword is telemetry, or remote monitoring.

MQTT is rapidly becoming the prevailed protocols to connect large numbers of devices. The mqtt_task implements an MQTT client.

Responsibilities of the task:

  • Connect to MQTT broker.
  • Initially, check if there is a coredump in flash memory, then publish it to MQTT.
  • Subscribe to control topics, for diagnostic commands.
  • Subscribing or publishing on MQTT topics on behalf of other processes.

Connect to MQTT broker

The code calls esp_mqtt_client_init and waits until the MQTT connection with the broker is established. It learns about a successful connection when it the callback function receives a MQTT_EVENT_CONNECTED.

If the connection were to be broken, the ESP IDF SDK will automatically reconnect to the broker.

Coredump to MQTT

As the devices that are deployed in the field, they have no serial monitor connected. Instead, we configure the ESP-IDF so the coredump is written to flash memory. Next time the ESP32 boots up, and it finds a coredump in flash, it uses the git submodule ESP32_coredump-to-server to publish the coredump to MQTT under the topic opnpool/data/coredump/ID.

Subscribe to control topics

Initially, the device asks MQTT task to subscribe to the topics listed below. Here ID is an unique device identifier.

        opnpool/ctrl
        opnpool/ctrl/ID
    

When another devices publishes a message on these topics, the MQTT task will handle the message. It supports the following values:

  • who, request device info such as device id, firmware version, WiFi details, MQTT details and heap
  • restart, restarts the device
  • htstart, starts heap trace (when enabled)
  • htstop, stops heap trace (when enabled)

When the MQTT task receives a message on these topics, it will take action and publish a reply to pool/data/ID/ as shown below.

Publish “who” to “opnpool/ctrl”
Response of publishing “who” to “opnpool/ctrl”

Subscribe/publish on behalf of others

The hass_task queues a message to ipc->to_mqtt_q in order to subscribe or publish on a topic. The mqtt_task forwards this request to the ESP-IDF SDK.

When subscribing, the topic is also added to the _subscribers list, in case the connection with the broker needs to get re-established.

When another MQTT client publishes on a topic that another process subscribed to, the message is forwarded to the ipc->to_pool_q.

hass_task

Home Assistant is an open source home automation that puts local control and privacy first.

The hass_task’s duties are

  • Listen for commands over MQTT.
  • Support MQTT device discovery.

Listen for commands over MQTT

Initially, the device subscribes to the MQTT topics that lets other MQTT clients change pool controller settings such as thermostat set point or heating mode.

It subscribes to the topics listed below.

        homeassistant/switch/opnpool/pool_circuit/set
        homeassistant/switch/opnpool/spa_circuit/set
        homeassistant/switch/opnpool/aux1_circuit/set
        homeassistant/switch/opnpool/aux2_circuit/set
        homeassistant/switch/opnpool/aux3_circuit/set
        homeassistant/switch/opnpool/ft1_circuit/set
        homeassistant/switch/opnpool/ft2_circuit/set
        homeassistant/switch/opnpool/ft3_circuit/set
        homeassistant/switch/opnpool/ft4_circuit/set
        homeassistant/climate/opnpool/pool_heater/set_mode
        homeassistant/climate/opnpool/pool_heater/set_tmp
        homeassistant/climate/opnpool/spa_heater/set_mode
        homeassistant/climate/opnpool/spa_heater/set_tmp
    

When another devices publishes a message on these topics, the MQTT task will forward forward it as a network message to the Pool task. The latter will call hass_tx_pair to handle the message.

For the switches, it supports the values ON and OFF. For a thermostat (climate) mode, it supports the values off, heat, solar_pref and solar. For the thermostat temperature it accept a value in Fahrenheit.

MQTT device discovery

We auto populate the Home Assistant entities using MQTT Device Discovery. This enables us to use MQTT devices with only minimal configuration effort on the side of Home Assistant.

Every five minute, the device publishes MQTT discovery messages, to announce its presence to Home Assistant.

Some examples of these configuration topics are:

                topic = homeassistant/sensor/opnpool/air_temp/config;
                value = {
                    "~": "homeassistant/sensor/opnpool/air_temp",
                    "name": "Pool air temp",
                    "stat_t": "~/state",
                    "unit_of_meas": "°F"
                }
            
Discovery example 1
            topic = homeassistant/climate/opnpool/pool_heater/config;
            value = {
                "~": "homeassistant/climate/opnpool/pool_heater",
                "name": "Pool pool heater",
                "mode_cmd_t": "~/set_mode",
                "temp_cmd_t": "~/set_temp",
                "mode_stat_tpl": "{{ value_json.mode }}",
                "mode_stat_t": "~/state",
                "temp_stat_tpl": "{{ value_json.target_temp }}",
                "temp_stat_t": "~/state",
                "curr_temp_t": "~/state",
                "curr_temp_tpl": "{{ value_json.current_temp }}",
                "min_temp": 15,
                "max_temp": 110,
                "temp_step": 1,
                "temp_unit": "F",
                "avty_t": "~/available",
                "pl_avail": "online",
                "pl_not_avail": "offline",
                "modes": ["off","heat"]
            }
        
Discovery example 2

For details on Home Assistant integration, refer to the corresponding chapter.

From there the device publishes the data as e.g.

homeassistant/sensor/opnpool/air_temp/state 55
homeassistant/climate/opnpool/pool_heater/state {"mode":"off","target_temp":0,"current_temp":0}
homeassistant/climate/opnpool/pool_heater/available online

Continue reading to learn about the Web UI.

Leave a Reply

Your email address will not be published. Required fields are marked *

 

This site uses Akismet to reduce spam. Learn how your comment data is processed.