Adapting the XCSoar Airspeed coding for use with TotalVario

 Airspeed device - using with TotalVario

Hardware Used

  • Same hardware as used in my November 2022 blog post.

Preamble 

Fortuitously I stumbled upon TotalVario ('TV'): https://totalvario.blogspot.com

The author best explains this very clever Android phone app as follows:


Total Vario is a non-commercial flight instrument app for hang glider and paraglider pilots.


The app was originally programmed for more than 3 years just for fun and personal use as a hang gliding instrument. The main aim was to provide all the essential information on one screen, with the best possible visibility and readability on a standard 5'' to 6'' smartphone, and to minimise the need for manual intervention in flight.

 
Over and above this description, a key feature is the care taken to ensure wind strength and direction is reported out as accurately as possible. For those HG pilots landing out in mountain valleys, surely a great feature!


Arduino Code Changes

  • Uses the Compass C-Probe NEMA (like) sentence structure
  • Only sends out pitot pressure-differential. TV calculates airspeed TAS
  • Faster transmit rates to ensure fastest possible Wind direction/speed calcs
 
/*********************************************************************

FOR USE WITH TotalVario

Tools > Board > Arduino MBed OS Boards > Seeed XIAO nRF52840
XIAO BLE Arduino device driver bug in v2.7.2
See: https://forum.seeedstudio.com/t/xiao-ble-sense-mbed-2-7-2-battery-charge-and-voltage-monitor-analogread-p0-31-does-not-work/266438

04 Jan - Reduced IntervalSDP to 199mS (~5 refreshes per second).
03 Jan - Fixed values working with test Xiao. Next step is to load on to actual device.
02 Jan - Sentence values now in deci numbers. Battery Voltage correct for both Volts & Battery %. TODO: Convert values into Hex.
01st Jan 23 - Modifiying sketch to use Compass PC-Probe NEMA Sentence. Commented out sensor reads so test XIAO can be used.

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

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

// ----------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 IntervalSDP = 199; // number of mS before SDP 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;

// Sensirion Differential Pressure Variables
float sdpValue; // Variable for the differential pressure
int16_t sdpPres; // Variable for differential pressure x 10

// Battery Voltage Variables
int adcin = 0;      // Variable for reading the Voltage-Divide network on Xiao
float volts = 3.40; // Variable for Battery Volts
int devBatt = 0; // Variable for Battery SOC as a %. Uses polynomial equation to approximate

// Battery Charging Variable
#define charging D10      // Variable for detecting USB plugged in, ie: Battery is charging

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

// NEMA pre-defined strings
String cmd = "$PCPROBE,T,,,,,,,,"; // PCPROBE prefix

int16_t Val = 15;
//String valFormatted = "";

String strW; // The sensor value converted to a character
String strX; // The sensor value converted to a character
String strY; // The sensor value converted to a character
String strZ; // The sensor value converted to a character

String C = "C"; // Variable to specify if Battery is charging
String delim = ",";
String msg = ""; // The final constructed NEMA sentence

// Function Timing
unsigned long currentMillis = 0;     // stores the value of millis() in each iteration of loop()
unsigned long previousSDPMillis = 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("SDP_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 > 4.0) {
    // 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
readSDP(); // This function reads SDPsends 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 & 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 =====================

// readSDP+++++++++++
void readSDP() // read SDP, append to a NEMA message, add CRC, then print / write to serial / BLE
{
if (currentMillis >= previousSDPMillis + IntervalSDP) // run 'readSDP' function only once time is up
{
  previousSDPMillis = currentMillis; // save the time when change was made
  int ret = sdp.readSample();
  if (ret == 0)
  {
    sdpValue = sdp.getDifferentialPressure();
  }
  sdpValue = sdpValue - 0.02;
  //sdpValue = 149.9; // temp line for testing only
  if (sdpValue < 0) {// If there is a negative pressure set Differential pressure to zero
    sdpValue = 0;
  }
  sdpPres = sdpValue * 10;  // multiply to remove decimal point
  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
  //ahtValue = 24.9; // temp line for testing only
  ambTemp = ahtValue * 10;  // multiply to remove decimal point
}
}

// 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
  //ahtValue = 88.1; // temp line for testing only
  ambHumd = ahtValue * 10; // multiply to remove decimal point
}
}

// 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;
  devBatt = -4370 + (2105 * volts) - (247.5144 * sq(volts)); // approximates battery SOC from Volts. Uses quadratic to approx.
  //Serial.println(devBatt); // Print volts for debugging
  if (devBatt < 0) // If battery Voltage is less than 0% set devBatt to 0%
  {
    devBatt = 0;
  }
  if (devBatt > 100) // If battery Voltage is more than 100% set devBatt to 100%
  {
    devBatt = 100;
  }
  //Serial.println(devBatt); // Print devBatt % for debugging
  //Serial.println(volts); // Print volts for debugging
  //devBatt = 100; // temp line for testing only
}
}

// flashLED+++++++++++
void flashLED() // flash the LED and write "C" to NEMA sentence 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); // For debugging - uncomment for use
    //int value = 1.0; // For debugging - delete/comment for use
    if (value > 0)
    {
      C = "C,";
      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
    C = " ,"; // Not charging is no character (empty space) Note!: Small bug in TV. Need a " " until next TV rev released.
    // c = '\0'; // Not charging is no character (empty space)
  }
}
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 FUNCTIONS =====================

// $$PCPROBE,T,Q0,Q1,Q2,Q3,ax,ay,az,temp,rh,batt,delta_press,abs_press,C,
// NOTE 1: No checksum used!
// NOTE 2: Sensor values are all now integers through x10 multiplication.

// buildMSG+++++++++++
void buildMSG()
{
// PC-Probe NEMA Sentence
Val = ambTemp; // submit the value to the variable: "Val"
strW = hexMSG(); // pass the value to function: hexMSG. Converts value from decimal to hexadecimal
Val = ambHumd; // submit the value to the variable: "Val"
strX = hexMSG(); // pass the value to function: hexMSG. Converts value from decimal to hexadecimal
Val = devBatt; // submit the value to the variable: "Val"
strY = hexMSG(); // pass the value to function: hexMSG. Converts value from decimal to hexadecimal
Val = sdpPres; // submit the value to the variable: "Val"
strZ = hexMSG(); // pass the value to function: hexMSG. Converts value from decimal to hexadecimal
msg = cmd + strW + delim + strX + delim + strY + delim + strZ + delim + delim + C; // concat
outputMSG(msg); // Call fuction outputMsg - print the entire message string, and append the CRC
}

// hexMSG+++++++++++
String hexMSG()
{
// int16_t Val = 15;
String valFormatted = "";
String valHex = "";
// Convert integer sensor values to Base Hex ASCII in Uppercase
if ((Val >= 0) && (Val < 16))  // if value is 0000-000F
{
  valHex = String(Val, HEX); // value is now in HEX base
  valFormatted = "000";
  valFormatted.concat(valHex); // adds 000 to the front of valHex (if valHex is only one character)
}
if ((Val >= 16) && (Val < 256))  // if value is 0010-00FF
{
  valHex = String(Val, HEX); // value is now in HEX base
  valFormatted = "00";
  valFormatted.concat(valHex); // adds 00 to the front of valHex (if valHex is only one character)
}
if ((Val >= 256) && (Val < 4096)) // if value is 0100-0FFF
{
  valHex = String(Val, HEX); // value is now in HEX base
  valFormatted = "0";
  valFormatted.concat(valHex); // adds 00 to the front of valHex (if valHex is only one character)
}
if (Val >= 4096)  // if value is 0100-0FFF
{
  valFormatted = String(Val, HEX); // value is now in HEX base
}
if ((Val >= -4096) && (Val < 0)) // if value is negative
{
  valFormatted = String(Val, HEX); // value is now in HEX base
  valFormatted.remove(0, 4); // remove the first 4 hex characters of the negative number
}
// The variable is now a string of this format "abcd"
valFormatted.toUpperCase();
// The variable is now a string of this format "ABCD"
return valFormatted;
}

// outputMSG+++++++++++
void outputMSG(String msg)
{
//Serial.println(msg); // Print msg for testing

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


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

C-Probe output strings

$PCPROBE,T,Q0,Q1,Q2,Q3,ax,ay,az,temp,rh,batt,delta_press,abs_press,C,

NOTE: No CheckSum used

• ”T” after “$PCPROBE” indicates that the string contains data.
Data are represented as signed, 16-bit hexadecimal integers.
The only exception is abs_press which is in signed 24-bits hex format.

• Q0, Q1, Q2, Q3: 3D orientation of the C-Probe in quaternion format. Heading, pitch, and roll can
be calculated as follows:
q0 = Q0 * 0.001;
q1 = Q1 * 0.001;
q2 = Q2 * 0.001;
q3 = Q3 * 0.001;
sin_pitch = -2 * (q0 * q2 - q3 * q1);
If sin_pitch > 1 or sin_pitch < -1, discard the data
pitch = asin(sin_pitch);
heading = M_PI + atan2(2*(q1 * q2 + q3 * q0), q3 * q3 - q0 * q0 - q1 * q1 + q2 * q2);
roll = atan2( 2 * (q0 * q1 + q3 * q2), q3 * q3 + q0 * q0 - q1 * q1 - q2 * q2);

• ax, ay, az: x, y, z components of the acceleration in units of 0.001 g.

• temp: temperature in units of 0.1°C.

• rh: relative humidity in units of 0.1%.

• batt: battery level from 0 to 100%.

• delta_press: differential pressure (dynamic – static) in units of 0.1 Pa.

• abs_press: absolute pressure in units of 1/400 Pa

C: is transmitted only if the C-Probe is being charged.
In this case, heat produced by the charging process is likely to affect the readings of the temperature and humidity sensors.

Example:

$PCPROBE,T,FD92,FF93,00D9,FD18,017E,FEDB,0370,0075,00D6,0064,001C,000000,,
(q0, q1, q2, q3) = (-0.622, -0.109, 0.217, -0.744)
(ax, ay, az) = (0.382, -0.293, 0.880)
Temp = 11.7°C
RH = 21.4%
Batt = 100%
Differential pressure dp = 2.8 Pa. For an air density d=1.2 kg/m3 (at air level, for example) the speed is
sqrt(2 dp / d) = 2.16 m/s = 7.78 km/h.
Absolute pressure = 0 (the absolute pressure sensor has to be enabled via software, see below)
Not charging.

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