Sensirion / python-i2c-sf06-lf

Sensirion Liquid Flow I2C driver for Python
https://sensirion.github.io/python-i2c-sf06-lf/
BSD 3-Clause "New" or "Revised" License
3 stars 3 forks source link

usage with i2c multiplexers #2

Closed jchavesc closed 1 year ago

jchavesc commented 2 years ago

Would you please consider adding a minimal python example implementation for multiple sensors using an i2c mux (e.g., TCA9548A), such as the Arduino example. We haven't been able to reach the sensors in the TCA9548A beyond the one in channel 0, and even there, the behaviour is not consistent. This request might be more applicable to other sensors, if addressed for the sensirion_i2c_driver, too. In any case, the docs here and for the i2c_driver are lacking clear guidance for applications needing i2c multiplexing. Thank you

psachs commented 2 years ago

Hi @jchavesc

Thank you for your request. So far we have not worked with multiplexers for the python drivers. With the current sensirion i2c driver package multiplexing will not work without some custom code. We will have a look into how we can provide support for multiplexing for the Python driver using the TCA9548A.

Best regards, Pascal

jchavesc commented 2 years ago

Thank you, @psachs . That will be much appreciated. We tried to adapt this StackOverflow solution to the SFxx series, but it didn't work. However, after running the adapated code, the sensor on channel 0 was visible to an I2c scan as if it was connected to the main bus, and the sensirion test example ran fine only for that sensor.

psachs commented 2 years ago

Hi @jchavesc

Sorry for the late response. Due to my holidays and lack of hardware it took me way longer then expected to test a solution. To get the multiplexer working you need to small additional classes to switch the channel on the multiplexer. channel can be between 0 and 7 on a TCA9548A. The first class is the command we send to the multiplexer, the second one is an additional I2C connection to send the command to the multiplexer.

from sensirion_i2c_driver import LinuxI2cTransceiver, I2cConnection, I2cCommand

class SelectChannelCommand(I2cCommand):
    def __init__(self, channel:int):
        super().__init__(tx_data=[1<<channel], rx_length=None, read_delay=0.0, 
                         timeout=0.5)

class Tca9548aI2cConnection(I2cConnection):
    def select_channel(self, channel: int) -> None:
        self.execute(slave_address=0x70, 
                     command=SelectChannelCommand(channel))

The multiplexer has a very simple interface. You send the channel you want to activate as a bit mask and it will expose this channel to the I2C.

To use the driver with multiple sensors you have to do something like this. I assume channels 1 and 3 are used on the multiplexer.

with LinuxI2cTransceiver('/dev/i2c-1') as i2c_transceiver:
    multiplexer = Tca9548aI2cConnection(i2c_transceiver)
    channel = I2cChannel(I2cConnection(i2c_transceiver),
                         slave_address=0x08,
                         crc=CrcCalculator(8, 0x31, 0xff, 0x0))
    sensor = Sf06LfDevice(channel)

    # Start measurement on Channel 1 
    multiplexer.select_channel(1)
    sensor.start_h2o_continuous_measurement()

    # Start measurement on Channel 3
    multiplexer.select_channel(3)
    sensor.start_h2o_continuous_measurement

Please find attached a full example on how a multiplexer could be used sf06_lf_multiplexer_example.zip ()

jchavesc commented 2 years ago

@psachs thank you so very much for this! Was also away from email this past few days

jchavesc commented 1 year ago

Thanks again @psachs for putting this together. We've been testing it and It works intermittently in our prototype set up: TCA9548A with 6 SLF3s-1330F. We don't think it is a wiring issue, since it works for each sensor independently when only their channel is called. If we add more channels to the run, say [0:3], that's when it starts to work erratically. Most of the time only 2 channels work. Interestingly, if we add a redundant loop above, it works for all channels:

C = [0, 1, 2, 3, 4, 5] 

for c in C:
    multiplexer_channels = [c]

    for multiplexer_channel in multiplexer_channels:
    ...

In its original form, the first loop calling stop_continuous_measurement for all sensors never fails. The second loop calling read_product_identifier and start_h2o_continuous_measurement starts returning exceptions for some sensors: I2C transceive failed: [Errno 121] Remote I/O error, which can change in different runs (i.e., different sensors return the error).

In the read_measurement_data loop, the sensors that failed above return a checksum error as per below example (no fluid is used on tests):

[Channel 1] a_flow: 0.00; a_temperature: 25.13; a_signaling_flags: 1; 
[Channel 2]: I2C error: Received wrong checksum 0xFF (expected 0xAC).
[Channel 3] a_flow: 0.00; a_temperature: 24.37; a_signaling_flags: 1; 
[Channel 4]: I2C error: Received wrong checksum 0xFF (expected 0xAC).
[Channel 5]: I2C error: Received wrong checksum 0xFF (expected 0xAC).

If any of the info above offers any hints as to what is causing the issues, we would appreciate any guidance. Thank you!

jchavesc commented 1 year ago

If I understand this correctly the errors above are not uncommon and we should implement some of this: https://sensirion.github.io/python-i2c-driver/error_handling.html?

jchavesc commented 1 year ago

Looks like if we add a while loop until the channel works, measurement proceed without error...at least momentarily on a small example. Unless that is not a wise course of action, please let us know if there is a better approach to error handling. Thanks again

    for multiplexer_channel in multiplexer_channels:

        try:
            multiplexer.select_channel(multiplexer_channel)
            (product_identifier, serial_number) = sensor.read_product_identifier()
            print(f"[Channel {multiplexer_channel}] product_identifier: {product_identifier}; " f"serial_number: {serial_number}; ")
            sensor.start_h2o_continuous_measurement()
        except BaseException as e:
            print(f'Channel [{multiplexer_channel}:]' + f'{e}')
            print('Trying again...')
            error_switch = True
            counter = 0
            while error_switch:
                try:
                    counter += 1
                    multiplexer.select_channel(multiplexer_channel)
                    (product_identifier, serial_number) = sensor.read_product_identifier()
                    print(f"[Channel {multiplexer_channel}] product_identifier: {product_identifier}; " f"serial_number: {serial_number}; ")
                    sensor.start_h2o_continuous_measurement()
                    print(f'It worked on try {counter}!\n')
                    error_switch = False
                except BaseException as e:
                    counter += 1
                    print(f'Channel [{multiplexer_channel}] failed on try {counter}:' + f'{e}')
            ...
psachs commented 1 year ago

If you see CRC errors it is mostly related to an unreliable connection. Please make sure your power supply is strong enough to handle all sensors and the cables and connectors are good.

There might also be a timing issue with the multiplexer and we need a small delay after switching channels to ensure communication is reliable. This is just a guess.

Using a while loop is a valid option and it definitely makes sense to add some error handling to a real world application. I would however suggest that we also figure out the root cause of the issue instead of just accepting the unreliable communication.

jchavesc commented 1 year ago

Thank you, @psachs. The cables could certainly be an issue. I have multiple cables daisy-chained to each sensor on this prototype setup--couldn't make long enough ones. So far the while loop is handling the errors, but we'll certainly look into the potential issues you suggest above. We have a good power supply (RQ-125D, 125W) for the whole system (Raspberry Pi, sensors, and one stepper motor), so I will put that issue as a lower priority to look into. Thanks again for the prompt response.