WisBlock RAK5005-0 + RAK4631 + RAK13002 packet optimisation

Hi. I have recently installed solar power such that we’re essentially off the grid. I have 3 points in the system that I monitor via 3x 240v AC relays. These are (i) Grid failure, (ii) Grid supervision (via a “loss of mains relay” - a very clever gadget that monitors df/dt on the grid), and (iii) Export level exceeded. When the associated mains voltage fails, the relay(s) drop out, and put an earth on pins WB_IO1, WB_IO2, WB_IO3. of a RAK13002 IO Adaptor. The pins are initially set high. The RAK4630 reads the “0” on the pins.

I collect this date and store it in bits 0,1,2 of a byte “currentState”. The integer value of the byte represents a particular combination of all the permutations possible on the system., eg the grid can stay up but the exports can go to zero. The grid can drop off (so no exports) but the solar syatem can stay live to provide a UPS supply to essential services eg pumps, broadband, electric fence, etc etc.

The system doesn’t fail that often, but I want to monitor how its going; there may be several months between failures. The relays fire within milli-seconds of each other, so monitoring them at, say 60 second intervals is too slow. I’m monitoring then at 1-second intervals. Over 6 months, I will be utilising a lot of bandwidith/transmission time for no outcome.

I send the information (encoded byte) to a RAK7249 gateway server. A RPi MQTT Client subscribes to the encoded byte message from the server. It then decodes that message according ti the permutation/combination of alarms that it represents, and this information in terms of the actions of the individual realys is displayed on a dashboard, along with other farm information, eg water levels.

So at the RAK4630 node site, I measure the “currentState” and compare it to the “lastState” or previous state. If there is no change I do not transmit a payload. So, essentially, the loop on the node is wizzing around for 6 months, say, with no message transmitted. That minimises the transmission time to a sniff once every 6 months or so. Some times the system will fail 6 times in a year!!

I have posted the code below. You will see that I do the WB_IO1, WB_IO2, WB_IO3 pin setup in “void setup()”. There is no “void loop()” as such. I read the pins in “void send_lora_frame(void)”. I then check “if (currentState != lastState)” and if it is the same, them I skip “lmh_send(&m_lora_app_data, g_CurrentConfirm);” and re-read the pins 1-second later.

However, the “Serial.print” monitor (which wont be connected once the system is operational) tells me everysecond “Sending frame now …”.

So my question is -

(i) is this coding strategy the correct way to go about this?

(ii) am I controlling the right loop or should this take place in the “void loop()”?

(iii) Should I care about the “Sending frame now …” message since there will be no monitor?

(iv) will the node still be “joined” at the 6-month point or will it have died?

Many thanks.

Here’s the code -

// RAK4631 node 5 (main garage) solar state
// WisBlock RAK4631 + RAK13002 IO adaptor
// This code reads 3 GPIO pins on the RAK13002 which are connected to 3 relays.  The pins are held high until such time
// the realy is de-energised and drops out putting an earth (low) on the pin.  The state of the 3 pins is continuously read 
// in the loop and each bit in the lower half of a byte variable is set according to the state of each realy, hi/low, 0/1.
// The value of the byte represents the permutations and combinations of the states of the 3 relays.  

// This data is sent from this node (5) to the lora gateway (Barn) and is subscribed to by the RPi Python MQTT client in the
// network room for display.
// The three pins represent the state of the solar installation -
//    Grid failure
//    Grid supervision activated
//    Export level exceeded
// Any change in the value of the byte causes the display associated with the MQTT to be refreshed.
// Uses OTAA.
// https://github.com/RAKWireless/WisBlock/blob/master/examples/RAK4630/communications/LoRa/LoRaWAN/LoRaWAN_OTAA_ABP/LoRaWAN_OTAA_ABP.ino
// For RAK13002, the accessible GPIO pin assignments are defined as follows in the Arduino IDE:

// WB_IO1 for IO1, GPIO1 pin
// WB_IO2 for IO2, GPIO2 pin
// WB_IO3 for IO3, GPIO3 pin
// WB_IO4 for IO4, GPIO4 pin
// WB_IO5 for IO5, GPIO5 pin
// WB_IO6 for IO6, GPIO6 pin
// WB_SW1 for SW1 pin
// WB_A0  for AIN0, ADC Input pin
// WB_A1  for AIN1, ADC Input pin

// ApplicationID = 5 (Solar State)

// devEUI[8]  = {not shown};  
// appEUI[8]  = {not shown};                           
// appKEY[16] = {not shown};

#include <Arduino.h>
#include <LoRaWan-RAK4630.h>                                                                             //http://librarymanager/All#SX126x
#include <SPI.h>
bool doOTAA = true;                                                                                      // OTAA is used by default.
#define SCHED_MAX_EVENT_DATA_SIZE APP_TIMER_SCHED_EVENT_DATA_SIZE                                        // Maximum size of scheduler events.
#define SCHED_QUEUE_SIZE 60										                                                           // Maximum number of events in the scheduler queue.
#define LORAWAN_DATERATE DR_0									                                                           // LoRaMac datarates definition, from DR_0 to DR_5
#define LORAWAN_TX_POWER TX_POWER_5							                                                         // LoRaMac tx power definition, from TX_POWER_0 to TX_POWER_15
#define JOINREQ_NBTRIALS 3										                                                           // Number of trials for the join request.
DeviceClass_t g_CurrentClass = CLASS_A;					                                                         // class definition
LoRaMacRegion_t g_CurrentRegion = LORAMAC_REGION_AU915;                                                  // Region:AU915
lmh_confirm g_CurrentConfirm = LMH_UNCONFIRMED_MSG;				                                               // confirm/unconfirm packet definition
uint8_t gAppPort = LORAWAN_APP_PORT;							                                                       // data port

// brief Structure containing LoRaWan parameters, needed for lmh_init()

// Foward declaration
static void lorawan_has_joined_handler(void);
static void lorawan_join_failed_handler(void);
static void lorawan_rx_handler(lmh_app_data_t *app_data);
static void lorawan_confirm_class_handler(DeviceClass_t Class);
static void send_lora_frame(void);

// Structure containing LoRaWan callback functions, needed for lmh_init()
static lmh_callback_t g_lora_callbacks = {BoardGetBatteryLevel, BoardGetUniqueId, BoardGetRandomSeed, lorawan_rx_handler, lorawan_has_joined_handler, lorawan_confirm_class_handler, lorawan_join_failed_handler};

// OTAA keys (keys are MSB)
uint8_t nodeDeviceEUI[8] = {not shown};
uint8_t nodeAppEUI[8]    = {not shown};
uint8_t nodeAppKey[16]   = {not shown};

// Private defination
#define LORAWAN_APP_DATA_BUFF_SIZE 64                                                                     // buffer size of the data to be transmitted.
#define LORAWAN_APP_INTERVAL 1000                                                                         // Defines for user timer, the application data transmission interval. 1s, value in [ms].
static uint8_t m_lora_app_data_buffer[LORAWAN_APP_DATA_BUFF_SIZE];                                        // Lora user application data buffer.
static lmh_app_data_t m_lora_app_data = {m_lora_app_data_buffer, 0, 0, 0, 0};                             // Lora user application data structure.
TimerEvent_t appTimer;
static uint32_t timers_init(void);
static uint32_t count = 0;
static uint32_t count_fail = 0;
const int RELAY1_PIN = WB_IO1;                                                                  
const int RELAY2_PIN = WB_IO2;                                                               
const int RELAY3_PIN = WB_IO3;
uint8_t currentState = 0;
uint8_t lastState = 0;

void setup()
  pinMode(RELAY1_PIN, INPUT_PULLUP);                                          // configure pin for input with pull up

  time_t timeout = millis();                                                  // Initialize Serial for debug output
  while (!Serial)
    if ((millis() - timeout) < 5000)

  lora_rak4630_init();                                                        // Initialize LoRa chip.
  Serial.println("Welcome to RAK4630 LoRaWan!!!");
  Serial.println("Type: OTAA");
  Serial.println("Region: AU915");
  uint32_t err_code;                                                         //creat a user timer to send data to server period
  err_code = timers_init();
  if (err_code != 0)
    Serial.printf("timers_init failed - %d\n", err_code);
    lmh_setDevEui(nodeDeviceEUI);                                              // Setup the EUIs and Keys
    err_code = lmh_init(&g_lora_callbacks, g_lora_param_init, doOTAA, g_CurrentClass, g_CurrentRegion);   // Initialize LoRaWan
  if (err_code != 0)
    Serial.printf("lmh_init failed - %d\n", err_code);
  lmh_join();                           // Start Join procedure

void loop()

void lorawan_has_joined_handler(void)                                                    // LoRa function for handling HasJoined event.
  Serial.println("OTAA Mode, Network Joined!");
  lmh_error_status ret = lmh_class_request(g_CurrentClass);
  if (ret == LMH_SUCCESS)
    TimerSetValue(&appTimer, LORAWAN_APP_INTERVAL);

static void lorawan_join_failed_handler(void)                                           // LoRa function for handling OTAA join failed
  Serial.println("OTAA join failed!");
  Serial.println("Check your EUI's and Keys's!");
  Serial.println("Check if a Gateway is in range!");

void lorawan_rx_handler(lmh_app_data_t *app_data)                                         // Function for handling LoRaWan received data from Gateway [in] app_data  Pointer to rx data
  Serial.printf("LoRa Packet received on port %d, size:%d, rssi:%d, snr:%d, data:%s\n", app_data->port, app_data->buffsize, app_data->rssi, app_data->snr, app_data->buffer);

void lorawan_confirm_class_handler(DeviceClass_t Class)
  Serial.printf("switch to class %c done\n", "ABC"[Class]);  
  m_lora_app_data.buffsize = 0;                                                           // Informs the server that switch has occurred ASAP
  m_lora_app_data.port = gAppPort;
  lmh_send(&m_lora_app_data, g_CurrentConfirm);

void send_lora_frame(void)
  if (lmh_join_status_get() != LMH_SET)
    //Not joined, try again later
  memset(m_lora_app_data.buffer, 0, LORAWAN_APP_DATA_BUFF_SIZE);
  m_lora_app_data.port = gAppPort;

  currentState = 0;
  // LOW = relay de-energised, HIGH = energised
  if (digitalRead(RELAY1_PIN)) {bitSet(currentState, 0);}
  if (digitalRead(RELAY2_PIN)) {bitSet(currentState, 1);}
  if (digitalRead(RELAY3_PIN)) {bitSet(currentState, 2);}
  if (currentState != lastState)
    m_lora_app_data.buffer[0] = 1;
    m_lora_app_data.buffer[1] = currentState;
    m_lora_app_data.buffsize  = 2;
    lmh_error_status error = lmh_send(&m_lora_app_data, g_CurrentConfirm);
    if (error == LMH_SUCCESS)
      Serial.printf("lmh_send ok count %d\n", count);
      Serial.printf("lmh_send fail count %d\n", count_fail);
  lastState = currentState;

void tx_lora_periodic_handler(void)                                                       // Function for handling user timerout event.
  TimerSetValue(&appTimer, LORAWAN_APP_INTERVAL);
  Serial.println("Sending frame now...");

uint32_t timers_init(void)                                                               // Function for the Timer initialization. Initializes the timer module. This creates and starts application timers.
  TimerInit(&appTimer, tx_lora_periodic_handler);
  return 0;
}type or paste code here

Hi John,

The Sending frame now... message is coming from the timer callback tx_lora_periodic_handler which is triggered every 1 second in your code. It doesn’t mean that it sends the data because tx_lora_periodic_handler is calling send_lora_frame where you actually check if a change has occured.
Remove the line

Serial.println("Sending frame now...");

from tx_lora_periodic_handler and you will only get a debug output if the firmware is really sending a packet.