pimoroni / pimoroni-pico

Libraries and examples to support Pimoroni Pico add-ons in C++ and MicroPython.
https://shop.pimoroni.com/collections/pico
MIT License
1.32k stars 496 forks source link

Can't get Pimoroni i2c breakouts to work with Micro Python Machine i2c? #359

Closed alphanumeric007 closed 2 years ago

alphanumeric007 commented 2 years ago

I have a Pimoroni VEML6076 UV sensor breakout that I want to use in Micro Python on a Pi Pico. It's discontinued so there is no Pimoroni Micro Python library. I have found one that does work in Micro Python using machine i2c.

import machine
sda=machine.Pin(4) # Explorer 20 Breakout 4
scl=machine.Pin(5) # Explorer 21 Breakout 5
i2c=machine.I2C(0,sda=sda, scl=scl, freq=400000)

import veml6075
import time

time.sleep_ms(500)

uv = veml6075.VEML6075(i2c)
connected = uv.initUV()

while True:
    UVI, UVIA, UVIB = uv.readUV()
    print (UVI)
    print (UVIA)
    print (UVIB)

    time.sleep(1.0)

The library file is as follows

import time
import ubinascii

class VEML6075:

    # Device information

    VEML6075_ADDR  = 0x10
    VEML6075_DEVID = 0x26

    REG_CONF        = 0x00  # Configuration register (options below)
    REG_UVA         = 0x07  # UVA register
    REG_UVD         = 0x08  # Dark current register (NOT DUMMY)
    REG_UVB         = 0x09  # UVB register
    REG_UVCOMP1     = 0x0A  # Visible compensation register
    REG_UVCOMP2     = 0x0B  # IR compensation register
    REG_DEVID       = 0x0C  # Device ID register

    CONF_IT_50MS    = 0x00  # Integration time = 50ms (default)
    CONF_IT_100MS   = 0x10  # Integration time = 100ms
    CONF_IT_200MS   = 0x20  # Integration time = 200ms
    CONF_IT_400MS   = 0x30  # Integration time = 400ms
    CONF_IT_800MS   = 0x40  # Integration time = 800ms
    CONF_IT_MASK    = 0x8F  # Mask off other config bits

    CONF_HD_NORM    = 0x00  # Normal dynamic seetting (default)
    CONF_HD_HIGH    = 0x08  # High dynamic seetting

    CONF_TRIG       = 0x04  # Trigger measurement, clears by itself

    CONF_AF_OFF     = 0x00  # Active force mode disabled (default)
    CONF_AF_ON      = 0x02  # Active force mode enabled (?)

    CONF_SD_OFF     = 0x00  # Power up
    CONF_SD_ON      = 0x01  # Power down

    # To calculate the UV Index, a bunch of empirical/magical coefficients need to
    # be applied to UVA and UVB readings to get a proper composite index value.

    UVA_A_COEF = 2.22 
    UVA_B_COEF = 1.33 
    UVB_C_COEF = 2.95 
    UVB_D_COEF = 1.74 

    # Once the above offsets and crunching is done, there's a last weighting
    # function to convert the ADC counts into the UV index values. This handles
    # both the conversion into irradiance (W/m^2) and the skin erythema weighting
    # by wavelength--UVB is way more dangerous than UVA, due to shorter
    # wavelengths and thus more energy per photon. These values convert the compensated values 

    UVA_RESPONSIVITY = 0.0011
    UVB_RESPONSIVITY = 0.00125

    def __init__(self, i2c=None):
        self.i2c = i2c
        self.address = self.VEML6075_ADDR

    # initialize device
    def initUV(self):
        try:
            deviceID = bytearray(2)
            self.i2c.readfrom_mem_into(self.address, self.REG_DEVID, deviceID)
            if (deviceID[0] != self.VEML6075_DEVID):
                return False
            else:   
                # Write Dynamic and Integration Time Settings to Sensor
                conf_data = bytearray(2)
                conf_data[0] = self.CONF_IT_100MS| \
                          self.CONF_HD_NORM| \
                          self.CONF_SD_OFF
                conf_data[1] = 0
                self.i2c.writeto_mem(self.address, self.REG_CONF, conf_data)  
                return True
        except:
            return False

    # Read UV data from device, return UV index
    def readUV(self):
        time.sleep_ms(150)  
        uv_data = bytearray(2)

        self.i2c.readfrom_mem_into(self.address,self.REG_UVA, uv_data)
        uva = int.from_bytes(uv_data, 'little')

        self.i2c.readfrom_mem_into(self.address,self.REG_UVD, uv_data)
        uvd = int.from_bytes(uv_data, 'little')

        self.i2c.readfrom_mem_into(self.address,self.REG_UVB, uv_data)
        uvb = int.from_bytes(uv_data, 'little')

        self.i2c.readfrom_mem_into(self.address,self.REG_UVCOMP1, uv_data)
        uvcomp1 = int.from_bytes(uv_data, 'little')

        self.i2c.readfrom_mem_into(self.address,self.REG_UVCOMP2, uv_data)
        uvcomp2 = int.from_bytes(uv_data, 'little')

        uvacomp = (uva-uvd) - (self.UVA_A_COEF * (uvcomp1-uvd)) - (self.UVA_B_COEF * (uvcomp2-uvd))
        uvbcomp = (uvb-uvd) - (self.UVB_C_COEF * (uvcomp1-uvd)) - (self.UVB_D_COEF * (uvcomp2-uvd))

# Do not allow negative readings which can occur in no UV light environments e.g. indoors
        if uvacomp < 0:  
            uvacomp = 0
        if uvbcomp < 0:
            uvbcomp = 0

        uvai = uvacomp * self.UVA_RESPONSIVITY
        uvbi = uvbcomp * self.UVB_RESPONSIVITY
        uvi = (uvai + uvbi) / 2

        return (uvi, uvai, uvbi)

The pickle I'm in is the above won't work if I try to use the Pimoroni i2c. If I substitute in the following: from pimoroni_i2c import PimoroniI2C PINS_BREAKOUT_GARDEN = {"sda": 4, "scl": 5} PINS_PICO_EXPLORER = {"sda": 20, "scl": 21} i2c = PimoroniI2C(**PINS_BREAKOUT_GARDEN) I get AttributeError: 'pimoroni_i2c' object has no attribute 'readfrom_mem_into'

And if I try to use any of the Pimoroni i2c breakouts with machine i2c

from breakout_bme280 import BreakoutBME280 bme = BreakoutBME280(i2c) I get ValueError: BreakoutBME280: Bad i2C object?

Any help with this would be greatly appreciated.

Gadgetoid commented 2 years ago

The two things are not cross compatible and probably wont ever be. You'd need to use both with their respective libraries.

(I suppose it's possible to make Pimoroni I2C's interface compatible with machine.I2C somewhat, but it really only exists as a pointer to an instance of the C++ I2C wrapper and doesn't implement any IO functionality visible to the Python layer)

Pimoroni I2C was written primarily to serve the C++ libraries and then wrapped in MicroPython so the code between C++ / MicroPython bindings wouldn't have to diverge too much.

alphanumeric007 commented 2 years ago

That's pretty well what Hel Gibbons told be. I could have sworn I had the VEML6075 working with the other breakouts? If so I've lost that file / info somewhere along the way. How much work is it to make a Pimoroni Micro Python library for the VEML6075?

Gadgetoid commented 2 years ago

Have I got a PR for you - https://github.com/pimoroni/pimoroni-pico/pull/361

Turns out most of machine.I2C is implemented as an extmod which is inherited by the port-specific version responsible for implementing how I2C transfer actually happen.

I can borrow that code, extending Pimoroni I2C to be completely interchangeable with machine.I2C().

The downside of this is a potential memory leak on the Pimoroni I2C object. We should probably try to use m_new to alloc this into MicroPython's memory so the GC will at least clean up the used memory, even if it doesn't call the class deconstructor.

Releases here - https://github.com/pimoroni/pimoroni-pico/actions/runs/2339289458

magic:

>>> import pimoroni_i2c
>>> PINS_BREAKOUT_GARDEN = {"sda": 4, "scl": 5}
>>> i2c = pimoroni_i2c.PimoroniI2C(**PINS_BREAKOUT_GARDEN, baudrate=2_000_000)
>>> dir(i2c)
['__class__', 'readinto', 'start', 'stop', 'write', 'init', 'readfrom', 'readfrom_into', 'readfrom_mem', 'readfrom_mem_into', 'scan', 'writeto', 'writeto_mem', 'writevto']
>>> i2c.scan()
[41]
>>> 
alphanumeric007 commented 2 years ago

That worked, as near as I can tell anyway. I'm getting values back, and no error messages. Breaks my dual display setup though? ImportError: no module named 'st7789'

Gadgetoid commented 2 years ago

Yup, it'll need merged in to main along with the dual display stuff for it all to work together :grimacing:

alphanumeric007 commented 2 years ago

I had feeling it was something like that. Thanks again Phil, =) I'll be watching for this to all become official.

Gadgetoid commented 2 years ago

Because I am a glutton for punishment I have also changed every single one of our I2C based sensors to accept machine.I2C and internally promote it to PimoroniI2C so things work both ways around... this has some drawbacks explained in my PR but also tidies up a few things for the better.

I would generally recommend using PimoroniI2C since it doesn't rely on magically creating new I2C objects behind the scenes, but on the off chance anyone expects machine.I2C to work (a perfectly reasonable expectation to be fair)... it now should.

If you want to see if it explodes with any of your sensors using machine.I2C and/or PimoroniI2C you can find builds here:

https://github.com/pimoroni/pimoroni-pico/actions/runs/2344622982

I'm broadly keen to avoid not-invented-here syndrome and get us closer to MicroPython rather than weird-Pimoroni-distorted-MicroPython. This has been a wild ride.

alphanumeric007 commented 2 years ago

Thanks Phil. Downloading as I type this. This is what all my pestering is in aid of. https://forums.pimoroni.com/t/pi-pico-based-weather-station-project/19503/2

Gadgetoid commented 2 years ago

I've merged everything into main and released a beta build for you - and others - to try out: https://github.com/pimoroni/pimoroni-pico/releases/tag/v1.18.8

alphanumeric007 commented 2 years ago

Awesome, downloading now. Should have the Pico Lip 16mb image tested before the day is out.

alphanumeric007 commented 2 years ago

Just dropped that new uf2 file to my Pico Lipo 16mb. Everything still works and I now have my UV sensor working. =)

I can't thank you enough for taking the time to do all this. All I have left sensor wise is the rain gauge and wind speed and direction. I'm not expecting any issues there, lots of code out there for that stuff.

Gadgetoid commented 2 years ago

Thank you- it's invaluable to have someone other than me actually trying these changes and feeding back so quickly!

And, of course, an awesome project to justify them :laughing: