OtaCore

In this class, we set up all of the basic network functionality for the specific feature modules, in addition to providing utility functions for logging and MQTT functionality. This class also contains the main command processor for commands received over MQTT:

#include <user_config.h>
#include <SmingCore/SmingCore.h>

These two includes are required to make use of the Sming framework. With them, we include the main headers of the SDK (user_config.h) and those of Sming (SmingCore.h). This also defines a number of preprocessor statements, such as to use the open source Light-Weight IP stack (LWIP) and to deal with some issues in the official SDK.

Also of note is the esp_cplusplus.h header, which is indirectly included this way. Its source file implements the new and delete functions, as well as a few handlers for class-related functionality, such as vtables when using virtual classes. This enables compatibility with the STL:

enum {
LOG_ERROR = 0,
LOG_WARNING,
LOG_INFO,
LOG_DEBUG,
LOG_TRACE,
LOG_XTRACE
};


enum ESP8266_pins {
ESP8266_gpio00 = 0x00001, // Flash
ESP8266_gpio01 = 0x00002, // TXD 0
ESP8266_gpio02 = 0x00004, // TXD 1
ESP8266_gpio03 = 0x00008, // RXD 0
ESP8266_gpio04 = 0x00010, //
ESP8266_gpio05 = 0x00020, //
ESP8266_gpio09 = 0x00040, // SDD2 (QDIO Flash)
ESP8266_gpio10 = 0x00080, // SDD3 (QDIO Flash)
ESP8266_gpio12 = 0x00100, // HMISO (SDO)
ESP8266_gpio13 = 0x00200, // HMOSI (SDI)
ESP8266_gpio14 = 0x00400, // SCK
ESP8266_gpio15 = 0x00800, // HCS
ESP8266_gpio16 = 0x01000, // User, Wake
ESP8266_mosi = 0x02000,
ESP8266_miso = 0x04000,
ESP8266_sclk = 0x08000,
ESP8266_cs = 0x10000
};

These two enumerations define the logging levels, and the individual GPIO and other pins of the ESP8266 that we may want to use. The values for the ESP8266 pin enumeration correspond to positions in a bitmask:

#define SCL_PIN 5
#define SDA_PIN 4

Here, we define the fixed pins for the I2C bus. These correspond to GPIO 4 and 5, also known as D1 and D2 on NodeMCU boards. The main reason for having these pins predefined is that they are two of the few safe pins on the ESP8266.

Many pins of the ESP8266 will change levels during startup before settling, which can cause unwanted behavior with any connected peripherals.

typedef void (*topicCallback)(String);
typedef void (*onInitCallback)();

We define two function pointers, one to be used by feature modules when they wish to register an MQTT topic, along with a callback function. The other is the callback we saw in the main function.


class OtaCore {
static Timer procTimer;
static rBootHttpUpdate* otaUpdater;
static MqttClient* mqtt;
static String MAC;
static HashMap<String, topicCallback>* topicCallbacks;
static HardwareSerial Serial1;
static String location;
static String version;
static int sclPin;
static int sdaPin;
static bool i2c_active;
static bool spi_active;
static uint32 esp8266_pins;

static void otaUpdate();
static void otaUpdate_CallBack(rBootHttpUpdate& update, bool result);
static void startMqttClient();
static void checkMQTTDisconnect(TcpClient& client, bool flag);
static void connectOk(IPAddress ip, IPAddress mask, IPAddress gateway);
static void connectFail(String ssid, uint8_t ssidLength, uint8_t *bssid, uint8_t reason);
static void onMqttReceived(String topic, String message);
static void updateModules(uint32 input);
static bool mapGpioToBit(int pin, ESP8266_pins &addr);

public:
static bool init(onInitCallback cb);
static bool registerTopic(String topic, topicCallback cb);
static bool deregisterTopic(String topic);
static bool publish(String topic, String message, int qos = 1);
static void log(int level, String msg);
static String getMAC() { return OtaCore::MAC; }
static String getLocation() { return OtaCore::location; }
static bool starti2c();
static bool startSPI();
static bool claimPin(ESP8266_pins pin);
static bool claimPin(int pin);
static bool releasePin(ESP8266_pins pin);
static bool releasePin(int pin);
};

The class declaration itself gives a good overview of the functionality provided by this class. The first thing we notice is that it is completely static. This ensures that this class's functionality is immediately initialized when the firmware starts, and that it can be accessed globally without having to worry about specific instances.

We can also see the first use of the uint32 type, which along with other integer types is defined similar to those in the cstdint header.

Moving on, here is the implementation:

#include <ota_core.h>

#include "base_module.h"

#define SPI_SCLK 14
#define SPI_MOSI 13
#define SPI_MISO 12
#define SPI_CS 15


Timer OtaCore::procTimer;
rBootHttpUpdate* OtaCore::otaUpdater = 0;
MqttClient* OtaCore::mqtt = 0;
String OtaCore::MAC;
HashMap<String, topicCallback>* OtaCore::topicCallbacks = new HashMap<String, topicCallback>();
HardwareSerial OtaCore::Serial1(UART_ID_1); // UART 0 is 'Serial'.
String OtaCore::location;
String OtaCore::version = VERSION;
int OtaCore::sclPin = SCL_PIN; // default.
int OtaCore::sdaPin = SDA_PIN; // default.
bool OtaCore::i2c_active = false;
bool OtaCore::spi_active = false;
uint32 OtaCore::esp8266_pins = 0x0;

We include the BaseModule class's header here, so that we can call its own initialization function later on after we have finished setting up the basic functionality. The static class members are also initialized here, with a number of default values assigned where relevant.

Of note here is the initializing of a second serial interface object in addition to the default Serial object instance. These correspond to the first (UART0, Serial) and second (UART1, Serial1) UART on the ESP8266.

With older versions of Sming, the SPIFFS-related file functions had trouble with binary data (due to internally assuming null-terminated strings), which is why the following alternative functions were added. Their naming is a slightly inverted version from the original function name to prevent naming collisions.

Since TLS certificates and other binary data files stored on SPIFFS have to be able to be written and read for the firmware to function correctly, this was a necessary compromise.

String getFileContent(const String fileName) {
file_t file = fileOpen(fileName.c_str(), eFO_ReadOnly);

fileSeek(file, 0, eSO_FileEnd);
int size = fileTell(file);
if (size <= 0) {
fileClose(file);
return "";
}

fileSeek(file, 0, eSO_FileStart);
char* buffer = new char[size + 1];
buffer[size] = 0;
fileRead(file, buffer, size);
fileClose(file);
String res(buffer, size);
delete[] buffer;
return res;
}

This function reads the entire contents of the specified file into a String instance that is returned.

void setFileContent(const String &fileName, const String &content) {
file_t file = fileOpen(fileName.c_str(), eFO_CreateNewAlways | eFO_WriteOnly);
fileWrite(file, content.c_str(), content.length());
fileClose(file);
}

This function replaces the existing content in a file with the new data in the provided String instance.

bool readIntoFileBuffer(const String filename, char* &buffer, unsigned int &size) {
file_t file = fileOpen(filename.c_str(), eFO_ReadOnly);

fileSeek(file, 0, eSO_FileEnd);
size = fileTell(file);
if (size == 0) {
fileClose(file);
return true;
}

fileSeek(file, 0, eSO_FileStart);
buffer = new char[size + 1];
buffer[size] = 0;
fileRead(file, buffer, size);
fileClose(file);
return true;
}

This function is similar to getFileContent(), but returns a simple character buffer instead of a String instance. It's mostly used for reading in the certificate data, which is passed into a C-based TLS library (called axTLS), where converting to a String instance would be wasteful with the copying involved, especially where certificates can be a few KB in size.

Next is the initialization function for this class:

bool OtaCore::init(onInitCallback cb) {
Serial.begin(9600);

Serial1.begin(SERIAL_BAUD_RATE);
Serial1.systemDebugOutput(true);

We first initialize the two UARTs (serial interfaces) in the NodeMCU. Although officially there are two UARTs in the ESP8266, the second one consists only out of a TX output line (GPIO 2, by default). Because of this, we want to keep the first UART free for applications requiring a full serial line, such as some sensors.

The first UART (Serial) is thus initialized so that we can later use it with feature modules, while the second UART (Serial1) is initialized to the default baud rate of 115,200, along with the system's debug output (WiFi/IP stack, and so on) being directed to this serial output as well. This second serial interface will thus be used solely for logging output.

         BaseModule::init(); 

Next, the BaseModule static class is initialized as well. This causes all feature modules active in this firmware to be registered, allowing them to be activated later on.

         int slot = rboot_get_current_rom();
u32_t offset;
if (slot == 0) { offset = 0x100000; }
else { offset = 0x300000; }
spiffs_mount_manual(offset, 65536);

Automatically mounting the SPIFFS filesystem while using the rBoot bootloader did not work with older releases of Sming, which is why we are doing it manually here. To do this, we get the current firmware slot from rBoot, using which we can pick the proper offset, either at the start of the second megabyte in the ROM, or of the fourth megabyte.

With the offset determined, we use the SPIFFS manual-mounting function with our offset and the size of the SPIFFS section. We are now able to read and write to our storage.


Serial1.printf(" SDK: v%s ", system_get_sdk_version());
Serial1.printf("Free Heap: %d ", system_get_free_heap_size());
Serial1.printf("CPU Frequency: %d MHz ", system_get_cpu_freq());
Serial1.printf("System Chip ID: %x ", system_get_chip_id());
Serial1.printf("SPI Flash ID: %x ", spi_flash_get_id());

Next, we print out a few system details to the serial debug output. This includes the ESP8266 SDK version we compiled against, the current free heap size, CPU frequency, the MCU ID (32-bit ID), and the ID of the SPI ROM chip.

         mqtt = new MqttClient(MQTT_HOST, MQTT_PORT, onMqttReceived);

We create a new MQTT client on the heap, providing the callback that will be called when we receive a new message. The MQTT broker host and port are filled in by the preprocessor from the details we added in the user Makefile for the project.


Serial1.printf(" Currently running rom %d. ", slot);

WifiStation.enable(true);
WifiStation.config(WIFI_SSID, WIFI_PWD);
WifiStation.connect();
WifiAccessPoint.enable(false);


WifiEvents.onStationGotIP(OtaCore::connectOk);
WifiEvents.onStationDisconnect(OtaCore::connectFail);

(*cb)();
}

As the final steps in the initialization, we output the current firmware slot that we are running from, then enable the Wi-Fi client while disabling the wireless access point (WAP) functionality. The WiFi client is told to connect to the WiFi SSID with the credentials that we specified previously in the Makefile.

Finally, we define the handlers for a successful WiFi connection and for a failed connection attempt, before calling the callback function we were provided with as a parameter.

After an OTA update of the firmware, the following callback function will be called:


void OtaCore::otaUpdate_CallBack(rBootHttpUpdate& update, bool result) {
OtaCore::log(LOG_INFO, "In OTA callback...");
if (result == true) { // success
uint8 slot = rboot_get_current_rom();
if (slot == 0) { slot = 1; } else { slot = 0; }

Serial1.printf("Firmware updated, rebooting to ROM slot %d... ", slot);
OtaCore::log(LOG_INFO, "Firmware updated, restarting...");
rboot_set_current_rom(slot);
System.restart();
}
else {
OtaCore::log(LOG_ERROR, "Firmware update failed.");
}
}

In this callback, we change the active ROM slot if the OTA update was successful, followed by a reboot of the system. Otherwise, we simply log an error and do not restart.

Next are a few MQTT-related functions:

bool OtaCore::registerTopic(String topic, topicCallback cb) {
OtaCore::mqtt->subscribe(topic);
(*topicCallbacks)[topic] = cb;
return true;
}

bool OtaCore::deregisterTopic(String topic) {
OtaCore::mqtt->unsubscribe(topic);
if (topicCallbacks->contains(topic)) {
topicCallbacks->remove(topic);
}

return true;
}

These two functions allow feature modules to respectively register and deregister an MQTT topic along with a callback. The MQTT broker is called with a subscription or unsubscribe request and the HashMap instance is updated accordingly:

bool OtaCore::publish(String topic, String message, int qos /* = 1 */) {
OtaCore::mqtt->publishWithQoS(topic, message, qos);
return true;
}

Any feature modules can publish an MQTT message on any topic using this function. The Quality of Service (QoS) parameter determines the publish mode. By default, messages are published in retain mode, meaning that the broker will retain the last published message for a particular topic.

The entry point for the OTA update functionality is found in the following function:

void OtaCore::otaUpdate() {
OtaCore::log(LOG_INFO, "Updating firmware from URL: " + String(OTA_URL));

if (otaUpdater) { delete otaUpdater; }
otaUpdater = new rBootHttpUpdate();

rboot_config bootconf = rboot_get_config();
uint8 slot = bootconf.current_rom;
if (slot == 0) { slot = 1; } else { slot = 0; }

otaUpdater->addItem(bootconf.roms[slot], OTA_URL + MAC);

otaUpdater->setCallback(OtaCore::otaUpdate_CallBack);
otaUpdater->start();
}

For an OTA update, we need to create a clean rBootHttpUpdate instance. We then need to configure this instance with the details of the current firmware slot, for which we obtain the configuration from rBoot and with it the current firmware slot number. This we use to give the number of the other firmware slot to the OTA updater.

Here, we only configure it to update the firmware slot, but we could also update the SPIFFS section for the other firmware slot as well this way. The firmware will be fetched over HTTP from the fixed URL we set before. The ESP8266's MAC address is affixed to the end of it as a unique query string parameter so that the update server knows which firmware image fits this system.

After setting the callback function that we looked at earlier, we start the update:

void OtaCore::checkMQTTDisconnect(TcpClient& client, bool flag) {
if (flag == true) { Serial1.println("MQTT Broker disconnected."); }
else {
String tHost = MQTT_HOST;
Serial1.println("MQTT Broker " + tHost + " unreachable."); }

procTimer.initializeMs(2 * 1000, OtaCore::startMqttClient).start();
}

Here, we define the MQTT disconnection handler. It is called whenever the connection with the MQTT broker fails so that we can try reconnecting after a two-second delay.

The flag parameter is set to true if we previously were connected, and false if the initial MQTT broker connection failed (no network access, wrong address, and so on).

Next is the function to configure and start the MQTT client:

void OtaCore::startMqttClient() {
procTimer.stop();
if (!mqtt->setWill("last/will", "The connection from this device is lost:(", 1, true)) {
debugf("Unable to set the last will and testament. Most probably there is not enough memory on the device.");
}

We stop the procTimer timer if it's running in case we were being called from a reconnect timer. Next, we set the last will and testament (LWT) for this device, which allows us to set a message that the MQTT broker will publish when it loses the connection with the client (us).

Next, we define three different execution paths, only one of which will be compiled, depending on whether we are using TLS (SSL), a username/password login, or anonymous access:

#ifdef ENABLE_SSL
mqtt->connect(MAC, MQTT_USERNAME, MQTT_PWD, true);
mqtt->addSslOptions(SSL_SERVER_VERIFY_LATER);

Serial1.printf("Free Heap: %d ", system_get_free_heap_size());

if (!fileExist("esp8266.client.crt.binary")) {
Serial1.println("SSL CRT file is missing: esp8266.client.crt.binary.");
return;
}
else if (!fileExist("esp8266.client.key.binary")) {
Serial1.println("SSL key file is missing: esp8266.client.key.binary.");
return;
}

unsigned int crtLength, keyLength;
char* crtFile;
char* keyFile;
readIntoFileBuffer("esp8266.client.crt.binary", crtFile, crtLength);
readIntoFileBuffer("esp8266.client.key.binary", keyFile, keyLength);

Serial1.printf("keyLength: %d, crtLength: %d. ", keyLength, crtLength);
Serial1.printf("Free Heap: %d ", system_get_free_heap_size());

if (crtLength < 1 || keyLength < 1) {
Serial1.println("Failed to open certificate and/or key file.");
return;
}

mqtt->setSslClientKeyCert((const uint8_t*) keyFile, keyLength,
(const uint8_t*) crtFile, crtLength, 0, true);
delete[] keyFile;
delete[] crtFile;

Serial1.printf("Free Heap: %d ", system_get_free_heap_size());

If we are using TLS certificates, we establish a connection with the MQTT broker, using our MAC as client identifier, then enable the SSL option for the connection. The available heap space is printed to the serial logging output for debugging purposes. Usually, at this point, we should have around 25 KB of RAM left, which is sufficient for holding the certificate and key in memory, along with the RX and TX buffers for the TLS handshake if the latter are configured on the SSL endpoint to be an acceptable size using the SSL fragment size option. We will look at this in more detail in Chapter 9, Example - Building Management and Control.

Next, we read the DER-encoded (binary) certificate and key files from SPIFFS. These files have a fixed name. For each file, we print out the file size, along with the current free heap size. If either file size is zero bytes, we consider the read attempt to have failed and we abort the connection attempt.

Otherwise, we use the key and certificate data with the MQTT connection, which should lead to a successful handshake and establishing an encrypted connection with the MQTT broker.

After deleting the key and certificate file data, we print out the free heap size to allow us to check that the cleanup was successful:

#elif defined USE_MQTT_PASSWORD
mqtt->connect(MAC, MQTT_USERNAME, MQTT_PWD);

When using an MQTT username and password to log in to the broker, we just need to call the previous function on the MQTT client instance, providing our MAC as client identifier along with the username and password:

#else
mqtt->connect(MAC);
#endif

To connect anonymously, we set up a connection with the broker and pass our MAC as the client identifier:

         mqtt->setCompleteDelegate(checkMQTTDisconnect);

mqtt->subscribe(MQTT_PREFIX"upgrade");
mqtt->subscribe(MQTT_PREFIX"presence/tell");
mqtt->subscribe(MQTT_PREFIX"presence/ping");
mqtt->subscribe(MQTT_PREFIX"presence/restart/#");
mqtt->subscribe(MQTT_PREFIX"cc/" + MAC);

delay(100);

mqtt->publish(MQTT_PREFIX"cc/config", MAC);
}

Here, we first set the MQTT disconnect handler. Then, we subscribe to a number of topics that we wish to respond to. These all relate to management functionality for this firmware, allowing the system to be queried and configured over MQTT.

After subscribing, we briefly (100 ms) wait to give the broker some time to process these subscriptions before we publish on the central notification topic, using our MAC to let any interested clients and servers know that this system just came online.

Next are the WiFi connection handlers:

void OtaCore::connectOk(IPAddress ip, IPAddress mask, IPAddress gateway) {
Serial1.println("I'm CONNECTED. IP: " + ip.toString());

MAC = WifiStation.getMAC();
Serial1.printf("MAC: %s. ", MAC.c_str());

if (fileExist("location.txt")) {
location = getFileContent("location.txt");
}
else {
location = MAC;
}

if (fileExist("config.txt")) {
String configStr = getFileContent("config.txt");
uint32 config;
configStr.getBytes((unsigned char*) &config, sizeof(uint32), 0);
updateModules(config);
}

startMqttClient();
}

This handler is called when we have successfully connected to the configured WiFi network using the provided credentials. After connecting, we keep a copy of our MAC in memory as our unique ID.

This firmware also supports specifying a user-defined string as our location or similar identifier. If one has been defined before, we load it from SPIFFS and use it; otherwise, our location string is simply the MAC.

Similarly, we load the 32-bit bitmask that defines the feature module configuration from SPIFFS if it exists. If not, all feature modules are initially left deactivated. Otherwise, we read the bitmask and pass it to the updateModules() function so that the relevant modules will be activated:

void OtaCore::connectFail(String ssid, uint8_t ssidLength, 
                                                   uint8_t* bssid, uint8_t reason) {
Serial1.println("I'm NOT CONNECTED. Need help :(");
debugf("Disconnected from %s. Reason: %d", ssid.c_str(), reason);

WDT.alive();

WifiEvents.onStationGotIP(OtaCore::connectOk);
WifiEvents.onStationDisconnect(OtaCore::connectFail);
}

If connecting to the Wi-Fi network fails, we log this fact, then tell the MCU's watchdog timer that we are still alive to prevent a soft restart before we attempt to connect again.

This finishes all of the initialization functions. Next up are the functions used during normal activity, starting with the MQTT message handler:

void OtaCore::onMqttReceived(String topic, String message) {
Serial1.print(topic);
Serial1.print(": ");
Serial1.println(message);

log(LOG_DEBUG, topic + " - " + message);

if (topic == MQTT_PREFIX"upgrade" && message == MAC) {
otaUpdate();
}
else if (topic == MQTT_PREFIX"presence/tell") {
mqtt->publish(MQTT_PREFIX"presence/response", MAC);
}
else if (topic == MQTT_PREFIX"presence/ping") {
mqtt->publish(MQTT_PREFIX"presence/pong", MAC);
}
else if (topic == MQTT_PREFIX"presence/restart" && message == MAC) {
System.restart();
}
else if (topic == MQTT_PREFIX"presence/restart/all") {
System.restart();
}

We registered this callback when we initially created the MQTT client instance. Every time a topic that we subscribed to receives a new message on the broker, we are notified and this callback receives a string containing the topic and another string containing the actual message (payload).

We can compare the topic with the topics we registered for, and perform the required operation, whether it is to perform an OTA update (if it specifies our MAC), respond to a ping request by returning a pong response with our MAC, or to restart the system.

The next topic is a more generic maintenance one, allowing one to configure active feature modules, set the location string, and request the current status of the system. The payload format consists out of the command string followed by a semicolon, and then the payload string:

   else if (topic == MQTT_PREFIX"cc/" + MAC) {
int chAt = message.indexOf(';'),
String cmd = message.substring(0, chAt);
++chAt;

String msg(((char*) &message[chAt]), (message.length() - chAt));

log(LOG_DEBUG, msg);

Serial1.printf("Command: %s, Message: ", cmd.c_str());
Serial1.println(msg);

We start by extracting the command from the payload string using a simple find and substring approach. We then read in the remaining payload string, taking care to read it in as a binary string. For this, we use the remaining string's length and as starting position, the character right after the semicolon.

At this point, we have extracted the command and payload and can see what we have to do:


if (cmd == "mod") {
if (msg.length() != 4) {
Serial1.printf("Payload size wasn't 4 bytes: %d ", msg.length());
return;
}

uint32 input;
msg.getBytes((unsigned char*) &input, sizeof(uint32), 0);
String byteStr;
byteStr = "Received new configuration: ";
byteStr += input;
log(LOG_DEBUG, byteStr);
updateModules(input);
}

This command sets which feature modules should be active. Its payload should be an unsigned 32-bit integer forming a bitmask, which we check to make sure that we have received exactly four bytes.

In the bitmask, the bits each match up with a module, which at this point are the following:

Bit position

Value

0x01

THPModule

0x02

CO2Module

0x04

JuraModule

0x08

JuraTermModule

0x10

MotionModule

0x20

PwmModule

0x40

IOModule

0x80

SwitchModule

0x100

PlantModule


Of these, the CO2, Jura, and JuraTerm modules are mutually exclusive, since they all use the first UART (Serial). If two or more of these are still specified in the bitmask, only the first module will be enabled and the others ignored. We will look at these other feature modules in more detail in Chapter 9, Example - Building Management and Control.

After we obtain the new configuration bitmask, we send it to the updateModules() function:

        else if (cmd == "loc") {
if (msg.length() < 1) { return; }
if (location != msg) {
location = msg;
fileSetContent("location.txt", location);
}
}

With this command, we set the new location string, if it is different then the current one, also saving it to the location file in SPIFFS to persist it across a reboot:

         else if (cmd == "mod_active") {
uint32 active_mods = BaseModule::activeMods();
if (active_mods == 0) {
mqtt->publish(MQTT_PREFIX"cc/response", MAC + ";0");
return;
}

mqtt->publish(MQTT_PREFIX"cc/response", MAC + ";" + String((const char*) &active_mods, 4));
}
else if (cmd == "version") {
mqtt->publish(MQTT_PREFIX"cc/response", MAC + ";" + version);
}
else if (cmd == "upgrade") {
otaUpdate();
}
}

The last three commands in this section return the current bitmask for the active feature modules, the firmware version, and trigger an OTA upgrade:

         else {
if (topicCallbacks->contains(topic)) {
(*((*topicCallbacks)[topic]))(message);
}
}
}

The last entry in the if...else block looks at whether the topic is perhaps found in our list of callbacks for the feature modules. If found, the callback is called with the MQTT message string.

Naturally, this means that only one feature module can register itself to a specific topic. Since each module tends to operate under its own MQTT sub-topic to segregate the message flow, this is generally not a problem:

void OtaCore::updateModules(uint32 input) {
Serial1.printf("Input: %x, Active: %x. ", input, BaseModule::activeMods());

BaseModule::newConfig(input);

if (BaseModule::activeMods() != input) {
String content(((char*) &input), 4);
setFileContent("config.txt", content);
}
}

This function is pretty simple. It mostly serves as a pass-through for the BaseModule class, but it also ensures that we keep the configuration file in SPIFFS up to date, writing the new bitmask to it when it has changed.

We absolutely must prevent unnecessary writes to SPIFFs, as the underlying Flash storage has finite write cycles. Limiting write cycles can significantly extend the lifespan of the hardware, as well as reduce overall system load:

bool OtaCore::mapGpioToBit(int pin, ESP8266_pins &addr) {
switch (pin) {
case 0:
addr = ESP8266_gpio00;
break;
case 1:
addr = ESP8266_gpio01;
break;
case 2:
addr = ESP8266_gpio02;
break;
case 3:
addr = ESP8266_gpio03;
break;
case 4:
addr = ESP8266_gpio04;
break;
case 5:
addr = ESP8266_gpio05;
break;
case 9:
addr = ESP8266_gpio09;
break;
case 10:
addr = ESP8266_gpio10;
break;
case 12:
addr = ESP8266_gpio12;
break;
case 13:
addr = ESP8266_gpio13;
break;
case 14:
addr = ESP8266_gpio14;
break;
case 15:
addr = ESP8266_gpio15;
break;
case 16:
addr = ESP8266_gpio16;
break;
default:
log(LOG_ERROR, "Invalid pin number specified: " + String(pin));
return false;
};

return true;
}

This function maps the given GPIO pin number to its position in the internal bitmask. It uses the enumeration we looked at for the header file for this class. With this mapping, we can set the used/unused state of GPIO pins of the ESP8266 module using just a single uint32 value:

void OtaCore::log(int level, String msg) {
String out(lvl);
out += " - " + msg;

Serial1.println(out);
mqtt->publish(MQTT_PREFIX"log/all", OtaCore::MAC + ";" + out);
}

In the logging method, we append the log level to the message string before writing it to the serial output, as well as publishing it on MQTT. Here, we publish on a single topic, but as a refinement you could log on a different topic depending on the specified level.

What makes sense here depends a great deal on what kind of backend you have set up to listen for and process logging output from the ESP8266 systems running this firmware:

bool OtaCore::starti2c() {
if (i2c_active) { return true; }

if (!claimPin(sdaPin)) { return false; }
if (!claimPin(sclPin)) { return false; }

Wire.pins(sdaPin, sclPin);
pinMode(sclPin, OUTPUT);
for (int i = 0; i < 8; ++i) {
digitalWrite(sclPin, HIGH);
delayMicroseconds(3);
digitalWrite(sclPin, LOW);
delayMicroseconds(3);
}

pinMode(sclPin, INPUT);

Wire.begin();
i2c_active = true;
}

This function starts the I2C bus if it hasn't been started already. It tries to register the pins it wishes to use for the I2C bus. If these are available, it will set the clock line (SCL) to output mode and first pulse it eight times to unfreeze any I2C devices on the bus.

After pulsing the clock line like his, we start the I2C bus on the pins and make a note of the active state of this bus.

Frozen I2C devices can occur if the MCU power cycles when the I2C devices do not, and remain in an indeterminate state. With this pulsing, we make sure that the system won't end up in a non-functional state, requiring manual intervention:
bool OtaCore::startSPI() {
if (spi_active) { return true; }

if (!claimPin(SPI_SCLK)) { return false; }
if (!claimPin(SPI_MOSI)) { return false; }
if (!claimPin(SPI_MISO)) { return false; }
if (!claimPin(SPI_CS)) { return false; }

SPI.begin();
spi_active = true;
}

Starting the SPI bus is similar to staring the I2C bus, except without a similar recovery mechanism:

bool OtaCore::claimPin(int pin) {
ESP8266_pins addr;
if (!mapGpioToBit(pin, addr)) { return false; }

return claimPin(addr);
}


bool OtaCore::claimPin(ESP8266_pins pin) {
if (esp8266_pins & pin) {
log(LOG_ERROR, "Attempting to claim an already claimed pin: " + String(pin));
log(LOG_DEBUG, String("Current claimed pins: ") + String(esp8266_pins));
return false;
}

log(LOG_INFO, "Claiming pin position: " + String(pin));

esp8266_pins |= pin;

log(LOG_DEBUG, String("Claimed pin configuration: ") + String(esp8266_pins));

return true;
}

This overloaded function is used to register a GPIO pin by a feature module before it starts, to ensure that no two modules attempt to use the same pins at the same time. One version accepts a pin number (GPIO) and uses the mapping function we looked at earlier to get the bit address in the esp8266_pins bitmask before passing it on to the other version of the function.

In that function, the pin enumeration is used to do a bitwise AND comparison. If the bit has not been set yet, it is toggled and true is returned. Otherwise, the function returns false and the calling module knows that it cannot proceed with its initialization:

bool OtaCore::releasePin(int pin) {
ESP8266_pins addr;
if (!mapGpioToBit(pin, addr)) { return false; }

return releasePin(addr);
}


bool OtaCore::releasePin(ESP8266_pins pin) {
if (!(esp8266_pins & pin)) {
log(LOG_ERROR, "Attempting to release a pin which has not been set: " + String(pin));
return false;
}

esp8266_pins &= ~pin;

log(LOG_INFO, "Released pin position: " + String(pin));
log(LOG_DEBUG, String("Claimed pin configuration: ") + String(esp8266_pins));

return true;
}

This overloaded function, to release a pin when a feature module is shutting down, works in a similar manner. One uses the mapping function to get the bit address, the other performs a bitwise AND operation to check that the pin has in fact been set, and toggles it to an off position with the bitwise OR assignment operator if it was set.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.83.62