Controlling Mega I2C via Python - pymata4/telemetrix

Hello all, I am working on a robotics project that involves a PCA9685 PWM controller. I am looking to use a python script to communicate with an Arduino Mega. I have been able to successfully connect to the Mega and change the states of digital pins and use PWM functions using pymata4. However, I am now trying to communicate with the PCA9685 which is connected to the Mega via I2C.

Here's the disclaimer: I have never tried to communicate with an I2C device "manually", that is to say sending the data without the help of a pre-made library.

I have spent hours trying to comb through the documentation for both the PCA9685 and pymata4, but I have been unable to get anything to work. I am trying to control a single servo on channel 0, but no matter what I have tried, it never wants to move.

I am all ears if anyone has any experience with the Firmata protocol/pymata. I have also read that using Telemetrix might also be a viable solution. I believe that @MrY is the author of the of these softwares, as there were some archived posts that led me here.

An input is greatly appreciated, thanks!

I have a PCA96895 library for telemetrix, which can be found here. I have not tested it recently, but it was tested and working when I published it in 2021.
I would be pleased to answer any questions you might have. If you find any issues, please report them here.

Thank you! I will definitely check that out now! I'll report back once I play around with it!

So, it took some messing around, but I did get it working!

I installed Telemetrix and the telemetrix-extensions, then uploaded the Telemetrix4arduino sketch onto the Mega and attempted to run the PCA9685 example...

I would run the sketch (Using PyCharm) and would get a threading error (Exception in thread Thread-2 (_reporter)) right after the connection to the board was established. This was using python 3.12.

I found that the issue was in the for loops that were using the servo.set_pwm( ) function. The time delays were outside of the for loops and I discovered that if I put a time delays (0.001s) in the loops then everything seemed to work as expected!

The threading stuff still goes over my head, so I'm not sure if it was an issue with the for loop trying to run as fast as possible or what.

Anyhow, I'm able to set the PWM signal and angles as I please now! I think I'll take that as a win for tonight, lol. Thank you again for not only pointing me in the right direction, but making the entire solution in the first place! :crazy_face:

I was also wondering about chaining several of the controllers together. I see that the default I2C address of 0x40 is set in the constants, but is there an easy way to address a chain of 3 or 4 of the PCA9685s?

I am glad that you got things working.
You can chain the devices, and to do so, you will need to specify the i2c address of each device as you instantiate TelemetrixPCA9685 for each.

For example, if you are using two devices and the first has an i2c address of 0x40, and the second, 0x41, the code might look like this:

# create a telemetrix instance
the_board = telemetrix.Telemetrix()

# set i2c pin mode
the_board.set_pin_mode_i2c()

# create an instance of the pc9685 handler
servo1 = telemetrix_pca9685.TelemetrixPCA9685(board=the_board)
servo2 = telemetrix_pca9685.TelemetrixPCA9685(i2c_address = 0x41, board=the_board)

Servo 1 uses the default address, so it does not need to be specified.

To answer your original question about how to create support for an i2c device, I first find a working Arduino library for the device and then port that library to Telemetrix. For the PCAS9685, I used the Adafruit library. If you compare the Adafruit file to the telemetrix driver, you will the see the similarities.
Programming an i2c device can be tricky, so starting from working code makes life simpler :grinning:

Ok, thanks!

I am having another issue now that I can't seem to resolve...

I am trying to add an Arduino Nano to the I2C bus as a slave device. I plan to eventually have several of these that all report data back to the mega/python script. For testing purposes, I have a 18B20 temperature sensor connected to Nano that is working and outputs the temperature in the serial monitor. I am using the Wire.h library to send the data from the Nano slave. Here is the code I have on the Nano:

#include <OneWire.h>
#include <DallasTemperature.h>
#include <Wire.h>

// Data wire is conntec to the Arduino digital pin 4
#define ONE_WIRE_BUS 6

// Setup a oneWire instance to communicate with any OneWire devices
OneWire oneWire(ONE_WIRE_BUS);

// Pass our oneWire reference to Dallas Temperature sensor 
DallasTemperature sensors(&oneWire);

byte TxByte = 0;

void I2C_TxHandler(void)
{
  Wire.write(TxByte);
  Serial.println("DATA TRANSMITTED");
}

void setup(void)
{
  // Start serial communication for debugging purposes
  Serial.begin(9600);
  // Start up the library
  sensors.begin();
  Wire.begin(0x55);
  Wire.onRequest(I2C_TxHandler);
}

void loop(void){ 
  // Call sensors.requestTemperatures() to issue a global temperature and Requests to all devices on the bus
  sensors.requestTemperatures(); 
  
  Serial.print("Celsius temperature: ");
  // Why "byIndex"? You can have more than one IC on the same bus. 0 refers to the first IC on the wire
  Serial.print(sensors.getTempCByIndex(0)); 
  Serial.print(" - Fahrenheit temperature: ");
  Serial.println(sensors.getTempFByIndex(0));
  byte TempData = sensors.getTempCByIndex(0);
  TxByte = TempData;
  // Serial.println(TempData, BIN);
  delay(10);
}

Then on the python side I am running:

from telemetrix import telemetrix
import sys
import time

# create a telemetrix instance
the_board = telemetrix.Telemetrix('COM3')

# set i2c pin mode
the_board.set_pin_mode_i2c()


def the_callback(data):
    print(data)


while True:
    try:
        the_board.i2c_read(0x55, None, 1, the_callback)
        time.sleep(.1)
    except KeyboardInterrupt:
        the_board.shutdown()
        sys.exit(0)

This results in the following error in the python console:

RuntimeError: i2c too many bytes received from i2c port 1 i2c address 85
object address : 000001B17B389FC0
object refcount : 3
object type : 00007FFF2EF5AD50
object type name: RuntimeError
object repr : RuntimeError('Shutdown failed - could not send stop streaming message')
lost sys.stderr

And the nano never prints "DATA TRANSMITTED" in the serial monitor.

Any input on this?

I think you may be missing the beginTransmission and endTransmission commands. This page provides an example. Your Wire.begin(0x55) setup is correct since the Nano is acting as a peripheral.

If that doesn't help, try simplifying things and requesting a byte using Python and replying on the Nano

I believe that the begin and endTransmission commands are only used in the event of the master sending data to the slave. Since the nano is acting as a slave I don't think that I need those (plus the beginTransmission command requires an address).

Here are a couple of things that I have also found:

  1. If I disconnect power to the Nano, the python script does not crash
  2. If I set the number_of_bytes to read to 0 then the python script does not crash
  3. It looks like the comparison is done on the arduino side with
else if (command_buffer[2] > current_i2c_port->available())
  {
    byte report_message[4] = {3, I2C_TOO_MANY_BYTES_RCVD, 1, address};
    Serial.write(report_message, 4);
    return;
  }

The "too many bites received" error is what I keep getting, and I think that means that I am asking the slave device for more bytes than it has available? So I'm not sure why there are no bytes available because it is my understanding that when the Nano receives a request for data, the Wire.onRequest function should cause an interrupt then write the data.

And how would I go about simplifying things like you mentioned? Sorry for pestering, like I said I'm still new to this and trying to learn :melting_face:

No problem, you are not pestering at all.
My previous post demonstrates that I clearly never connected two Arduinos via i2c. Sorry about the misdirection.

What I mean by simplifying is to connect the Arduinos without any sensors or actuators, like in this article. Initially, don't add telemetrix to the mix until you are satisfied that communication is working with one of the Arduinos acting as a master.
Once you are satisfied that this is working, try using Telemetrix again. The "too many bytes" message occurs when you issue an i2c read, and the Telemetrix4Arduino sketch checks to see how many bytes are available to be read. That message is generated if that number exceeds the number of bytes requested by a telemetrix i2c_read API call.
To try to understand this, you might consider modifying Telemetrix4Arduino to dump what was read to the serial port.
To do this, you can call send_debug_info in a loop when too many bytes is detected. Similar to what is done here.
Hopefully, this makes sense. If I can help in any way, don't hesitate to ask.

After spending several more hours on the project, I have finally got it working.

Turns out that the issue was user error (for the most part).

After taking your suggestion and trying to get just the two Arduinos to talk to one another (and not succeeding), I realized that I had looked at a Nano pinout that was not for the version I have. So I had the SDA and SCL lines hooked up to pins D4 and D5 on the Nano when it was supposed to be A4 and A5. After fixing this I also discovered that one of the breadboarding wires I was using was broken and not making a connection. After fixing these issues I got the boards to talk to one another, and then I was able to use the i2c_read() function to get the temperature data from the Nano. Then I ran into an issue where the python script would run fine for a short time, then throw an error (again it was the too many bites). I found I also got this error if I disconnected the Nano while the script was running. After some digging, I found that both the Serial library (I was using it to print the temperature from the Nano) and the Wire library utilized interrupts. After removing the Serial.print() lines everything seems to be running well. I was even able to add the PCA9685 board into the mix with no issues.

So all seems to be functioning for the time being (until I add something else that messes everything up lol)

On a side note, I did notice that in the Telemetrix documentation under the i2c_read() function, it says that the callback returns a data list:

[I2C_READ_REPORT, address, register, count of data bytes, data bytes, time-stamp]

But the list I was getting was slightly different and after looking at the Telemetrix4Arduino sketch it looks like it is actually reported as:

[I2C_READ_REPORT, i2c_port, count of data bytes, address, register, data bytes, time-stamp]

Thank you again for your help, I'd still be pulling my hair out otherwise! :grin:

That's excellent news. Thanks for pointing out the documentation error, which I will correct. I had things documented correctly in the code that handles the report, but for some reason, I did not update the information in i2c_read.

When I find myself in the middle of inexplicable results (and this has happened to me more times than I wish to reveal), I have found that by taking the time to rebuild things slowly, starting with a minimal solution and then adding complexity slowly, the solution reveals itself more quickly than trying to debug with all the complexity in place.
Good luck with your project.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.