Adding a New Sensor
Adding a new sensor is easy! Here is a summary of the steps:
- Create an SFE_QUAD_Sensor_NewSensorName.h file for it, using the existing files as a template
- You will find the individual sensor header files in the library src/src sub-folder
- Edit src/SFE_QUAD_Sensors.h :
- Add a #define INCLUDE_SFE_QUAD_SENSOR_NewSensorName for the new sensor - in case the user wants to select which sensors to include
- Add a new entry for the sensor in enum SFEQUADSensorType (3 lines for each new sensor)
- Add a new entry for the sensor in SFE_QUAD_Sensor *sensorFactory (4 lines for each new sensor)
- Add a new entry for the sensor in void deleteSensor(SFE_QUAD_Sensor *sensor, SFEQUADSensorType type) (4 lines for each new sensor)
- Edit src/SFE_QUAD_Headers.h :
- Add a new entry for the sensor (3 lines for each new sensor)
- Edit .github/workflows/compile-sketch.yml :
- Add a - name: Update NewSensorName entry for the new sensor
- This automates copying the Arduino Library files into the src/src sub-folder
- The latest versions of the library .h. and .cpp will be copied and added to a GitHub pull request automatically
That's all folks!
A description of each step is included below:
SFE_QUAD_Sensor_NewSensorName.h
The SFE_QUAD_Sensor_NewSensorName.h file is the interface between the SparkFun Qwiic Universal Auto-Detect SFE_QUAD_Sensor class and the underlying Arduino Library for that sensor.
One of the requirements for SparkFun Qwiic Universal Auto-Detect was to be able to use the existing Arduino Library for any sensor as-is and without modification. The header file provides a standard set of methods to allow the sensor's senses to be: configured; enabled; and read.
You can use the existing SFE_QUAD_Sensor_SensorName.h files as a template for the new sensor.
If you are adding support for a 'simple' sensor which has no settings and requires no configuration, then the src/src/BME280 header file is a good choice.
For a more complex sensor, the src/src/VL53L1X header file is a good starting point.
Compiler Guard
The first two lines of the header file are a compiler guard. They prevent the file from being included more than once when the code compiles.
The name used in the #ifndef
and #define
must be globally unique.
Replace the (e.g.) BME280 with the name of your new sensor. If you are adding a new sensor called FOO, the first two lines become:
#ifndef SPARKFUN_QUAD_SENSOR_HEADER_FOO_H // <=== Update this with the new sensor type
#define SPARKFUN_QUAD_SENSOR_HEADER_FOO_H // <=== Update this with the new sensor type
Header File Name
Now is a good time to save the header file using its new name. In this case the file will be saved as: src/src/SFE_QUAD_Sensor_FOO.h
#include the Sensor Library Header file
Line 4 includes the header file of the Arduino Library for the sensor.
The Arduino Library files will be copied into a sub-folder called src/src/FOO. You can, if you wish, create the FOO sub-folder and add the library .h and .cpp files manually. This will allow the code to compile while you are testing it. However, the .github/workflows/compile-sketch.yml file will do this for you automatically once you have edited it. compile-sketch.yml also ensures that the copy of the Arduino Library stays up to date. Any changes to the main Arduino Library are automatically merged into this library whenever changes are pushed. See below for details.
Change line 4 so it will include the library header file correctly.
#include "FOO/SparkFun_FOO_Arduino_Library.h" // <=== Update this with the new sensor library header file
!!! note: Always enclose the include file folder and name in double quotes. Do not use less-than and greater-than. This ensures that the copy of the library in the src/src/FOO sub-folder is included, not a copy pointed to by the Arduino IDE path.
CLASSNAME and CLASSTITLE
CLASSNAME is the name of the sensor class as defined in its Arduino Library. Open the Arduino Library header file to find the name of the class. Copy and paste the name into the #define CLASSNAME.
CLASSTITLE is how the sensor class is referred to within Qwiic Universal Auto-Detect. Take the CLASSNAME and prefix it with SFE_QUAD_Sensor_ to form the CLASSTITLE:
#define CLASSNAME FOO // <=== Update this with the new sensor type
#define CLASSTITLE SFE_QUAD_Sensor_FOO // <=== Update this with the new sensor type
SENSE_COUNT
The SENSE_COUNT is the number of senses this sensor has. A sensor can have more than one sense. E.g. the BME280 has three: Pressure, Temperature and Humidity. Update the SENSE_COUNT with the number of senses:
SETTING_COUNT and CONFIGURATION_ITEM_COUNT
The SETTING_COUNT is the number of things which can be set on this sensor. Settings are set one at a time via the built-in setting menu settingMenu
.
Sensors can also have one or more Configuration Items. These are Settings which need to be stored in storage media (SD, EEPROM, LittleFS) so they can be read and applied easily.
Settings are applied manually and individually via the settingMenu. Configuration Items are applied all together with a single call of applySensorAndMenuConfiguration
.
Settings are usually also Configuration Items, but not always. The number of Settings is usually equal to, or greater than, the number of Configuration Items, but not always.
A simple sensor, like the BME280, has zero settings and zero configuration items.
A more complex sensor will have one or more settings and configuration items.
Change the definitions to match the new sensor:
#define SETTING_COUNT 0 // <=== Update this with the number of things that can be set on this sensor
#define CONFIGURATION_ITEM_COUNT 0 // <=== Update this with the number of things that can be configured on this sensor
Settings vs. Configuration Items
The VL53L1X has five settings:
- Distance Mode: Short
- Distance Mode: Long
- Intermeasurement Period
- Crosstalk
- Offset
But it only has four configuration items requiring storage:
- Distance Mode
- Intermeasurement Period
- Crosstalk
- Offset
We do it this way so that the user can change the distance mode with a single key press.
We could have used a single distance mode BOOL
setting, representing Short vs. Long, but:
- The user would have had to select the distance mode setting
- Then enter a valid
BOOL
(0 or 1) - The code in
setSetting
would have had to validate the choice before applying it
By using two NONE
choices, we both make things easier for the user and simplify the code.
This is discussed again in setSetting below.
SENSOR_I2C_ADDRESSES
Sensors usually only have one I2C address, but sometimes can have multiple addresses.
SENSOR_I2C_ADDRESSES is an array containing all the valid addresses for this sensor.
detectSensors
will check each address consecutively when detecting which senors are attached.
Update SENSOR_I2C_ADDRESSES with the valid addresses for the new sensor:
#define SENSOR_I2C_ADDRESSES const uint8_t sensorI2cAddresses[] = {0x76, 0x77} // <=== Update this with the I2C addresses for this sensor
detectSensor
At the simplest level, we can detect if a sensor is attached by checking if its I2C address is acknowledged.
A simple I2C port scanner will scan through all valid addresses checking for an acknowledgement:
for (int i = 1; i < 127; i++)
{
port.beginTransmission(i);
if (port.endTransmission() == 0)
{
Serial.print("Something detected at address 0x");
Serial.println(i, HEX);
}
}
However:
- Getting an acknowledgement does not tell us what type of sensor was detected at that address, just that something was detected
- The
beginTransmission
+endTransmission
test can cause some sensors to produce errors
A better way is to use the sensor's begin
method. However, the begin
can sometimes take a long time to complete if no sensor is connected.
For the BME280, we use the beginTransmission
+ endTransmission
test as it gives a fast indication of whether a device is connected (beginI2C
is slow if nothing is connected).
Followed by its beginI2C
method for full confidence that we are detecting a BME280.
The BME280 has two valid I2C addresses, so we need to tell the code which address to use with the device->setI2CAddress(sensorAddress);
.
// Detect the sensor. ===> Adapt this to match the sensor type <===
bool detectSensor(uint8_t sensorAddress, TwoWire &port)
{
port.beginTransmission(sensorAddress); // Scan the sensor address first. beginI2C takes a long time if no device is connected
if (port.endTransmission() == 0)
{
CLASSNAME *device = (CLASSNAME *)_classPtr;
device->setI2CAddress(sensorAddress);
return (device->beginI2C(port));
}
else
return (false);
}
The VL53L1X's detectSensor
is slightly simpler, but again we use the beginTransmission
+ endTransmission
test for speed:
// Detect the sensor. ===> Adapt this to match the sensor type <===
bool detectSensor(uint8_t sensorAddress, TwoWire &port)
{
port.beginTransmission(sensorAddress); // Scan the sensor address first
if (port.endTransmission() == 0)
{
CLASSNAME *device = (CLASSNAME *)_classPtr;
return (device->begin() == 0);
}
else
return (false);
}
Adapt the template code to match the new sensor.
beginSensor
Sensors almost always require their begin
method to be called before communication can take place. Again, you should adapt the template code to match the new sensor.
The VL53L1X's begin
method returns 0 when the device is begun successfully, so its beginSensor
is:
// Begin the sensor. ===> Adapt this to match the sensor type <===
bool beginSensor(uint8_t sensorAddress, TwoWire &port)
{
CLASSNAME *device = (CLASSNAME *)_classPtr;
return (device->begin() == 0);
}
The BME280 returns true
when successful, so its beginSensor
is:
// Begin the sensor. ===> Adapt this to match the sensor type <===
bool beginSensor(uint8_t sensorAddress, TwoWire &port)
{
CLASSNAME *device = (CLASSNAME *)_classPtr;
device->setI2CAddress(sensorAddress);
return (device->beginI2C(port));
}
The sensor has already been detected, so you do not need to 'scan' the address again, but you do need to call begin
again here.
The begin
in detectSensor
is 'lost' as the sensor has not been added to the linked-list of sensors at that point.
initializeSensor
Each sensor is initialized - if required - when mySensors.initializeSensors();
is called.
Some sensors - like the BME280 - don't require initialization. So its initializeSensor
is essentially 'empty'. Unless a custom initializer
has been defined (see Example4), it simply returns true
. If a custom initializer has been defined, then that is called before returning true
.
// Initialize the sensor. ===> Adapt this to match the sensor type <===
bool initializeSensor(uint8_t sensorAddress, TwoWire &port)
{
if (_customInitializePtr == NULL) // Has a custom initialize function been defined?
{
return (true);
}
else
{
_customInitializePtr(sensorAddress, port, _classPtr); // Call the custom initialize function
return (true);
}
}
The VL53L1X does require initialization. As a minimum, we need to set the distance mode and instruct it to startRanging
:
// Initialize the sensor. ===> Adapt this to match the sensor type <===
bool initializeSensor(uint8_t sensorAddress, TwoWire &port)
{
if (_customInitializePtr == NULL) // Has a custom initialize function been defined?
{
CLASSNAME *device = (CLASSNAME *)_classPtr;
if (_shortDistanceMode)
device->setDistanceModeShort();
else
device->setDistanceModeLong();
device->startRanging();
return (true);
}
else
{
_customInitializePtr(sensorAddress, port, _classPtr); // Call the custom initialize function
return (true);
}
}
_shortDistanceMode
is an extra bool member variable we've added to the sensor class. It is initialized to true
when the sensor object is created and added to the linked list:
bool _shortDistanceMode;
CLASSTITLE(TwoWire &port)
{
_sensorAddress = 0;
_muxAddress = 0;
_muxPort = 0;
_classPtr = new CLASSNAME(port);
_next = NULL;
_logSense = new bool[SENSE_COUNT + 1];
for (size_t i = 0; i <= SENSE_COUNT; i++)
_logSense[i] = true;
_customInitializePtr = NULL;
_shortDistanceMode = true;
}
_shortDistanceMode
records or indicates whether the sensor is in short or long distance mode.
Why did we do it this way? Couldn't we have used a BOOL
configuration item for it instead? Yes, we could have done it that way, but, as we explained above:
- The user would have had to select the distance mode setting
- Then enter a valid
BOOL
(0 or 1) - The code in
setSetting
would have had to validate the choice before applying it
By using two NONE
choices, and storing the choice in _shortDistanceMode
, we both make things easier for the user and simplify the code.
getSenseName
getSenseName
returns the name of each sense as const char *
as it will appear in loggingMenu
.
The number of case
statements must match SENSE_COUNT. Adapt the template code
to match the number of senses for the new sensor; add or remove case
statements as necessary.
// Return the name of the name of the specified sense. ===> Adapt this to match the sensor type <===
const char *getSenseName(uint8_t sense)
{
switch (sense)
{
case 0:
return ("Pressure (Pa)");
break;
case 1:
return ("Temperature (C)");
break;
case 2:
return ("Humidity (%)");
break;
default:
return (NULL);
break;
}
return (NULL);
}
getSenseReading
getSenseReading
is the method which calls the appropriate 'read' method for the selected sense
.
It is called by getSensorReadings
.
getSenseReading
calls the Arduino Library method to read that sense and converts the reading into text format:
// Return the specified sense reading as text. ===> Adapt this to match the sensor type <===
bool getSenseReading(uint8_t sense, char *reading)
{
CLASSNAME *device = (CLASSNAME *)_classPtr;
switch (sense)
{
case 0:
_sprintf._dtostrf((double)device->readFloatPressure(), reading); // Get the pressure
return (true);
break;
case 1:
_sprintf._dtostrf((double)device->readTempC(), reading); // Get the temperature
return (true);
break;
case 2:
_sprintf._dtostrf((double)device->readFloatHumidity(), reading); // Get the humidity
return (true);
break;
default:
return (false);
break;
}
return (false);
}
Again, the number of case
statements must match SENSE_COUNT. And, of course, the order of the case
statements must
be the same as getSenseName
.
Looking closely at the code for the Pressure (sense 0):
_sprintf._dtostrf((double)device->readFloatPressure(), reading);
The code is:
- Calling the Arduino Library
readFloatPressure()
method, using the_classPtr
- The result is being cast to double
_sprintf._dtostrf
is a helper function from theSFE_QUAD_Sensors_sprintf
class which converts the double to textsprintf
is not supported correctly on all platforms (Artemis / Apollo3 especially) so we added the helper method to the sensor class to ensure doubles are always converted to text correctly
- The text is copied into the char array
reading
If you like exponent-format, there is an additional helper function named _sprintf._etoa
which will convert a double to exponent-format text.
getSensorReadings
pieces the text readings together in CSV format and retruns them in readings
.
If the sense methods return an integer (instead of float or double), then getSensorReadings
does use sprintf
to print the reading as text:
// Return the specified sense reading as text. ===> Adapt this to match the sensor type <===
bool getSenseReading(uint8_t sense, char *reading)
{
CLASSNAME *device = (CLASSNAME *)_classPtr;
switch (sense)
{
case 0:
sprintf(reading, "%d", device->getDistance());
return (true);
break;
case 1:
sprintf(reading, "%d", device->getRangeStatus());
return (true);
break;
case 2:
sprintf(reading, "%d", device->getSignalRate());
return (true);
break;
default:
return (false);
break;
}
return (false);
}
getSettingName
Simple sensors, like the BME280, have no settings or configuration items. getSettingName
simply returns NULL
.
// Return the name of the name of the specified setting. ===> Adapt this to match the sensor type <===
const char *getSettingName(uint8_t setting)
{
switch (setting)
{
default:
return (NULL);
break;
}
return (NULL);
}
For the VL53L1X, getSettingName
returns the name of each setting as it will appear in settingMenu
:
// Return the name of the name of the specified setting. ===> Adapt this to match the sensor type <===
const char *getSettingName(uint8_t setting)
{
switch (setting)
{
case 0:
return ("Distance Mode: Short");
break;
case 1:
return ("Distance Mode: Long");
break;
case 2:
return ("Intermeasurement Period");
break;
case 3:
return ("Crosstalk");
break;
case 4:
return ("Offset");
break;
default:
return (NULL);
break;
}
return (NULL);
}
The number of case
statements must match SETTING_COUNT.
getSettingType
getSettingType
returns the SFE_QUAD_Sensor_Setting_Type_e
data type for the setting.
If the sensor has no settings (SETTING_COUNT is zero), then getSettingType
simply returns false:
// Return the type of the specified setting. ===> Adapt this to match the sensor type <===
bool getSettingType(uint8_t setting, SFE_QUAD_Sensor_Setting_Type_e *type)
{
switch (setting)
{
default:
return (false);
break;
}
return (true);
}
But if there are settings, it returns the data type which matches the type required by the Arduino Library setting method:
// Return the type of the specified setting. ===> Adapt this to match the sensor type <===
bool getSettingType(uint8_t setting, SFE_QUAD_Sensor_Setting_Type_e *type)
{
switch (setting)
{
case 0:
case 1:
*type = SFE_QUAD_SETTING_TYPE_NONE;
break;
case 2:
case 3:
case 4:
*type = SFE_QUAD_SETTING_TYPE_UINT16_T;
break;
default:
return (false);
break;
}
return (true);
}
The SFE_QUAD_Sensor_Setting_Type_e
setting types are:
SFE_QUAD_SETTING_TYPE_NONE
SFE_QUAD_SETTING_TYPE_BOOL
SFE_QUAD_SETTING_TYPE_FLOAT
SFE_QUAD_SETTING_TYPE_DOUBLE
SFE_QUAD_SETTING_TYPE_INT
SFE_QUAD_SETTING_TYPE_UINT8_T
SFE_QUAD_SETTING_TYPE_UINT16_T
SFE_QUAD_SETTING_TYPE_UINT32_T
All except NONE
are self explanatory. If the Arduino Library setting method requires an int
then set *type
to SFE_QUAD_SETTING_TYPE_INT
. Etc..
If the setting type is anything other than NONE
, the settingMenu
will call getSettingValueDouble
and cast the result to the appropriate type before calling setSetting
.
The NONE
type has no value, it simply causes the menu to do something when that menu option is selected.
Looking at the code above, the NONE
type is used for settings 0 and 1: "Distance Mode: Short" and "Distance Mode: Long".
The matching code in setSetting
then does something without needing a value from getSettingValueDouble
.
For the other three cases, a uint16_t
will be passed to setSetting
since that is what the Arduino Library methods require.
setSetting
If the sensor has no settings (SETTING_COUNT is zero), then setSetting
simply returns false:
// Set the specified setting. ===> Adapt this to match the sensor type <===
bool setSetting(uint8_t setting, SFE_QUAD_Sensor_Every_Type_t *value)
{
CLASSNAME *device = (CLASSNAME *)_classPtr;
switch (setting)
{
default:
return (false);
break;
}
return (true);
}
But for sensors like the VL53L1X, we do of course want setSettings
to do something. Let's break it down into case 0-1 and 2-4:
// Set the specified setting. ===> Adapt this to match the sensor type <===
bool setSetting(uint8_t setting, SFE_QUAD_Sensor_Every_Type_t *value)
{
CLASSNAME *device = (CLASSNAME *)_classPtr;
switch (setting)
{
case 0:
device->stopRanging();
_shortDistanceMode = true;
device->setDistanceModeShort();
device->startRanging();
break;
case 1:
device->stopRanging();
_shortDistanceMode = false;
device->setDistanceModeLong();
if (device->getIntermeasurementPeriod() < 140)
device->setIntermeasurementPeriod(140);
device->startRanging();
break;
For the two NONE
types, settings 0 and 1: "Distance Mode: Short" and "Distance Mode: Long" the code in the case statement changes the sensor's distance mode accordingly.
For case 0 ("Distance Mode: Short"), setSetting
:
- Stops the sensor with its
stopRanging()
method - Sets the member variable
_shortDistanceMode
totrue
so we have a record of the mode - Sets the distance mode to short with
setDistanceModeShort()
- (Re)starts the sensor with
startRanging()
The code for case 1 ("Distance Mode: Long") is similar, except:
_shortDistanceMode
is set tofalse
- For the long distance mode, the sensor's measurement period cannot be shorter than 140. The measurement period is increased if necessary
For settings cases 2-4, the UINT16_T
setting value is checked to make sure it is within the correct limits and is then passed to the Arduino Library set method:
case 2:
device->stopRanging();
if (value->UINT16_T < 20)
value->UINT16_T = 20;
if (!_shortDistanceMode)
if (value->UINT16_T < 140)
value->UINT16_T = 140;
if (value->UINT16_T > 1000)
value->UINT16_T = 1000;
device->setIntermeasurementPeriod(value->UINT16_T);
device->startRanging();
break;
case 3:
device->stopRanging();
if (value->UINT16_T > 4000)
value->UINT16_T = 4000;
device->setXTalk(value->UINT16_T);
device->startRanging();
break;
case 4:
device->stopRanging();
if (value->UINT16_T > 4000)
value->UINT16_T = 4000;
device->setOffset(value->UINT16_T);
device->startRanging();
break;
default:
return (false);
break;
}
return (true);
}
getConfigurationItemName
The Configuration Item methods are almost identical to the Settings methods. Remember that Configuration Items are simply Settings which can be written to and read from storage.
For the VL53L1X, "Distance Mode: Short" and "Distance Mode: Long" are combined into a BOOL
for storage. But, other than that,
the Configuration Items match the Settings.
getConfigurationItemName
returns a pointer to the name of each configuration item.
The number of case
statements must match CONFIGURATION_ITEM_COUNT.
Some important points:
- Configuration Item names must not contain spaces
- Use underscores where necessary
- The names should be unique
- Keep the names short but meaningful
- Use abbreviations where possible
- These names occupy storage media space which - for EEPROM - can be limited
- Never use commas in the names
- The configurations are stored in CSV format
// Return the name of the configuration item
// Use underscores, not spaces
const char *getConfigurationItemName(uint8_t configItem)
{
switch (configItem)
{
case 0:
return ("Dist_Mode");
break;
case 1:
return ("IM_Period");
break;
case 2:
return ("Xtalk");
break;
case 3:
return ("Offset");
break;
default:
return (NULL);
break;
}
return (NULL);
}
getConfigurationItemType
getConfigurationItemType
is very similar to getSettingType
.
For the VL53L1X, the only difference is that the two distance mode NONE
types have been integrated into a single BOOL
// Return the type of the specified configuration item
bool getConfigurationItemType(uint8_t configItem, SFE_QUAD_Sensor_Setting_Type_e *type)
{
switch (configItem)
{
case 0:
*type = SFE_QUAD_SETTING_TYPE_BOOL;
break;
case 1:
case 2:
case 3:
*type = SFE_QUAD_SETTING_TYPE_UINT16_T;
break;
default:
return (false);
break;
}
return (true);
}
getConfigurationItem
getConfigurationItem
calls the Arduino Library's get
function for that configuration item. The value is returned in the
appropriate field of the SFE_QUAD_Sensor_Every_Type_t
.
For the VL53L1X, the three uint16_t
configuration items are returned in value->UINT16_T
.
The distance mode (config item 0) is simply read from the _shortDistanceMode
member variable. We could have made use of the Library's
getDistanceMode
method and converted the return value (1 or 2) to bool
. Doing it this way avoids an unnecessary I2C bus transaction.
// Get (read) the sensor configuration item
bool getConfigurationItem(uint8_t configItem, SFE_QUAD_Sensor_Every_Type_t *value)
{
CLASSNAME *device = (CLASSNAME *)_classPtr;
switch (configItem)
{
case 0:
value->BOOL = _shortDistanceMode;
break;
case 1:
value->UINT16_T = device->getIntermeasurementPeriod();
break;
case 2:
value->UINT16_T = device->getXTalk();
break;
case 3:
value->UINT16_T = device->getOffset();
break;
default:
return (false);
break;
}
return (true);
}
setConfigurationItem
setConfigurationItem
is very similar to setSetting
.
For the VL53L1X's distance mode, we use the value read from storage to update the _shortDistanceMode
member variable.
// Set (write) the sensor configuration item
bool setConfigurationItem(uint8_t configItem, SFE_QUAD_Sensor_Every_Type_t *value)
{
CLASSNAME *device = (CLASSNAME *)_classPtr;
switch (configItem)
{
case 0:
_shortDistanceMode = value->BOOL;
device->stopRanging();
if (_shortDistanceMode)
device->setDistanceModeShort();
else
device->setDistanceModeLong();
device->startRanging();
break;
case 1:
device->stopRanging();
device->setIntermeasurementPeriod(value->UINT16_T);
device->startRanging();
break;
case 2:
device->stopRanging();
device->setXTalk(value->UINT16_T);
device->startRanging();
break;
case 3:
device->stopRanging();
device->setOffset(value->UINT16_T);
device->startRanging();
break;
default:
return (false);
break;
}
return (true);
}
SFE_QUAD_Sensors.h
When adding a new sensor, src/SFE_QUAD_Sensors.h needs to be modified in three places:
INCLUDE_SFE_QUAD_SENSOR_NewSensorName
Add a #define INCLUDE_SFE_QUAD_SENSOR_NewSensorName
for the new sensor. This allows the user to select which sensors to include in the code build.
By default, all sensors are included. Only including selected sensors speeds up the compilation time and reduces the amount of program memory used.
However, SFE_QUAD_Sensors.h will be overwritten each time the library is updated. We have not (yet) been able to find a way of defining which sensors to include in the main .ino file. If you can think of a way of doing this - which works on all platforms - then please send us a Pull Request!
For our fictitious FOO sensor, we would insert the #define in alphabetical order after CCS811:
// To select which sensors to include:
// comment #define INCLUDE_SFE_QUAD_SENSOR_ALL
// uncomment one or more #define INCLUDE_SFE_QUAD_SENSOR_
//#define INCLUDE_SFE_QUAD_SENSOR_ADS122C04 // Include individual sensors
//#define INCLUDE_SFE_QUAD_SENSOR_AHT20
//#define INCLUDE_SFE_QUAD_SENSOR_BME280
//#define INCLUDE_SFE_QUAD_SENSOR_CCS811_5A
//#define INCLUDE_SFE_QUAD_SENSOR_CCS811_5B
//#define INCLUDE_SFE_QUAD_SENSOR_FOO
The alphabetical ordering is not important, it just makes the list quicker to read.
SFEQUADSensorType
Scrolling ~halfway down SFE_QUAD_Sensors.h, you will find enum SFEQUADSensorType
near the start of the class SFE_QUAD_Sensors
.
We need to add three lines for the new sensor. The ordering here is important as the enum SFEQUADSensorType
is stored with each Configuration Item.
The new sensor must be added to the end of the enum
just before SFE_QUAD_Sensor_Number_Of_Sensors
. Inserting it anywhere else will prevent existing saved configurations being used with the
updated library.
#if defined(INCLUDE_SFE_QUAD_SENSOR_ALL) || defined(INCLUDE_SFE_QUAD_SENSOR_VL53L1X)
Sensor_VL53L1X,
#endif
#if defined(INCLUDE_SFE_QUAD_SENSOR_ALL) || defined(INCLUDE_SFE_QUAD_SENSOR_FOO)
Sensor_FOO,
#endif
SFE_QUAD_Sensor_Number_Of_Sensors // Must be last. <=== Add new sensors _above this line_ to preserve the existing enum values
};
However, if you look closely at enum SFEQUADSensorType
, you will see that the MS8607 appears before the MS5637.
#if defined(INCLUDE_SFE_QUAD_SENSOR_ALL) || defined(INCLUDE_SFE_QUAD_SENSOR_MS8607) // MS8607 must be before MS5637 (otherwise MS8607 will appear as a MS5637)
Sensor_MS8607,
#endif
#if defined(INCLUDE_SFE_QUAD_SENSOR_ALL) || defined(INCLUDE_SFE_QUAD_SENSOR_MS5637)
Sensor_MS5637,
#endif
This is because the MS8607 is essentially a MS5637 with an additional built-in humidity sensor (with its own I2C address). The MS8607 must be detected before the MS5637 otherwise it will appear as a MS5637 and a MS8607. detectSensors
contains some additional code to prevent the re-detection of an MS8607 as a MS5637.
There may be similar cases where it is necessary to detect sensors in a particular order and for the new sensor to be inserted part-way through enum SFEQUADSensorType
.
If that happens, and you are sending us a Pull Request, please make this clear in the notes.
We may still be able to merge your Pull Request, but we will need to make everyone aware that the new version is not backward-compatible with saved configurations from previous versions.
sensorFactory
We need to add the new sensor to the sensorFactory
. This is the method which returns a new object of the requested sensor class.
The order here is not important. Insert the new sensor alphabetically (unless there is a good reason not to):
#if defined(INCLUDE_SFE_QUAD_SENSOR_ALL) || defined(INCLUDE_SFE_QUAD_SENSOR_CCS811_5B)
if (type == Sensor_CCS811_5B)
return new SFE_QUAD_Sensor_CCS811_5B;
#endif
#if defined(INCLUDE_SFE_QUAD_SENSOR_ALL) || defined(INCLUDE_SFE_QUAD_SENSOR_FOO)
if (type == Sensor_FOO)
return new SFE_QUAD_Sensor_FOO;
#endif
#if defined(INCLUDE_SFE_QUAD_SENSOR_ALL) || defined(INCLUDE_SFE_QUAD_SENSOR_LPS25HB)
if (type == Sensor_LPS25HB)
return new SFE_QUAD_Sensor_LPS25HB;
#endif
deleteSensor
The final change to SFE_QUAD_Sensors.h is to add the new sensor to the deleteSensor
.
This is the method which deletes the sensor in a safe way, casting sensor
to the correct class.
The order here is not important. Insert the new sensor alphabetically (unless there is a good reason not to):
#if defined(INCLUDE_SFE_QUAD_SENSOR_ALL) || defined(INCLUDE_SFE_QUAD_SENSOR_CCS811_5B)
if (type == Sensor_CCS811_5B)
delete (SFE_QUAD_Sensor_CCS811_5B *)sensor;
#endif
#if defined(INCLUDE_SFE_QUAD_SENSOR_ALL) || defined(INCLUDE_SFE_QUAD_SENSOR_FOO)
if (type == Sensor_FOO)
delete (SFE_QUAD_Sensor_FOO *)sensor;
#endif
#if defined(INCLUDE_SFE_QUAD_SENSOR_ALL) || defined(INCLUDE_SFE_QUAD_SENSOR_LPS25HB)
if (type == Sensor_LPS25HB)
delete (SFE_QUAD_Sensor_LPS25HB *)sensor;
#endif
SFE_QUAD_Headers.h
When adding a new sensor, src/SFE_QUAD_Headers.h needs to be modified to include the header file for the new sensor.
The order here is not important. Insert the new sensor alphabetically (unless there is a good reason not to):
#if defined(INCLUDE_SFE_QUAD_SENSOR_ALL) || defined(INCLUDE_SFE_QUAD_SENSOR_CCS811_5B)
#include "src/SFE_QUAD_Sensor_CCS811_5B.h"
#endif
#if defined(INCLUDE_SFE_QUAD_SENSOR_ALL) || defined(INCLUDE_SFE_QUAD_SENSOR_FOO)
#include "src/SFE_QUAD_Sensor_FOO.h"
#endif
#if defined(INCLUDE_SFE_QUAD_SENSOR_ALL) || defined(INCLUDE_SFE_QUAD_SENSOR_LPS25HB)
#include "src/SFE_QUAD_Sensor_LPS25HB.h"
#endif
.github/workflows/compile-sketch.yml
The final change is to update .github/workflows/compile-sketch.yml to include the new sensor's Arduino Library.
Any changes to the sensor's Arduino Library are automatically merged into the copy in this library. That way, this library stays up to date with any and all changes to the individual Arduino Libraries.
The entry for our fictitious FOO sensor would be something like:
- name: Update FOO
run: |
cd ./src/src/
mkdir -p FOO
cd FOO
curl -O https://raw.githubusercontent.com/sparkfun/SparkFun_FOO_Arduino_Library/main/src/SparkFun_FOO_Arduino_Library.h
curl -O https://raw.githubusercontent.com/sparkfun/SparkFun_FOO_Arduino_Library/main/src/SparkFun_FOO_Arduino_Library.cpp
You need to include the full raw.githubusercontent.com address for the library files:
- Navigate to the Arduino Library on GitHub
- Navigate to the
src
sub-folder - Open the
.h
file - Click the RAW button to view the file's raw content
- Copy and paste the address from your browser into compile-sketch.yml
- Repeat for the
.cpp
file
If the Arduino Library contains more than the standard .h
and .cpp
files, include those too. E.g. looking at the SGP40:
- name: Update SGP40
run: |
cd ./src/src/
mkdir -p SGP40
cd SGP40
curl -O https://raw.githubusercontent.com/sparkfun/SparkFun_SGP40_Arduino_Library/main/src/SparkFun_SGP40_Arduino_Library.h
curl -O https://raw.githubusercontent.com/sparkfun/SparkFun_SGP40_Arduino_Library/main/src/SparkFun_SGP40_Arduino_Library.cpp
curl -O https://raw.githubusercontent.com/sparkfun/SparkFun_SGP40_Arduino_Library/main/src/sensirion_arch_config.h
curl -O https://raw.githubusercontent.com/sparkfun/SparkFun_SGP40_Arduino_Library/main/src/sensirion_voc_algorithm.h
curl -O https://raw.githubusercontent.com/sparkfun/SparkFun_SGP40_Arduino_Library/main/src/sensirion_voc_algorithm.c
Finally, you need to check how the .cpp
file includes its .h
file. In Arduino examples, you will often see files included like this:
The less-than and greater-than tell the Arduino IDE compiler to search its PATH for SparkFun_FOO_Arduino_Library.h
.
SparkFun_FOO_Arduino_Library.h
will normally be in a \library
sub-folder.
For this library, we want to ensure the copy of the Arduino Library in the src\src
sub-folder is used, not the copy from the IDE PATH.
Look inside the .cpp
file. If you see:
then you need to include one extra line in compile-sketch.yml so that the less-than and greater-than are replaced automatically with double-quotes:
- name: Update FOO
run: |
cd ./src/src/
mkdir -p FOO
cd FOO
curl -O https://raw.githubusercontent.com/sparkfun/SparkFun_FOO_Arduino_Library/main/src/SparkFun_FOO_Arduino_Library.h
curl -O https://raw.githubusercontent.com/sparkfun/SparkFun_FOO_Arduino_Library/main/src/SparkFun_FOO_Arduino_Library.cpp
# SparkFun_FOO_Arduino_Library.cpp uses #include <SparkFun_FOO_Arduino_Library.h>. We need to replace the < and > with double quotes
sed -i 's/<SparkFun_FOO_Arduino_Library.h>/'\"'SparkFun_FOO_Arduino_Library.h'\"'/g' SparkFun_FOO_Arduino_Library.cpp