Firmware

The firmware is developed as a platformio project. It uses the espressif32 platform and arduino framework.

Configuration

Configuration is done through compiler flags to limit the need to modify the firmware.

I2C

Define the SDA and SCL pins as:

build_flags = 

    -D PIN_SDA=21
    -D PIN_SCL=22

Publish interval

The publishing interval is defined as:

build_flags = 

    -D PUBLISH_INTERVAL_SECS=10
    -D MODE_DEEP_SLEEP_MINS=0

Deep sleep has been investigated as a mechanism for extending battery life. At this revision, it doesn't work pending resolving an issue around waking up and reconnecting the LoRa radio. For now, set it to 0 minutes to disable the feature in firmware.

Authentication credentials

LoRa uses a The Things Netwotk (TTN) application (see LoRa for details). The firmware requires the app eui, dev ei, and app key to connect. To avoid exposing these authentication credentials in version control, they are obtained from a local file and provided as compiler flags.

To configure, create a file named secret_auth_XXX.py in the same directory as platformio.ini. add them to this file in the following format:

# TTN 'heltec lora environment sensor' credentials

keys = {
  "APP_EUI": "XXX",
  "DEV_EUI": "XXX",
  "APP_KEY": "XXX",
}

print(f"-D APP_EUI={keys['APP_EUI']} -D DEV_EUI={keys['DEV_EUI']} -D APP_KEY={keys['APP_KEY']} ")

Finally, add the following buid flag specifier in platformio.ini:

build_flags = 

    !python secret_auth_XXX.py

LoRa

The firmware uses Francois Riotte's library TheThingsNetwork_esp32. This requires a number of compiler definitions for specifying e.g. the LoRaWAN specification, region, etc. as described here.

Check platformio.ini for actual variables, but these typically are:

[env:ttgo-lora32-v21]

build_flags = 

    -D ARDUINO_LMIC_PROJECT_CONFIG_H_SUPPRESS
    -D CFG_eu868=1
    -D CFG_sx1276_radio=1
    -D LMIC_LORAWAN_SPEC_VERSION=LMIC_LORAWAN_SPEC_VERSION_1_0_3

Additional flags are required to specify the pins used for LoRa boards which are not pre-integrated (which the TTGO LoRa v21 is not). These are specified as follows:

build_flags = 

    -D NSS=18
    -D RST=14
    -D DIO0=26
    -D DIO1=33
    -D DIO2=32

This spreadsheet may help you identify the pins for your microcontroller.

Code structure

The code employs a simple plugin architecture to allow new devices to be added relatively easily. It defines a Sensor object representing the Lora driver object, and one or more Entity objects providing attributes (using the language of Home Assistant). At setup, the lora and entities are registered and started. The runloop drives a timer for the publishing interval.

If this isn't clear: an 'Entity' is something like an SHT31 temperature and humidity sensor. 'Temperature' and 'humidity' are attributes of that sensor. A 'Sensor' here is the combination of sensors and lora radio comprising the device.

Protocol buffer

The device employes Google's Protocol Buffers for serialising and transmitting data. This has the advantage of generating very efficient packets, which reduces power requirements and extends battery life. It has the disadvantage of being a pain to implement on microcontrollers. I've made it as simple as possible to extend.

The schema is defined in /protobuf-packet/packet.proto. String sizes for any metadata must be defined in /protobuf-packet/packet.options. Together, these define a Packet data structure that can be processed be e.g. node-RED. If you alter this, you need to regenerate the header and implementation code in proto (don't alter these by hand) - see /protobuf-packet/README.md for instructions.

The result of this is a set of constants you can use in your entity implementations to define entity and attribute properties -- look at an existing Entity implementationfor how to do it.

Entity class

This handles starting the entity, reading the data, and serialising it. Each entity object provides a start() method and a readData() method. start() returns true if startup and initialising was successful. readData() obtains the latest data from the sensor and serialises it for encoding by the LoRa class.

Battery

An entity class (EntityBattery) is defined for reporting the device battery voltage. This may require calibration on your battery as follows:

build_flags = 

    -D BATTERY_CORRECTION_FACTOR=0.885

I set it to one, measured the voltage and compared it to the reported value, then computed the correction factor.

Lora class

This handles starting the radio, joining The Things Network, serialising the readings from the sensors, and transmitting it. The device will hang during start() if it cannot join. publish() takes a vector of readings from the sensors, encodes it as a protocol buffer Packet, and transmits it.

Sensor class

This handles registering entities and the radio, starting them, reading their data at a defined interval, and sending the data to the LoRa for transmission.

Add new entities like so:

#include <EntityMyExcellentThing.h>
EntityMyExcellentThing  excellentThing;
void setup()
{
    ...
    sensor.registerEntity( &excellentThing );
}