Driver Code

Now that you have a template for starting the driver let's fill in the code to convert from the Arduino library to a Python & CircuitPython module.  First open the adafruit_vl6180x.py created in the previous step and notice it's populated with license information and a documentation template, but otherwise blank and ready to be populated with Python code:

# The MIT License (MIT)
#
# Copyright (c) 2017 Tony DiCola for Adafruit Industries
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""
`adafruit_vl6180x`
====================================================

TODO(description)

* Author(s): Tony DiCola
"""

Adding a description is a good idea as it will be parsed by tools like Read The Docs to generate documentation:

"""
`adafruit_vl6180x`
====================================================

This is a CircuitPython driver for the VL6180X time-of-flight distance sensor.

* Author(s): Tony DiCola
"""

Now for the actual code, to start you'll want to very carefully review the Arduino library you're porting.  For the VL6180X library it's a simple Arduino library that's defined by a single header and cpp file.  The header file defines register addresses and other values, but most importantly the interface for the VL6180X class:

class Adafruit_VL6180X {
 public:
  Adafruit_VL6180X();
  boolean begin(void);
  uint8_t readRange(void);
  float   readLux(uint8_t gain);
  uint8_t readRangeStatus(void);

 private:
  void loadSettings(void);

  void write8(uint16_t address, uint8_t data);
  void write16(uint16_t address, uint16_t data);

  uint16_t read16(uint16_t address);
  uint8_t read8(uint16_t address);

  uint8_t _i2caddr;
};

You can almost directly convert this to a stub of a Python class:

class Adafruit_VL6180X:

    def __init__(self):
        pass

    def begin(self):
        return True

    def read_range(self):
        return 0

    def read_lux(self, gain):
        return 0

    def read_range_status(self):
        return 0

    def _load_settings(self):
        pass

    def _write_8(self, address, data):
        pass

    def _write_16(self, address, data):
        pass

    def _read_8(self, address):
        return 0

    def _read_16(self, address):
        return 0

All of the same functions as the Arduino library are created but slightly changed to more closely file Python's PEP8 style conventions (underscores to separate words instead of camel casing, a _ prefix to denote 'private' functions).

There are also a set of #define values at the top of the header which we'll most likely need to include in our driver.  Here's the Arduino code:

#define VL6180X_DEFAULT_I2C_ADDR 0x29

#define VL6180X_REG_IDENTIFICATION_MODEL_ID    0x000
#define VL6180X_REG_SYSTEM_INTERRUPT_CONFIG    0x014
#define VL6180X_REG_SYSTEM_INTERRUPT_CLEAR     0x015
#define VL6180X_REG_SYSTEM_FRESH_OUT_OF_RESET  0x016
#define VL6180X_REG_SYSRANGE_START             0x018
#define VL6180X_REG_SYSALS_START               0x038
#define VL6180X_REG_SYSALS_ANALOGUE_GAIN       0x03F
#define VL6180X_REG_SYSALS_INTEGRATION_PERIOD_HI  0x040
#define VL6180X_REG_SYSALS_INTEGRATION_PERIOD_LO  0x041
#define VL6180X_REG_RESULT_ALS_VAL             0x050
#define VL6180X_REG_RESULT_RANGE_VAL           0x062
#define VL6180X_REG_RESULT_RANGE_STATUS        0x04d
#define VL6180X_REG_RESULT_INTERRUPT_STATUS_GPIO       0x04f

#define VL6180X_ALS_GAIN_1         0x06
#define VL6180X_ALS_GAIN_1_25      0x05
#define VL6180X_ALS_GAIN_1_67      0x04
#define VL6180X_ALS_GAIN_2_5       0x03
#define VL6180X_ALS_GAIN_5         0x02
#define VL6180X_ALS_GAIN_10        0x01
#define VL6180X_ALS_GAIN_20        0x00
#define VL6180X_ALS_GAIN_40        0x07

#define VL6180X_ERROR_NONE         0
#define VL6180X_ERROR_SYSERR_1     1
#define VL6180X_ERROR_SYSERR_5     5
#define VL6180X_ERROR_ECEFAIL      6
#define VL6180X_ERROR_NOCONVERGE   7
#define VL6180X_ERROR_RANGEIGNORE  8
#define VL6180X_ERROR_SNR          11
#define VL6180X_ERROR_RAWUFLOW     12
#define VL6180X_ERROR_RAWOFLOW     13
#define VL6180X_ERROR_RANGEUFLOW   14
#define VL6180X_ERROR_RANGEOFLOW   15

Python doesn't have a concept of compile-time defines since it's an interpreted language.  This means each of these defines should be created as variables--typically module-level variables with all capitol names (to follow PEP8 convention).  Be aware that each of these variables will take a small amount of memory so ideally you specify them as const in case the variables can be better optimized. 

Here's how the Python version of these values would look, and they should be placed before the class definition:

VL6180X_DEFAULT_I2C_ADDR = const(0x29)

VL6180X_REG_IDENTIFICATION_MODEL_ID    = const(0x000)
VL6180X_REG_SYSTEM_INTERRUPT_CONFIG    = const(0x014)
VL6180X_REG_SYSTEM_INTERRUPT_CLEAR     = const(0x015)
VL6180X_REG_SYSTEM_FRESH_OUT_OF_RESET  = const(0x016)
VL6180X_REG_SYSRANGE_START             = const(0x018)
VL6180X_REG_SYSALS_START               = const(0x038)
VL6180X_REG_SYSALS_ANALOGUE_GAIN       = const(0x03F)
VL6180X_REG_SYSALS_INTEGRATION_PERIOD_HI  = const(0x040)
VL6180X_REG_SYSALS_INTEGRATION_PERIOD_LO  = const(0x041)
VL6180X_REG_RESULT_ALS_VAL             = const(0x050)
VL6180X_REG_RESULT_RANGE_VAL           = const(0x062)
VL6180X_REG_RESULT_RANGE_STATUS        = const(0x04d)
VL6180X_REG_RESULT_INTERRUPT_STATUS_GPIO       = const(0x04f)

VL6180X_ALS_GAIN_1         = const(0x06)
VL6180X_ALS_GAIN_1_25      = const(0x05)
VL6180X_ALS_GAIN_1_67      = const(0x04)
VL6180X_ALS_GAIN_2_5       = const(0x03)
VL6180X_ALS_GAIN_5         = const(0x02)
VL6180X_ALS_GAIN_10        = const(0x01)
VL6180X_ALS_GAIN_20        = const(0x00)
VL6180X_ALS_GAIN_40        = const(0x07)

VL6180X_ERROR_NONE         = const(0)
VL6180X_ERROR_SYSERR_1     = const(1)
VL6180X_ERROR_SYSERR_5     = const(5)
VL6180X_ERROR_ECEFAIL      = const(6)
VL6180X_ERROR_NOCONVERGE   = const(7)
VL6180X_ERROR_RANGEIGNORE  = const(8)
VL6180X_ERROR_SNR          = const(11)
VL6180X_ERROR_RAWUFLOW     = const(12)
VL6180X_ERROR_RAWOFLOW     = const(13)
VL6180X_ERROR_RANGEUFLOW   = const(14)
VL6180X_ERROR_RANGEOFLOW   = const(15)


class Adafruit_VL6180X:
  ...

Now let's start filling in function implementations.  We'll start with the initializer which is like the constructor in Arduino/C++ code.  Code in the initializer is responsible for creating the object and setting its internal state to a default mode.  If you look at the constructor in the Arduino library you'll see it doesn't actually need to do anything:

Adafruit_VL6180X::Adafruit_VL6180X(void) {
}

With Arduino it can access the I2C bus from global objects and doesn't need to keep a reference to the bus.  This is in contrast to CircuitPython and MicroPython where you typically need to keep a unique reference to the bus.  The initializer is a good place to do this, and in particular an I2CDevice instance from the bus device module can be created.  The typical convention is for a driver to take in a reference to the I2C bus as a parameter and optionally any specific or overridden address value (some devices allow modifying their I2C address).

Modify the Python initializer to look like:

def __init__(self, i2c, address=VL6180X_DEFAULT_I2C_ADDR):
        self._device = I2CDevice(i2c, address)

And since you're using the I2CDevice class and its associated module be sure to add an import at the top of the file after the comments:

from adafruit_bus_device.i2c_device import I2CDevice

For many Arduino libraries they simplify access to registers with explicit read and write functions, like the write8 and read8 functions in the VL6180X library.  Let's start by filling in those functions first as they're the core functions used by all other functions to interact with the device.  The Arduino versions look like:

// Read 1 byte from the VL6180X at 'address'
uint8_t Adafruit_VL6180X::read8(uint16_t address)
{
  uint8_t data;

  Wire.beginTransmission(_i2caddr);
  Wire.write(address>>8);
  Wire.write(address);
  Wire.endTransmission();

  Wire.requestFrom(_i2caddr, (uint8_t)1);
  uint8_t r = Wire.read();

#if defined(I2C_DEBUG)
  Serial.print("\t$"); Serial.print(address, HEX); Serial.print(": 0x"); Serial.println(r, HEX);
#endif

  return r;
}


// Read 2 byte from the VL6180X at 'address'
uint16_t Adafruit_VL6180X::read16(uint16_t address)
{
  uint16_t data;

  Wire.beginTransmission(_i2caddr);
  Wire.write(address>>8);
  Wire.write(address);
  Wire.endTransmission();

  Wire.requestFrom(_i2caddr, (uint8_t)2);
  while(!Wire.available());
  data = Wire.read();
  data <<= 8;
  while(!Wire.available());
  data |= Wire.read();
  
  return data;
}

// write 1 byte
void Adafruit_VL6180X::write8(uint16_t address, uint8_t data)
{
  Wire.beginTransmission(_i2caddr);
  Wire.write(address>>8);
  Wire.write(address);
  Wire.write(data);  
  Wire.endTransmission();

#if defined(I2C_DEBUG)
  Serial.print("\t$"); Serial.print(address, HEX); Serial.print(" = 0x"); Serial.println(data, HEX);
#endif
}


// write 2 bytes
void Adafruit_VL6180X::write16(uint16_t address, uint16_t data)
{
  Wire.beginTransmission(_i2caddr);
  Wire.write(address>>8);
  Wire.write(address);
  Wire.write(data>>8);
  Wire.write(data);
  Wire.endTransmission();
}

You'll want to be a bit familiar with Arduino's I2C interface to understand this code, but at a high level the comments describe what's happening in each function.  For a given 16-bit register address (remember the VL6180X is somewhat special and uses these larger addresses) one or two bytes of data are read or written to the device.  We can convert these to Python code using the I2CDevice class as follows:

    def _write_8(self, address, data):
        # Write 1 byte of data from the specified 16-bit register address.
        with self._device:
            self._device.write(bytes([(address >> 8) & 0xFF,
                                       address & 0xFF,
                                       data]))

    def _write_16(self, address, data):
        # Write a 16-bit big endian value to the specified 16-bit register
        # address.
        with self._device:
            self._device.write(bytes([(address >> 8) & 0xFF,
                                       address & 0xFF,
                                      (data >> 8) & 0xFF,
                                       data & 0xFF]))

    def _read_8(self, address):
        # Read and return a byte from the specified 16-bit register address.
        with self._device:
            self._device.write(bytes([(address >> 8) & 0xFF,
                                       address & 0xFF]),
                               stop=False)
            result = bytearray(1)
            self._device.read_into(result)
            return result[0]

    def _read_16(self, address):
        # Read and return a 16-bit unsigned big endian value read from the
        # specified 16-bit register address.
        with self._device:
            self._device.write(bytes([(address >> 8) & 0xFF,
                                       address & 0xFF]),
                               stop=False)
            result = bytearray(2)
            self._device.read_into(result)
            return (result[0] << 8) | result[1]

The Python implementations all rely on the I2CDevice to perform low level reads and writes, just like you saw manually at the REPL in the beginning of this guide.  However one big difference with Python and Arduino code is that numeric and byte values in Python don't have types and you sometimes need to manipulate bytes to pack and unpack large values.

For example all the registers of the VL6180X are specified as 16-bit big endian values (the most significant byte first).  A number like 0x123 in Python has to be converted into two bytes using bit shifts and masks like:

>>> value = 0x123
>>> high_byte = (value >> 8) & 0xFF
>>> low_byte = value & 0xFF
>>> hex(high_byte)
'0x1'
>>> hex(low_byte)
'0x23'

The masking operations (& 0xFF) are very important as they tell Python you mean to convert from its arbitrarily long numeric values to a single byte value.  If you miss this step your code will at best throw an error that a non-byte value was found and at worst silently fail with a completely unexpected (usually negative) value!

Likewise when reading a 16-bit big-endian value from the VL6180X it has to be packed back into a single numeric value with bitshifts and boolean operations:

>>> low_byte = 0x23
>>> high_byte = 0x1
>>> value = (high_byte << 8) | low_byte
>>> hex(value)
'0x123'

Another option for performing these conversions is to use the struct module, however it adds some complexity and memory usage that you might not desire.  For a simple case of packing or unpacking a few 16-bit values doing it in place with boolean operations and shifts as above is the easiest option.

With the core read and write operations implemented let's move on to implementing the other functions.  The begin function is typically where initialization happens and looks like:

boolean Adafruit_VL6180X::begin(void) {
  _i2caddr = VL6180X_DEFAULT_I2C_ADDR;
  Wire.begin();

  if (read8(VL6180X_REG_IDENTIFICATION_MODEL_ID) != 0xB4) {
    return false;
  }

  //if (read8(VL6180X_REG_SYSTEM_FRESH_OUT_OF_RESET) == 0x01) {
    loadSettings();
  //}

  write8(VL6180X_REG_SYSTEM_FRESH_OUT_OF_RESET, 0x00);

  return true;
}

This is easy to convert to Python using the _read_8 and _write_8 functions we filled in previously:

    def begin(self):
        """Initialize access to the sensor.  Returns True if successful."""
        if self._read_8(VL6180X_REG_IDENTIFICATION_MODEL_ID) != 0xB4:
            return False
        self._load_settings()
        self._write_8(VL6180X_REG_SYSTEM_FRESH_OUT_OF_RESET, 0x00)
        return True

We don't need to save the I2C address or initialize the I2C/Wire module since that was done in the Python class' initializer when it created the I2CDevice instance.

Notice also a docstring was added with a small description of what the function does.  This string will be read by the documentation engine and automatically added to API docs!  In Python it's a best practice to add docstrings to all public/user-facing functions as it creates the core of user documentation.

Let's fill in _load_settings next, first look at the Arduino version:

void Adafruit_VL6180X::loadSettings(void) {
    // load settings!

    // private settings from page 24 of app note
    write8(0x0207, 0x01);
    write8(0x0208, 0x01);
    write8(0x0096, 0x00);
    write8(0x0097, 0xfd);
    write8(0x00e3, 0x00);
    write8(0x00e4, 0x04);
    write8(0x00e5, 0x02);
    write8(0x00e6, 0x01);
    write8(0x00e7, 0x03);
    write8(0x00f5, 0x02);
    write8(0x00d9, 0x05);
    write8(0x00db, 0xce);
    write8(0x00dc, 0x03);
    write8(0x00dd, 0xf8);
    write8(0x009f, 0x00);
    write8(0x00a3, 0x3c);
    write8(0x00b7, 0x00);
    write8(0x00bb, 0x3c);
    write8(0x00b2, 0x09);
    write8(0x00ca, 0x09);
    write8(0x0198, 0x01);
    write8(0x01b0, 0x17);
    write8(0x01ad, 0x00);
    write8(0x00ff, 0x05);
    write8(0x0100, 0x05);
    write8(0x0199, 0x05);
    write8(0x01a6, 0x1b);
    write8(0x01ac, 0x3e);
    write8(0x01a7, 0x1f);
    write8(0x0030, 0x00);

    // Recommended : Public registers - See data sheet for more detail
    write8(0x0011, 0x10);       // Enables polling for 'New Sample ready'
                                // when measurement completes
    write8(0x010a, 0x30);       // Set the averaging sample period
                                // (compromise between lower noise and
                                // increased execution time)
    write8(0x003f, 0x46);       // Sets the light and dark gain (upper
                                // nibble). Dark gain should not be
                                // changed.
    write8(0x0031, 0xFF);       // sets the # of range measurements after
                                // which auto calibration of system is
                                // performed
    write8(0x0040, 0x63);       // Set ALS integration time to 100ms
    write8(0x002e, 0x01);       // perform a single temperature calibration
                                // of the ranging sensor

    // Optional: Public registers - See data sheet for more detail
    write8(0x001b, 0x09);       // Set default ranging inter-measurement
                                // period to 100ms
    write8(0x003e, 0x31);       // Set default ALS inter-measurement period
                                // to 500ms
    write8(0x0014, 0x24);       // Configures interrupt on 'New Sample
                                // Ready threshold event'
}

This is all just a set of _write_8 calls that we can directly convert to Python:

    def _load_settings(self):
        # private settings from page 24 of app note
        self._write_8(0x0207, 0x01)
        self._write_8(0x0208, 0x01)
        self._write_8(0x0096, 0x00)
        self._write_8(0x0097, 0xfd)
        self._write_8(0x00e3, 0x00)
        self._write_8(0x00e4, 0x04)
        self._write_8(0x00e5, 0x02)
        self._write_8(0x00e6, 0x01)
        self._write_8(0x00e7, 0x03)
        self._write_8(0x00f5, 0x02)
        self._write_8(0x00d9, 0x05)
        self._write_8(0x00db, 0xce)
        self._write_8(0x00dc, 0x03)
        self._write_8(0x00dd, 0xf8)
        self._write_8(0x009f, 0x00)
        self._write_8(0x00a3, 0x3c)
        self._write_8(0x00b7, 0x00)
        self._write_8(0x00bb, 0x3c)
        self._write_8(0x00b2, 0x09)
        self._write_8(0x00ca, 0x09)
        self._write_8(0x0198, 0x01)
        self._write_8(0x01b0, 0x17)
        self._write_8(0x01ad, 0x00)
        self._write_8(0x00ff, 0x05)
        self._write_8(0x0100, 0x05)
        self._write_8(0x0199, 0x05)
        self._write_8(0x01a6, 0x1b)
        self._write_8(0x01ac, 0x3e)
        self._write_8(0x01a7, 0x1f)
        self._write_8(0x0030, 0x00)
        # Recommended : Public registers - See data sheet for more detail
        self._write_8(0x0011, 0x10)   # Enables polling for 'New Sample ready'
                                      # when measurement completes
        self._write_8(0x010a, 0x30)   # Set the averaging sample period
                                      # (compromise between lower noise and
                                      # increased execution time)
        self._write_8(0x003f, 0x46)   # Sets the light and dark gain (upper
                                      # nibble). Dark gain should not be
                                      # changed.
        self._write_8(0x0031, 0xFF)   # sets the # of range measurements after
                                      # which auto calibration of system is
                                      # performed
        self._write_8(0x0040, 0x63)   # Set ALS integration time to 100ms
        self._write_8(0x002e, 0x01)   # perform a single temperature calibration
                                      # of the ranging sensor

        # Optional: Public registers - See data sheet for more detail
        self._write_8(0x001b, 0x09)   # Set default ranging inter-measurement
                                      # period to 100ms
        self._write_8(0x003e, 0x31)   # Set default ALS inter-measurement period
                                      # to 500ms
        self._write_8(0x0014, 0x24)   # Configures interrupt on 'New Sample
                                      # Ready threshold event'

Now we can fill in the functions people will use, like read_range.  First the Arduino version:

uint8_t Adafruit_VL6180X::readRange(void) {
  // wait for device to be ready for range measurement
  while (! (read8(VL6180X_REG_RESULT_RANGE_STATUS) & 0x01));

  // Start a range measurement
  write8(VL6180X_REG_SYSRANGE_START, 0x01);

  // Poll until bit 2 is set
  while (! (read8(VL6180X_REG_RESULT_INTERRUPT_STATUS_GPIO) & 0x04));

  // read range in mm
  uint8_t range = read8(VL6180X_REG_RESULT_RANGE_VAL);

  // clear interrupt
  write8(VL6180X_REG_SYSTEM_INTERRUPT_CLEAR, 0x07);

  return range;
}

Nothing looks particularly complicated with this code, it's just a few loops waiting for a specific register result and then read and write calls to manipulate the registers.  The Python version would look like:

    def read_range(self):
        """Read the range of an object in front of sensor and return it in mm."""
        # wait for device to be ready for range measurement
        while not (self._read_8(VL6180X_REG_RESULT_RANGE_STATUS) & 0x01):
            pass
        # Start a range measurement
        self._write_8(VL6180X_REG_SYSRANGE_START, 0x01);
        # Poll until bit 2 is set
        while not (self._read_8(VL6180X_REG_RESULT_INTERRUPT_STATUS_GPIO) & 0x04):
            pass
        # read range in mm
        range_ = self._read_8(VL6180X_REG_RESULT_RANGE_VAL)
        # clear interrupt
        self._write_8(VL6180X_REG_SYSTEM_INTERRUPT_CLEAR, 0x07)
        return range_

The Python code is almost exactly like the Arduino code--they both just use the underlying read and write functions to talk to the hardware.  The only gotcha is to be careful of variables with names like range as those are built-in Python function names and might cause problems.  The typical convention in Python is to add an underscore to denote the variable as a local instance vs. the built-in function or object.

We can go on to read_range_status, a very simple one register read.  In Arduino:

uint8_t Adafruit_VL6180X::readRangeStatus(void) {
  return (read8(VL6180X_REG_RESULT_RANGE_STATUS) >> 4);
}

And the Python version:

    def read_range_status(self):
        """Retrieve the status/error from a previous range read."""
        return self._read_8(VL6180X_REG_RESULT_RANGE_STATUS) >> 4

Finally the read_lux function, first the Arduino version:

float Adafruit_VL6180X::readLux(uint8_t gain) {
  uint8_t reg;

  reg = read8(VL6180X_REG_SYSTEM_INTERRUPT_CONFIG);
  reg &= ~0x38;
  reg |= (0x4 << 3); // IRQ on ALS ready
  write8(VL6180X_REG_SYSTEM_INTERRUPT_CONFIG, reg);
  
  // 100 ms integration period
  write8(VL6180X_REG_SYSALS_INTEGRATION_PERIOD_HI, 0);
  write8(VL6180X_REG_SYSALS_INTEGRATION_PERIOD_LO, 100);

  // analog gain
  if (gain > VL6180X_ALS_GAIN_40) {
    gain = VL6180X_ALS_GAIN_40;
  }
  write8(VL6180X_REG_SYSALS_ANALOGUE_GAIN, 0x40 | gain);

  // start ALS
  write8(VL6180X_REG_SYSALS_START, 0x1);

  // Poll until "New Sample Ready threshold event" is set
  while (4 != ((read8(VL6180X_REG_RESULT_INTERRUPT_STATUS_GPIO) >> 3) & 0x7));

  // read lux!
  float lux = read16(VL6180X_REG_RESULT_ALS_VAL);

  // clear interrupt
  write8(VL6180X_REG_SYSTEM_INTERRUPT_CLEAR, 0x07);

  lux *= 0.32; // calibrated count/lux
  switch(gain) { 
  case VL6180X_ALS_GAIN_1: 
    break;
  case VL6180X_ALS_GAIN_1_25: 
    lux /= 1.25;
    break;
  case VL6180X_ALS_GAIN_1_67: 
    lux /= 1.76;
    break;
  case VL6180X_ALS_GAIN_2_5: 
    lux /= 2.5;
    break;
  case VL6180X_ALS_GAIN_5: 
    lux /= 5;
    break;
  case VL6180X_ALS_GAIN_10: 
    lux /= 10;
    break;
  case VL6180X_ALS_GAIN_20: 
    lux /= 20;
    break;
  case VL6180X_ALS_GAIN_40: 
    lux /= 20;
    break;
  }
  lux *= 100;
  lux /= 100; // integration time in ms


  return lux;
}

There's a bit more going on in this function, but if you carefully read it you'll see it's only using the basic read and write functions with a bit of math and other bit operations.  A Python version would look like:

    def read_lux(self, gain):
        """Read the lux (light value) from the sensor and return it."""
        reg = self._read_8(VL6180X_REG_SYSTEM_INTERRUPT_CONFIG)
        reg &= ~0x38
        reg |= (0x4 << 3) # IRQ on ALS ready
        self._write_8(VL6180X_REG_SYSTEM_INTERRUPT_CONFIG, reg)
        # 100 ms integration period
        self._write_8(VL6180X_REG_SYSALS_INTEGRATION_PERIOD_HI, 0)
        self._write_8(VL6180X_REG_SYSALS_INTEGRATION_PERIOD_LO, 100)
        # analog gain
        if gain > VL6180X_ALS_GAIN_40:
            gain = VL6180X_ALS_GAIN_40
        self._write_8(VL6180X_REG_SYSALS_ANALOGUE_GAIN, 0x40 | gain)
        # start ALS
        self._write_8(VL6180X_REG_SYSALS_START, 0x1)
        # Poll until "New Sample Ready threshold event" is set
        while 4 != ((self._read_8(VL6180X_REG_RESULT_INTERRUPT_STATUS_GPIO) >> 3) & 0x7):
            pass
        # read lux!
        lux = self._read_16(VL6180X_REG_RESULT_ALS_VAL)
        # clear interrupt
        self._write_8(VL6180X_REG_SYSTEM_INTERRUPT_CLEAR, 0x07);
        lux *= 0.32 # calibrated count/lux
        if gain == VL6180X_ALS_GAIN_1:
            pass
        elif gain == VL6180X_ALS_GAIN_1_25:
            lux /= 1.25
        elif gain == VL6180X_ALS_GAIN_1_67:
            lux /= 1.76
        elif gain == VL6180X_ALS_GAIN_2_5:
            lux /= 2.5
        elif gain == VL6180X_ALS_GAIN_5:
            lux /= 5
        elif gain == VL6180X_ALS_GAIN_10:
            lux /= 10
        elif gain == VL6180X_ALS_GAIN_20:
            lux /= 20
        elif gain == VL6180X_ALS_GAIN_40:
            lux /= 20
        lux *= 100
        lux /= 100 # integration time in ms
        return lux

Not much changes, however notice Python doesn't support switch statements and a cascade of if and elif statements is necessary.

Phew, that's it as far as the porting of codes goes!  For reference here's the complete adafruit_vl6180x.py file:

# The MIT License (MIT)
#
# Copyright (c) 2017 Tony DiCola for Adafruit Industries
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""
`adafruit_vl6180x`
====================================================

This is a CircuitPython driver for the VL6180X time-of-flight distance sensor.

* Author(s): Tony DiCola
"""
from adafruit_bus_device.i2c_device import I2CDevice


VL6180X_DEFAULT_I2C_ADDR = const(0x29)

VL6180X_REG_IDENTIFICATION_MODEL_ID    = const(0x000)
VL6180X_REG_SYSTEM_INTERRUPT_CONFIG    = const(0x014)
VL6180X_REG_SYSTEM_INTERRUPT_CLEAR     = const(0x015)
VL6180X_REG_SYSTEM_FRESH_OUT_OF_RESET  = const(0x016)
VL6180X_REG_SYSRANGE_START             = const(0x018)
VL6180X_REG_SYSALS_START               = const(0x038)
VL6180X_REG_SYSALS_ANALOGUE_GAIN       = const(0x03F)
VL6180X_REG_SYSALS_INTEGRATION_PERIOD_HI  = const(0x040)
VL6180X_REG_SYSALS_INTEGRATION_PERIOD_LO  = const(0x041)
VL6180X_REG_RESULT_ALS_VAL             = const(0x050)
VL6180X_REG_RESULT_RANGE_VAL           = const(0x062)
VL6180X_REG_RESULT_RANGE_STATUS        = const(0x04d)
VL6180X_REG_RESULT_INTERRUPT_STATUS_GPIO       = const(0x04f)

VL6180X_ALS_GAIN_1         = const(0x06)
VL6180X_ALS_GAIN_1_25      = const(0x05)
VL6180X_ALS_GAIN_1_67      = const(0x04)
VL6180X_ALS_GAIN_2_5       = const(0x03)
VL6180X_ALS_GAIN_5         = const(0x02)
VL6180X_ALS_GAIN_10        = const(0x01)
VL6180X_ALS_GAIN_20        = const(0x00)
VL6180X_ALS_GAIN_40        = const(0x07)

VL6180X_ERROR_NONE         = const(0)
VL6180X_ERROR_SYSERR_1     = const(1)
VL6180X_ERROR_SYSERR_5     = const(5)
VL6180X_ERROR_ECEFAIL      = const(6)
VL6180X_ERROR_NOCONVERGE   = const(7)
VL6180X_ERROR_RANGEIGNORE  = const(8)
VL6180X_ERROR_SNR          = const(11)
VL6180X_ERROR_RAWUFLOW     = const(12)
VL6180X_ERROR_RAWOFLOW     = const(13)
VL6180X_ERROR_RANGEUFLOW   = const(14)
VL6180X_ERROR_RANGEOFLOW   = const(15)


class Adafruit_VL6180X:

    def __init__(self, i2c, address=VL6180X_DEFAULT_I2C_ADDR):
        self._device = I2CDevice(i2c, address)

    def begin(self):
        """Initialize access to the sensor.  Returns True if successful."""
        if self._read_8(VL6180X_REG_IDENTIFICATION_MODEL_ID) != 0xB4:
            return False
        self._load_settings()
        self._write_8(VL6180X_REG_SYSTEM_FRESH_OUT_OF_RESET, 0x00)
        return True

    def read_range(self):
        """Read the range of an object in front of sensor and return it in mm."""
        # wait for device to be ready for range measurement
        while not (self._read_8(VL6180X_REG_RESULT_RANGE_STATUS) & 0x01):
            pass
        # Start a range measurement
        self._write_8(VL6180X_REG_SYSRANGE_START, 0x01);
        # Poll until bit 2 is set
        while not (self._read_8(VL6180X_REG_RESULT_INTERRUPT_STATUS_GPIO) & 0x04):
            pass
        # read range in mm
        range_ = self._read_8(VL6180X_REG_RESULT_RANGE_VAL)
        # clear interrupt
        self._write_8(VL6180X_REG_SYSTEM_INTERRUPT_CLEAR, 0x07)
        return range_

    def read_lux(self, gain):
        """Read the lux (light value) from the sensor and return it."""
        reg = self._read_8(VL6180X_REG_SYSTEM_INTERRUPT_CONFIG)
        reg &= ~0x38
        reg |= (0x4 << 3) # IRQ on ALS ready
        self._write_8(VL6180X_REG_SYSTEM_INTERRUPT_CONFIG, reg)
        # 100 ms integration period
        self._write_8(VL6180X_REG_SYSALS_INTEGRATION_PERIOD_HI, 0)
        self._write_8(VL6180X_REG_SYSALS_INTEGRATION_PERIOD_LO, 100)
        # analog gain
        if gain > VL6180X_ALS_GAIN_40:
            gain = VL6180X_ALS_GAIN_40
        self._write_8(VL6180X_REG_SYSALS_ANALOGUE_GAIN, 0x40 | gain)
        # start ALS
        self._write_8(VL6180X_REG_SYSALS_START, 0x1)
        # Poll until "New Sample Ready threshold event" is set
        while 4 != ((self._read_8(VL6180X_REG_RESULT_INTERRUPT_STATUS_GPIO) >> 3) & 0x7):
            pass
        # read lux!
        lux = self._read_16(VL6180X_REG_RESULT_ALS_VAL)
        # clear interrupt
        self._write_8(VL6180X_REG_SYSTEM_INTERRUPT_CLEAR, 0x07);
        lux *= 0.32 # calibrated count/lux
        if gain == VL6180X_ALS_GAIN_1:
            pass
        elif gain == VL6180X_ALS_GAIN_1_25:
            lux /= 1.25
        elif gain == VL6180X_ALS_GAIN_1_67:
            lux /= 1.76
        elif gain == VL6180X_ALS_GAIN_2_5:
            lux /= 2.5
        elif gain == VL6180X_ALS_GAIN_5:
            lux /= 5
        elif gain == VL6180X_ALS_GAIN_10:
            lux /= 10
        elif gain == VL6180X_ALS_GAIN_20:
            lux /= 20
        elif gain == VL6180X_ALS_GAIN_40:
            lux /= 20
        lux *= 100
        lux /= 100 # integration time in ms
        return lux

    def read_range_status(self):
        """Retrieve the status/error from a previous range read."""
        return self._read_8(VL6180X_REG_RESULT_RANGE_STATUS) >> 4

    def _load_settings(self):
        # private settings from page 24 of app note
        self._write_8(0x0207, 0x01)
        self._write_8(0x0208, 0x01)
        self._write_8(0x0096, 0x00)
        self._write_8(0x0097, 0xfd)
        self._write_8(0x00e3, 0x00)
        self._write_8(0x00e4, 0x04)
        self._write_8(0x00e5, 0x02)
        self._write_8(0x00e6, 0x01)
        self._write_8(0x00e7, 0x03)
        self._write_8(0x00f5, 0x02)
        self._write_8(0x00d9, 0x05)
        self._write_8(0x00db, 0xce)
        self._write_8(0x00dc, 0x03)
        self._write_8(0x00dd, 0xf8)
        self._write_8(0x009f, 0x00)
        self._write_8(0x00a3, 0x3c)
        self._write_8(0x00b7, 0x00)
        self._write_8(0x00bb, 0x3c)
        self._write_8(0x00b2, 0x09)
        self._write_8(0x00ca, 0x09)
        self._write_8(0x0198, 0x01)
        self._write_8(0x01b0, 0x17)
        self._write_8(0x01ad, 0x00)
        self._write_8(0x00ff, 0x05)
        self._write_8(0x0100, 0x05)
        self._write_8(0x0199, 0x05)
        self._write_8(0x01a6, 0x1b)
        self._write_8(0x01ac, 0x3e)
        self._write_8(0x01a7, 0x1f)
        self._write_8(0x0030, 0x00)
        # Recommended : Public registers - See data sheet for more detail
        self._write_8(0x0011, 0x10)   # Enables polling for 'New Sample ready'
                                      # when measurement completes
        self._write_8(0x010a, 0x30)   # Set the averaging sample period
                                      # (compromise between lower noise and
                                      # increased execution time)
        self._write_8(0x003f, 0x46)   # Sets the light and dark gain (upper
                                      # nibble). Dark gain should not be
                                      # changed.
        self._write_8(0x0031, 0xFF)   # sets the # of range measurements after
                                      # which auto calibration of system is
                                      # performed
        self._write_8(0x0040, 0x63)   # Set ALS integration time to 100ms
        self._write_8(0x002e, 0x01)   # perform a single temperature calibration
                                      # of the ranging sensor

        # Optional: Public registers - See data sheet for more detail
        self._write_8(0x001b, 0x09)   # Set default ranging inter-measurement
                                      # period to 100ms
        self._write_8(0x003e, 0x31)   # Set default ALS inter-measurement period
                                      # to 500ms
        self._write_8(0x0014, 0x24)   # Configures interrupt on 'New Sample
                                      # Ready threshold event'

    def _write_8(self, address, data):
        # Write 1 byte of data from the specified 16-bit register address.
        with self._device:
            self._device.write(bytes([(address >> 8) & 0xFF,
                                       address & 0xFF,
                                       data]))

    def _write_16(self, address, data):
        # Write a 16-bit big endian value to the specified 16-bit register
        # address.
        with self._device:
            self._device.write(bytes([(address >> 8) & 0xFF,
                                       address & 0xFF,
                                      (data >> 8) & 0xFF,
                                       data & 0xFF]))

    def _read_8(self, address):
        # Read and return a byte from the specified 16-bit register address.
        with self._device:
            self._device.write(bytes([(address >> 8) & 0xFF,
                                       address & 0xFF]),
                               stop=False)
            result = bytearray(1)
            self._device.read_into(result)
            return result[0]

    def _read_16(self, address):
        # Read and return a 16-bit unsigned big endian value read from the
        # specified 16-bit register address.
        with self._device:
            self._device.write(bytes([(address >> 8) & 0xFF,
                                       address & 0xFF]),
                               stop=False)
            result = bytearray(2)
            self._device.read_into(result)
            return (result[0] << 8) | result[1]

Let's try testing it out on the hardware.  Copy the adafruit_vl6180x.py to your board's filesystem.  Then connect to the REPL and initialize I2C as you did before:

import board
import busio
i2c = busio.I2C(board.SCL, board.SDA)

Now import the module you created and try creating an instance of the Adafruit_VL6180X class.  Remember you need to pass it the I2C bus instance:

import adafruit_vl6180x
vl6180x = vl6180x.Adafruit_VL6180X(i2c)

Note if your driver is very large or complex you might run into an error that your board ran out of memory.  Unfortunately memory is a big constraint with small microcontrollers and it can be tricky to optimize and reduce memory usage.  One simple thing to do is to convert your driver to a .mpy file and try loading it again.  Remember every time you update the driver code you must regenerate and copy over the new .mpy file!

Next remember you should call begin to initialize the library and verify it can talk to the chip.  If begin succeeds it will return True:

vl6180x.begin()

Now try reading the range in millimeters:

vl6180x.read_range()

You should see a value of 255 if something is too far away for the sensor to detect (it only can read up to about 6 inches in front of itself).  As you move an object closer or further away try calling the function again to see the value change, for example moving a hand close and then further away results in values like:

You can read the status and lux values too:

vl6180x.read_range_status()
vl6180x.read_lux(adafruit_vl6180x.VL6180X_ALS_GAIN_1)

Notice you need to pass a gain value to the read_lux function.  The global variables at the top of the module are what you can use in place of the #define values typically used by Arduino libraries.  Remember to reference these values with the module name preceding them!

That's all there is to the basics of porting an Arduino library to Python and CircuitPython.  Continue on to the next page to learn how to simplify the Python code so it's more friendly and easy to use.

Last updated on 2017-10-16 at 05.31.24 PM Published on 2017-10-16 at 05.31.56 PM