Club

The club header declares the classes that form the core of the project, and is responsible for dealing with the inputs from the switches, controlling the relays, and updating the status of the club room:

#include <wiringPi.h>
#include <wiringPiI2C.h>

The first thing of note in this header file are the includes. They add the basic WiringPi GPIO functionality to our code, as well as those for I2C usage. Further WiringPi one could include for other projects requiring such functionality would be SPI, UART (serial), software PWM, Raspberry Pi (Broadcom SoC) specific functionality, and others:

enum Log_level {
LOG_FATAL = 1,
LOG_ERROR = 2,
LOG_WARNING = 3,
LOG_INFO = 4,
LOG_DEBUG = 5
};

We define the different log levels we will be using as an enum:

 class Listener;

We forward declare the Listener class, as we will be using it in the implementation for these classes, but don't want to include the entire header for it yet:

class ClubUpdater : public Runnable {
TimerCallback<ClubUpdater>* cb;
uint8_t regDir0;
uint8_t regOut0;
int i2cHandle;
Timer* timer;
Mutex mutex;
Mutex timerMutex;
Condition timerCnd;
bool powerTimerActive;
bool powerTimerStarted;

public:
void run();
void updateStatus();
void writeRelayOutputs();
void setPowerState(Timer &t);
};

The ClubUpdater class is responsible for configuring the I2C-based GPIO expander, which controls the relays, as well as handling any updates to the club status. A Timer instance from the POCO framework is used to add a delay to the power status relay, as we will see when we look at the implementation.

This class inherits from the POCO Runnable class, which is the base class that's expected by the POCO Thread class, which is a wrapper around native threads.

The two uint8_t member variables mirror two registers on the I2C GPIO expander device, allowing us to set the direction and value of the output pins on the device, which effectively controls the attached relays:

class Club {
static Thread updateThread;
static ClubUpdater updater;

static void lockISRCallback();
static void statusISRCallback();

public:
static bool clubOff;
static bool clubLocked;
static bool powerOn;
static Listener* mqtt;
static bool relayActive;
static uint8_t relayAddress;
static string mqttTopic; // Topic we publish status updates on.

static Condition clubCnd;
static Mutex clubCndMutex;
static Mutex logMutex;
static bool clubChanged ;
static bool running;
static bool clubIsClosed;
static bool firstRun;
static bool lockChanged;
static bool statusChanged;
static bool previousLockValue;
static bool previousStatusValue;

static bool start(bool relayactive, uint8_t relayaddress, string topic);
static void stop();
static void setRelay();
static void log(Log_level level, string msg);
};

The Club class can be regarded as the input side of the system, setting up and handling the ISRs (interrupt handlers), as well as acting as the central (static) class with all of the variables pertaining to the club status, such as the status of the lock switch, status switch, and status of the power system (club open or closed).

This class is made fully static so that it can be used freely by different parts of the program to inquire about the room status.

Moving on, here is the implementation:

#include "club.h"

#include <iostream>

using namespace std;


#include <Poco/NumberFormatter.h>

using namespace Poco;


#include "listener.h"

Here, we include the Listener header so that we can use it. We also include the POCO NumberFormatter class to allow us to format integer values for logging purposes:

 #define REG_INPUT_PORT0              0x00
#define REG_INPUT_PORT1 0x01
#define REG_OUTPUT_PORT0 0x02
#define REG_OUTPUT_PORT1 0x03
#define REG_POL_INV_PORT0 0x04
#define REG_POL_INV_PORT1 0x05
#define REG_CONF_PORT0 0x06
#define REG_CONG_PORT1 0x07
#define REG_OUT_DRV_STRENGTH_PORT0_L 0x40
#define REG_OUT_DRV_STRENGTH_PORT0_H 0x41
#define REG_OUT_DRV_STRENGTH_PORT1_L 0x42
#define REG_OUT_DRV_STRENGTH_PORT1_H 0x43
#define REG_INPUT_LATCH_PORT0 0x44
#define REG_INPUT_LATCH_PORT1 0x45
#define REG_PUD_EN_PORT0 0x46
#define REG_PUD_EN_PORT1 0x47
#define REG_PUD_SEL_PORT0 0x48
#define REG_PUD_SEL_PORT1 0x49
#define REG_INT_MASK_PORT0 0x4A
#define REG_INT_MASK_PORT1 0x4B
#define REG_INT_STATUS_PORT0 0x4C
#define REG_INT_STATUS_PORT1 0x4D
#define REG_OUTPUT_PORT_CONF 0x4F

Next, we define all of the registers of the target GPIO expander device, the NXP PCAL9535A. Even though we only use two of these registers, it's generally a good practice to add the full list to simplify later expansion of the code. A separate header can be used as well to allow one to easily use different GPIO expanders without significant changes to your code, or any at all:

 #define RELAY_POWER 0
#define RELAY_GREEN 1
#define RELAY_YELLOW 2
#define RELAY_RED 3

Here, we define which functionality is connected to which relay, corresponding to a specific output pin of the GPIO expander chip. Since we have four relays, four pins are used. These are connected to the first bank (of two in total) of eight pins on the chip.

Naturally, it is important that these definitions match up with what is physically hooked up to those relays. Depending on the use case, one could make this configurable as well:

bool Club::clubOff;
bool Club::clubLocked;
bool Club::powerOn;
Thread Club::updateThread;
ClubUpdater Club::updater;
bool Club::relayActive;
uint8_t Club::relayAddress;
string Club::mqttTopic;
Listener* Club::mqtt = 0;

Condition Club::clubCnd;
Mutex Club::clubCndMutex;
Mutex Club::logMutex;
bool Club::clubChanged = false;
bool Club::running = false;
bool Club::clubIsClosed = true;
bool Club::firstRun = true;
bool Club::lockChanged = false;
bool Club::statusChanged = false;
bool Club::previousLockValue = false;
bool Club::previousStatusValue = false;

As Club is a fully static class, we initialize all of its member variables before we move into the ClubUpdater class's implementation:

void ClubUpdater::run() {
regDir0 = 0x00;
regOut0 = 0x00;
Club::powerOn = false;
powerTimerActive = false;
powerTimerStarted = false;
cb = new TimerCallback<ClubUpdater>(*this, &ClubUpdater::setPowerState);
timer = new Timer(10 * 1000, 0);

When we start an instance of this class, its run() function gets called. Here, we set a number of defaults. The direction and output register variables are initially set to zero. The club room power status is set to false, and the power timer-related Booleans are set to false, as the power timer is not active yet. This timer is used to set a delay before the power is turned on or off, as we will see in more detail in a moment.

By default, the delay on this timer is ten seconds. This can, of course, also be made configurable:

if (Club::relayActive) {
Club::log(LOG_INFO, "ClubUpdater: Starting i2c relay device.");
i2cHandle = wiringPiI2CSetup(Club::relayAddress);
if (i2cHandle == -1) {
Club::log(LOG_FATAL, string("ClubUpdater: error starting
i2c relay device."));
return;
}

wiringPiI2CWriteReg8(i2cHandle, REG_CONF_PORT0, 0x00);
wiringPiI2CWriteReg8(i2cHandle, REG_OUTPUT_PORT0, 0x00);

Club::log(LOG_INFO, "ClubUpdater: Finished configuring the i2c
relay device's registers.");
}

Next, we set up the I2C GPIO expander. This requires the I2C device address, which we passed to the Club class earlier on. What this setup function does is ensure that there is an active I2C device at this address on the I2C bus. After this, it should be ready to communicate with. It is also possible to skip this step via setting the relayActive variable to false. This is done by setting the appropriate value in the configuration file, which is useful when running integration tests on a system without an I2C bus or connected device.

With the setup complete, we write the initial values of the direction and output registers for the first bank. Both are written with null bytes so that all eight pins they control are set to both output mode and to a binary zero (low) state. This way, all relays connected to the first four pins are initially off:

          updateStatus();

Club::log(LOG_INFO, "ClubUpdater: Initial status update complete.");
Club::log(LOG_INFO, "ClubUpdater: Entering waiting condition.");

while (Club::running) {
Club::clubCndMutex.lock();
if (!Club::clubCnd.tryWait(Club::clubCndMutex, 60 * 1000)) {.
Club::clubCndMutex.unlock();
if (!Club::clubChanged) { continue; }
}
else {
Club::clubCndMutex.unlock();
}

updateStatus();
}
}

After completing these configuration steps, we run the first update of the club room status, using the same function that will also be called later on when the inputs change. This results in all of the inputs being checked and the outputs being set to a corresponding status.

Finally, we enter a waiting loop. This loop is controlled by the Club::running Boolean variable, allowing us to break out of it via a signal handler or similar. The actual waiting is performed using a condition variable, which we wait for here until either a time-out occurs on the one-minute wait (after which, we return to waiting after a quick check), or we get signaled by one of the interrupts that we will set later on for the inputs.

Moving on, we look at the function that's used to update the status of the outputs:

void ClubUpdater::updateStatus() {
Club::clubChanged = false;

if (Club::lockChanged) {
string state = (Club::clubLocked) ? "locked" : "unlocked";
Club::log(LOG_INFO, string("ClubUpdater: lock status changed to ") + state);
Club::lockChanged = false;

if (Club::clubLocked == Club::previousLockValue) {
Club::log(LOG_WARNING, string("ClubUpdater: lock interrupt triggered, but value hasn't changed. Aborting."));
return;
}

Club::previousLockValue = Club::clubLocked;
}
else if (Club::statusChanged) {
string state = (Club::clubOff) ? "off" : "on";
Club::log(LOG_INFO, string("ClubUpdater: status switch status changed to ") + state);
Club::statusChanged = false;

if (Club::clubOff == Club::previousStatusValue) {
Club::log(LOG_WARNING, string("ClubUpdater: status interrupt triggered, but value hasn't changed. Aborting."));
return;
}

Club::previousStatusValue = Club::clubOff;
}
else if (Club::firstRun) {
Club::log(LOG_INFO, string("ClubUpdater: starting initial update run."));
Club::firstRun = false;
}
else {
Club::log(LOG_ERROR, string("ClubUpdater: update triggered, but no change detected. Aborting."));
return;
}

The first thing we do when we enter this update function is to ensure that the Club::clubChanged Boolean is set to false so that it can be set again by one of the interrupt handlers.

After this, we check what has changed exactly on the inputs. If the lock switch got triggered, its Boolean variable will have been set to true, or the variable for the status switch will likely have been triggered. If this is the case, we reset the variable and compare the newly read value with the last known value for that input.

As a sanity check, we ignore the triggering if the value hasn't changed. This could happen if the interrupt got triggered due to noise, such as when the signal wire for a switch runs near power lines. Any fluctuation in the latter would induce a surge in the former, which can trigger the GPIO pin's interrupt. This is one obvious example of both the reality of dealing with a non-ideal physical world and a showcase for the importance of both the hardware and software in how they affect the reliability of a system.

In addition to this check, we log the event using our central logger, and update the buffered input value for use in the next run.

The last two cases in the if/else statement deal with the initial run, as well as a default handler. When we initially run this function the way we saw earlier, no interrupt will have been triggered, so obviously we have to add a third situation to the first two for the status and lock switches:

    if (Club::clubIsClosed && !Club::clubOff) {
Club::clubIsClosed = false;

Club::log(LOG_INFO, string("ClubUpdater: Opening club."));

Club::powerOn = true;
try {
if (!powerTimerStarted) {
timer->start(*cb);
powerTimerStarted = true;
}
else {
timer->stop();
timer->start(*cb);
}
}
catch (Poco::IllegalStateException &e) {
Club::log(LOG_ERROR, "ClubUpdater: IllegalStateException on timer start: " + e.message());
return;
}
catch (...) {
Club::log(LOG_ERROR, "ClubUpdater: Unknown exception on timer start.");
return;
}

powerTimerActive = true;

Club::log(LOG_INFO, "ClubUpdater: Started power timer...");

char msg = { '1' };
Club::mqtt->sendMessage(Club::mqttTopic, &msg, 1);

Club::log(LOG_DEBUG, "ClubUpdater: Sent MQTT message.");
}
else if (!Club::clubIsClosed && Club::clubOff) {
Club::clubIsClosed = true;

Club::log(LOG_INFO, string("ClubUpdater: Closing club."));

Club::powerOn = false;

try {
if (!powerTimerStarted) {
timer->start(*cb);
powerTimerStarted = true;
}
else {
timer->stop();
timer->start(*cb);
}
}
catch (Poco::IllegalStateException &e) {
Club::log(LOG_ERROR, "ClubUpdater: IllegalStateException on timer start: " + e.message());
return;
}
catch (...) {
Club::log(LOG_ERROR, "ClubUpdater: Unknown exception on timer start.");
return;
}

powerTimerActive = true;

Club::log(LOG_INFO, "ClubUpdater: Started power timer...");

char msg = { '0' };
Club::mqtt->sendMessage(Club::mqttTopic, &msg, 1);

Club::log(LOG_DEBUG, "ClubUpdater: Sent MQTT message.");
}

Next, we check whether we have to change the status of the club room from closed to open, or the other way around. This is determined by checking whether the club status (Club::clubOff) Boolean has changed relative to the Club::clubIsClosed Boolean, which stores the last known status.

Essentially, if the status switch is changed from on to off or the other way around, this will be detected and a change to the new status will be started. This means that a power timer will be started, which will turn the non-permanent power in the club room on or off after the preset delay.

The POCO Timer class requires that we first stop the timer before starting it if it has been started previously. This requires us to add one additional check.

In addition, we also use our reference to the MQTT client class to send a message to the MQTT broker with the updated club room status, here as either an ASCII 1 or 0. This message can be used to trigger other systems, which could update an online status for the club room, or be put to even more creative uses.

Naturally, the exact payload of the message could be made configurable.

In the next section, we will update the colors on the status light, taking into account the state of power in the room. For this, we use the following table:

Color

Status switch

Lock switch

Power status

Green

On

Unlocked

On

Yellow

Off

Unlocked

Off

Red

Off

Locked

Off

Yellow and red

On

Locked

On

 

Here is the implementation:


if (Club::clubOff) {
Club::log(LOG_INFO, string("ClubUpdater: New lights, clubstatus off."));

mutex.lock();
string state = (Club::powerOn) ? "on" : "off";
if (powerTimerActive) {
Club::log(LOG_DEBUG, string("ClubUpdater: Power timer active, inverting power state from: ") + state);
regOut0 = !Club::powerOn;
}
else {
Club::log(LOG_DEBUG, string("ClubUpdater: Power timer not active, using current power state: ") + state);
regOut0 = Club::powerOn;
}

if (Club::clubLocked) {
Club::log(LOG_INFO, string("ClubUpdater: Red on."));
regOut0 |= (1UL << RELAY_RED);
}
else {
Club::log(LOG_INFO, string("ClubUpdater: Yellow on."));
regOut0 |= (1UL << RELAY_YELLOW);
}

Club::log(LOG_DEBUG, "ClubUpdater: Changing output register to: 0x" + NumberFormatter::formatHex(regOut0));

writeRelayOutputs();
mutex.unlock();
}

We first check the state of the club room power, which tells us what value to use for the first bit of the output register. If the power timer is active, we have to invert the power state, as we want to write the current power state, not the future state that is stored in the power state Boolean.

If the club room's status switch is in the off position, then the state of the lock switch determines the final color. With the club room locked, we trigger the red relay, otherwise we trigger the yellow one. The latter would indicate the intermediate state, where the club room is off but not yet locked.

The use of a mutex here is to ensure that the writing of the I2C device's output register—as well as updating the local register variable—is done in a synchronized manner:

    else { 
Club::log(LOG_INFO, string("ClubUpdater: New lights, clubstatus on."));

mutex.lock();
string state = (Club::powerOn) ? "on" : "off";
if (powerTimerActive) {
Club::log(LOG_DEBUG, string("ClubUpdater: Power timer active, inverting power state from: ") + state);
regOut0 = !Club::powerOn; // Take the inverse of what the timer callback will set.
}
else {
Club::log(LOG_DEBUG, string("ClubUpdater: Power timer not active, using current power state: ") + state);
regOut0 = Club::powerOn; // Use the current power state value.
}

if (Club::clubLocked) {
Club::log(LOG_INFO, string("ClubUpdater: Yellow & Red on."));
regOut0 |= (1UL << RELAY_YELLOW);
regOut0 |= (1UL << RELAY_RED);
}
else {
Club::log(LOG_INFO, string("ClubUpdater: Green on."));
regOut0 |= (1UL << RELAY_GREEN);
}

Club::log(LOG_DEBUG, "ClubUpdater: Changing output register to: 0x" + NumberFormatter::formatHex(regOut0));

writeRelayOutputs();
mutex.unlock();
}
}

If the club room's status switch is set to on, we get two other color options, with green being the usual one, which sees both the club room unlocked and the status switch enabled. If, however, the latter is on but the room is locked, we would get yellow and red.

After finishing the new contents of the output register, we always use the writeRelayOutputs() function to write our local version to the remote device, thus triggering the new relay state:

void ClubUpdater::writeRelayOutputs() {
wiringPiI2CWriteReg8(i2cHandle, REG_OUTPUT_PORT0, regOut0);

Club::log(LOG_DEBUG, "ClubUpdater: Finished writing relay outputs with: 0x"
+ NumberFormatter::formatHex(regOut0));
}

This function is very simple, and uses WiringPi's I2C API to write a single 8-bit value to the connected device's output register. We also log the written value here:

   void ClubUpdater::setPowerState(Timer &t) {
Club::log(LOG_INFO, string("ClubUpdater: setPowerState called."));

mutex.lock();
if (Club::powerOn) { regOut0 |= (1UL << RELAY_POWER); }
else { regOut0 &= ~(1UL << RELAY_POWER); }

Club::log(LOG_DEBUG, "ClubUpdater: Writing relay with: 0x" + NumberFormatter::formatHex(regOut0));

writeRelayOutputs();

powerTimerActive = false;
mutex.unlock();
}

In this function, we set the club room power state to whatever value its Boolean variable contains. We use the same mutex as we used when updating the club room status colors. However, we do not create the contents of the output register from scratch here, instead opting to toggle the first bit in its variable.

After toggling this bit, we write to the remote device as usual, which will cause the power in the club room to toggle state.

Next, we look at the static Club class, starting with the first function we call to initialize it:

bool Club::start(bool relayactive, uint8_t relayaddress, string topic) {
Club::log(LOG_INFO, "Club: starting up...");

relayActive = relayactive;
relayAddress = relayaddress;
mqttTopic = topic;

wiringPiSetup();

Club::log(LOG_INFO, "Club: Finished wiringPi setup.");

pinMode(0, INPUT);
pinMode(7, INPUT);
pullUpDnControl(0, PUD_DOWN);
pullUpDnControl(7, PUD_DOWN);
clubLocked = digitalRead(0);
clubOff = !digitalRead(7);

previousLockValue = clubLocked;
previousStatusValue = clubOff;

Club::log(LOG_INFO, "Club: Finished configuring pins.");

wiringPiISR(0, INT_EDGE_BOTH, &lockISRCallback);
wiringPiISR(7, INT_EDGE_BOTH, &statusISRCallback);

Club::log(LOG_INFO, "Club: Configured interrupts.");

running = true;
updateThread.start(updater);

Club::log(LOG_INFO, "Club: Started update thread.");

return true;
}

With this function, we start the entire club monitoring system, as we saw earlier in the application entry point. It accepts a few parameters, allowing us to turn the relay functionality on or off, the relay's I2C address (if using a relay), and the MQTT topic on which to publish changes to the club room status.

After setting the values for member variables using those parameters, we initialize the WiringPi framework. There are a number of different initialization functions offered by WiringPi, which basically differ in how one can access the GPIO pins.

The wiringPiSetup() function we use here is generally the most convenient one to use, as it will use virtual pin numbers that map to the underlying Broadcom SoC pins. The main advantage of the WiringPi numbering is that it remains constant between different revisions of the Raspberry Pi SBCs.

With the use of either Broadcom (BCM) numbers or the physical position of the pins in the header on the SBC's circuit board, we risk that this changes between board revisions, but the WiringPi numbering scheme can compensate for this.

For our purposes, we use the following pins on the SBC:

Lock switch

Status switch

BCM

17

4

Physical position

11

7

WiringPi

0

7

 

After initializing the WiringPi library, we set the desired pin mode, making both of our pins into inputs. We then enable a pull-down on each of these pins. This enables a built-in pull-down resistor in the SoC, which will always try to pull the input signal low (referenced to ground). Whether or not one needs a pull-down or pull-up resistor enabled for an input (or output) pin depends on the circumstances, especially the connected circuit.

It's important to look at the behavior of the connected circuit; if the connected circuit has a tendency to "float" the value on the line, this would cause undesirable behavior on the input pin, with the value randomly changing. By pulling the line either low or high, we can be certain that what we read on the pin is not just noise.

With the mode set on each of our pins, we read out the values on them for the first time, which allows us to run the update function from the ClubUpdater class with the current values in a moment. Before we do that, however, we first register our interrupt methods for both pins.

An interrupt handler is little more than a callback that gets called whenever the specified event occurs on the specified pin. The WiringPi ISR function accepts the pin number, the type of event, and a reference to the handler function we wish to use. For the event type we picked here, we will have our interrupt handler triggered every time the value on the input pin goes from high to low, or the other way around. This means that it will be triggered when the connected switch goes from on to off, or off to on.

Finally, we started the update thread by using the ClubUpdater class instance and pushing it into its own thread:

void Club::stop() {
running = false;
}

Calling this function will allow the loop in the run() function of ClubUpdater to end, which will terminate the thread it runs in, allowing the rest of the application to safely shut down as well:

void Club::lockISRCallback() {
clubLocked = digitalRead(0);
lockChanged = true;

clubChanged = true;
clubCnd.signal();
}


void Club::statusISRCallback() {
clubOff = !digitalRead(7);
statusChanged = true;

clubChanged = true;
clubCnd.signal();
}

Both of our interrupt handlers are pretty simple. When the OS receives the interrupt, it triggers the respective handler, which results in them reading the current value of the input pin, inverting the value as needed. The statusChanged or lockChanged variable is set to true to indicate to the update function which of the interrupts got triggered.

We do the same for the clubChanged Boolean variable before signaling the condition variable on which the run loop of ClubUpdate is waiting.

The last part of this class is the logging function:

void Club::log(Log_level level, string msg) {
logMutex.lock();
switch (level) {
case LOG_FATAL: {
cerr << "FATAL:t" << msg << endl;
string message = string("ClubStatus FATAL: ") + msg;
if (mqtt) {
mqtt->sendMessage("/log/fatal", message);
}

break;
}
case LOG_ERROR: {
cerr << "ERROR:t" << msg << endl;
string message = string("ClubStatus ERROR: ") + msg;
if (mqtt) {
mqtt->sendMessage("/log/error", message);
}

break;
}
case LOG_WARNING: {
cerr << "WARNING:t" << msg << endl;
string message = string("ClubStatus WARNING: ") + msg;
if (mqtt) {
mqtt->sendMessage("/log/warning", message);
}

break;
}
case LOG_INFO: {
cout << "INFO: t" << msg << endl;
string message = string("ClubStatus INFO: ") + msg;
if (mqtt) {
mqtt->sendMessage("/log/info", message);
}

break;
}
case LOG_DEBUG: {
cout << "DEBUG:t" << msg << endl;
string message = string("ClubStatus DEBUG: ") + msg;
if (mqtt) {
mqtt->sendMessage("/log/debug", message);
}

break;
}
default:
break;
}

logMutex.unlock();
}

We use another mutex here to synchronize the log outputs in the system log (or console) and to prevent concurrent access to the MQTT class when different parts of the application call this function simultaneously. As we will see in a moment, this logging function is used in other classes as well.

With this logging function, we can log both locally (system log) and remotely using MQTT.

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

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