SmartData
SmartData is a data class for storing and controlling data. It is an enhanced version of its underlying type and can be used to control how a variable updates, or trigger other actions when it is updated. For full documentation on SmartData, please see the SmartData Reference Function.
The basics
A SmartData object needs to be created with an underlying data type. This can be float, double, int, uint, short, unsigned short, uint8_t, int8_t, and bool (or pointers to arrays of those types, or pointer to an array of options, both of which will be discussed later).
#include "smartData.h"
SmartData<double> someDouble;
SmartData<uint16_t> someUint16 = 5;
SmartData<bool> someBool = false;
As you can see, the syntax is simply SmartData<dataType>. An object can be simply declared like someDouble is, or it can be initialized to a value like someUint16 and someBool. SmartData objects are typically declared as globals so they can be accessed by multiple functions, and by qCommand. At this point, you can use the SmartData objects just like you would the normal underlying type:
someUint16++;
if (someBool) {
someDouble *= 1.2 + 3*someUint16;
}
At this point the only advantage of using a SmartData object instead of a regular data-type is that when used with qControl, SmartData objects will automatically update the qControl view when their value changes. Regular objects only get updated when adjusted by qControl. So for any variables that get changed programmatically, it's worth using SmartData objects if you are using qControl. However, by using a SmartData object is other functionality we can take advantage:
Controlling Updates
One key feature of SmartData objects is that we can limit how they get updated. The simplest is a helper function setLimits which lets us set a min and max value for an object. Any attempt to write a value outside of that range will be coerced into range. Here's an example
Serial.print(Data); // prints 10SmartData<int16_t> Data = 34; //initialize SmartData scalar with type int16_t and value of 34
Data.setLimits(10,25); // Set range from 10 to 25.
Serial.print(Data); // prints 25 as data was coerced into range
Data = 2; //out of range, will set to 10.
Serial.print(Data); // prints 10
Data = 15; //normal setting of Data
Serial.print(Data); // prints 15
Data.set(33); //out of range, will set to 25.
Serial.print(Data); // prints 25
Sometimes, you may want something more complex than a simple range limits. In this case, you can write your own function that controls updates to a SmartData object. This can be done with the setSetter function which sets a function that gets called whenever a SmartData object is updated. Note that setSetter replaces setLimits and vice versa -- whichever was called most recently will be active. Here's an example that limits how much a SmartData object can change at one time:
uint16_t limitRateOfChange(uint16_t input, uint16_t oldValue) {
//only allow changes of 10 or less, otherwise limit the update to that change
if ( input < oldValue) {
//value decreased
if ((oldValue - input) > 10) { //changed too much, limit to change of -10.
return oldValue - 10;
}
} else {
if ((input - oldValue) > 10) { //changed too much, limit to change of 10.
return oldValue + 10;
}
}
return input;
}
SmartData<uint16_t> Data(5); //initialize SmartData scalar with type uint16_t and value of 5
Data.setLimits(4,10); //set limits but this will be overwritten
Data.setSetter(limitRateOfChange); //use limitRateOfChange instead of setLimits to control updates to Data
Data = 30; // tries to set data to 30, but is limited by limitRange function only increase by 10 to 15
Serial.print(Data); // prints 15
Data.set(12); // set data to 12
Serial.print(Data); // prints 12
Data = 0; // tries to set data to 0, but is limited to decrease by 10 to 2
Serial.print(Data); // prints 2
Run on Update
Another feature of SmartData is the ability to run a function when a SmartData's value changes. This can be useful for pre-calculating some values and having them updated only when the inputs change. In the example below, we want to rescale the ADC Input 1 data into a noise density with units of and we need to divide by the square root of the bandwidth. However, since the bandwidth can change, this scaling value isn't constant. The simplest solution is to re-calculate the square root every time we got new data, but this which is very inefficient. Instead we can just redo the square root calculation when the bandwidth value changes. Here's the example:
SmartData<double> Bandwidth = 100;
double sqrtBandwidth;
void setup() {
Bandwidth.runOnUpdate(calcSqrt);
}
//This function only called when Bandwidth changes
void calcSqrt(void) {
sqrtBandwidth = sqrt(Bandwidth);
}
void getADC1(void) {
// this is a fast function that is called often that uses sqrtBandwidth but
// does not need to calculate the square root on each run
double adc1 = readADC1_from_ISR(); //store ADC1 voltage measurement
writeDAC1(adc1 / sqrtBandwidth);
}
Now whenever Bandwidth changes, either via user-input or programmatically, the calcSqrt function will run and the value of the sqrtBandwidth will update.
Arrays
SmartData can be used not just for single data types, but also for arrays. Using SmartData is necessary for qControl, however it also provides some helper functions to make loading data into an array easier. Setting up SmartData for an array is slightly different as you first need to create the underlying store (the array itself) and then create a SmartData object that references that storage array. Here's how its done:
float dataArray[500];
SmartData<float*> Data(dataArray); //SmartData array of floats to hold the data
Because the SmartData object just points to the underlying data, you can read/write to the data either through the SmartData object or directly to the array. Below are some examples of equivalent operations:
//Get the 5th element
Data.get(5);
dataArray[5]
//Write to the 7th element:
Data.set(123.45, 7);
dataArray[7] = 123.45;
However, SmartData has some additional functionality. For one, it has a setNext function that writes to the next element in the array and stops when the array is full. Here's an example:
int16_t dataArray[5];
SmartData<int16_t*> Data(dataArray); //SmartData array of floats to hold the data
Data.setNext(-5); //sets 0th element to -5
Data.setNext(-4); //sets 1st element to -4
Data.setNext(-3); //sets 2nd element to -3
Data.setNext(-2); //sets 3rd element to -2
Data.setNext(-1); //sets 4th element to -1
Data.setNext(0); //does nothing as the array is full
This makes writing sequential data to the array simpler and avoids worrying about writing to out of bounds as writing to dataArray[5] would do. Additionally, when setNext writes the last available data element, it tells qControl that the array is full and qControl will then read the full array and update the display. This update will reset the array index, so subsequently calling setNext will write the 0th element. So your code can call setNext over and over and once the array is full, it will skip the update until qControl has read the full array and then it will start over and get more updates. So running setNext every time new ADC data comes in will result in qControl plotting the ADC data as it comes in. Any data that comes if after the array is full but before the array is read by qControl will get skipped.
There are also helper functions isEmpty and isFull and more advanced functions documented in SmartData for Arrays Reference Function.
An Array of Options
There is one other type of data the SmartData supports: an array of options. This is a bit analogous to enums in C/C++ and lets you program a set of allowed values for a SmartData object. Additionally, each option can have a name, which is used in qControl. Here's a quick example:
Option<uint8_t> ServoStateOptions[] {
{0, "Ramp"},
{1, "Unlock"},
{2, "Lock"}
};
SmartData<Option<uint8_t>*> servoState(ServoStateOptions);
void setup(void) {
qC.assignVariable("Mode", &servoState);
}
Now the SmartData object servoState can only be set to 0, 1 or 2. But in qControl, we will control those states by their names Ramp, Unlock, and Lock:
In addition, when setting the value of servoState, either with qControl, or with Serial Commands or in your program, any invalid values will be ignored:
servoState = 1; //servoState set to Unlock
servoState = 3; //invalid value, servoState will not update. Still set to unlock