Zvonko Bockaj
Published © GPL3+

nRF52832 Helium Network Garage Notifier

Get open/closed status of your garage (or any other) door, over Helium Network.

AdvancedShowcase (no instructions)510
nRF52832 Helium Network Garage Notifier

Things used in this project

Hardware components

nRF52 Development Kit
Nordic Semiconductor nRF52 Development Kit
×1

Software apps and online services

KiCad
KiCad
Arduino IDE
Arduino IDE

Story

Read more

Custom parts and enclosures

Hammond RL6105BK housing

Off-the-shelf housing that I used for this project. PCB outline adjusted exactly for this model.

Hammond RL6105BK step model

Schematics

KiCad custom library

KiCad schematic export

KiCad project

Code

Arduino code

Arduino
Create new sketch and copy the code in there.
/*******************************************************************************
 * Copyright (c) 2015 Thomas Telkamp and Matthijs Kooijman
 * Copyright (c) 2018 Terry Moore, MCCI
 *
 * Permission is hereby granted, free of charge, to anyone
 * obtaining a copy of this document and accompanying files,
 * to do whatever they want with them without any restriction,
 * including, but not limited to, copying, modification and redistribution.
 * NO WARRANTY OF ANY KIND IS PROVIDED.
 *
 * This uses OTAA (Over-the-air activation), where where a DevEUI and
 * application key is configured, which are used in an over-the-air
 * activation procedure where a DevAddr and session keys are
 * assigned/generated for use with all further communication.
 *
 * Note: LoRaWAN per sub-band duty-cycle limitation is enforced (1% in
 * g1, 0.1% in g2), but not the TTN fair usage policy (which is probably
 * violated by this sketch when left running for longer)!
 * To use this sketch, first register your application and device with
 * the things network, to set or generate an AppEUI, DevEUI and AppKey.
 * Multiple devices can use the same AppEUI, but each device has its own
 * DevEUI and AppKey.
 *
 * Do not forget to define the radio type correctly in
 * arduino-lmic/project_config/lmic_project_config.h or from your BOARDS.txt.
 *
 *******************************************************************************/
 
#include <bluefruit.h>
#include <lmic.h>
#include <hal/hal.h>
#include <SPI.h>

#include <CayenneLPP.h>

#define GPIO_IN_MR 27
#define GPIO_ADC_VCC 30

#define LORA_MSG_REPEAT 1

// Init CayenneLPP Payload
CayenneLPP lpp(51);
uint8_t tx_complete = 0xFF;
uint8_t last_status = 0xFF;
uint8_t mr_status = 0xFF;
int used_led = LED_GREEN;
unsigned long start_time = 0;
unsigned long led_millis = 0;
uint8_t repeat_cnt = 0xFF;

static osjob_t sendjob;

 // This EUI must be in little-endian format, so least-significant-byte
 // first. When copying an EUI from Helium Console, reverse the bytes.
 static const u1_t PROGMEM APPEUI[8]={FILL_ME_IN};
 void os_getArtEui (u1_t* buf) { memcpy_P(buf, APPEUI, 8);}
  
 // This should also be in little endian format, see above.
 static const u1_t PROGMEM DEVEUI[8]={FILL_ME_IN};
 void os_getDevEui (u1_t* buf) { memcpy_P(buf, DEVEUI, 8);}
  
// This key should be in big endian format, a key taken from Helium Console can be copied as-is.
static const u1_t PROGMEM APPKEY[16] = {FILL_ME_IN};
void os_getDevKey (u1_t* buf) {  memcpy_P(buf, APPKEY, 16);}

// Pin mapping
const lmic_pinmap lmic_pins = {
    .nss = 11,
    .rxtx = LMIC_UNUSED_PIN,
    .rst = 3,
    .dio = {15, 16, 7},
    .rxtx_rx_active = 0,
    .rssi_cal = 0,
    .spi_freq = 4000000, // set SPI to 4MHz (default is 1MHz)
};

void onEvent (ev_t ev) {
    //Serial.print(os_getTime());
    //Serial.print(": ");
    switch(ev) {
        case EV_SCAN_TIMEOUT:
            //Serial.println(F("EV_SCAN_TIMEOUT"));
            break;
        case EV_BEACON_FOUND:
            //Serial.println(F("EV_BEACON_FOUND"));
            break;
        case EV_BEACON_MISSED:
            //Serial.println(F("EV_BEACON_MISSED"));
            break;
        case EV_BEACON_TRACKED:
            //Serial.println(F("EV_BEACON_TRACKED"));
            break;
        case EV_JOINING:
            //Serial.println(F("EV_JOINING"));
            break;
        case EV_JOINED:
            //Serial.println(F("EV_JOINED"));

            // Disable link check validation (automatically enabled
            // during join, but because slow data rates change max TX
            // size, we don't use it in this example.
            // LMIC_setLinkCheckMode(0); // ZB: actually it seems not needed in latest libraries, disabled for now
            LMIC_setDrTxpow(DR_SF9, 14); // ZB: not officially recommended to change this after join,
                                         // but having bad signal with SF7 due to concrete walls.
                                         // Disable if using in area with strong signal / nearby hotspots
            break;
        /*
        || This event is defined but not used in the code. No
        || point in wasting codespace on it.
        ||
        || case EV_RFU1:
        ||     Serial.println(F("EV_RFU1"));
        ||     break;
        */
        case EV_JOIN_FAILED:
            //Serial.println(F("EV_JOIN_FAILED"));
            break;
        case EV_REJOIN_FAILED:
            //Serial.println(F("EV_REJOIN_FAILED"));
            break;
        case EV_TXCOMPLETE:
            //Serial.println(F("EV_TXCOMPLETE (includes waiting for RX windows)"));
            /*if (LMIC.txrxFlags & TXRX_ACK)
              Serial.println(F("Received ack"));
            if (LMIC.dataLen) {
              Serial.println(F("Received "));
              Serial.println(LMIC.dataLen);
              Serial.println(F(" bytes of payload"));
            }*/
            // set flag to confirm we are done sending
            tx_complete = 1;
            break;
        case EV_LOST_TSYNC:
            //Serial.println(F("EV_LOST_TSYNC"));
            break;
        case EV_RESET:
            //Serial.println(F("EV_RESET"));
            break;
        case EV_RXCOMPLETE:
            // data received in ping slot
            //Serial.println(F("EV_RXCOMPLETE"));
            break;
        case EV_LINK_DEAD:
            //Serial.println(F("EV_LINK_DEAD"));
            break;
        case EV_LINK_ALIVE:
            //Serial.println(F("EV_LINK_ALIVE"));
            break;
        /*
        || This event is defined but not used in the code. No
        || point in wasting codespace on it.
        ||
        || case EV_SCAN_FOUND:
        ||    Serial.println(F("EV_SCAN_FOUND"));
        ||    break;
        */
        case EV_TXSTART:
            //Serial.println(F("EV_TXSTART"));
            break;
        case EV_TXCANCELED:
            //Serial.println(F("EV_TXCANCELED"));
            break;
        case EV_RXSTART:
            /* do not print anything -- it wrecks timing */
            break;
        case EV_JOIN_TXCOMPLETE:
            //Serial.println(F("EV_JOIN_TXCOMPLETE: no JoinAccept"));
            break;
            
        default:
            //Serial.print(F("Unknown event: "));
            //Serial.println((unsigned) ev);
            break;
    }
}

void do_send(osjob_t* j){
    // Check if there is not a current TX/RX job running
    if (LMIC.opmode & (OP_TXRXPEND | OP_POLL | OP_TXDATA)) 
    {
      //Serial.print("LMIC opmode err: ");
      //Serial.println(LMIC.opmode, HEX);     
    }
    else 
    {
      // prepare data
      mr_status = digitalRead(GPIO_IN_MR);
      last_status = mr_status; // potentially override status in main loop

      // save LED to show status during sending
      if (mr_status == 0) used_led = LED_GREEN;
      else used_led = LED_RED;
      
      // Clear Payload
      lpp.reset();
      lpp.addDigitalInput(1, mr_status);
      
      // reset tx_complete flag, to continuously poll lmic until tx is finished
      tx_complete = 0;               

      // Prepare upstream data transmission at the next possible time.
      // transmit on port 1 (the first parameter); you can use any value from 1 to 223 (others are reserved).
      // don't request an ack (the last parameter, if not zero, requests an ack from the network).
      // Remember, acks consume a lot of network resources; don't ask for an ack unless you really need it.
      lmic_tx_error_t ret = LMIC_setTxData2(1, lpp.getBuffer(), lpp.getSize(), 0);

      //Serial.print("LMIC setTxData ret: ");
      //Serial.println(ret, HEX); 

      //Serial.println("Packet queued");
    }
}

void setup() {
  // put your setup code here, to run once:
  //Serial.begin(115200);

  // Uncomment to blocking wait for Serial connection
  // while ( !Serial ) delay(10);

  //Serial.println("GarageNotifier Init");
  //Serial.println("--------------------------");

  Bluefruit.begin();
  Bluefruit.autoConnLed(false);
  //Bluefruit.setTxPower(4);    // Check bluefruit.h for supported values  

  pinMode(GPIO_IN_MR, INPUT);

  ledOff(LED_GREEN);
  ledOff(LED_RED); 

  // ZB: Noticed that SPI pins get left in weird states after RFM95 goes to sleep
  // Setting them as INPUT PD fixes this (didn't dig into how this interacts with SPI lib)
  pinMode(PIN_SPI_MISO, INPUT_PULLDOWN); 
  pinMode(PIN_SPI_MOSI, INPUT_PULLDOWN); 
  pinMode(PIN_SPI_SCK, INPUT_PULLDOWN); 
  
  // ZB: Adafruit uses fixed SPI pins as defined in SPI driver. To work around this, 
  // default Feather variant can be hacked: C:\Users\username\Documents\ArduinoData\packages\adafruit\hardware\nrf52\1.3.0\variants\feather_nrf52832
  // alternative and better approach would be to copy the nrf52832 variant and create your own
  
  // LMIC init
  os_init();
  // Reset the MAC state. Session and pending data transfers will be discarded.
  LMIC_reset();

  // define LMIC_ENABLE_arbitrary_clock_error in lmic_project_config.h if more is needed
  // disabled here, no benefit was visible (defaults to 0.1%)
  // LMIC_setClockError(1 * MAX_CLOCK_ERROR / 100);
  
  LMIC_setLinkCheckMode(0); // Disable link-check mode and ADR, because ADR tends to complicate testing.
  LMIC_setDrTxpow(DR_SF7, 14); // SF7 = shortest time on air. 14 dBm output (max for EU)
  // LMIC_selectSubBand(1); // ZB: Sub band 2 only for US, not relevant in EU (and will result in errors)

  // Start job (sending automatically starts OTAA too)
  do_send(&sendjob); 
}

// helper func
void lora_send_msg (void)
{
  tx_complete = 0; 
  start_time = millis();
  // request new transmission
  os_setCallback(&sendjob, do_send); 
}

void loop() {
  // run LMIC loop continuously until tx is finished
  while (1) 
  {
    os_runloop_once();
    //delay(1);

    // break out if not sending
    if (tx_complete != 0) break;
    
    if ((millis() - led_millis) > 50)
    {
      // use green/red LED as tx status info, until sent
      digitalToggle(used_led);
      led_millis = millis();
    }
    
    if ((millis() - start_time) > 5*60*1000)
    {
      //Serial.println("LMIC timeout!");
      start_time = millis();

      //Serial.print(" crit.jobs.t="); Serial.println(os_queryTimeCriticalJobs(ms2osticks(1000)));

      if (LMIC.opmode & OP_JOINING)
      {
        // if joining, don't reset LMIC
        //Serial.println("Joining, LMIC reset skipped!");
        continue;
      }

      // reset LMIC and send packet, triggers join again
      os_init();
      LMIC_reset();
      LMIC_setLinkCheckMode(0);
      do_send(&sendjob);
    }
  }

  ledOff(LED_GREEN);
  ledOff(LED_RED);
  
  // loop delay when no tx in progress, to save power (goes automatically to sleep in this period)
  delay(1000);
  // run os_getTime so micros() overflow is handled properly by LMIC+HAL in the background
  os_getTime();
  os_runloop_once();
  
  mr_status = digitalRead(GPIO_IN_MR);
  if (last_status != mr_status)
  {   
    // send on MR status change, or periodic send 8h after last packet, 
    // to keep LMIC operating normally
    last_status = mr_status;    

    /*Serial.print("Send. os:");
    Serial.print(os_getTime());
    Serial.print(" micros:");
    Serial.print(micros());

    Serial.print(" op=");  Serial.print(LMIC.opmode, HEX);*/
   
    repeat_cnt = 0;
    lora_send_msg();    
  }
  else if ((millis() - start_time > 1000*30) && (repeat_cnt < LORA_MSG_REPEAT))
  {
    // repeat message for more chance to get through when signal is bad
    // this ends up being less battery wasteful than using ACK and letting SF increase
    repeat_cnt++;
    lora_send_msg(); 
  }
  else if (millis() - start_time > 1000*60*60*8)
  {
    // send periodic message after 8h to keep LMIC operating normally
    // no repeat needed for this
    lora_send_msg(); 
  }
}

lib_hack.zip

C/C++
Files needed to create GarageNotifier variant in Adafruit library. Copy into
C:\Users\your_username\Documents\ArduinoData\packages\adafruit\hardware\nrf52\1.3.0
No preview (download only).

Credits

Zvonko Bockaj

Zvonko Bockaj

2 projects • 3 followers
Embedded developer, hardware and firmware.

Comments