Skip to content
Tomáš Silber edited this page Sep 29, 2024 · 5 revisions

Basic

This repository contains HW schematics and functional SW of measuring station based on Adafruit feather 32u4 dev kit with integrated LoRa transceiver RFM95.

SW basics

This project is using arduino-lmic library for communication over LoRaWAN protocol using TTN (The Things Network) which provides necessary network infrastructure for LoRa IoT devices.

Current project configuration is set to support following modules/sensors:

  • Sensirion SPS30 - Optical air quality sensor,
  • HTU21DF - sensor of temperature and humidity,
  • MAX31865 - high precision resistance to digital convertor for precise measurement of temperature using pt1000 - platinum thermistor.

Following libraries are being used within the project:

project build and configuration

This project is using Platformio. Platformio is an open-source ecosystem for embedded development. You can easily download Platformio as an visual code extension.

You just need to download this repository, extract and import project within visual code: image

Add existing project and select folder containing platformio.ini file image

Next open project image

Project structure:

  • src/config.hpp - contains configurations macros
  • src/main.cpp - contains main functionality
  • platformio.init - main project configuration, contains library dependencies, compilation flags etc.

image

Compilation

Measuring station can be compiled as either reference or normal station. Whatever or not reference station is used depends on value of macro REFERENCE found in config.hpp.

Reference station supports only temperature measurement using PT1000. Normal station supports HTU21DF and Sensirion SPS30 but does not support PT1000.

In case of debuging, macro DEBUG shall be enabled for serial output into the console. In case of release binary, this macro should never be enabled.

Behaviour

After powering up the station, void setup() is being called first. In this function modules, sensors and LMIC library including various of callout functions mentioned below.

After setup() function is finished, void loop() is called, this functions contains call to function os_runloop_once(). This function is responsible for activating functionality within the LMIC library.

NOTE: loop() function shall not contain any heavy logic as LMIC library is very sensitive to timing issues.

Following callout functions are available:

  • void event_callback(void *pUserData, ev_t ev) - This functions is main handler of events including setup for next round of transimision.
  • void do_send(osjob_t *j) - Main handler of data transmission to gateway (TX).
  • void rx_callback(void *pUserData, u1_t port, const u1_t *pMessage, size_t nMessage) - Main handler of received data (RX) from TTN.

TTN setup

Register/login to TTN and access console. Within the console you need to create new application under which devices will be registered.

device registration

In order to create new device, you need to manually specify these settings: NOTE: It is important to select correct bandwidth and LoRaWAN specification. Otherwise end device might not function properly or not at all.

image

Advanced settings:

image

Under advanced settings, ABP activation method needs to be selected together with Class A. Also it is recommended to check "skip registration on Join Server" for development purposes.

Payload formatter

Following uplink payload formatter needs to be setup for application/end device. This will transform payload (raw bytes) received from end device to meaningful format for further processing on user server.

function sflt162f(rawSflt16) { // rawSflt16 is the 2-byte number decoded from wherever; // it's in range 0..0xFFFF // bit 15 is the sign bit // bits 14..11 are the exponent // bits 10..0 are the the mantissa. Unlike IEEE format, // the msb is explicit; this means that numbers // might not be normalized, but makes coding for // underflow easier. // As with IEEE format, negative zero is possible, so // we special-case that in hopes that JavaScript will // also cooperate. // // The result is a number in the open interval (-1.0, 1.0); //

`// throw away high bits for repeatability.`
`rawSflt16 &= 0xFFFF;`

`// special case minus zero:`
`if (rawSflt16 == 0x8000)`
    `return -0.0;`

`// extract the sign.`
`var sSign = ((rawSflt16 & 0x8000) != 0) ? -1 : 1;`

`// extract the exponent`
`var exp1 = (rawSflt16 >> 11) & 0xF;`

`// extract the "mantissa" (the fractional part)`
`var mant1 = (rawSflt16 & 0x7FF) / 2048.0;`

`// convert back to a floating point number. We hope`
`// that Math.pow(2, k) is handled efficiently by`
`// the JS interpreter! If this is time critical code,`
`// you can replace by a suitable shift and divide.`
`var f_unscaled = sSign * mant1 * Math.pow(2, exp1 - 15);`

`return f_unscaled;`
`}`

function Decoder(bytes, port) { var decoded = {}; decoded.bytes = bytes;

`if(port == 1)`
`{`
  `//Temperature and humidity`
  `rawTemp = bytes[0] + bytes[1] * 256;`
  `decoded.temperature = sflt162f(rawTemp) * 100;`

  `rawHumid = bytes[2] + bytes[3] * 256;`
  `decoded.humidity = sflt162f(rawHumid) * 100;`

  `//Particle mass concontration`
  `pm1p0 = bytes[4] + bytes[5] * 256;`
  `decoded.pm1p0 = sflt162f(pm1p0) * 100;`

  `pm2p5 = bytes[6] + bytes[7] * 256;`
  `decoded.pm2p5 = sflt162f(pm2p5) * 100;`

  `pm4p0 = bytes[8] + bytes[9] * 256;`
  `decoded.pm4p0 = sflt162f(pm4p0) * 100;`

  `pm10p0 = bytes[10] + bytes[11] * 256;`
  `decoded.pm10p0 = sflt162f(pm10p0) * 100;`

  `//Particle number concontration`
  `nc0p5 = bytes[12] + bytes[13] * 256;`
  `decoded.nc0p5 = sflt162f(nc0p5) * 100; //7 mereni peti`

  `nc1p0 = bytes[14] + bytes[15] * 256;`
  `decoded.nc1p0 = sflt162f(nc1p0) * 100; //8`

  `nc2p5 = bytes[16] + bytes[17] * 256;`
  `decoded.nc2p5 = sflt162f(nc2p5) * 100;`

  `nc4p0 = bytes[18] + bytes[19] * 256;`
  `decoded.nc4p0 = sflt162f(nc4p0) * 100;`

  `nc10p0 = bytes[20] + bytes[21] * 256;`
  `decoded.nc10p0 = sflt162f(nc10p0) * 100;`

  `typical = bytes[22] + bytes[23] * 256;`
  `decoded.typical_value = sflt162f(typical) * 100;`
`}`
`else if (port == 2)`
`{`
  `rawTemp = bytes[0] + bytes[1] * 256;`
  `decoded.temperature = sflt162f(rawTemp) * 100;`

  `rawHumid = bytes[2] + bytes[3] * 256;`
  `decoded.humidity = sflt162f(rawHumid) * 100;`

  `//Particle mass concontration`
  `pm1p0 = bytes[4] + bytes[5] * 256;`
  `decoded.pm1p0 = sflt162f(pm1p0) * 100;`

  `pm2p5 = bytes[6] + bytes[7] * 256;`
  `decoded.pm2p5 = sflt162f(pm2p5) * 100;`

  `pm4p0 = bytes[8] + bytes[9] * 256;`
  `decoded.pm4p0 = sflt162f(pm4p0) * 100;`

  `pm10p0 = bytes[10] + bytes[11] * 256;`
  `decoded.pm10p0 = sflt162f(pm10p0) * 100;`

  `//Particle number concontration`
  `nc0p5 = bytes[12] + bytes[13] * 256;`
  `decoded.nc0p5 = sflt162f(nc0p5) * 100;`

  `nc1p0 = bytes[14] + bytes[15] * 256;`
  `decoded.nc1p0 = sflt162f(nc1p0) * 100;`

  `nc2p5 = bytes[16] + bytes[17] * 256;`
  `decoded.nc2p5 = sflt162f(nc2p5) * 100;`

  `nc4p0 = bytes[18] + bytes[19] * 256;`
  `decoded.nc4p0 = sflt162f(nc4p0) * 100;`

  `nc10p0 = bytes[20] + bytes[21] * 256;`
  `decoded.nc10p0 = sflt162f(nc10p0) * 100;`

  `typical = bytes[22] + bytes[23] * 256;`
  `decoded.typical_value = sflt162f(typical) * 100;`
`}`
`return decoded;`

}

API keys

In order to access data from user server you need to setup access through API keys:

image

Copy private key and store it secretly. image

Server - TTN integration

TTN supports various methods of getting data from end devices to server:

image

Server setup

Currently Node-RED palette is already prepared for accessing data from TTN. [Node-RED]( Node-RED is a programming tool) for making quickly easy solutions of IoT appplications.

The fastest and most convenient way to setup Node-RED is through Docker-compose.

I recommend to use Portainer for container management.

In default configuration Node-RED is accessible on http://[Server-IP]:1880/

Before importing palette you need to install node-red-contrib-postgresql pallete: image

After palette is installed you can import flow:

go to menu -> import and import .json file with flow.

image

image

Imported flow:

image

Please note that imported flow needs to be configured before usage. TTN login needs to be setup and PostgreSQL database needs to be setup. Example of PostgreSQL database can be found here.