Airspeed device - for XCTrack (Nordic Semiconductor nRF - BLE UART Emulation)

Airspeed device - for XCTrack


Hardware Used

  • Adafruit ItsyBitsy nRF52840 Express - Bluetooth LE (Adafruit BLE)
  • 200mA/Hr Li-Ion battery
  • Sensirion SDP32 Differential Pressure Transducer (125Pa = upto 54km/hr)
  • SHT40 Temperature & Humidity Sensor
  • Minature SPDT slide switch for power/charge
  • Female header socket as a battery charge port
  • A resistor Voltage divider (1/1.4781) across the battery for battery Voltage

Helpful Bluetooth Low Energy (BLE) Android utility apps

  • Serial Bluetooth Terminal
  • BLE Scanner
  • Bluefruit Connect

Arduino Code

Note the libraries needed!

/*********************************************************************
  NOTE: Only works for XCTrack. Not XCSoar.
 
  Current draw with BLE paired = 4.56mA
  Current draw DotStar on full  = 9.43mA
  This sketch = 8.39mA (Note: pulses with DotStar current for watchdog)
*********************************************************************/

//========================================

// ----------LIBRARIES--------------

#include <bluefruit.h>
#include <Adafruit_LittleFS.h>
#include <InternalFileSystem.h>
#include <Adafruit_DotStar.h>
#include <SparkFun_SDP3x_Arduino_Library.h> // http://librarymanager/All#SparkFun_SDP3x
#include <Wire.h>
//#include <Adafruit_TinyUSB.h> // for Serial
#include <avr/dtostrf.h> // for dtostrf
#include "Adafruit_SHT4x.h"

// ---------CONSTANTS---------------

// DotStar
#define NUMPIXELS 1 // There is only one pixel on the board's DotStar
#define DATAPIN 8
#define CLOCKPIN 6

// Function Timing
const int IntervalIAS = 650; // number of mS before IAS function repeats
const int IntervalXCT = 3100; // number of mS before XCTOD function repeats
const int IntervalTHM = 4800; // number of mS before Temperature Humidity function repeats
const int IntervalBTV = 30150; // number of mS before Battery Voltage function repeats
const int IntervalDSR = 5250; // number of mS before DotStar function repeats
const int flashDuration = 85; // millisecs that DotStar is on

// -----------CLASSES---------------

// BLE Services
BLEDfu  bledfu;  // OTA DFU service
BLEDis  bledis;  // device information
BLEUart bleuart; // uart over ble

// Dual Pressure Sensor
SDP3X mySensor; //create an object of the SDP3X class

// Temperature & Humidity Sensor
Adafruit_SHT4x sht4 = Adafruit_SHT4x();

// DotStar
Adafruit_DotStar strip(NUMPIXELS, DATAPIN, CLOCKPIN, DOTSTAR_BRG);

//------------VARIABLES-------------

// Airspeed Variables
float rho; // Density of Air kg/m3 (variable dependent on temperature)
float const abspressure = 101325; // Absolute pressure at Sealevel (Pa)
float const specificgas = 287.058; // Specific gas constant for dry air (J/kg.K)
float const correctionFactor = 1.000000; // Correction factor for Pitot hardware
float velmps = 0; // Calculated Velocity (m/s)
float velkts = 0; // Calculated Velocity (kts)
float velkmh = 0; // Calculated Velocity (km/hr)

// Battery Voltage Variables
int adcin    = A2; // Battery Voltage divider
float adcvalue = 0;
float mv_per_lsb = 3600.0F / 1024.0F; // 10-bit ADC with 3.6V input range
float millivolts = 1111;
int decivolts = 111;

// NEMA CheckSum Variables
const byte buff_len = 90;
char CRCbuffer[buff_len];
// NEMA CheckSum pre-defined strings
String delim = ",";
String splat = "*";
String msg1 = ""; // NEMA sentence
String crcNEW = "";

// XCTOD sentence
String msg2 = ""; // $XCTOD prefix
float ambTemp = 0;
float ambHumi = 0;

// Function Timing
unsigned long currentMillis = 0;     // stores the value of millis() in each iteration of loop()
unsigned long previousIASMillis = 0; // will store last time the LED was updated
unsigned long previousXCTMillis = 0;
unsigned long previousTHMMillis = 0;
unsigned long previousBTVMillis = 0;
unsigned long previousDSRMillis = 0;
int flashDSTbrightness = 0;          // used to record whether the DotStar is on or off

//===================SETUP========================

void setup()
{
  //Serial.begin(115200);
  Wire.begin();

  //#if CFG_DEBUG
  //  // Blocking wait for connection when debug mode is enabled via IDE
  //  while ( !Serial ) yield();
  //#endif

  // Setup the BLE LED to be enabled on CONNECT
  Bluefruit.autoConnLed(true);

  // Config the peripheral connection with maximum bandwidth
  // more SRAM required by SoftDevice
  // Note: All config***() function must be called before begin()
  Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);

  Bluefruit.begin();
  Bluefruit.setTxPower(-12); // Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4
  //Bluefruit.setName(getMcuUniqueID()); // useful testing with multiple central connections or (eg):  Bluefruit.setName("Name");
  Bluefruit.setName("IAStest");
  Bluefruit.Periph.setConnectCallback(connect_callback);
  Bluefruit.Periph.setDisconnectCallback(disconnect_callback);

  // To be consistent OTA DFU should be added first if it exists
  bledfu.begin();

  // Configure and Start Device Information Service
  bledis.setManufacturer("Adafruit Industries");
  bledis.setModel("ItsyBitsy Feather");
  bledis.begin();

  // Configure and Start BLE Uart Service
  bleuart.begin();

  // Set up and start advertising
  startAdv();
  {
    // Sensirion SDP3 Dual Pressure sensor setup
    mySensor.stopContinuousMeasurement();
    if (mySensor.begin() == false)
    {
      while (1); // Do nothing more
    }
    mySensor.startContinuousMeasurement(true, true); // Request continuous measurements with mass flow temperature compensation and with averaging
    //    Serial.println("SDP3x started");
    delay(500);
  }

  {
    // Sensirion SHT4 Temp & Humidity sensor setup
    if (! sht4.begin())
    {
      //Serial.println("Couldn't find SHT4x");
      while (1) delay(1);
    }
    //Serial.println("Found SHT4x sensor");
    //Serial.print("Serial number 0x");
    //Serial.println(sht4.readSerial(), HEX);
    sht4.setPrecision(SHT4X_LOW_PRECISION);
    sht4.setHeater(SHT4X_NO_HEATER);
    delay(500);
  }

  {
    // DotStar setup and powerup LED flash
    strip.begin(); // Initialize pins for output
    strip.show();  // Turn DotStar LED off (off because nothing set)
    // Dotstar flash
    strip.setPixelColor(0, 255, 0, 0); // strip.setPixelColor(pixel#, green, red, blue);
    strip.setBrightness(255); // 0 to 255 full brightness
    strip.show();  // Set the DotStar to the above parameters
    delay(2000); // The DotStar will be lit up for this time
    strip.setBrightness(0); // 0 is 'off'
    strip.show();  // Set the DotStar to the above parameters
  }

}

void startAdv(void)
{
  // Advertising packet
  Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
  Bluefruit.Advertising.addTxPower();
  // Include bleuart 128-bit uuid
  Bluefruit.Advertising.addService(bleuart);
  // Secondary Scan Response packet (optional)
  // Since there is no room for 'Name' in Advertising packet
  Bluefruit.ScanResponse.addName();
  Bluefruit.Advertising.restartOnDisconnect(true);
  Bluefruit.Advertising.setInterval(32, 244);    // in unit of 0.625 ms
  Bluefruit.Advertising.setFastTimeout(30);      // number of seconds in fast mode
  Bluefruit.Advertising.start(0);                // 0 = Don't stop advertising after n seconds
}

//===================LOOP========================

void loop()
{
  currentMillis = millis(); // Capture the latest value of millis()
  readTH(); // This function reads ambient Temp & Humidity
  readBV(); // This function reads Battery Voltage
  readIAS(); // This function reads IAS, calcs CRC & builds the NEMA sentence. Sends via BLE
  buildXCTOD(); // This function builds the XCTOD sentence and sends via BLE
  flashDSTR(); // This function flashes the DotStar as a device heartbeat
}

//================FUNCTIONS=====================

// callback invoked when central connects
void connect_callback(uint16_t conn_handle)
{
  // Get the reference to current connection
  BLEConnection* connection = Bluefruit.Connection(conn_handle);

  char central_name[32] = { 0 };
  connection->getPeerName(central_name, sizeof(central_name));
  //Serial.print("Connected to ");
  //Serial.println(central_name);
}

void disconnect_callback(uint16_t conn_handle, uint8_t reason)
{
  (void) conn_handle;
  (void) reason;
  //Serial.print("Disconnected, reason = 0x"); Serial.println(reason, HEX);
}

// readIAS+++++++++++
void readIAS() // read IAS, append to a NEMA message, add CRC, then print / write to serial / BLE
{
  if (currentMillis - previousIASMillis >= IntervalIAS) // run 'readIAS' function only once time is up
  {
    float diffPressure; // Storage for the differential pressure
    float sdpTemp; // Storage for the temperature

    mySensor.readMeasurement(&diffPressure, &sdpTemp); // Read the measurement
    {
      diffPressure = diffPressure - 0.02;
      if (diffPressure < 0) // If there is a negative pressure set Differential pressure to zero
        diffPressure = 0;
    }
    rho = abspressure / (specificgas * (ambTemp + 273.15)); // we use +273 to convert from Kelvin to Celcius
    velmps = sqrt((2 * diffPressure * correctionFactor) / rho);
    velkts = 1.9438444924574 * velmps;
    velkmh = 3.6 * velmps;

    buildMSG1(); // Call function to build the DigiFly NEMA sentence, then transmit to BLE

    previousIASMillis += IntervalIAS; // save the time when change was made
  }
}

// BUILD MSG1 FUNCTIONS --------------------------------------------------------

// $PDGFTL1,x,y,z,...*

// buildMSG1+++++++++++
void buildMSG1()
{
  // DigiFly NEMA Sentence with checksum
  char strX[8];
  char strY[8];
  float x = velkmh; // Airspeed in km/hr. I have set to display in Knots on my XCTrack screen
  float y = decivolts; // Voltage "330" = '3.3V battery' on XCTrack screen
  String cmd = "$PDGFTL1,,,,";    // a command name
  dtostrf(x, 1, 0, strX); // format float value to string XX
  dtostrf(y, 1, 0, strY); // format float value to string YY
  msg1 = cmd + delim + strX + delim + delim + delim + delim + strY + delim + splat;
  outputMSG1(msg1); // Call fuction outputMsg1 - print the entire message string, and append the CRC
}

// convertToCRC+++++++++++used by 'outputMsg1' function to calculate XOR CRC suffix+++++++
byte convertToCRC(char *buff)
{
  // NMEA CRC: XOR each byte with previous for all chars between '$' and '*'
  char c;
  byte i;
  byte start_with = 0;    // index of starting char in msg1
  byte end_with = 0;      // index of starting char in msg1
  byte crc = 0;

  for (i = 0; i < buff_len; i++) {
    c = buff[i];
    if (c == '$') {
      start_with = i;
    }
    if (c == '*') {
      end_with = i;
    }
  }
  if (end_with > start_with) {
    for (i = start_with + 1; i < end_with; i++) { // XOR every character between '$' and '*'
      crc = crc ^ buff[i] ;  // xor the next char
    }
  }
  else { // else if error, print a msg1
    //   Serial.println("CRC ERROR");
  }
  return crc;
  // based on code by Elimeléc López - July-19th-2013
}

// outputMSG1+++++++++++
void outputMSG1(String msg1)
{
  msg1.toCharArray(CRCbuffer, sizeof(CRCbuffer)); // put complete string into CRCbuffer
  byte crc = convertToCRC(CRCbuffer); // call function to compute the crc value

  if (crc < 16) { // if CRC is 0-F
    String crcHEX = String(crc, HEX); // crc is now in HEX base
    crcNEW = "0";
    crcNEW.concat(crcHEX); // adds a character 0 to the front of crc (if crc is only one character)
  }
  else {
    crcNEW = String(crc, HEX); // crc is now in HEX base
  }

  msg1.concat(crcNEW); // adds crcNEW to end of msg1
  msg1.concat(" \n"); //

  uint8_t buf1[64];          // create a variable 'buf1' of size 64
  msg1.getBytes(buf1, msg1.length() + 1);    // move msg1 characters into 'buf1', for length of msg1
  bleuart.write( buf1, msg1.length() + 0 ); // write 'buf1' characters to BLE, for length of msg1

}

// BUILD MSG2 FUNCTION --------------------------------------------------------

// $XCTOD,x,y,z,.

// buildXCTOD+++++++++++
void buildXCTOD() // Call function to build the XCTrack $XCTOD sentence, then transmit to BLE
{
  if (currentMillis - previousXCTMillis >= IntervalXCT) // run 'buildXCTOD' function only once time is up
  {
    // XCTOD XCTrack Sentence (does not use a checksum)
    char strU[8];
    char strV[8];
    char strW[8];
    float u = ambTemp; // Ambient temperature from Sensiron SHT4
    float v = ambHumi; // Ambient humidity from Sensiron SHT4
    float w = velkts; // Velocity Knots calculated from Sensiron SDP3 Pa
    String cmd = "$XCTOD,"; // a command name
    dtostrf(u, 1, 0, strU); // format float value to string UU
    dtostrf(v, 1, 0, strV); // format float value to string VV
    dtostrf(w, 1, 0, strW); // format float value to string WW
    msg2 = cmd + strU + delim + strV + delim + strW;
    msg2.concat(" \n"); //

    uint8_t buf2[64];          // create a variable 'buf2' of size 64
    msg2.getBytes(buf2, msg2.length() + 1);   // move msg1 characters into 'buf2', for length of msg2
    bleuart.write( buf2, msg2.length() + 0 ); // write 'buf2' characters to BLE, for length of msg2

    previousXCTMillis += IntervalXCT; // save the time when change was made
  }
}

// END OF BUILD MSG'S ---------------------------------------------------------

// readTH+++++++++++
void readTH() // read Temp & Hum
{
  if (currentMillis - previousTHMMillis >= IntervalTHM) // run 'readTH' function only once time is up
  {
    sensors_event_t humidity, temp;
    sht4.getEvent(&humidity, &temp);// populate temp and humidity objects with fresh data
    ambTemp = temp.temperature;
    ambHumi = humidity.relative_humidity;

    previousTHMMillis += IntervalTHM; // save the time when change was made
  }
}

// readBV+++++++++++
void readBV() // read battery Volts
{
  if (currentMillis - previousBTVMillis >= IntervalBTV) // run 'readBV' function only once time is up
  {
    adcvalue = analogRead(adcin);
    millivolts = adcvalue * mv_per_lsb;
    decivolts = millivolts / 10 * 1.4781; // divide by 10 for mV -> dV. x 1.4781 for R divider

    previousBTVMillis += IntervalBTV; // save the time when change was made
  }
}

// flashDSTR+++++++++++
void flashDSTR() // Flash the DotStart blue
{
  if (flashDSTbrightness == 0) // if the DotStar is off, wait for the interval to expire before turning it on
  {
    if (currentMillis - previousDSRMillis >= IntervalDSR) // time is up, so turn DostStar on
    {
      flashDSTbrightness = 255;
      strip.setPixelColor(0, 0, 0, 255); // strip.setPixelColor(pixel#, green, red, blue);
      strip.setBrightness(flashDSTbrightness); // 0 to 255 full brightness
      strip.show();  // Set the DotStar to the above parameters
      previousDSRMillis += IntervalDSR; // save the time when change was made
    }
  }
  else // ie if DotStar is on. If on, we must wait for the duration to expire before turning it off
  {
    if (currentMillis - previousDSRMillis >= flashDuration) // time is up, so turn DotStar off
    {
      flashDSTbrightness = 0;
      strip.setBrightness(flashDSTbrightness); // 0 is 'off'
      strip.show();  // set the DotStar to the above parameters
      previousDSRMillis += flashDuration; // save the time when change was made
    }
  }
}

//========================================

A word about $XCTOD

If XCTrack sees a "$XCTOD,x,y,z,..." sentence and one or more XCTOD labels are configured in the app via the app's config dialogue, then free text data can be displayed on your XCTrack display. In this code I have used this for: temperature, humidity and airspeed.

XCTrack does nothing with this data, it just displays it per your configuration. I use it for standing on launch and looking at the day's conditions pre-flight.