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.

Top view of temperature sensor breakout above an OLED display FeatherWing. The OLED display reads "MCP9808 Temp: 24.19ºC"
The MCP9808 digital temperature sensor is one of the more accurate/precise we've ever seen, with a typical accuracy of ±0.25°C over the sensor's -40°C to...
$4.95
In Stock

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.

Create a new Header File

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;
  }
Wait - my I2C breakout has more than one sensor reading!

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.

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.

Adafruit has an example of a "perfect" pull request here.

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.

This guide was first published on Mar 10, 2022. It was last updated on Jul 16, 2024.

This page (Adding the I2C Component Driver) was last updated on Mar 08, 2024.

Text editor powered by tinymce.