Forest LoRa

Weather sensing in Smokeshire

LoRa What?

  • Long-Range
  • Low-power
  • Wireless
    • 433 and 915MHz bands
  • Low throughput

LoRa Why?

LoRa Why?

  • Long-range
    • >= 1km LoS with basic quarter-wave antenna
    • Many km with better antenna and tuning
  • Low power
  • Unlicensed spectrum in 915MHz band

To build a sensor network...

To build on what I know...

Layers of Complexity

Things to do...

  • Build component hardware (clients and "gateway")
  • Configure and manage network
  • Write network software?
  • Read and process sensor data
  • Transmit and receive data
  • Store data
  • Visualize data

Months of Frustration

Failstorm the First

Failstorm I: I thought I was Ordering...

Adafruit Feather 32u4 RFM95 LoRa Radio - 868 or 915 MHz

Adafruit Feather 32u4 RFM95 LoRa Radio - 868 or 915 MHz

Failstorm I: I actually ordered...

NO MCU

Persevering...

Perservering...

Sparkfun ESP8266 thing

WON'T WORK WITH ADAFRUIT FTDI PROGRAMMER

NOT REALLY ENOUGH FREE PINS ANYWAY

Failstorm, Part Deux

Adafruit Feather 32u4 Basic Proto

Persevering...

 The 32u4 doesn't have a lot of IRQs

 The 32u4 doesn't have a lot of IRQs and the only ones available are on pins 0, 1, 2, 3 which are also the Serial RX/TX and I2C pins. So it's not great because you have to give up one of those pins.

 The 32u4 doesn't have a lot of IRQs and the only ones available are on pins 0, 1, 2, 3

 The 32u4 doesn't have a lot of IRQs and the only ones available are on pins 0, 1, 2, 3 which are also the Serial RX/TX and I2C pins.

(╯°□°)╯︵ ┻━┻

Capitulation

The Ultimate Arsenal

Adafruit Feather M0 Proto

Nice bit of performance, low power, Feather form factor

500mAh LiPo Batteries

Feathers have built-in LiPO charging hardware (nice).

 

Wish I'd bought ones with more capacity.

LoRa Breakout Modules (900Mhz Band)

Less complicated than working with the blasted FeatherWings

I2C Sensors

The MCP9808 is a favorite of mine because low-power (super low-power) and accurate.

 

The BMEs add pressure and humidity but are far less efficient (at least WRT the Arduino libraries I have for them).

Let the Games Begin!

Soldering Party

Everything onto headers...

LoRa Radios Need Antennae

Programming the M0s

Working with LoRa

Working with LoRa

RadioHead Library

Packet Radio for Embedded

RH_RF95 Driver

Low-level driver that supports the Semtech LoRa chip I'm using...

A first test with Adafruit Example Code

// LoRa 9x_TX
// -*- mode: C++ -*-
// Example sketch showing how to create a simple messaging client (transmitter)
// with the RH_RF95 class. RH_RF95 class does not provide for addressing or
// reliability, so you should only use RH_RF95 if you do not need the higher
// level messaging abilities.
// It is designed to work with the other example LoRa9x_RX

#include <SPI.h>
#include <RH_RF95.h>

#define RFM95_CS 10
#define RFM95_RST 9
#define RFM95_INT 2

// Change to 434.0 or other frequency, must match RX's freq!
#define RF95_FREQ 915.0

// Singleton instance of the radio driver
RH_RF95 rf95(RFM95_CS, RFM95_INT);

void setup() 
{
  pinMode(RFM95_RST, OUTPUT);
  digitalWrite(RFM95_RST, HIGH);

  while (!Serial);
  Serial.begin(9600);
  delay(100);

  Serial.println("Arduino LoRa TX Test!");

  // manual reset
  digitalWrite(RFM95_RST, LOW);
  delay(10);
  digitalWrite(RFM95_RST, HIGH);
  delay(10);

  while (!rf95.init()) {
    Serial.println("LoRa radio init failed");
    while (1);
  }
  Serial.println("LoRa radio init OK!");

  // Defaults after init are 434.0MHz, modulation GFSK_Rb250Fd250, +13dbM
  if (!rf95.setFrequency(RF95_FREQ)) {
    Serial.println("setFrequency failed");
    while (1);
  }
  Serial.print("Set Freq to: "); Serial.println(RF95_FREQ);
  
  // Defaults after init are 434.0MHz, 13dBm, Bw = 125 kHz, Cr = 4/5, Sf = 128chips/symbol, CRC on

  // The default transmitter power is 13dBm, using PA_BOOST.
  // If you are using RFM95/96/97/98 modules which uses the PA_BOOST transmitter pin, then 
  // you can set transmitter powers from 5 to 23 dBm:
  rf95.setTxPower(23, false);
}

int16_t packetnum = 0;  // packet counter, we increment per xmission

void loop()
{
  Serial.println("Sending to rf95_server");
  // Send a message to rf95_server
  
  char radiopacket[20] = "Hello World #      ";
  itoa(packetnum++, radiopacket+13, 10);
  Serial.print("Sending "); Serial.println(radiopacket);
  radiopacket[19] = 0;
  
  Serial.println("Sending..."); delay(10);
  rf95.send((uint8_t *)radiopacket, 20);

  Serial.println("Waiting for packet to complete..."); delay(10);
  rf95.waitPacketSent();
  // Now wait for a reply
  uint8_t buf[RH_RF95_MAX_MESSAGE_LEN];
  uint8_t len = sizeof(buf);

  Serial.println("Waiting for reply..."); delay(10);
  if (rf95.waitAvailableTimeout(1000))
  { 
    // Should be a reply message for us now   
    if (rf95.recv(buf, &len))
   {
      Serial.print("Got reply: ");
      Serial.println((char*)buf);
      Serial.print("RSSI: ");
      Serial.println(rf95.lastRssi(), DEC);    
    }
    else
    {
      Serial.println("Receive failed");
    }
  }
  else
  {
    Serial.println("No reply, is there a listener around?");
  }
  delay(1000);
}
// Arduino9x_RX
// -*- mode: C++ -*-
// Example sketch showing how to create a simple messaging client (receiver)
// with the RH_RF95 class. RH_RF95 class does not provide for addressing or
// reliability, so you should only use RH_RF95 if you do not need the higher
// level messaging abilities.
// It is designed to work with the other example Arduino9x_TX

#include <SPI.h>
#include <RH_RF95.h>

#define RFM95_CS 10
#define RFM95_RST 9
#define RFM95_INT 2

// Change to 434.0 or other frequency, must match RX's freq!
#define RF95_FREQ 915.0

// Singleton instance of the radio driver
RH_RF95 rf95(RFM95_CS, RFM95_INT);

// Blinky on receipt
#define LED 13

void setup() 
{
  pinMode(LED, OUTPUT);     
  pinMode(RFM95_RST, OUTPUT);
  digitalWrite(RFM95_RST, HIGH);

  while (!Serial);
  Serial.begin(9600);
  delay(100);

  Serial.println("Arduino LoRa RX Test!");
  
  // manual reset
  digitalWrite(RFM95_RST, LOW);
  delay(10);
  digitalWrite(RFM95_RST, HIGH);
  delay(10);

  while (!rf95.init()) {
    Serial.println("LoRa radio init failed");
    while (1);
  }
  Serial.println("LoRa radio init OK!");

  // Defaults after init are 434.0MHz, modulation GFSK_Rb250Fd250, +13dbM
  if (!rf95.setFrequency(RF95_FREQ)) {
    Serial.println("setFrequency failed");
    while (1);
  }
  Serial.print("Set Freq to: "); Serial.println(RF95_FREQ);

  // Defaults after init are 434.0MHz, 13dBm, Bw = 125 kHz, Cr = 4/5, Sf = 128chips/symbol, CRC on

  // The default transmitter power is 13dBm, using PA_BOOST.
  // If you are using RFM95/96/97/98 modules which uses the PA_BOOST transmitter pin, then 
  // you can set transmitter powers from 5 to 23 dBm:
  rf95.setTxPower(23, false);
}

void loop()
{
  if (rf95.available())
  {
    // Should be a message for us now   
    uint8_t buf[RH_RF95_MAX_MESSAGE_LEN];
    uint8_t len = sizeof(buf);
    
    if (rf95.recv(buf, &len))
    {
      digitalWrite(LED, HIGH);
      RH_RF95::printBuffer("Received: ", buf, len);
      Serial.print("Got: ");
      Serial.println((char*)buf);
       Serial.print("RSSI: ");
      Serial.println(rf95.lastRssi(), DEC);
      
      // Send a reply
      uint8_t data[] = "And hello back to you";
      rf95.send(data, sizeof(data));
      rf95.waitPacketSent();
      Serial.println("Sent a reply");
      digitalWrite(LED, LOW);
    }
    else
    {
      Serial.println("Receive failed");
    }
  }
}

Tx (Transmit)

Rx (Receive)

RadioHead Library

"Manager" Classes

RHReliableDatagram

Helpfully gives me

  • Addressing
  • ACKs
  • Retries
  • Waiting for channel clear
  • You know, network stuff

Client Addressing

  • RHReliableDatagram Addresses are uint8_t
  • Translation: 8-bit, 0-255 available
  • 255 reserved (broadcast address)
  • It sure would be nice not to have to designate client addresses in the firmware...

"Hardware" Addressing

/**
 *  Return most significant 8 bits of the first 32 bits of
 *  this MCU's device ID for use as a device address.
 *  This is for SAMD MCUs only
 */
uint8_t getID() {
  volatile uint32_t val1;
  volatile uint32_t *ptr1 = (volatile uint32_t *)0x0080A00C;
  val1 = *ptr1;
  return val1 >> 24;
}

Client and Server Firmware

Re-sharpening my C++ Stick

Clients: Reading Sensor Data

  • This part is easy
  • Relied on pre-built libraries mostly

Clients: Sending Sensor Data

  • This part requires a little work
  • Data needs to be transmitted as uint8_t byte arrays
  • How to represent data structured from multiple types?
typedef struct {
  uint32_t offset;
  float mcp_temperature;
  float mpl_pressure;
  float mpl_temperature;
  uint8_t deviceID;
  uint8_t capabilities;
} ClientPacket;
ClientPacket packet;

/**
 * Put things in the packet struct...
**/

/**
 * Create a uint8_t array of the same memory
 * size as packet
*/
uint8_t data[sizeof(packet)];

// memcpy all the things (and subsequently transmit)
memcpy(data, &packet, sizeof(packet));

memcopy all the things

/**
 * Init struct...
**/
ClientPacket lastPacket;

/**
 * Copy received byte buffer into it
**/

memcpy(&lastPacket, buf, sizeof(lastPacket));

memcopy all the things

First Round of Firmware is Go!

On Clients

  • Read sensor data
  • Send data to gateway and expect ACK
  • Sleep radio and (when possible) sensors for a while

I can put it in a tree!

I can make it talk to me!

On "Server"

  • Wait for incoming messages from clients
  • Send ACK when receive
  • Store data and make available to a controller

WCD Controller

Building a Controller with the Tessel 2

Topology Review

"Server"/"Gateway" as I2C Slave

  • Sorry about terminology
  • Controller should periodically poll the gateway via I2C for client packet data
  • Controller should do something useful with that data

Target Communication

  • Tessel: "Hey, send me x bytes"
  • Gateway: [x bytes]

Best-laid plans of mice and men

Hitting a Wall

Brain Melting

Day 1

What's wrong with the Tessel?

Mom, it doesn't work

  • Attempting to read I2C data off of current M0 gateway hangs forever or throws port errors
  • You know the drill:
    • Update the hell out of everything
    • Minimize test cases
    • Try different configurations
    • Mess with hardware (e.g. pull-up resistors)
    • Tessel hardware API (no J5)
    • Google, Google, Google
    • Ask for help

Brain and Soul Melting, Day 2

Altering test case to firmware

  • Ultra-simple setup
  • 2 M0s
  • Connected SDA/SCL (I2C) to each other
  • Using ultra-basic Arduino example of master reader/slave writer (https://www.arduino.cc/en/Tutorial/MasterReader)
  • Guess what?
  • It doesn't work

What's wrong with the M0?

Blargh

Google, Google, ask for help

Brain Melting, Day 3

What's wrong with Wire?

Brain Melting, Day 3a

Screw this.

Compromise

Arduino Uno as "Gateway"

Good News

  • Gateway/server firmware works on Arduino
  • That's good.

Less Good News

  • The ATMega 328P on the Uno is no match to the M0
  • Especially headache-inducing with respect to memory constraints
    • Total available memory 2k
    • After my firmware is running, only about 600 bytes RAM left, even after optimization

But it works.

Because I am a Moron

Last-ditch Effort with Edison

  • Not a bad piece of hardware
    • Dual-core Intel Atom
    • Peripherals
  • Outstandingly cruel to configure
  • Requires three wired connections to set up
  • But I did it
  • And I got the blink example going
  • And then

ENRAGING FAIL

Intel Edison with Arduino

Building the WCD App

App on Tessel 2

  • Request data (I2C)
  • Log data
  • Serve data from webserver

WCD App: Request Data

Somewhat of a Compromise

  • Uno doesn't have much memory
  • Clients aren't sending data that often
  • Controller polls a lot

WCD App: Log Data

Round and Round

  • JSON?
  • Binary?

Back to Binary

  • Logged as binary files
    • Logging to USB thumb drive on Tessel
    • One file per day
  • Logged in 64-byte chunks
    • Leaves more room for more data later
  • Why binary?
    • Faster. No conversion during poll/log time
    • Smaller file size
      • Takes less room on disk
      • Requires less memory from Tessel when munging

WCD App: Web App

The known: FAST

  • Express JS
    • Static server for HTML/JS assets
    • API endpoint to deliver data

Fun with Binary

Endian-ness

  • Both the M0 and ATMega 328P are Little-Endian
  • That's fine, though, because...

Node.js Buffer

  • Provides ability to read and write binary data BE and LE
  • Useful stuff to learn!
  const parsedChunk = {
    date           : chunk.readDoubleLE(0),
    offset         : chunk.readUInt32LE(8),
    mcp_temperature: chunk.readFloatLE(12),
    mpl_pressure   : chunk.readFloatLE(16),
    mpl_temperature: chunk.readFloatLE(20),
    deviceID       : chunk.readUInt8(24),
    capabilities   : chunk.readUInt8(25)
  };

Parsing Data out of Buffer "chunk"

WCD App: Charting

Slightly Frustrating

  • Took a while to find an easy, quick library
  • Lots of dead weight and dependencies
  • But didn't want to take the time to go full-on

Pushing work to the Client

  • Tessel always delivers same data structure from data endpoint
  • Munging of data for client libraries is done in-browser

Ultimately Chose Highcharts

Demo and Source

Forest LoRA

By lyzadanger

Forest LoRA

  • 758