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 [...]
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 ];
}
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...