Test app for integrating RBRTridente sensor into smart mooring using bristlemouth

The folks at SoFarOcean HQ worked with me this week on setting up an RBRTridente three parameter sensor to broadcast over the Smart Mooring using Bristlemouth. The code below was modified from sample code for a single parameter sonde and replaces the user_code.cpp

It takes the three optical sensor outputs (pre-set on the sensor to output as backscatter, chlorophyll-a, fDOM), aggregates them over a time interval, produces statistics (mean, standard deviation, min, max, number of samples) and sends them to the Spotter to broadcast over cellular. As written, it aggregates 2Hz sampling every 1 min.

#include "debug.h"
#include "util.h"
#include "user_code.h"
#include "bristlefin.h"
#include "bsp.h"
#include "stm32_rtc.h"
#include "task_priorities.h"
#include "uptime.h"
#include "bm_printf.h"
#include "bm_pubsub.h"
#include "lwip/inet.h"
#include "bm_network.h"
#include "usart.h"
#include "payload_uart.h"
#include "LineParser.h"
#include "array_utils.h"

#define LED_ON_TIME_MS 20
#define LED_PERIOD_MS 1000

/// For Turning Numbers Into Data
// How often to compute and return statistics
#define backscatter_AGG_PERIOD_MIN 1
// 10 min => 60,000 ms
#define backscatter_AGG_PERIOD_MS (backscatter_AGG_PERIOD_MIN * 60 * 1000)
/* We have enough RAM that we can keep it simple for shorter durations - use 64 bit doubles, buffer all readings.
   We could be much more RAM and precision efficient by using numerical methods like Kahan summation and Welford's algorithm.*/
#define MAX_backscatter_SAMPLES ((backscatter_AGG_PERIOD_MS / 500) + 10) // 1 minutes @ 2Hz + 10 extra samples for padding
#define MAX_chlorophyll_SAMPLES (MAX_backscatter_SAMPLES)
#define MAX_fDOM_SAMPLES (MAX_backscatter_SAMPLES)

typedef struct {
  uint16_t sample_count;
  double min;
  double max;
  double mean;
  double stdev;
  double* values;
} __attribute__((__packed__)) backscatter_t;

typedef struct {
  uint16_t sample_count;
  double min;
  double max;
  double mean;
  double stdev;
  double* values;
} __attribute__((__packed__)) chlorophyll_t;

typedef struct {
  uint16_t sample_count;
  double min;
  double max;
  double mean;
  double stdev;
  double* values;
} __attribute__((__packed__)) fDOM_t;
// We'll allocate the values buffer later.

typedef struct {
 backscatter_t backscatter;
 chlorophyll_t chlorophyll;
 fDOM_t fDOM;
} __attribute__((__packed__)) TridenteData_t;

TridenteData_t TridenteData_data = {};

/// For Turning Text Into Numbers
/*
 * Setup a LineParser to turn the ASCII serial data from the RBR Coda3 into numbers
 * Lines from the RBR Coda3 Depth sensor look like:
 *    80000, 10.1433
 *    - comma-separated values
 *    - an unsigned integer representing the tick time of the Coda sensor
 *    - a floating point number representing the backscatter in deci-bar
 *    - a 256 character buffer should be more than enough */
// For unsigned ints, let's use 64 bits, and for floating point let's use 64 bit doubles.
//   We've got luxurious amounts of RAM on this chip, and it's much easier to avoid roll-overs and precision issues
//   by using it vs. troubleshooting them because we prematurely optimized things.
ValueType valueTypes[] = {TYPE_UINT64, TYPE_DOUBLE, TYPE_DOUBLE, TYPE_DOUBLE};
// Declare the parser here with separator, buffer length, value types array, and number of values per line.
//   We'll initialize the parser later in setup to allocate all the memory we'll need.
LineParser parser(",", 256, valueTypes, 4);

// A timer variable we can set to trigger a pulse on LED2 when we get payload serial data
static int32_t ledLinePulse = -1;
/*
 * This function is called from the payload UART library in src/lib/bristlefin/payload_uart.cpp::processLine function.
 * Every time the uart receives the configured termination character (newline character by default),
 * It will:
 * -- print the line to Dev Kit USB console.
 * -- print the line to Spotter USB console.
 * -- write the line to a payload_data.log on the Spotter SD card.
 * -- call this function, so we can do custom things with the data.
 * In this case, we parse and evalute new backscatter, chlorophyll, and fDOM readings.
 * */
void PLUART::userProcessLine(uint8_t *line, size_t len) {
  /// NOTE - this function is called from the LPUartRx task. Interacting with the same data as setup() and loop(),
  ///   which are called from the USER task, is not thread safe!

  // trigger a pulse on LED2
  ledLinePulse = uptimeGetMs();
  // Now when we get a line of text data, our LineParser turns it into numeric values.
  if (parser.parseLine(reinterpret_cast<const char*>(line), len)) {
    printf("parsed values: %llu | %f\n", parser.getValue(0).data, parser.getValue(1).data);
  }
  else {
    printf("Error parsing line!\n");
    return;
  }
  // Now let's aggregate those values into statistics
  if (TridenteData_data.backscatter.sample_count >= MAX_backscatter_SAMPLES) {
    printf("ERR - No more room in backscatter reading buffer, already have %d readings!\n", MAX_backscatter_SAMPLES);
    return;
  }
  if (xUserDataMutex == NULL) {
    printf("ERR - user data Mutex NULL!\n");
    return;
  }
  if(xSemaphoreTake(xUserDataMutex, portMAX_DELAY) == pdTRUE) {
    
    double backscatter_reading = parser.getValue(1).data.double_val;
    TridenteData_data.backscatter.values[TridenteData_data.backscatter.sample_count++] = backscatter_reading;
    if (TridenteData_data.backscatter.sample_count == 1) {
      TridenteData_data.backscatter.max = backscatter_reading;
      TridenteData_data.backscatter.min = backscatter_reading;
    } else if (backscatter_reading < TridenteData_data.backscatter.min) TridenteData_data.backscatter.min = backscatter_reading;
    else if (backscatter_reading > TridenteData_data.backscatter.max) TridenteData_data.backscatter.max = backscatter_reading;
    printf("count: %u/%d, min: %f, max: %f\n", TridenteData_data.backscatter.sample_count, MAX_backscatter_SAMPLES, TridenteData_data.backscatter.min, TridenteData_data.backscatter.max);
    
    double chlorophyll_reading = parser.getValue(2).data.double_val;
    TridenteData_data.chlorophyll.values[TridenteData_data.chlorophyll.sample_count++] = chlorophyll_reading;
    if (TridenteData_data.chlorophyll.sample_count == 1) {
      TridenteData_data.chlorophyll.max = chlorophyll_reading;
      TridenteData_data.chlorophyll.min = chlorophyll_reading;
    } else if (chlorophyll_reading < TridenteData_data.chlorophyll.min) TridenteData_data.chlorophyll.min = chlorophyll_reading;
    else if (chlorophyll_reading > TridenteData_data.chlorophyll.max) TridenteData_data.chlorophyll.max = chlorophyll_reading;
    printf("count: %u/%d, min: %f, max: %f\n", TridenteData_data.chlorophyll.sample_count, MAX_chlorophyll_SAMPLES, TridenteData_data.chlorophyll.min, TridenteData_data.chlorophyll.max);
    
    double fDOM_reading = parser.getValue(3).data.double_val;
    TridenteData_data.fDOM.values[TridenteData_data.fDOM.sample_count++] = fDOM_reading;
    if (TridenteData_data.fDOM.sample_count == 1) {
      TridenteData_data.fDOM.max = fDOM_reading;
      TridenteData_data.fDOM.min = fDOM_reading;
    } else if (fDOM_reading < TridenteData_data.fDOM.min) TridenteData_data.fDOM.min = fDOM_reading;
    else if (fDOM_reading > TridenteData_data.fDOM.max) TridenteData_data.fDOM.max = fDOM_reading;
    printf("count: %u/%d, min: %lf, max: %lf\n", TridenteData_data.fDOM.sample_count, MAX_fDOM_SAMPLES, TridenteData_data.fDOM.min, TridenteData_data.fDOM.max);
    
    xSemaphoreGive(xUserDataMutex);
  }
}

void setup(void) {
  /* USER ONE-TIME SETUP CODE GOES HERE */
  // Allocate memory for data buffer.
  TridenteData_data.backscatter.values = static_cast<double *>(pvPortMalloc(sizeof(double ) * MAX_backscatter_SAMPLES));
  TridenteData_data.chlorophyll.values = static_cast<double *>(pvPortMalloc(sizeof(double ) * MAX_backscatter_SAMPLES));
  TridenteData_data.fDOM.values = static_cast<double *>(pvPortMalloc(sizeof(double ) * MAX_backscatter_SAMPLES));

  // Initialize our LineParser, which will allocated any needed memory for parsing.
  parser.init();
  // Setup the UART – the on-board serial driver that talks to the RS232 transceiver.
  PLUART::initPayloadUart(USER_TASK_PRIORITY);
  // Baud set per expected baud rate of the sensor.
  PLUART::setBaud(9600);
  // Set a line termination character per protocol of the sensor.
  PLUART::setTerminationCharacter('\n');
  // Turn on the UART.
  serialEnable(&PLUART::uart_handle);
  // Enable the input to the Vout power supply.
  BF::enableVbus();
  // ensure Vbus stable before enable Vout with a 5ms delay.
  vTaskDelay(pdMS_TO_TICKS(5));
  // enable Vout, 12V by default.
  BF::enableVout();
}

void loop(void) {
  /* USER LOOP CODE GOES HERE */
  /// This aggregates readings into stats, and sends them along to Spotter
  static u_int32_t backscatterStatsTimer = uptimeGetMs();
      char rtcTimeBuffer[32];
  if ((u_int32_t)uptimeGetMs() - backscatterStatsTimer >= backscatter_AGG_PERIOD_MS) {
    backscatterStatsTimer = uptimeGetMs();
      double mean = 0, stdev = 0, min = 0, max = 0;
      uint16_t n_samples = 0;
      if(xSemaphoreTake(xUserDataMutex, portMAX_DELAY) == pdTRUE && TridenteData_data.backscatter.sample_count) {
        RTCTimeAndDate_t timeAndDate;
      if (rtcGet(&timeAndDate) == pdPASS) {
        sprintf(rtcTimeBuffer, "%04u-%02u-%02uT%02u:%02u:%02u.%03u",
                timeAndDate.year,
                timeAndDate.month,
                timeAndDate.day,
                timeAndDate.hour,
                timeAndDate.minute,
                timeAndDate.second,
                timeAndDate.ms);
        } else {
          strcpy(rtcTimeBuffer, "0");
        }
        mean = getMean(TridenteData_data.backscatter.values, TridenteData_data.backscatter.sample_count);
        stdev = getStd(TridenteData_data.backscatter.values, TridenteData_data.backscatter.sample_count, mean);
        min = TridenteData_data.backscatter.min;
        max = TridenteData_data.backscatter.max;
        n_samples = TridenteData_data.backscatter.sample_count;
        TridenteData_data.backscatter.sample_count = 0;
bm_fprintf(0, "payload_data_agg.log",
                 "tick: %llu, rtc: %s, n: %u, min: %lf, max: %lf, mean: %lf, std: %lf\n",
                 uptimeGetMs(), rtcTimeBuffer, n_samples, min, max, mean, stdev);
      bm_printf(0, "[rbr-agg] | tick: %llu, rtc: %s, n: %u, min: %lf, max: %lf, mean: %lf, std: %lf",
                uptimeGetMs(), rtcTimeBuffer, n_samples, min, max, mean, stdev);
      printf("[rbr-agg] | tick: %llu, rtc: %s, n: %u, min: %lf, max: %lf, mean: %lf, std: %lf\n",
             uptimeGetMs(), rtcTimeBuffer, n_samples, min, max, mean, stdev);
        mean = getMean(TridenteData_data.chlorophyll.values, TridenteData_data.chlorophyll.sample_count);
        stdev = getStd(TridenteData_data.chlorophyll.values, TridenteData_data.chlorophyll.sample_count, mean);
        min = TridenteData_data.chlorophyll.min;
        max = TridenteData_data.chlorophyll.max;
        n_samples = TridenteData_data.chlorophyll.sample_count;
        TridenteData_data.chlorophyll.sample_count = 0;
bm_fprintf(0, "payload_data_agg.log",
                 "tick: %llu, rtc: %s, n: %u, min: %lf, max: %lf, mean: %lf, std: %lf\n",
                 uptimeGetMs(), rtcTimeBuffer, n_samples, min, max, mean, stdev);
      bm_printf(0, "[rbr-agg] | tick: %llu, rtc: %s, n: %u, min: %lf, max: %lf, mean: %lf, std: %lf",
                uptimeGetMs(), rtcTimeBuffer, n_samples, min, max, mean, stdev);
      printf("[rbr-agg] | tick: %llu, rtc: %s, n: %u, min: %lf, max: %lf, mean: %lf, std: %lf\n",
             uptimeGetMs(), rtcTimeBuffer, n_samples, min, max, mean, stdev);
        mean = getMean(TridenteData_data.fDOM.values, TridenteData_data.fDOM.sample_count);
        stdev = getStd(TridenteData_data.fDOM.values, TridenteData_data.fDOM.sample_count, mean);
        min = TridenteData_data.fDOM.min;
        max = TridenteData_data.fDOM.max;
        n_samples = TridenteData_data.fDOM.sample_count;
        TridenteData_data.fDOM.sample_count = 0;
bm_fprintf(0, "payload_data_agg.log",
                 "tick: %llu, rtc: %s, n: %u, min: %lf, max: %lf, mean: %lf, std: %lf\n",
                 uptimeGetMs(), rtcTimeBuffer, n_samples, min, max, mean, stdev);
      bm_printf(0, "[rbr-agg] | tick: %llu, rtc: %s, n: %u, min: %lf, max: %lf, mean: %lf, std: %lf",
                uptimeGetMs(), rtcTimeBuffer, n_samples, min, max, mean, stdev);
      printf("[rbr-agg] | tick: %llu, rtc: %s, n: %u, min: %lf, max: %lf, mean: %lf, std: %lf\n",
             uptimeGetMs(), rtcTimeBuffer, n_samples, min, max, mean, stdev);
        xSemaphoreGive(xUserDataMutex);
      }

    uint8_t tx_data[128] = {};
    
    memcpy(tx_data, (uint8_t*)(&TridenteData_data), sizeof(TridenteData_t));
    if(spotter_tx_data(tx_data, sizeof(TridenteData_t), BM_NETWORK_TYPE_CELLULAR_IRI_FALLBACK)){
      printf("%llut - %s | Sucessfully sent Spotter transmit data request\n", uptimeGetMs(), rtcTimeBuffer);
    }
    else {
      printf("%llut - %s | Failed to send Spotter transmit data request\n", uptimeGetMs(), rtcTimeBuffer);
    }
  }

  /// This checks for a trigger set by ledLinePulse when data is received from the payload UART.
  ///   Each time this happens, we pulse LED2 Green.
  static bool led2State = false;
  // If LED2 is off and the ledLinePulse flag is set (done in our payload UART process line function), turn it on Green.
  if (!led2State && ledLinePulse > -1) {
    BF::setLed(2, BF::LED_GREEN);
    led2State = true;
  }
  // If LED2 has been on for LED_ON_TIME_MS, turn it off.
  else if (led2State && ((u_int32_t)uptimeGetMs() - ledLinePulse >= LED_ON_TIME_MS)) {
    BF::setLed(2, BF::LED_OFF);
    ledLinePulse = -1;
    led2State = false;
  }

  /// This section demonstrates a simple non-blocking bare metal method for rollover-safe timed tasks,
  ///   like blinking an LED.
  /// More canonical (but more arcane) modern methods of implementing this kind functionality
  ///   would bee to use FreeRTOS tasks or hardware timer ISRs.
  static u_int32_t ledPulseTimer = uptimeGetMs();
  static u_int32_t ledOnTimer = 0;
  static bool led1State = false;
  // Turn LED1 on green every LED_PERIOD_MS milliseconds.
  if (!led1State && ((u_int32_t)uptimeGetMs() - ledPulseTimer >= LED_PERIOD_MS)) {
    BF::setLed(1, BF::LED_GREEN);
    ledOnTimer = uptimeGetMs();
    ledPulseTimer += LED_PERIOD_MS;
    led1State = true;
  }
    // If LED1 has been on for LED_ON_TIME_MS milliseconds, turn it off.
  else if (led1State && ((u_int32_t)uptimeGetMs() - ledOnTimer >= LED_ON_TIME_MS)) {
    BF::setLed(1, BF::LED_OFF);
    led1State = false;
  }
  /*
    DO NOT REMOVE
    This vTaskDelay delay is REQUIRED for the FreeRTOS task scheduler
    to allow for lower priority tasks to be serviced.
    Keep this delay in the range of 10 to 100 ms.
  */
  vTaskDelay(pdMS_TO_TICKS(10));
}

4 Likes

Thanks for posting this, @rcmahon! Looking forward to working on this more with you!

Z

Stumbled across this, thanks for sharing. This was one of the sensors I was looking to integrate in the not too distant future so thanks a bunch! Once I get there I’ll let you know how I get on!!

1 Like

This is great to see! I realise I’m late to the party but if you’d like more information about the RBRtridente (and the other RBR sensors which all behave the same way) feel free to reach out (I work at RBR!)

1 Like

Hey there @gregjohnson1024 it’s great to have you here to help us with our integrations. I’m a big fan of the Tridente!