Now that you have built WipperSnapper, it's time to add the driver for the I2C sensor.
Get Setup
As an example, we'll be creating a new WipperSnapper I2C component for the MCP9808 High Accuracy I2C Temperature Sensor Breakout. This breakout is perfect to use as an example as it only reports the temperature reading.
You will need to alter/edit the code below to reflect the sensor you're adding, keeping in mind that sensors that report more readings (temperature, humidity, air quality, etc) require more code.

WipperSnapper firmware is based on Arduino. You will need an Arduino library that already contains the driver code for your sensor.
This page is using the Adafruit MCP9808 sensor breakout as an example. The corresponding Arduino library for this sensor is listed on the Arduino Library Manager and publicly available on GitHub.
In your IDE/text editor of choice, open the src folder within Adafruit_WipperSnapper_Arduino.
WipperSnapper's I2C drivers are stored as header (.h
) files within the src/components/i2c/drivers folder and each driver follows the naming convention WipperSnapper_I2C_Driver_SensorName.h
.
For our example, we'll create a new header file called WipperSnapper_I2C_Driver_MCP9808.h
within src/components/i2c/drivers and add the following skeleton code to the file:
/*! * @file WipperSnapper_I2C_Driver_MCP9808.h * * Device driver for the MCP9808 Temperature sensor. * * Adafruit invests time and resources providing this open source code, * please support Adafruit and open-source hardware by purchasing * products from Adafruit! * * Copyright (c) Brent Rubell 2023 for Adafruit Industries. * * MIT license, all text here must be included in any redistribution. * */ #ifndef WipperSnapper_I2C_Driver_MCP9808_H #define WipperSnapper_I2C_Driver_MCP9808_H #include "WipperSnapper_I2C_Driver.h" #include <Adafruit_MCP9808.h> /**************************************************************************/ /*! @brief Class that provides a driver interface for a MCP9808 sensor. */ /**************************************************************************/ class WipperSnapper_I2C_Driver_MCP9808 : public WipperSnapper_I2C_Driver { public: /*******************************************************************************/ /*! @brief Constructor for a MCP9808 sensor. @param i2c The I2C interface. @param sensorAddress 7-bit device address. */ /*******************************************************************************/ WipperSnapper_I2C_Driver_MCP9808(TwoWire *i2c, uint16_t sensorAddress) : WipperSnapper_I2C_Driver(i2c, sensorAddress) { _i2c = i2c; _sensorAddress = sensorAddress; } /*******************************************************************************/ /*! @brief Destructor for an MCP9808 sensor. */ /*******************************************************************************/ ~WipperSnapper_I2C_Driver_MCP9808() { // Called when a MCP9808 component is deleted. delete _mcp9808; } /*******************************************************************************/ /*! @brief Initializes the MCP9808 sensor and begins I2C. @returns True if initialized successfully, False otherwise. */ /*******************************************************************************/ bool begin() { // TO-DO: Initialization code goes here! } protected: Adafruit_MCP9808 *_mcp9808; ///< Pointer to MCP9808 temperature sensor object }; #endif // WipperSnapper_I2C_Driver_MCP9808
At the top of the code are include guards which prevent the compiler from including multiple instances of this class.
Next, we'll include the base I2C class (#include"WipperSnapper_I2C_Driver.h"
) and the Arduino driver library for the I2C sensor (in this example, #include
<Adafruit_MCP9808.h>
).
The constructor (WipperSnapper_I2C_Driver_MCP9808()
) and destructor (~WipperSnapper_I2C_Driver_MCP9808()
) are called when the component is either initialized or deleted via the WipperSnapper website. The constructor is passed the I2C object and the sensor address from the new component form.
At the bottom, underneath protected
, you'll need to create a pointer to access the sensor driver (in this example, *_mcp9808
).
Add begin()
The begin()
function contains the code required to initialize the sensor's driver and returns True upon successful initialization.
The MCP9808 has a very simple initialization routine:
/*******************************************************************************/ /*! @brief Initializes the MCP9808 sensor and begins I2C. @returns True if initialized successfully, False otherwise. */ /*******************************************************************************/ bool begin() { _mcp9808 = new Adafruit_MCP9808(); return _mcp9808->begin((uint8_t)_sensorAddress, _i2c); }
About I2C Sensors in WipperSnapper
Before explaining how to add a function to read the sensor, let's first talk a little bit about how this works within WipperSnapper.
WipperSnapper utilizes the Adafruit Unified Sensor Driver library which provides a single type for all sensor readings, sensors_event_t
, and enforces standardized SI units for each type of sensor.
By reducing all data to a single sensors_event_t
'type' and settling on specific, standardized SI units for each sensor family, the same sensor types return values which are compatible with any similar sensor. This enables us to support all types of sensor breakouts without worrying about the units matching and prevents code re-use.
This section of the Adafruit Unified Sensor Driver's GitHub README provides an in-depth explanation of how the driver works.
Add a function to read a sensor
Next, a function needs to be added to read the value of the sensor. The WipperSnapper_I2C_Driver.h file contains base class implementations for reading each type of sensor prefixed by getEvent_
.
The MCP9808 contains an ambient temperature sensor. Looking within the WipperSnapper_I2C_Driver.h, the getEventAmbientTemp()
function "reads an ambient temperature sensor".
Let's add an empty getEventAmbientTemp() function to our header file:
/*******************************************************************************/ /*! @brief Gets the MCP9808's current temperature. @param tempEvent Pointer to an Adafruit_Sensor event. @returns True if the temperature was obtained successfully, False otherwise. */ /*******************************************************************************/ bool getEventAmbientTemp(sensors_event_t *tempEvent) { // TODO: Add code here! }
Add code to read the sensor's temperature. The temperature is stored within the sensor_event_t
type's temperature
field.
/*******************************************************************************/ /*! @brief Gets the MCP9808's current temperature. @param tempEvent Pointer to an Adafruit_Sensor event. @returns True if the temperature was obtained successfully, False otherwise. */ /*******************************************************************************/ bool getEventAmbientTemp(sensors_event_t *tempEvent) { tempEvent->temperature = _mcp9808->readTempC(); return true; }
WipperSnapper uses the Adafruit Unified Sensor Driver to handle reading sensors and enforces standardized SI units for every type of sensor. In WipperSnapper_I2C_Driver.h, there are functions for each predefined sensor type within the Adafruit Unified Sensor Driver.
The sensor driver header file should implement a getEventTYPE()
function call for each value to be read.
- For examples of implementations (some might match your sensor!), check out Adafruit_Wippersnapper_Arduino/tree/main/src/components/i2c/drivers folder.
Include the new driver in the I2C Base Driver
With the sensor driver complete, the next step is to add it to the base I2C class file, src/components/i2c/WipperSnapper_I2C.h
Open the file src/components/i2c/WipperSnapper_I2C.h
. At the top, add a line to #include
the MCP9808 driver:
... #include "drivers/WipperSnapper_I2C_Driver_DPS310.h" #include "drivers/WipperSnapper_I2C_Driver_SCD30.h" #include "drivers/WipperSnapper_I2C_Driver_MCP9808.h"
At the bottom of this file, add a new pointer to the MCP9808 driver:
private: WipperSnapper_I2C_Driver_MCP9808 *_mcp9808 = nullptr; ...
Add Handling Code to the I2C Base Driver
Finally, we'll need to add code to detect and initialize the MCP9808. Within src/components/i2c/WipperSnapper_I2C.cpp
, locate the initI2CDevice()
function:
/*******************************************************************************/ /*! @brief Initializes I2C device driver. @param msgDeviceInitReq A decoded I2CDevice initialization request message. @returns True if I2C device is initialized and attached, False otherwise. */ /*******************************************************************************/ bool WipperSnapper_Component_I2C::initI2CDevice( wippersnapper_i2c_v1_I2CDeviceInitRequest *msgDeviceInitReq) { WS_DEBUG_PRINT("Attempting to initialize I2C device: "); WS_DEBUG_PRINTLN(msgDeviceInitReq->i2c_device_name); uint16_t i2cAddress = (uint16_t)msgDeviceInitReq->i2c_device_address; ...
Within initI2CDevice(), add code to detect the I2C sensor and, if detected, initialize it.
bool WipperSnapper_Component_I2C::initI2CDevice( wippersnapper_i2c_v1_I2CDeviceInitRequest *msgDeviceInitReq) { ... } else if (strcmp("mcp9808", msgDeviceInitReq->i2c_device_name) == 0) { _mcp9808 = new WipperSnapper_I2C_Driver_MCP9808(this->_i2c, i2cAddress); if (!_mcp9808->begin()) { WS_DEBUG_PRINTLN("ERROR: Failed to initialize MCP9808!"); _busStatusResponse = wippersnapper_i2c_v1_BusResponse_BUS_RESPONSE_DEVICE_INIT_FAIL; return false; } _mcp9808->configureDriver(msgDeviceInitReq); drivers.push_back(_mcp9808); WS_DEBUG_PRINTLN("MCP9808 Initialized Successfully!"); ... }
Test the new I2C Sensor Driver
Assuming you have successfully built WipperSnapper using PlatformIO, navigate to the PlatformIO tab and select the board you are using to test.
Click Build.
Once PlatformIO has built WipperSnapper successfully, it'll display SUCCESS at the bottom of VSCode (or terminal):
Next, upload the modified WipperSnapper firmware to your board by navigating to PlatformIO and clicking Upload.
From the PlatformIO tab, click Monitor. A new serial monitor should open. You should see the following debug output stating WipperSnapper is running.
Additionally, you should see your device appear as Online on io.adafruit.com. From the device page on Adafruit IO, click + New Component.
As part of the development process, your component appears under the component picker as "in development". This allows you to test the component before giving us (Adafruit) final approval to make it live.
At the top of the component picker, click the "Show Dev" checkbox. Your sensor should appear in the I2C Components list with a badge showing "In Development"
Click your component. On the monitor, you should see an I2C Scan command being executed on the development board.
On Adafruit IO, configure your component and click Create Component.
After clicking Create Component, the sensor should initialize. After the period you specified, it should read the sensor's value and publish it to Adafruit IO.
You should see the new value appear on your WipperSnapper device page.
It's good practice to keep the monitor open during this process as its useful for detecting errors and tracing them back.
Almost done - let's add a pull request to the Adafruit WipperSnapper library so others can use this sensor.
Create a Pull Request to WipperSnapper Arduino
First, read over the Doxygen page on this guide and run Doxygen on your code. Then, format your code using clang-format.
Once you've run Doxygen and linted using clang-format, you're ready to submit a pull request adding the sensor to the WipperSnapper Arduino library.
Commit and push your files to a branch on local your fork of Adafruit_WipperSnapper_Arduino and open a new pull request on the Adafruit_WipperSnapper_Arduino repository.
Once reviewed and accepted, Adafruit will include support for your sensor in the latest version of the Adafruit IO Wippersnapper library. We will also remove the "in-development" flag from the component to make it live on Adafruit IO.
Page last edited March 08, 2024
Text editor powered by tinymce.