ev3dev / ev3dev-lang-python

Pure python bindings for ev3dev
MIT License
422 stars 146 forks source link

How can I use a generic I2C sensor? #782

Open IanLewis42 opened 1 year ago

IanLewis42 commented 1 year ago

I'm trying to use a generic I2C sensor with my EV3, specifically, a Nintendo Wii Motion Plus (https://en.wikipedia.org/wiki/Wii_MotionPlus) I used this sensor with my NXT a couple of years ago, so I think my hardware interface should be OK.

What is the best way to do 'raw' i2c writes and reads?

I tried using the Sensor class, but it can't seem to find my sensor.

Code:

#!/usr/bin/env python3
from ev3dev2.sensor import Sensor

def main():

    wmp = Sensor('ev3-ports:in4:i2c83') #I2C address is 0xA6, unshifted is 0x53 = decimal 83
    wmp.command(0xFE, 0x04)

if __name__ == '__main__':
    main()

Output:

Starting: brickrun --directory="/home/robot/vscode-hello-python-master" "/home/robot/vscode-hello-python-master/wmp.py"
----------
Traceback (most recent call last):
  File "/home/robot/vscode-hello-python-master/wmp.py", line 10, in <module>
    main()
  File "/home/robot/vscode-hello-python-master/wmp.py", line 6, in main
    wmp = Sensor('ev3-ports:in4:i2c83') #I2C address is 0xA6, unshifted is 0x53 = decimal 83
  File "/usr/lib/python3/dist-packages/ev3dev2/sensor/__init__.py", line 78, in __init__
    super(Sensor, self).__init__(self.SYSTEM_CLASS_NAME, name_pattern, name_exact, **kwargs)
  File "/usr/lib/python3/dist-packages/ev3dev2/__init__.py", line 223, in __init__
    chain_exception(DeviceNotFound("%s is not connected." % self), None)
  File "/usr/lib/python3/dist-packages/ev3dev2/__init__.py", line 54, in chain_exception
    raise exception from cause
ev3dev2.DeviceNotFound: Sensor(ev3-ports:in4:i2c83) is not connected.
dlech commented 1 year ago

The ev3dev-lang-python classes are for LEGO-specific devices. The best way to use generic device is to put the port in i2c-other mode (this can be done using ev3dev-lang-python or by using the port browser in the EV3 user interface). Then use the Python smbus library for I2C operations (from smbus import SMBus).

IanLewis42 commented 1 year ago

Thanks very much for your help. I don't (yet) have meaningful data coming back from the sensor, but there's many possible reasons for that.

Do you have a good reference / examples for smbus? There seem to be several similarly named libraries, and I'm not sure that I'm looking at the right documentation!

dlech commented 1 year ago

I think there might be a few examples in some of the issues at https://github.com/ev3dev/ev3dev/issues

I don't think there are proper docs for the library, but is is basically a Python wrapper around https://www.kernel.org/doc/html/latest/i2c/smbus-protocol.html

IanLewis42 commented 1 year ago

Thanks for the assistance. I had some hardware issues, that are now resolved, but I now get the following error.

Traceback (most recent call last):

  File "/home/robot/vscode-hello-python-master/wmp.py", line 65, in <module>
     main()
  File "/home/robot/vscode-hello-python-master/wmp.py", line 37, in main
    bus.write_byte(address,0xFE)
OSError: [Errno 6] No such device or address

Relevant section of code:

p4 = LegoPort(INPUT_4)
    p4.mode = 'other-i2c'

    debug_print(p4)
    debug_print(p4.address)
    debug_print(p4.mode)
    debug_print(p4.status)

    sleep(0.5)

    bus = SMBus(6)
    debug_print(bus)

    address = 0xA6>>1
    bus.write_byte(address, 0xFE)

WIthout going too deep into I2C protocol, I belive this is because the slave (Wii Motion Plus) doesn't acknowledge the first I2C transaction, and the SMBus library concludes there's nothing there.

Is there a way to either tell SMBus to ignore this, or a lower-level way to just push bytes onto the I2C bus?

I found https://www.kernel.org/doc/html/latest/i2c/i2c-protocol.html which looks like it might do it, but I don't know if there's a way to access it from Python. I also don't know what it means by 'setting these flags for I2C messages'.

dlech commented 1 year ago

Do you have docs on what the wii motion plus expects?

For most devices I've worked with, the read_i2c_block_data() and write_i2c_block_data() methods are all that is needed.

IanLewis42 commented 1 year ago

Official docs? No, I doubt Nintendo released them. There's reverse-engineered stuff online, though: https://123a321.wordpress.com/2009/08/21/read-motion-plus-via-i2c/ http://wiibrew.org/wiki/Wiimote/Extension_Controllers/Wii_Motion_Plus

The odd thing about the WMP is that (according to everything I've read) you need to do an initial write of 0xFE 0x04 to I2C address 0xA6 (or 0x53, depending on how you like your I2C addresses shifted) but all subsequent accesses are to I2C address 0xA4 (/0x52)

I do have a working NXT setup, which I dug out and had a look at with an oscilloscope. I was surprised at what I saw. The NXT code was supposed to do what I've described above, but the scope showed that the initial write to 0x53 was terminated when the address was NACK'd, so the bytes 0xFE, 0x04 never make it onto the bus. Despite this, the WMP responds to the subsequent accesses to I2C address 0x52, and sends back valid data.

nxt 1000ms

The EV3, for comparison, does the same write to 0x53, also NACK'd, and aborted, but the driver error stops the code continuing.

ev3 5ms

One more thing perhaps worth mentioning is that if I run my code with nothing plugged in to the sensor port, it runs happily, just returning 0x00 for all data bytes. The difference is, presumably, that with nothing plugged in, there's no pullup on SDA, and the I2C controller/driver sees SDA 'low' as an ACK, and so carries on.

I realise that failure to ACK the 0x53 write is the WMPs fault, but it seems as though if I could just persaude the driver to ignore this, I might be able to get it to work.

Apologies for the long post, but does anyone know how to get lower-level access to the I2C, to persuade it to ignore the NACK?

dlech commented 1 year ago

So for the first command, wouldn't it be this?

bus.write_byte_data(address, 0xFE, 0x04)
IanLewis42 commented 1 year ago

Yes, it would. Sorry, that was the first thing I tried, I then went through lots of different variations, but they all do the same thing, i.e. fail with OSError: [Errno 6] No such device or address. The bus acivity stops immediately after the NACK of the address.

dlech commented 1 year ago

If I'm reading right, it looks like the NXT just writes the 0x53 without the 0xFE, 0x04 data. If that is the case, perhaps the quick write method could be used to just send the 0x53 address.

IanLewis42 commented 1 year ago

You are reading correctly. (Though to be clear, the NXT code is supposed to be writing the 0xFE, 0x04. I assume the write is terminated because of the NACK)

Yes, I've tried using the quick write method. Same result, OSError: [Errno 6] No such device or address

dlech commented 1 year ago

Does the data still go over the bus though? You can catch this error and ignore it and then try the next commands at the second address.

IanLewis42 commented 1 year ago

Apologies for the extremely slow response. I eventually gave up on this, so I'm not really asking for any more help, but thought I should perhaps describe what I tried.

I used the try-except as suggested, so my code looked like this:

    while True:
        block = 0
        try:
            bus.write_byte(address, 0x00)
        except Exception as e:
            debug_print("Data write error", e)
        try:
            block = bus.read_i2c_block_data(address, 0, 6)
        except Exception as e:
            debug_print("Data read error", e)

This worked, in that it kept running, and kept putting data on the bus, but I nerver saw a response from the WMP.

There are some scope captures below, As far as I can tell, it looks like it should. I eventually managed to break my NXT interface too, so maybe it was always the hardware interface all along.

ev3 2ms analogue ev3 1000ms