Airspeed device - for XCSoar (Texas Instruments CC254x - BLE UART Emulation)

Airspeed device - for XCSoar

Hardware Used

  • Seeed XIAO BLE (nRF52840) - BT LE (Seeed XIAO BLE)
  • 200mA/Hr Li-Ion battery
  • Sensirion SDP31 Differential Pressure Transducer (500Pa = 100km/hr)
  • ASAIR AHT25 Temperature & Humidity Sensor
  • CMT-4023S-SMT-TR Piezzo Buzzer (for battery level notification at powerup)
  • Ancillary components for piezzo (transistor, back EMF diode, resistors)
  • Miniature SPDT slide switch for power/charge

Helpful Bluetooth Low Energy (BLE) Android utility apps

  • BLE Terminal by mightyIT
  • BLE Scanner
  • Bluefruit Connect

Arduino Code

Note the libraries needed!

/*********************************************************************

  FOR USE WITH XCSOAR

  Tools > Board > Arduino MBed OS Boards > Seeed XIAO nRF52840 Sense

  Current draw with BLE paired = 0.V / 93.3Ohms = 6.26mA
  Current draw LED on = 0.V / 93.3Ohms = .mA
  This sketch = 0.69V / 93.3Ohms = 7.4mA
  With sensors = 7.7mA

  25th Nov - Corrected flashLED function
  23rd Nov - Converted LED battery charging delay timing to non-blocking timing.
  15th Nov - Brought battery Voltage sense/threshold values down | Added BLE "Battery Charging" message (Working). Need to add a noDelay for LED flash duration.
  6th Nov - This sketch modified to if test USB power is connected (+5V). Search "D10" & "charging".
  2nd Nov - All working but SDP needs to be checked with a pitot. Chg LED lit when batt connected.
  31st October 22 - Compiles. SDP & AHT not working.

  WORKING WITH BLE AT IntervalIAS = 1250mS | 650mS | 250mS

*********************************************************************/

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

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

#include <HardwareBLESerial.h>
#include <Wire.h> // for I2C
#include <sdpsensor.h> // Sensirion library
#include <AHTxx.h>
#include <avr/dtostrf.h> // for dtostrf

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

// Function Timing
const int IntervalIAS = 551; // number of mS before IAS function repeats and also sends all BLE data
const int IntervalPOL = 500; // number of mS before Poll HardwareBLESerial function repeats
const int IntervalTHM = 4800; // number of mS before Temperature Humidity function repeats
const int IntervalHum = 9700; // number of mS before Humidity function prints RH%
const int IntervalBTV = 9963; // number of mS before Battery Voltage function repeats
const int IntervalLED = 3000; // number of mS before the LED indicating USB plugged in is on
const int LightLED = 500; // millisecs that the LED indicating USB plugged in is on

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

// BLE Services
HardwareBLESerial &bleSerial = HardwareBLESerial::getInstance();

// Dual Pressure Sensor
SDP3XSensor sdp;

// Temperature & Humidity Sensor
AHTxx aht20(AHTXX_ADDRESS_X38, AHT2x_SENSOR); //sensor address, sensor type

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

// Startup Melody Notes & Variables
#define NOTE_C6  1047
#define NOTE_E6  1319
#define NOTE_G6  1568
#define REST      0
int tempo = 90;
int buzzer = 1;
int melody[] = {
  NOTE_C6, 16, NOTE_G6, 16, NOTE_E6, 16, NOTE_C6, 32, NOTE_G6, -16, NOTE_E6, 8,
};
int notes = sizeof(melody) / sizeof(melody[0]) / 2;
int wholenote = (60000 * 4) / tempo;
int divider = 0, noteDuration = 0;

// Airspeed Variables
float diffPressure; // Storage for the differential pressure
float sdpTemp; // Storage for the temperature
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 = 0;      // Variable for reading the Voltage-Divide network on Xiao
float volts = 3.40; // Battery Volts

// Battery Charging Variable
#define charging D10      // Variable for detecting USB plugged in - so battery is charging

// Ambient Environment Variables
float ahtValue; //to store T/Rh result temporarily
float ambTemp = 0;

// NEMA CheckSum Variables
const byte buff_len = 90;
char CRCbuffer[buff_len];

// NEMA pre-defined strings
String cmd = "$POV,"; // OpenVario prefix
char strX[8]; // The sensor value converted to a character
String Y = ""; // The OpenVario sensor type ("A" = Airspeed, "T" = Temperature, etc)
float x = 0; // The sensor value
String delim = ",";
String splat = "*";
String msg = ""; // The final constructed NEMA sentence
String crcNEW = "";

// Function Timing
unsigned long currentMillis = 0;     // stores the value of millis() in each iteration of loop()
unsigned long previousIASMillis = 0;
unsigned long previousPOLMillis = 0;
long previousTHMMillis = 0;
long previousHumMillis = 0;
long previousBTVMillis = 0;
long previousLEDMillis = 0;
bool flashLEDstate = 0;     // used to record whether the LED is on or off


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

void setup()
{

  // Initialize the LED's as outputs and ensure they are off.
  pinMode(LEDR, OUTPUT);
  digitalWrite(LEDR, HIGH);  // ensure the LED off
  pinMode(LEDG, OUTPUT);
  digitalWrite(LEDG, HIGH);  // ensure the LED off
  pinMode(LEDB, OUTPUT);
  digitalWrite(LEDB, HIGH);  // ensure the LED off

  // Setup Xiao Battery Monitoring
  pinMode(P0_31, INPUT);    //Battery Voltage monitoring pin
  pinMode(P0_14, OUTPUT);   //Enable Battery Voltage monitoring pin
  digitalWrite(P0_14, LOW); //Enable

  // Setup Xiao Battery Monitoring ADC
  analogReference(AR_INTERNAL2V4);  //Vref=2.4V
  analogReadResolution(12);         //12bits

  // Setup Charge Monitoring
  pinMode(D10, INPUT);    //USB Voltage divided and fed to D10. Low =< 0.99V, High => 2.31V

  // Setup Xiao Battery Charging Rate
  pinMode(P0_13, OUTPUT);   //Charge Current setting pin
  digitalWrite(P0_13, LOW); //Charge Current = 100mA (13 High = 50mA)

  // Setup Buzzer Output
  pinMode(D1, OUTPUT);    //D1 output to Buzzer

  // Set up HardwareBLESerial and set BLE Name
  bleSerial.beginAndSetupBLE("IAS_probe");

  // Initialise I2C
  Wire.begin();

  // Initialise Serial Monitor (only used for debugging)
  Serial.begin(115200);
  delay(200); // let serial console settle

  // Initialise Sensirion SDP
  int ret = sdp.init();
  if (ret == 0) {
  }
  else {
    while (true) {
      delay(1000);
    }
  }

  // Initialise Asair AHT2x Temp|Humidity Sensor
  while (aht20.begin() != true) {
    delay(200);
  }

  // Startup Melody
  {
    for (int thisNote = 0; thisNote < notes * 2; thisNote = thisNote + 2) {
      divider = melody[thisNote + 1];
      if (divider > 0) {
        // regular note, just proceed
        noteDuration = (wholenote) / divider;
      } else if (divider < 0) {
        // dotted notes are represented with negative durations!!
        noteDuration = (wholenote) / abs(divider);
        noteDuration *= 1.5; // increases the duration in half for dotted notes
      }
      tone(buzzer, melody[thisNote], noteDuration * 0.9);
      delay(noteDuration);
      noTone(buzzer);
    }
    delay(200);
  }

  // Battery SoC Beeps
  adcin = analogRead(P0_31);
  volts = ((510e3 + 1000e3) / 510e3) * 2.4 * adcin / 4096;
  int n;
  // float volts = 4.2; // for testing logic
  {
    if (volts > 3.9) {
      // fully charged ... four beeps
      for (n = 1; n <= 4; n++)
      {
        tone(buzzer, 880, 200);
        delay(400);
      }
    } else if (volts > 3.8) {
      // well charged ... three beeps
      for (n = 1; n <= 3; n++)
      {
        tone(buzzer, 880, 200);
        delay(400);
      }
    } else if (volts > 3.7) {
      // some charge ... two beeps
      for (n = 1; n <= 2; n++)
      {
        tone(buzzer, 880, 200);
        delay(400);
      }
    }
    else {
      // discharged ... one beep
      tone(buzzer, 440, 300);
      delay(400);
    }
  }
  noTone(buzzer);
  delay(100);

}

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

void loop()
{
  currentMillis = millis(); // Capture the latest value of millis()
  pollBLE(); // This function must be called regularly to perform BLE updates
  readIAS(); // This function reads IASsends to buildMSG for BLEprinting
  readTp(); // This function reads ambient Temperature and sends to buildMSG for BLEprinting
  readHm(); // This function prints ambient Humidity and sends to buildMSG for BLEprinting
  readBV(); // This function reads Battery Voltage and sends to buildMSG for BLEprinting
  flashLED(); // This function flashes the LED
  // Note: function 'buildMSG' takes the sensor value, calcs CRC & builds the NEMA sentence. Then BLEprints it.
}


//================ POLL BLESERIAL FUNCTION =====================

// pollBLE+++++++++++
void pollBLE() // Call function to poll BLE
{
  if (currentMillis >= previousPOLMillis + IntervalPOL) // run 'pollBLE' function only once time is up
  {
    previousPOLMillis = currentMillis; // save the time when change was madebleSerial.poll();
    bleSerial.poll(); // this must be called regularly to perform BLE updates
  }
}


//================ SENSOR FUNCTIONS =====================

// 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
  {
    previousIASMillis = currentMillis; // save the time when change was made
    int ret = sdp.readSample();
    if (ret == 0) {
      diffPressure = sdp.getDifferentialPressure();
      sdpTemp = sdp.getTemperature();
    }
    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;
    // OpenVario variables
    x = velkmh; // Airspeed in km/hr. I have set to display in Knots on my XCSoar screen
    Y = "S";
    buildMSG(); // Call function to build the NEMA sentence, then transmit to BLE
  }
}

// readTH+++++++++++
void readTp() // read Temp & Hum and print Temp (only temperature)
{
  if (currentMillis >= previousTHMMillis + IntervalTHM) // run 'readTH' function only once time is up
  {
    previousTHMMillis = currentMillis; // save the time when change was made
    ahtValue = aht20.readTemperature(); //read 6-bytes via I2C, takes 80 milliseconds
    ambTemp = ahtValue;
    // OpenVario variables
    x = ahtValue; // Temperature in degC
    Y = "T";
    buildMSG(); // Call function to build the NEMA sentence, then transmit to BLE
  }
}

// readHm+++++++++++
void readHm() // print Humidity
{
  if (currentMillis >= previousHumMillis + IntervalHum) // run 'readTH' function only once time is up
  {
    previousHumMillis = currentMillis; // save the time when change was made
    ahtValue = aht20.readHumidity(); //read another 6-bytes via I2C, takes 80 milliseconds
    // OpenVario variables
    x = ahtValue; // Humidity RH%
    Y = "H";
    buildMSG(); // Call function to build the NEMA sentence, then transmit to BLE
  }
}

// readBV+++++++++++
void readBV() // read battery Volts
{
  if (currentMillis >= previousBTVMillis + IntervalBTV) // run 'readBV' function only once time is up
  {
    previousBTVMillis = currentMillis; // save the time when change was made
    adcin = analogRead(P0_31);
    volts = ((510e3 + 1000e3) / 510e3) * 2.4 * adcin / 4096;
    // OpenVario variables
    x = volts; // Airspeed in km/hr. I have set to display in Knots on my XCSoar screen
    Y = "V";
    buildMSG(); // Call function to build the NEMA sentence, then transmit to BLE
  }
}

// flashLED+++++++++++
void flashLED() // flash the LED if USB is plugged in
{
  // If the USB power is connected go to next step to send BLE "Charging" message and flash LED's
  if (flashLEDstate == 0) // if the LED is off, wait for the interval to expire before turning it on
  {
    if (currentMillis - previousLEDMillis >= IntervalLED) // time is up, so turn LED on
    {
      // Test to see if USB power is connected (so if battery is charging)
      int value = digitalRead(charging);
      if (value > 0)
      {
        bleSerial.println("Battery is charging"); // BLE Print
        // Serial.println("Battery is charging"); // Serial Print
        digitalWrite(LEDB, LOW);  // LED on
        flashLEDstate = 1;  // Set the LED state to 1 or "on"
        previousLEDMillis += IntervalLED ; // save the time when change was made
      }
    }
  }
  else // ie if LED is on. If on, we must wait for the duration to expire before turning it off
  {
    if (currentMillis - previousLEDMillis >= LightLED) // time is up, so turn LED off
    {
      digitalWrite(LEDB, HIGH);  // LED off
      flashLEDstate = 0;  // Set the LED state to 0 or "off"
      previousLEDMillis += LightLED; // save the time when change was made
    }
  }
}

//================ MESSAGE BUILD FUNCTIONS =====================

// $POV,<type>,<value>,<type>,<value>,...*<checksum>

// buildMSG+++++++++++
void buildMSG()
{
  // OpenVario NEMA Sentence with checksum (XCSoar requires checksum)
  dtostrf(x, 1, 1, strX); // format float value x to string XX
  msg = cmd + Y + delim + strX + splat;
  outputMSG(msg); // Call fuction outputMsg - print the entire message string, and append the CRC
}

// convertToCRC+++++++++++used by 'outputMsg' 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 msg
  byte end_with = 0;      // index of starting char in msg
  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 msg
    //   Serial.println("CRC ERROR");
  }
  return crc;
  // based on code by Elimeléc López - July-19th-2013
}

// outputMSG+++++++++++
void outputMSG(String msg)
{
  msg.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
  }

  msg.concat(crcNEW); // adds crcNEW to end of msg

  //Serial.println(msg); // Print msg

  // Convert string to a character array and BLE Print
  int buff_len = msg.length() + 1;
  char buff_array[buff_len];
  msg.toCharArray(buff_array, buff_len);
  bleSerial.println(buff_array);
}


 

A word about the OpenVario Sentence ($POV,<type>,<value>,<type>,<value>, .....*<Checksum>)

XCSoar can understand and use many different NEMA sentences for receiving sensor input. This includes Bluetooth Serial ('classic'). Classic however uses more power than the newer Bluetooth BLE (Bluetooth Low Energy) system.

For the lowest possible power use with Bluetooth sensor data streaming, I wanted to use BLE. XCSoar however expects true BLE communication, which does not actually have a serial protocol. There does seem to be an exception, this being Nordic Semiconductors' proprietary UART/Serial Port Emulation over BLE.

With Arduino, we use the HardwareBLESerial library to utilise this protocol to send sensor data via BLE.

The NEMA sentence protocol (using BLE communication) I have used is the OpenVario structure. This (to me) seems quite elegant and logical. The prefix is: "$POV", with the rest of the sentence describing the data that is about to follow and then the data itself. The sentence ends with a hex XOR checksum.

Here are the various OpenVario data types we can use and most importantly, that XCSoar can understand:

Airspeed
  S: true airspeed in km/h
  Example: $POV,S,123.45*05

Pressure
  P: static pressure in hPa
  Example: $POV,P,1018.35*39
 
  Q: dynamic pressure in Pa
  Example: $POV,Q,23.3*04
 
  R: total pressure in hPa
  Example: $POV,R,1025.17*35

Temperature (XCSoar displays this as "OAT" - outside air temperature)
  T: temperature in deg C
  Example: $POV,T,23.52*35

Humidity
  H: humidity in %RH
  Example: $POV,H,88*01


Voltage
(XCSoar will not display this if using a phone's GPS sensor - it defaults to phone battery V)
  V: battery voltage in V
  Example: $POV,V,11.99*31        

Vario
  E: TE vario in m/s
  Example: $POV,E,2.15*14