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"
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
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;
}
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;
}
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
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;
}
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);
}
}
That’s it for the firmware! Let’s see how it goes on power profiler:
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;
};
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:
- ojousima.ruuvi_endpoints.ts written above
- node-influx
- noble
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.
Now all there is to do is to wait and and check on live.ojousima.net every now and then for the data.