Weltweiter kostenloser Versand ab 120 € Bestellwert – Zahlung mit PayPal und Stripe – Hergestellt in Finnland

Eigenes Ruuvi-Datenformat an einem Tag

Graph mit Spannungsübersicht

Kürzlich haben wir bei der Entwicklung der offiziellen Ruuvi-Firmware etwas Interessantes bemerkt: Die Batteriespannung zeigt einen erheblichen Spannungseinbruch bei der Funkübertragung.

Diese Entdeckung rechtfertigt weitere Untersuchungen zur Messung des Batteriestands. Hier stellen wir den allgemeinen Arbeitsablauf vor. Bitte beachte, dass wir unsere Arbeit auf der Firmware-Version 3.140-alpha basieren – die hier gegebenen Codebeispiele funktionieren möglicherweise nicht mit späteren Branches.

Gute Planung ist der halbe Erfolg.

Bevor wir uns ans Programmieren machen, sollten wir festlegen, was wir eigentlich erreichen wollen. Unser Ziel ist es, drei verschiedene Spannungsmessmethoden hinsichtlich ihrer Vorhersagekraft für die verbleibende Batterielebensdauer zu vergleichen:

  • Wir müssen jede dieser Messungen in unserer Firmware implementieren
  • Die Daten müssen Luftfeuchtigkeit und Temperatur enthalten
  • Die Daten müssen einen laufenden Zähler enthalten, um spontane Neustarts zu erkennen
  • Der Stromverbrauch sollte ausreichen, um die Batterie innerhalb eines angemessenen Zeitrahmens zu entleeren
  • Die Daten müssen per Bluetooth an ein Gateway gesendet werden, das die Daten an einen externen Server weiterleitet.
  • Der externe Server muss über eine benutzerfreundliche Oberfläche zur Anzeige der Daten verfügen.

Ohne weitere Umschweife, legen wir los.

Implementierung jeder dieser Messungen

Wir schauen uns 3 verschiedene Methoden zur Spannungsabtastung an. Die erste ist der „naive“ Ansatz, die Spannung ab und zu zu messen. Die zweite ist eine ausgefeiltere Methode, bei der direkt nach Funkaktivität gemessen wird. Die dritte besteht darin, nach der Funkaktivität zu messen und einige Millisekunden später erneut, um zu sehen, wie stark die Spannung unter Last einbricht.

Einrichten des Projekts

Zuerst wollen wir dieses Projekt vom Haupt-Branch abspalten. Erstellen wir ein leeres Repository ojousima.nrf52_batterytest.c auf Github und spiegeln das ruuvi.firmware.c dorthin. Das Github-Repository wird über die Github-Benutzeroberfläche erstellt.

Das vorhandene Repository wird geklont und der neue Master mit diesen Konsolenbefehlen eingerichtet:

$ 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

Als Nächstes benennen wir die Projektdateien um:

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

Und dann ersetzen wir alle Vorkommen von ruuvi.firmware.c durch ojousima.nrf52_batterytest.c. Achte darauf, das Ersetzen nicht im gesamten Projekt auszuführen, da es bei Ausführung im Ordner .git deinen Git-Index beschädigen könnte.

$ 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'

Committen wir unsere Arbeit und prüfen, ob sie sich mit Segger Embedded Studio sauber kompilieren lässt.

$ git add .
$ git commit -m "Rename project"
Ausgabe zeigt, dass bisher alles gut läuft
Bisher läuft alles gut

Konfigurieren des Projekts

Als Nächstes passen wir die application_config/application_config.h an. Der Beschleunigungssensor wird deaktiviert und wir entfernen die Spannungsmodusauswahl von ruuvi.firmware.c. Die ADC-Auflösung wird in Zeile 77 auf 12 Bit gesetzt.

/**
 * 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

Rohdaten anzeigen

Implementierung der Tasks

Wir müssen 2 Tasks in unserer Anwendung anpassen: task_adc und task_advertising. Unser task_adc wird tatsächlich etwas einfacher, da wir den Modus des ADC nicht nach dem ausgewählten Modus konfigurieren müssen, sondern alle Messungen durchführen.

#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;
}

Rohdaten anzeigen

Wir haben die Logik zur Auswahl des Messmodus entfernt und task_adc_data_get so konfiguriert, dass der ADC genau dann gemessen wird, wenn Daten in Zeile 134 angefordert werden. Dieses Verhalten ähnelt der Ruuvi-Firmware 1.x und 2.x. Wir geben hier auch die zuvor gemessenen und gespeicherten Werte after tx und droop zurück.

In task_advertisement.c entfernen wir die Ruuvi Endpoints und fügen 0xBA als unser eigenes APPLICATION_DATA_FORMAT hinzu. Der eigene Endpoint app_endpoint_ba wird in Zeile 85 aufgerufen und die kodierten Daten werden in Zeile 86 gepuffert, um gesendet zu werden.

/**
 * 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;
}

Rohdaten anzeigen

Erstellen eines neuen Datenformats

Wir nehmen das Ruuvi-Datenformat 5 als Basis und erstellen app_endpoint_ba.c und -.h aus dem vorherigen 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

Rohdaten anzeigen

Unser Datenformat-Header hat die Datenversion an Index 1, falls wir das Format in Zukunft ändern möchten. Temperatur und Luftfeuchtigkeit werden auf die gleiche Weise wie in Datenformat 5 kodiert. Spannungen werden als uint16_t Millivolt kodiert. Jetzt haben wir unser Datenformat definiert, die Implementierung befindet sich in der .c-Datei unten

/**
 * 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;
}

Rohdaten anzeigen

Der Stromverbrauch sollte ausreichen, um die Batterie innerhalb eines angemessenen Zeitrahmens zu entleeren

Da wir weder Beschleunigungssensor noch Taste verwenden, entfernen wir die Beschleunigungssensor- und Tasten-Tasks aus dem Projekt. Schließlich lassen wir die grüne LED nach der Initialisierung eingeschaltet, um eine ausreichende Stromentnahme aus der Batterie zu erzeugen. Die LEDs verbrauchen jeweils etwa 1 mA, sodass wir bei einer konstanten Entnahme von 1 mA maximal 1.000 Stunden – oder 42 Tage – Laufzeit für unseren Test erwarten können.

/**
 * 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);
  }
}

Rohdaten anzeigen

Das war’s für die Firmware! Schauen wir mal, wie es im Power Profiler aussieht:

Leistungsprofil für den RuuviTag
Unser Tag verbraucht etwa 1,1 mA bei 3,1 V
Leistungsprofil mit Verbrauchsanzeige
Der Verbrauch beträgt etwa 0,61 mA bei 2,5 V

Es ist wichtig, zwei Dinge zu beachten: Unsere konstante Stromentnahme ist doch nicht so konstant, da der Strom durch die LED abnimmt, wenn unsere Batteriespannung sinkt. Ein weiterer wichtiger Punkt ist, dass unsere Funkübertragungen bei niedrigerer Spannung aufgrund des DC/DC-Wandlers, der dem Funk konstante Leistung liefern muss, deutlich mehr Strom verbrauchen.

Schreiben eines Interpreters für die Daten

Da ich bei jeder Gelegenheit gerne etwas Neues lerne, implementieren wir den Interpreter mit Typescript. Wir folgen Carl-Johan Kihls Tutorial, wie man ein Typescript-Modul für NPM erstellt. Bald haben wir einen Interpreter, komplett mit Tests. Die Quellen befinden sich unter 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;
};

Rohdaten anzeigen

Unser Interpreter hat nichts Kompliziertes, wir parsen einfach die Bytes aus dem Array und kehren das Packing um, das im C-Code vorgenommen wurde.

Gateway-Software

Unsere Gateway-Software wird mit NodeJS implementiert und verwendet:

Der Kürze halber gehen wir nicht auf die Details des Gateways ein. Die Grundidee ist einfach genug: Wir hören mit Noble die BLE-Advertisements ab, prüfen, ob die Daten von Ruuvi Innovations stammen und ob sie im 0xBA-Format vorliegen. Falls ja, parsen wir die Daten mit ojousima.ruuvi_endpoints.ts und leiten sie mit node-influx an InfluxDB weiter.

Für diejenigen, die daran interessiert sind, das Experiment zu replizieren, befinden sich die Quellen für das Gateway hier. Es sollte beachtet werden, dass das Projekt von Noble abhängt, und Noble hat ab Oktober 2018 eine schlechte Unterstützung für NodeJS 10.x. Verwende NVM, um das Programm unter NodeJS 8.12 auszuführen.

Server

Unser Server wird ein Digital Ocean Droplet live.ojousima.net sein. Das Droplet wurde mit 2 GB RAM + 50 GB SSD dimensioniert, was mehr als ausreichend für die Datenmenge ist, die wir erwarten zu sammeln. Wir richten InfluxDB und Grafana auf dem Server ein und ändern die Passwörter, um zu vermeiden, dass Spaßvögel mit unserem Experiment herumspielen 😉

Wie beim Gateway gehen wir hier nicht ins Detail. Schau dir „Raspberry Pi 3 als Ruuvi Gateway einrichten an – als allgemeine Anleitung zu InfluxDB + Grafana unter Debian.

Testen

Jetzt haben wir die Software geklärt. Die Hardware ist der einfache Teil – wir flashen die Firmware auf 16 RuuviTags, bestücken 8 davon mit Standardbatterien und 8 mit Extended-Range-Batterien.

Tags und Batterien
Tags + Batterien
Tags im Gefrierschrank
Erste Gruppe im hinteren Teil eines Gefrierschranks

Jetzt müssen wir nur noch abwarten und hin und wieder auf live.ojousima.net nach den Daten schauen.