CocoLoRa – an ATtiny based LoRa node

Submitted by oliver on Sat, 09/17/2016 - 10:00
Not quite your everyday coconut...

Not quite your everyday coconut...

Concept

I started this project to show that a low power LoRa node with a RN2483 module and a sensor could be built with a fairly simple microcontroller with low pin count (such as an ATtiny 85) and a handful of simple, preferably cheap, components. It is possible because the RN2483 module has its own microcontroller (a Microchip PIC) built in which runs the LoRa stack. The host microcontroller just needs to send some simple commands to the module over a serial connection.

Currently, the project is just one thing (one coconut) in the Internet of Things. It measures the temperature in its environment and transmits it, along with its current supply voltage, to The Things Network. Data is finally received, stored and displayed graphically on ThingSpeak. In the end, the project aims to demonstrate a full "loop" with at least two things (coconuts) connected: The Nuts Network. (Also see: Smart Coconuts for Stupid Cities by the Center for Alternative Coconut Research.)

Code

I wrote the following code in Arduino IDE and programmed the ATtiny using an Arduino Uno as ISP. The sketch currently uses 5114 bytes (62%) of the ATtiny's 8192 bytes program storage space. 132 bytes are used for global variables.

Head

Include libraries Software Serial (the ATtiny has no hardware serial) and avr.sleep, and define some variables:

/**
 * ATtiny / RN2483 LoRa node
 * transmits temperature (from a TMP36 sensor) and supply voltage
 * 20.07.2016 - 01.09.2016 - oliver walkhoff
 */


// settings
#define baud 9600    // 9600 | 19200 | 57600
#define vcc_low 2000 // low voltage limit for transmitting (ATtiny 85V: 1.8V, RN2483: 2.1V)
byte last = 0; // type of data transmitted last. 0: temperature, 1: voltage

// pins on the ATtiny
#define power 3      // supply for RN2483 and TMP36 (ic pin 2)
#define temp A2      // TMP36 output (ic pin 3)
#define TX 1         // serial TX (ic pin 6)
#define RX 0         // serial RX (ic pin 5, currently not used)

// needed for sleep
#include <avr/sleep.h>
#include <avr/wdt.h>
#ifndef cbi
  #define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
#ifndef sbi
  #define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif
volatile boolean f_wdt = 1; // flag is set when a watchdog timeout occurs

// needed for serial
#include <SoftwareSerial.h>
SoftwareSerial mySerial(RX, TX); // RX, TX

Setup

Before the ATtiny can use Software Serial, the correct value for OSCCAL needs to be found.

Credentials for joining The Things Network (OTAA or ABP) will be provided once you have set up your application and registered your device in console.thethingsnetwork.org.

void setup() {

  // set up watchdog timer
  setup_watchdog(8); // '8' means approximately 4 seconds sleep per cycle

  // calibrate internal oscillator of this ATtiny
  OSCCAL = 0x95; // find correct value using "tuning ATtiny oscillator"

  delay(2000); // wait until the module has started

  // trigger module autobaud sequence to set baud rate
  autoBaud(baud);

  // (alternative) open serial comm at RN2483 module's default baud rate of 57600
  // mySerial.begin(57600);
  // delay(100);

  // module: configure GPIO pin for LED
  mySerial.print(F("sys set pinmode GPIO7 digout\r\n")); // since firmware 1.0.1
  delay(10);

  // module: say hello
  flashLED();
  flashLED();
  flashLED();

  // module: join The Things Network (OTAA)
  mySerial.print(F("mac set appeui <our ttn appeui>\r\n")); // from console.thethingsnetwork.org
  mySerial.print(F("mac set deveui <our ttn deveui>\r\n"));
  mySerial.print(F("mac set appkey <our ttn appkey>\r\n"));
  mySerial.print(F("mac save\r\n"));
  delay(5000);
  mySerial.print(F("mac join otaa\r\n"));
  delay(5000);

  // (alternative) module: join The Things Network (ABP)
  /*
  mySerial.print(F("mac set appeui <our ttn appeui>\r\n")); // from console.thethingsnetwork.org
  mySerial.print(F("mac set devaddr <our ttn devaddr>\r\n"));
  mySerial.print(F("mac set nwkskey <our ttn nwkskey>\r\n"));
  mySerial.print(F("\r\n")); // TODO ?
  mySerial.print(F("mac save\r\n"));
  delay(1000);
  mySerial.print(F("mac join abp\r\n"));
  delay(1000);
  */

}

Loop

Every time the ATtiny wakes up, it calculates its supply voltage (mV), gets a temperature reading (degrees C, multiplied with 10 so we get an integer) and sends either (4 bytes, depending on what was transmitted the last time) to the RN2483. It then waits and goes to sleep again. (We will add RX to the serial communication in the next version, which will eliminate the fixed delay. Also, voltage and temperature will be transmitted at once. Both changes will hopefully prolong battery life.)

void loop() {

  if (f_wdt == 1) {  // wait for timed out watchdog
    f_wdt = 0;       // reset flag

    /* turn on supply for external components */
    pinMode(power, OUTPUT);
    digitalWrite(power, HIGH);
    delay(100);
    
    /* get voltage */
    long volt = getVcc();
    // int volt = getVcc(); // (alternative) using integer
    
    /* get temperature */
    int temp_reading = analogRead(temp); // 0-1023
    // long temp_mv = temp_reading * 3300L / 1023; // 0-3300 in mV // (alternative) approximate, not using actual vcc
    long temp_mv = temp_reading * volt / 1023; // 0-3300 in mV
    int temp_c = temp_mv - 500; // TMP36: use 500 mV offset. 0: -500 | 1000: +500
    // need to divide by 10.0 later in order to get degrees C with one decimal

    /*
    // for debugging
    mySerial.print("volt: ");
    mySerial.print(volt);
    mySerial.print(" temp_reading: ");
    mySerial.print(temp_reading);
    mySerial.print(" temp_mv: ");
    mySerial.print(temp_mv);
    mySerial.print(" temp_c: ");
    mySerial.println(temp_c);
    */

    /* module: wake up */
    // only do this when battery voltage is high enough
    if (volt > vcc_low) {
      autoBaud(baud);

      /* module: flash LED */
      flashLED();
  
      /* module: transmit data */
      // convert int to char for transmission
      // we need 2 bytes for each int value
      char hex[5];
  
      if (last == 0) {
        // transmit voltage
        sprintf(hex, "%04X", volt); // format as 4 digit hex with leading zeroes
        last = 1;
      } else {
        // transmit temperature
        sprintf(hex, "%04X", temp_c);
        last = 0;
      }
      // TODO: maybe use strcpy and then strcat to concatenate before mySerial.print
      mySerial.print(F("mac tx uncnf 1 "));
      mySerial.print(hex);
      mySerial.print(F("\r\n"));
      delay(2000); // must wait otherwise module will not go to sleep!
      // TODO: instead parse response from module using RX...

    } // end if volt > vcc_low

    /* module: sleep */
    mySerial.print(F("sys sleep 1800000\r\n")); // 30 min
    
    /* turn off supply for external components */
    digitalWrite(power, LOW);
    pinMode(power, INPUT);
    
    /* sleep */
    for (int i = 0; i < 7; i++) { // sleep 7 cycles (of length set in setup_watchdog())
      system_sleep(); // sleep once for the duration set with setup_watchdog()
    }
    sbi(ADCSRA,ADEN); // switch analog to digital converter ON after wake up

  }
  
}

Helper function: get rail voltage

This function finds the supply voltage Vcc by comparing the ADC value to the ATtiny's internal 1.1 V reference voltage. This method is described on the Arduino Forum and further explained on josh.com. The actual value of the internal reference voltage can be slightly off 1.1 V due to variations in manufacturing. I measured Vcc with a voltmeter and found the reference voltage of this particular ATtiny to be 1.14115 V.

/* get rail voltage */
long getVcc() {
  // read 1.1V reference against AVcc
  // set the reference to Vcc and the measurement to the internal 1.1V reference
  #if defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
    ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
  #elif defined (__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
    ADMUX = _BV(MUX5) | _BV(MUX0);
  #elif defined (__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)
    ADMUX = _BV(MUX3) | _BV(MUX2);
  #else
    ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
  #endif

  delay(2); // wait for Vref to settle
  ADCSRA |= _BV(ADSC); // start conversion
  while (bit_is_set(ADCSRA, ADSC)); // measuring

  uint8_t low  = ADCL; // must read ADCL first - it then locks ADCH
  uint8_t high = ADCH; // unlocks both

  long result = (high << 8) | low;

  // result = 1126400L / result; // calculate Vcc (in mV); 1126400 = 1.1*1024*1000 // generally
  result = 1168538L / result; // calculate Vcc (in mV); 1168538 = 1.14115*1024*1000 // for this ATtiny
                         
  return result;
  // return (int) result; // (alternative) using integer
}

Helper functions: sleep and miscellaneous

We use the ATtiny's sleep mode PWR_DOWN which should minimize its power consumption during sleep.

flashLED flashes an LED which is connected to one of the RN2483's GPIO pins. The function was only really used in a development version to test serial communication with the RN2483.

/* enter system into sleep state */
// system wakes up when watchdog is timed out
void system_sleep() {
  cbi(ADCSRA, ADEN);                   // switch analog to digital converter OFF
 
  set_sleep_mode(SLEEP_MODE_PWR_DOWN); // sleep mode is set here
  sleep_enable();
 
  sleep_mode();                        // now enter sleep mode
 
  sleep_disable();                     // system will continue from here when watchdog timed out
  // sbi(ADCSRA, ADEN);                // switch analog to digital converter ON
}


/* set up watchdog timeout */
// 0: 16ms, 1: 32ms, 2: 64ms, 3: 128ms, 4: 250ms,
// 5: 500ms, 6: 1 sec, 7: 2 sec, 8: 4 sec, 9: 8 sec
void setup_watchdog(int ii) {
  byte bb;
  int ww;
  if (ii > 9 ) ii = 9;
  bb = ii & 7;
  if (ii > 7) bb |= (1<<5);
  bb |= (1<<WDCE);
  ww = bb;
  
  MCUSR &= ~(1<<WDRF);                  // clear the reset flag
  WDTCR |= (1<<WDCE) | (1<<WDE);        // start timed sequence
  WDTCR = bb;                           // set new watchdog timeout value
  WDTCR |= _BV(WDIE);                   // enable the watchdog interrupt
}


/* watchdog interrupt service */
// this is executed when watchdog timed out
ISR(WDT_vect) {
  f_wdt = 1;  // set global flag
}


/* module auto-baud sequence */
// used to wake module (see RN2483 Command Reference, section 1.4)
void autoBaud(int baudRate) {
  pinMode(TX, OUTPUT);
  digitalWrite(TX, LOW);
  delay(5);
  digitalWrite(TX, HIGH);
  mySerial.begin(9600);
  mySerial.write(0x55);
}


/* module flash LED */
void flashLED() {
  mySerial.print(F("sys set pindig GPIO7 1\r\n"));
  delay(100);
  mySerial.print(F("sys set pindig GPIO7 0\r\n"));
  delay(400);
}

Hardware

Components

  • ATtiny 85V microcontroller (Atmel, part ATtiny 85V-10PU), operates at Vcc down to 1.8V
  • RN2483 LoRa module (Microchip, part RN2483-I/RM101)
  • TMP36 temperature sensor (Analog Devices, part TMP36GT9Z)
  • Antenna

868 MHz antenna

I wound a spiral antenna from copper transformer wire:

  • coil length: 13mm
  • stub length: 4.5mm
  • coil diameter: 5.5mm
  • windings:
  • wire diameter:

A straight wire (83mm) could also be used as an antenna, but the spiral uses less height (in the coconut), and it looks great too. We still need to test which one performs better...

Board

I used perfboard (FR-4), cut it to a circle with a jigsaw, and smoothed the edges (!) with a file and sanding paper. Traces were cut with the help of a 3mm drill bit.

Battery

There are two AAA batteries, in a plastic holder. Battery life is currently about [...]

Under the hood

Under the hood

Back-End

This section describes a workaround which I created in 2016 to move data from The Things Network back-end to ThingSpeak. It won't be needed any longer because ThingSpeak can now natively subscribe to MQTT topics. I will update this as soon as I have migrated this application to the new console.thethingsnetwork.org.

Legacy: Node-RED flow on IBM Bluemix

The Things Network provides a MQTT broker at staging.thethingsnetwork.org. A Node-RED flow (cloud hosted at IBM Bluemix) is subscribed to messages from our node. The flow creates a HTTP GET request to our channel at ThingSpeak.

This code lives on IBM Bluemix (node-red): http://cocolora.eu-gb.mybluemix.net

Decode function

Since the nut doesn't transmit any key that would tell us which value is voltage and which is temperature, I simply use the fact that voltage is always greater than 1000. Not ideal but it works:

// decode base64
var bytes = new Buffer(msg.payload.payload, 'base64'); // payload as hex

// get decimal value
var val = (bytes[0] << 8) | bytes[1];

// return output
if (val > 1000) { // voltage
    msg.payload = val;
    return [ msg, null ];
} else { // temperature
    msg.payload = val / 10.0;
    return [ null, msg ];
}
Flow in Node-RED. The appeui is our MQTT topic.

Flow in Node-RED. The appeui is our MQTT topic.

Live data on ThingSpeak channel ID 157060:

To dos

  • Add RX to receive responses from the RN2483. Currently serial is just TX, unidirectional from the ATtiny to the module, with fixed delays after each command. With RX, communication will be shorter and sleep will be longer, so using less power
  • Check the watchdog timeout setting in the RN2483 (maybe the module could go to sleep more quickly?)
  • Reset the module if needed / re-join TTN if needed
  • Add a humidity sensor
  • Add coconuts...