World wide free shipping on orders over 100€ - PayPal and Stripe payments - Made in Finland

Custom Ruuvi Data Format in a Day

Graph showing voltage overview

Recently while we developed official Ruuvi Firmware we noticed something interesting: the battery voltage has significant voltage droop on radio transmission.

The discovery warrants some further investigation on how to measure battery levels. Here we introduce the general workflow. Please note that we’ll be basing our work on the firmware version 3.140-alpha, the code examples given here might not work with later branches.

Good planning is halfway to success.

Before dashing off to write some code, let’s decide on what we’re actually willing to accomplish. Our goal is to compare three different voltage sampling methods for their predictive power on remaining battery life:

  • We need to implement each of these measurements on our firmware
  • Data must include humidity and temperature
  • Data must include a running counter to detect spontaneous reboots
  • Power consumption should be enough to drain the battery within a reasonable timeframe
  • The data has to be sent via Bluetooth to a gateway which will relay data to an external server.
  • The external server must have a convenient UI for displaying the data.

Without further ado, let’s get started.

Implement each of these measurements

We’re examining 3 different methods for the voltage sampling. First is “naive” approach of sampling the voltage every now and then. Second is more sophisticated method of taking the sample right after radio activity. Third is to sample the voltage after radio activity and again some milliseconds later to see the depth of voltage droop under load.

Setting up the project

First we want to splice off this project from the mainstream branch. Let’s create an empty repository ojousima.nrf52_batterytest.c on Github and mirror the ruuvi.firmware.c to it. Github repository is created via Github UI.

The existing repository is cloned and new master is setup with these console commands:

$ git clone --bare https://github.com/ruuvi/ruuvi.firmware.c.git
$ cd ruuvi.firmware.c.git/
$ git push --mirror https://github.com/ojousima/ojousima.nrf52_batterytest.c.git
$ cd ..
$ rm -rf ruuvi.firmware.c.git/
$ git clone https://github.com/ojousima/ojousima.nrf52_batterytest.c
$ cd ojousima.nrf52_batterytest.c
$ git checkout 3.14.0-alpha
$ git branch -D master
$ git checkout -b master
$ git push -f origin master
$ git submodule update --init --recursive

Next we rename the project files:

$ find . -name "ruuvi.firmware.c*" -exec sh -c 'mv "$1" "${1/ruuvi.firmware.c/ojousima.nrf52_batterytest.c}"' _ {} \;

And then we replace any instances to ruuvi.firmware.c with ojousima.nrf52_batterytest.c. Be careful to not run the replace on entire project, as running it on folder .git might corrupt your git index.

$ grep -rl "ruuvi.firmware.c" ./application_config/* | xargs sed -i -e 's/ruuvi.firmware.c/ojousima.nrf52_batterytest.c/g'
$ grep -rl "ruuvi.firmware.c" ./targets/* | xargs sed -i -e 's/ruuvi.firmware.c/ojousima.nrf52_batterytest.c/g'

Let’s commit our work and check that it compiles nicely with Segger Embedded Studio.

$ git add .
$ git commit -m "Rename project"
Output showing that so far, so good
So far, so good

Configuring the project

Next we adjust the application_config/application_config.h. Accelerometer is disabled and we remove voltage mode selection of ruuvi.firmware.c. ADC resolution is set to 12 bits on line 77.

/**
 * Application configuration constants
 *
 * License: BSD-3
 * Author: Otso Jousimaa <otso@ojousima.net>
 */

#ifndef APPLICATION_CONFIG_H
#define APPLICATION_CONFIG_H

#define APPLICATION_FW_VERSION "Batterytest 1.0.0"

// Pick a power of 2 for nRF5 backend. 128 is recommended minimum
#define APPLICATION_LOG_BUFFER_SIZE              256

// Use nRF5 SDK15
#define NRF5_SDK15_PLATFORM_ENABLED              1

/**
 * Environmental sensor configuration
 **/
// Sample rate is in Hz. This configures only the sensor,
// not transmission rate of data.
#define APPLICATION_ENVIRONMENTAL_SAMPLERATE RUUVI_DRIVER_SENSOR_CFG_MIN

// Resolution and scale cannot be adjusted on BME280
#define APPLICATION_ENVIRONMENTAL_RESOLUTION RUUVI_DRIVER_SENSOR_CFG_DEFAULT
#define APPLICATION_ENVIRONMENTAL_SCALE      RUUVI_DRIVER_SENSOR_CFG_DEFAULT

// Valid values for BME280 are: (RUUVI_DRIVER_SENSOR_DSP_)LAST, IIR, OS
// IIR slows step response but lowers noise
// OS increases power consumption but lowers noise.
// See https://blog.ruuvi.com/humidity-sensor-673c5b7636fc
// and https://blog.ruuvi.com/dsp-compromises-3f264a6b6344
#define APPLICATION_ENVIRONMENTAL_DSPFUNC    RUUVI_DRIVER_SENSOR_DSP_IIR

// No effect on _LAST, use 1. On _OS and _IIR valid values are 2, 4, 8 and 16.
#define APPLICATION_ENVIRONMENTAL_DSPPARAM   RUUVI_DRIVER_SENSOR_CFG_MAX

// (RUUVI_DRIVER_SENSOR_CFG_)SLEEP, SINGLE or CONTINUOUS
#define APPLICATION_ENVIRONMENTAL_MODE       RUUVI_DRIVER_SENSOR_CFG_CONTINUOUS

// Allow BME280 support compilation
#define RUUVI_INTERFACE_ENVIRONMENTAL_BME280_ENABLED 1

/**
 * Accelerometer configuration
 **/
// 1, 10, 25, 50, 100, 200 for LIS2DH12
#define APPLICATION_ACCELEROMETER_SAMPLERATE 10

// 8, 10, 12 for LIS2DH12
#define APPLICATION_ACCELEROMETER_RESOLUTION 10

// 2, 4, 8, 16 for LIS2DH12
#define APPLICATION_ACCELEROMETER_SCALE   RUUVI_DRIVER_SENSOR_CFG_MIN

// LAST or HIGH_PASS
#define APPLICATION_ACCELEROMETER_DSPFUNC RUUVI_DRIVER_SENSOR_DSP_LAST
#define APPLICATION_ACCELEROMETER_DSPPARAM 1

// SLEEP or CONTINUOUS
#define APPLICATION_ACCELEROMETER_MODE RUUVI_DRIVER_SENSOR_CFG_SLEEP

// Up to scale of sensor, interpreted as "at least"
#define APPLICATION_ACCELEROMETER_ACTIVITY_THRESHOLD 0.100f


// Allow LIS2DH12 support compilation
#define RUUVI_INTERFACE_ACCELERATION_LIS2DH12_ENABLED 0

/**
 *  ADC configuration
 */
 // Valid for continuous mode
#define APPLICATION_ADC_SAMPLERATE         RUUVI_DRIVER_SENSOR_CFG_MIN
#define APPLICATION_ADC_RESOLUTION         12
#define APPLICATION_ADC_SCALE              RUUVI_DRIVER_SENSOR_CFG_DEFAULT
#define APPLICATION_ADC_DSPFUNC            RUUVI_DRIVER_SENSOR_DSP_LAST
#define APPLICATION_ADC_DSPPARAM           1
// Note: call to task_adc_sample will leave the ADC in single-shot mode.
#define APPLICATION_ADC_MODE               RUUVI_DRIVER_SENSOR_CFG_SINGLE
// Milliseconds between active and recovered tx
#define APPLICATION_BATTERY_DROOP_DELAY_MS 10

/**
 * Bluetooth configuration
 *
 */
// Avoid "even" values such as 100 or 1000 to eventually drift apart
// from the devices transmitting at same interval
#define APPLICATION_ADVERTISING_INTERVAL 1010
#define APPLICATION_ADVERTISING_POWER    RUUVI_BOARD_TX_POWER_MAX
#define APPLICATION_DATA_FORMAT 0xBA

/**
 * NFC configuration
 */
// Longest text in a text field, i.e. "FW: ojousima.nrf52_batterytest.c 3.10.0
#define APPLICATION_COMMUNICATION_NFC_TEXT_BUFFER_SIZE (32)
// Longest binary message
#define APPLICATION_COMMUNICATION_NFC_DATA_BUFFER_SIZE \
        APPLICATION_COMMUNICATION_NFC_TEXT_BUFFER_SIZE
// 3 text records (version, address, id) and a data record
#define APPLICATION_COMMUNICATION_NFC_MAX_RECORDS      (4)
// 2 length bytes + 3 * text record + 1 * data record + 4 * 9 bytes for record header
// Conservers RAM for 3 * text buffer size + 1 * data buffer size + NDEF_FILE_SIZE
#define APPLICATION_COMMUNICATION_NFC_NDEF_FILE_SIZE   (166)

/**
 * Task scheduler configuration
 */
#define APPLICATION_TASK_DATA_MAX_SIZE 0
#define APPLICATION_TASK_QUEUE_MAX_LENGTH 10

/**
 * Flags which determine which c-modules are compiled in.
 * These modules may reserve some RAM and FLASH, so if you
 * do not need module you can disable it. The modules might also
 * have some dependencies between themselves.
 */
#define APPLICATION_ADC_ENABLED                     1
#define APPLICATION_COMMUNICATION_ENABLED           1
#define APPLICATION_COMMUNICATION_BLUETOOTH_ENABLED 1
#define APPLICATION_COMMUNICATION_NFC_ENABLED       1
#define APPLICATION_GPIO_ENABLED                    1
#define APPLICATION_GPIO_INTERRUPT_ENABLED          1
#define APPLICATION_ENVIRONMENTAL_MCU_ENABLED       1
#define APPLICATION_ENVIRONMENTAL_BME280_ENABLED    1
#define APPLICATION_POWER_ENABLED                   1
#define APPLICATION_RTC_MCU_ENABLED                 1
#define APPLICATION_SCHEDULER_ENABLED               1
#define APPLICATION_SPI_ENABLED                     1
#define APPLICATION_TIMER_ENABLED                   1
#define APPLICATION_YIELD_ENABLED                   1
#define APPLICATION_LOG_ENABLED                     1
// (RUUVI_INTERFACE_LOG)_ERROR, _WARNING, _INFO, _DEBUG
#define APPLICATION_LOG_LEVEL                       RUUVI_INTERFACE_LOG_INFO

// Choose one. RTT is recommended, but does not work on devices
// with readback protection enabled
#define APPLICATION_LOG_BACKEND_RTT_ENABLED         1
//#define APPLICATION_LOG_BACKEND_UART_ENABLED      0 // UART not implemented

#endif

View raw

Implementing the tasks

We need to adjust 2 tasks in our application: task_adc and task_advertising. Our task_adc actually becomes a little simpler since we don’t need to configure the mode of ADC by the selected mode, but we rather run all the measurements.

#include "application_config.h"
#include "ruuvi_boards.h"
#include "ruuvi_driver_error.h"
#include "ruuvi_driver_sensor.h"
#include "ruuvi_interface_adc.h"
#include "ruuvi_interface_adc_mcu.h"
#include "ruuvi_interface_communication_radio.h"
#include "ruuvi_interface_log.h"
#include "ruuvi_interface_scheduler.h"
#include "ruuvi_interface_rtc.h"
#include "ruuvi_interface_timer.h"
#include "task_adc.h"
#include "task_led.h"

#include <stddef.h>
#include <stdio.h>
#include <inttypes.h>

RUUVI_PLATFORM_TIMER_ID_DEF(adc_timer);
static ruuvi_driver_sensor_t adc_sensor = {0};
static volatile uint64_t t_sample = 0;
static volatile float droop = 0;
static volatile float after_tx = 0;

/* Use these functions for using ADC at regular, timed intervals
 * Remember to start the timer at init
 */
//handler for scheduled accelerometer event
static void task_adc_scheduler_sample(void *p_event_data, uint16_t event_size)
{
  ruuvi_driver_status_t status = RUUVI_DRIVER_SUCCESS;

  // Take new sample
  status |= task_adc_sample();
  // Log warning if adc sampling failed.
  RUUVI_DRIVER_ERROR_CHECK(status, ~RUUVI_DRIVER_ERROR_FATAL);
}

// Timer callback, schedule event here or execute it right away if it's timing-critical
static void task_adc_timer_cb(void* p_context)
{
  ruuvi_driver_status_t status = RUUVI_DRIVER_SUCCESS;
  ruuvi_interface_adc_data_t rest;
  // Take new ADC sample
  status |= task_adc_sample();

  // Read new sample
  status |= adc_sensor.data_get(&rest);
  RUUVI_DRIVER_ERROR_CHECK(status, RUUVI_DRIVER_SUCCESS);

  // Subtract active sample from rest sample
  droop = rest.adc_v - after_tx;
  if(0.0 > droop) { droop = 0.0; }
}


// Callback for radio event. Sample voltage under load, schedule new measurement
static void task_adc_trigger_on_radio
(const ruuvi_interface_communication_radio_activity_evt_t evt)
{

  // If event is after radio activity
  if(RUUVI_INTERFACE_COMMUNICATION_RADIO_AFTER == evt)
  {
    t_sample = ruuvi_platform_rtc_millis();
    task_adc_sample();

    // Read radio-synched ADC sample
    ruuvi_driver_status_t status = RUUVI_DRIVER_SUCCESS;
    ruuvi_interface_adc_data_t active;
    status |= adc_sensor.data_get(&active);
    RUUVI_DRIVER_ERROR_CHECK(status, RUUVI_DRIVER_SUCCESS);
    after_tx = active.adc_v;
    ruuvi_platform_timer_start(adc_timer, APPLICATION_BATTERY_DROOP_DELAY_MS);
  }
}


static ruuvi_driver_status_t task_adc_configure(void)
{
  ruuvi_driver_sensor_configuration_t config;
  config.samplerate    = APPLICATION_ADC_SAMPLERATE;
  config.resolution    = APPLICATION_ADC_RESOLUTION;
  config.scale         = APPLICATION_ADC_SCALE;
  config.dsp_function  = APPLICATION_ADC_DSPFUNC;
  config.dsp_parameter = APPLICATION_ADC_DSPPARAM;
  config.mode          = APPLICATION_ADC_MODE;
  if(NULL == adc_sensor.data_get) { return RUUVI_DRIVER_ERROR_INVALID_STATE; }
  return adc_sensor.configuration_set(&adc_sensor, &config);
}

ruuvi_driver_status_t task_adc_init(void)
{
  ruuvi_driver_status_t err_code = RUUVI_DRIVER_SUCCESS;
  ruuvi_driver_bus_t bus = RUUVI_DRIVER_BUS_NONE;
  uint8_t handle = RUUVI_INTERFACE_ADC_AINVDD;

  // Initialize timer for adc task.
  // Note: the timer is not started.
  ruuvi_interface_timer_mode_t mode = RUUVI_INTERFACE_TIMER_MODE_SINGLE_SHOT;
  err_code |= ruuvi_platform_timer_create(&adc_timer, mode, task_adc_timer_cb);

  err_code |= ruuvi_interface_adc_mcu_init(&adc_sensor, bus, handle);
  RUUVI_DRIVER_ERROR_CHECK(err_code, RUUVI_DRIVER_SUCCESS);

  if(RUUVI_DRIVER_SUCCESS == err_code)
  {
    err_code |= task_adc_configure();

    ruuvi_interface_communication_radio_activity_callback_set
    (task_adc_trigger_on_radio);

    return err_code;
  }

  // Return error if ADC could not be configured
  return RUUVI_DRIVER_ERROR_NOT_FOUND;
}


ruuvi_driver_status_t task_adc_sample(void)
{
  if(NULL == adc_sensor.mode_set) { return RUUVI_DRIVER_ERROR_INVALID_STATE; }
  uint8_t mode = RUUVI_DRIVER_SENSOR_CFG_SINGLE;
  return adc_sensor.mode_set(&mode);
}

ruuvi_driver_status_t task_adc_data_get(ruuvi_interface_adc_data_t* const data)
{
  if(NULL == data) { return RUUVI_DRIVER_ERROR_NULL; }
  if(NULL == adc_sensor.data_get) { return RUUVI_DRIVER_ERROR_INVALID_STATE; }
  ruuvi_driver_status_t err_code = RUUVI_DRIVER_SUCCESS;
  // Take new sample
  task_adc_sample();
  // Get latest data
  adc_sensor.data_get(data);
  // Fill reserved field with after tx, droop data
  data->reserved0 = after_tx;
  data->reserved1 = droop;

  return err_code;
}

View raw

We have removed logic to pick which sampling mode to use and configured the task_adc_data_get to sample the ADC right when data is requested at line 134. This behavior is similar to Ruuvi Firmware 1.x and 2.x. We also return the previously measured and stored after tx and droop values here.

On task_advertisement.c we remove the Ruuvi Endpoints and add 0xBA as our custom APPLICATION_DATA_FORMAT. The custom endpoint app_endpoint_ba is called on line 85 and encoded data is buffered to be sent on line 86.

/**
 * Ruuvi Firmware 3.x advertisement tasks.
 *
 * License: BSD-3
 * Author: Otso Jousimaa <otso@ojousima.net>
 **/

#include "application_config.h"
#include "app_endpoint_ba.h"
#include "ruuvi_boards.h"
#include "ruuvi_driver_error.h"
#include "ruuvi_interface_adc.h"
#include "ruuvi_interface_communication_ble4_advertising.h"
#include "ruuvi_interface_communication_radio.h"
#include "ruuvi_interface_environmental.h"
#include "ruuvi_interface_scheduler.h"
#include "ruuvi_interface_timer.h"
#include "task_adc.h"
#include "task_advertisement.h"
#include "task_environmental.h"

RUUVI_PLATFORM_TIMER_ID_DEF(advertisement_timer);
static ruuvi_interface_communication_t channel;

//handler for scheduled advertisement event
static void task_advertisement_scheduler_task(void *p_event_data, uint16_t event_size)
{
  // Update BLE data
  task_advertisement_send_ba();

}

// Timer callback, schedule advertisement event here.
static void task_advertisement_timer_cb(void* p_context)
{
  ruuvi_platform_scheduler_event_put(NULL, 0, task_advertisement_scheduler_task);
}

ruuvi_driver_status_t task_advertisement_init(void)
{
  ruuvi_driver_status_t err_code = RUUVI_DRIVER_SUCCESS;
  err_code |= ruuvi_interface_communication_ble4_advertising_init(&channel);
  err_code |= ruuvi_interface_communication_ble4_advertising_tx_interval_set
  (APPLICATION_ADVERTISING_INTERVAL);

  int8_t target_power = APPLICATION_ADVERTISING_POWER;
  err_code |= ruuvi_interface_communication_ble4_advertising_tx_power_set
  (&target_power);

  err_code |= ruuvi_interface_communication_ble4_advertising_manufacturer_id_set
  (RUUVI_BOARD_BLE_MANUFACTURER_ID);

  err_code |= ruuvi_platform_timer_create(&advertisement_timer,
                                           RUUVI_INTERFACE_TIMER_MODE_REPEATED,
                                           task_advertisement_timer_cb);
  err_code |= ruuvi_platform_timer_start(advertisement_timer,
                                         APPLICATION_ADVERTISING_INTERVAL);
  return err_code;
}

ruuvi_driver_status_t task_advertisement_send_ba(void)
{
  ruuvi_driver_status_t err_code = RUUVI_DRIVER_SUCCESS;
  static uint16_t sequence = 0;
  sequence++;

  ruuvi_interface_adc_data_t battery;
  ruuvi_interface_environmental_data_t environmental;

  // Get data from sensors
  err_code |= task_environmental_data_get(&environmental);
  err_code |= task_adc_data_get(&battery);

  app_endpoint_ba_data_t data;
  data.humidity_rh = environmental.humidity_rh;
  data.temperature_c = environmental.temperature_c;
  data.simple_v = battery.adc_v;
  data.radio_v = battery.reserved0;
  data.droop_v  = battery.reserved1;
  data.measurement_count = sequence;


  ruuvi_interface_communication_message_t message;
  message.data_length = APP_ENDPOINT_BA_DATA_LENGTH;
  app_endpoint_ba_encode(message.data, &data, RUUVI_DRIVER_FLOAT_INVALID);
  err_code |= channel.send(&message);

  return err_code;
}

View raw

Making new data format

We take the Ruuvi dataformat 5 as a base and make app_endpoint_ba.c and -.h out of the previous code.

/**
 * Application endpoint helper.
 * Defines necessary data for creating application spceific BA broadcast
 *
 * License: BSD-3
 * Author: Otso Jousimaa <otso@ojousima.net>
 */

#ifndef APP_ENDPOINT_BA_H
#define APP_ENDPOINT_BA_H
#include "ruuvi_endpoints.h"

#define APP_ENDPOINT_BA_DESTINATION                 0xBA
#define APP_ENDPOINT_BA_VERSION                     0
#define APP_ENDPOINT_BA_DATA_LENGTH                 14

#define APP_ENDPOINT_BA_OFFSET_HEADER               0
#define APP_ENDPOINT_BA_OFFSET_VERSION              1
#define APP_ENDPOINT_BA_OFFSET_TEMPERATURE_MSB      2
#define APP_ENDPOINT_BA_OFFSET_TEMPERATURE_LSB      3
#define APP_ENDPOINT_BA_OFFSET_HUMIDITY_MSB         4
#define APP_ENDPOINT_BA_OFFSET_HUMIDITY_LSB         5
#define APP_ENDPOINT_BA_OFFSET_SIMPLE_MSB           6
#define APP_ENDPOINT_BA_OFFSET_SIMPLE_LSB           7
#define APP_ENDPOINT_BA_OFFSET_RADIO_MSB            8
#define APP_ENDPOINT_BA_OFFSET_RADIO_LSB            9
#define APP_ENDPOINT_BA_OFFSET_DROOP_MSB            10
#define APP_ENDPOINT_BA_OFFSET_DROOP_LSB            11
#define APP_ENDPOINT_BA_OFFSET_SEQUENCE_COUNTER_MSB 12
#define APP_ENDPOINT_BA_OFFSET_SEQUENCE_COUNTER_LSB 13



typedef struct{
  float humidity_rh;
  float pressure_pa;
  float temperature_c;
  float simple_v;
  float radio_v;
  float droop_v;
  uint16_t measurement_count;
}app_endpoint_ba_data_t;

/**
 * Encode given data to given buffer in app format BA.
 *
 * parameter data: uint8_t array with length of 24 bytes.
 * parameter data: Struct containing all necessary information for
 *                 encoding the data into buffer
 * parameter invalid: A float which signals that given data point is invalid.
 */
ruuvi_endpoint_status_t app_endpoint_ba_encode(uint8_t* const buffer,
                                               const app_endpoint_ba_data_t* data,
                                               const float invalid);


#endif

View raw

Our data format header has data version at index 1 in case we want to change the format in the future. Temperature and humidity will be encoded the same way as in data format 5. Voltages will be encoded as uint16_t millivolts. Now we have defined our data format, implementation is in the .c file below

/**
 * Application endpoint helper.
 * Defines necessary data for creating application spceific BA broadcast
 *
 * License: BSD-3
 * Author: Otso Jousimaa <otso@ojousima.net>
 */

#include "app_endpoint_ba.h"
#include "ruuvi_endpoints.h"
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
#include <math.h>

ruuvi_endpoint_status_t app_endpoint_ba_encode(uint8_t* const buffer,
                                               const app_endpoint_ba_data_t* data,
                                               const float invalid)
{
  if(NULL == buffer  || NULL == data) { return RUUVI_ENDPOINT_ERROR_NULL; }

  buffer[APP_ENDPOINT_BA_OFFSET_HEADER]  = APP_ENDPOINT_BA_DESTINATION;
  buffer[APP_ENDPOINT_BA_OFFSET_VERSION] = APP_ENDPOINT_BA_VERSION;

  // HUMIDITY
  uint16_t humidity = 0;
  if(invalid != data->humidity_rh && 0 < data->humidity_rh)
  {
    //Humidity (16bit unsigned) in 0.0025% (0-163.83% range, though realistically 0-100%)
     humidity = (uint16_t)(data->humidity_rh*400);
  }
  buffer[APP_ENDPOINT_BA_OFFSET_HUMIDITY_MSB] = (humidity >> 8);
  buffer[APP_ENDPOINT_BA_OFFSET_HUMIDITY_LSB] = humidity & 0xFF;

  // Temperature is in 0.005 degrees
  int16_t temperature = 0;
  if(invalid != data->temperature_c)
  {
    temperature = (int16_t)(data->temperature_c * 200 );
  }
  buffer[APP_ENDPOINT_BA_OFFSET_TEMPERATURE_MSB] = (temperature >> 8);
  buffer[APP_ENDPOINT_BA_OFFSET_TEMPERATURE_LSB] = (temperature & 0xFF);

  // voltages
  uint16_t simple_v = 0;
  uint16_t radio_v = 0;
  uint16_t droop_v = 0;
  if(invalid != data->simple_v)
  {
    // Convert to millivolts
    simple_v = (data->simple_v > 0) ? data->simple_v * 1000 : 0;
    buffer[APP_ENDPOINT_BA_OFFSET_SIMPLE_MSB] = (simple_v >> 8);
    buffer[APP_ENDPOINT_BA_OFFSET_SIMPLE_LSB] = (simple_v & 0xFF);
  }

  if(invalid != data->radio_v)
  {
    // Convert to millivolts
    radio_v = (data->radio_v > 0) ? data->radio_v * 1000 : 0;
    buffer[APP_ENDPOINT_BA_OFFSET_RADIO_MSB] = (radio_v >> 8);
    buffer[APP_ENDPOINT_BA_OFFSET_RADIO_LSB] = (radio_v & 0xFF);
  }

  if(invalid != data->droop_v)
  {
    // Convert to millivolts
    droop_v = (data->droop_v > 0) ? data->droop_v * 1000 : 0;
    buffer[APP_ENDPOINT_BA_OFFSET_DROOP_MSB] = (droop_v >> 8);
    buffer[APP_ENDPOINT_BA_OFFSET_DROOP_LSB] = (droop_v & 0xFF);
  }

  buffer[APP_ENDPOINT_BA_OFFSET_SEQUENCE_COUNTER_MSB] = (data->measurement_count >> 8);
  buffer[APP_ENDPOINT_BA_OFFSET_SEQUENCE_COUNTER_LSB] = (data->measurement_count & 0xFF);

  return RUUVI_ENDPOINT_SUCCESS;
}

View raw

Power consumption should be enough to drain the battery within a reasonable timeframe

As we won’t be using accelerometer or button, we remove the accelerometer and button tasks from the project. Finally we leave the green led on after initialization to introduce sufficient power drain on the battery. The LEDs consume approximately 1 mA each, so with one 1 mA constant drain we can expect maximum of 1 000 hours — or 42 days — of runtime for our test.

/**
 * Ruuvi Firmware 3.x code. Reads the sensors onboard RuuviTag and broadcasts the sensor data in a manufacturer specific format.
 *
 * License: BSD-3
 * Author: Otso Jousimaa <otso@ojousima.net>
 **/

#include "application_config.h"
#include "ruuvi_interface_gpio.h"
#include "ruuvi_interface_log.h"
#include "ruuvi_interface_scheduler.h"
#include "ruuvi_interface_yield.h"
#include "ruuvi_boards.h"
#include "task_adc.h"
#include "task_advertisement.h"
#include "task_environmental.h"
#include "task_led.h"
#include "task_nfc.h"
#include "task_power.h"
#include "task_rtc.h"
#include "task_scheduler.h"
#include "task_spi.h"
#include "task_timer.h"
#include "test_sensor.h"

#include <stdio.h>

int main(void)
{
  // Init logging
  ruuvi_driver_status_t status = RUUVI_DRIVER_SUCCESS;
  status |= ruuvi_platform_log_init(APPLICATION_LOG_LEVEL);
  RUUVI_DRIVER_ERROR_CHECK(status, RUUVI_DRIVER_SUCCESS);

  // Init yield
  status |= ruuvi_platform_yield_init();
  RUUVI_DRIVER_ERROR_CHECK(status, RUUVI_DRIVER_SUCCESS);

  // Init GPIO
  status |= ruuvi_platform_gpio_init();
  RUUVI_DRIVER_ERROR_CHECK(status, RUUVI_DRIVER_SUCCESS);

  // Initialize LED gpio pins, turn RED led on.
  status |= task_led_init();
  status |= task_led_write(RUUVI_BOARD_LED_RED, TASK_LED_ON);
  RUUVI_DRIVER_ERROR_CHECK(status, RUUVI_DRIVER_SUCCESS);

  // Initialize SPI
  status |= task_spi_init();
  RUUVI_DRIVER_ERROR_CHECK(status, RUUVI_DRIVER_SUCCESS);

  // Initialize RTC, timer and scheduler
  status |= task_rtc_init();
  status |= task_timer_init();
  status |= task_scheduler_init();
  RUUVI_DRIVER_ERROR_CHECK(status, RUUVI_DRIVER_SUCCESS);

  // Initialize power
  status |= task_power_dcdc_init();
  RUUVI_DRIVER_ERROR_CHECK(status, RUUVI_DRIVER_SUCCESS);

  // Initialize nfc
  status |= task_nfc_init();
  RUUVI_DRIVER_ERROR_CHECK(status, RUUVI_DRIVER_SUCCESS);

  // Initialize ADC
  status |= task_adc_init();
  RUUVI_DRIVER_ERROR_CHECK(status, RUUVI_DRIVER_SUCCESS);

  // Initialize environmental- nRF52 will return ERROR NOT SUPPORTED on RuuviTag basic
  // if DSP was configured, log warning
  status |= task_environmental_init();
  RUUVI_DRIVER_ERROR_CHECK(status, RUUVI_DRIVER_SUCCESS);

  // Initialize BLE
  status |= task_advertisement_init();
  RUUVI_DRIVER_ERROR_CHECK(status, RUUVI_DRIVER_SUCCESS);

  // Turn RED led off. Turn GREEN LED on if no errors occured
  status |= task_led_write(RUUVI_BOARD_LED_RED, TASK_LED_OFF);
  if(RUUVI_DRIVER_SUCCESS == status)
  {
    status |= task_led_write(RUUVI_BOARD_LED_GREEN, TASK_LED_ON);
    ruuvi_platform_delay_ms(1000);
  }

  while (1)
  {
    // Turn off activity led
    status = task_led_write(RUUVI_BOARD_LED_RED, !RUUVI_BOARD_LEDS_ACTIVE_STATE);
    // Sleep
    status |= ruuvi_platform_yield();
    // Turn on activity led
    status |= task_led_write(RUUVI_BOARD_LED_RED, RUUVI_BOARD_LEDS_ACTIVE_STATE);
    // Execute scheduled tasks
     status |= ruuvi_platform_scheduler_execute();
    // Reset only on fatal error
    RUUVI_DRIVER_ERROR_CHECK(status, ~RUUVI_DRIVER_ERROR_FATAL);
  }
}

View raw

That’s it for the firmware! Let’s see how it goes on power profiler:

Power profile for the Ruuvi Tag
Our tag consumes approximately 1.1 mA at 3.1 V
Power profile indicating consumption
The consumption is approximately 0.61 mA at 2.5 V

It’s important to note two things: Our constant current drain isn’t so constant after all, as the current through LED drops as our battery voltage comes down. Other important point to consider is that our radio transmissions consume a lot more current at lower voltage due to DC/DC converter having to supply constant power to the radio.

Writing an interpreter for the data

Since I enjoy learning something new on every chance I get, let’s implement the interpreter with Typescript. We’ll follow Carl-Johan Kihls tutorial on how to make a typescript module to NPM. Soon we have interpreter, complete with tests. Sources are at ojousima.ruuvi_endpoints.ts.

import { BatteryBroadcast } from './batterybroadcast';

const versionStart     = 1;
const versionEnd       = versionStart +1;
const temperatureStart = versionEnd;
const temperatureEnd   = temperatureStart + 2;
const humidityStart    = temperatureEnd;
const humidityEnd      = humidityStart + 2;
const simpleStart      = humidityEnd;
const simpleEnd        = simpleStart + 2;
const radioStart       = simpleEnd;
const radioEnd         = radioStart + 2;
const droopStart       = radioEnd;
const droopEnd         = droopStart + 2;
const measurementStart = droopEnd;
const measurementEnd   = measurementStart + 2;

export const dfbaparser = (data: Uint8Array): BatteryBroadcast => {
  const robject: BatteryBroadcast = new BatteryBroadcast();
  if (0xBA !== data[0]) {
    throw new Error('Not DF BA data');
  }

  robject.dataFormat = 0xBA;

  const version = data[versionStart];
  robject.version = version;

  const temperatureBytes = data.slice(temperatureStart, temperatureEnd);
  let temperature = temperatureBytes[0] * 256 + temperatureBytes[1];
  // two's complement
  if (temperature > 32767) {
    temperature -= 65536;
  }
  // Temperature is in units of 0.005 C -> divide by 200
  robject.temperatureC = temperature / 200;

  const humidityBytes = data.slice(humidityStart, humidityEnd);
  let humidity = humidityBytes[0] * 256 + humidityBytes[1];
  // Humidity is in units of 0.0025 % -> divide by 400
  robject.humidityRh = humidity / 400;

  const simpleBytes = data.slice(simpleStart, simpleEnd);
  let simple = simpleBytes[0] * 256 + simpleBytes[1];
  // two's complement
  if (simple > 32767) {
    simple -= 65536;
  } 
  robject.simpleVoltageV = simple / 1000;

  const radioBytes = data.slice(radioStart, radioEnd);
  let radio = radioBytes[0] * 256 + radioBytes[1];
  // two's complement
  if (radio > 32767) {
    radio -= 65536;
  } 
  robject.radioVoltageV = radio / 1000;

  const droopBytes = data.slice(droopStart, droopEnd);
  let droop = droopBytes[0] * 256 + droopBytes[1];
  // two's complement
  if (droop > 32767) {
    droop -= 65536;
  } 
  robject.droopVoltageV = droop / 1000;

  const measurementBytes = data.slice(measurementStart, measurementEnd);
  let measurement = measurementBytes[0] * 256 + measurementBytes[1];
  robject.measurementSequence = measurement;

  return robject;
};

View raw

Our interpreter has nothing complicated, just parse out the bytes from array and reverse the packing made in the C code.

Gateway software

Our gateway software will be implemented with NodeJS using:

For the sake of brevity we won’t go into the details of the gateway. Basic idea is simple enough: we listen in to the BLE advertisements with Noble, check if data is from Ruuvi Innovations and if it is in 0xBA format. If yes, we parse the data using ojousima.ruuvi_endpoints.ts and relay it to InfluxDB using node-influx.

For those who are interested in replicating the experiment, sources for the gateway are here. It should be noted that the project depends on Noble, and Noble has poor support on NodeJS 10.x as of October 2018. Use NVM to run the program under NodeJS 8.12.

Server

Our server will be a Digital Ocean droplet live.ojousima.net. The droplet was sized at 2 GB RAM + 50 GB SSD which is more than enough for the amount of data we expect to collect. We set up InfluxDB and Grafana on the server, and change the passwords to avoid any tricksters playing games with our experiment 😉

As with the gateway, we won’t be going in depth here. Check out “Setting up Raspberry Pi 3 as a Ruuvi Gateway for a general guide on InfluxDB + Grafana on Debian.

Testing it out

Now we have the software figured out. Hardware is easy part, we flash the firmware on 16 RuuviTags, load 8 of them with standard batteries and 8 of them with extended range batteries.

Tags and batteries
Tags + batteries
Tags in the freezer
First group in the back of a freezer

Now all there is to do is to wait and and check on live.ojousima.net every now and then for the data.

Featured Products

Find the right products for your measuring needs
  • RuuviTag sensor measures temperature, air humidity, air pressure and motionIn stock
    Made in Europe, Finland. Worldwide free shipping on orders over...
    Read more

    RuuviTag Sensor (4in1)

    39,90
  • Ruuvi KitIn stock
    Every Ruuvi Gateway product is shipped with...
    Read more

    Ruuvi Sensor Measurement Kit

    339,00
  • Ruuvi GatewayIn stock
    Note! Every Ruuvi Gateway product is shipped with a...
    Read more

    Ruuvi Gateway Router

    199,00
  • RuuviTag Pro 3in1 measures temperature, air humidity and motionIn stock
    Made in Europe, Finland. Worldwide free shipping on orders over...
    Read more

    RuuviTag Pro Sensor

    49,9055,90
    This product has multiple variants. The options may be chosen on the product page