I2C communications and sprintf

Hello All,

I’m working on some code that moves data between a PC and Arduino over rosserial and multiple Arduinos over I2C wire. The problem I have is probably very simple but obviously not for me. So when the Master requests data from the slave it receives it on “Wire.read()” as a char. I than want to convert it to string and add it to other strings using sprintf.
I get this error message

“C:\Users\Dave\Dropbox\Software\Arduino\ROS_to_Master_-Slave_to_ROS\ROS_to_Master_-Slave_to_ROS_151222-1415\ROS_to_Master_-Slave_to_ROS_151222-1415.ino:67:117: warning: format ‘%s’ expects argument of type ‘char*’, but argument 6 has type ‘String’ [-Wformat=]”

with this code:

  Wire.beginTransmission(SLAVE_ADDRESS);
  Wire.write(newPosValuePart);
  Wire.endTransmission();

  Wire.requestFrom(SLAVE_ADDRESS, 9);
  while (Wire.available())
  {
    newValfrmSlave = Wire.read();
    newValfrmSlaveSTR=String(newValfrmSlave);
  }
  sprintf(newValToCPU,"%s DA  %s NPV  %i SA  %s   OPV",driverAddress,newPosValuePart,SLAVE_ADDRESS,newValfrmSlaveSTR);
  str_msg.data = newValToCPU;
  oldFbPos.publish( &str_msg );
  nh.spinOnce();
  delay(500);

It seems that “newValfrmSlaveSTR=String(newValfrmSlave);” is not actually converting the char to string.

Any advice on how to fix this?

Thanks

Wire.read() only reads a byte, (char), not a string.

‘sprintf()’ expects a pointer to a char array, not a String object.

So you’re receiving a byte, converting it to a String object, then passing it to a function that expects a pointer to a char array.
Edit: And only the last byte will be passed to sprintf(), after conversion to a String object, I should have added

“String” is bad. Use char arrays.

It seems that "newValfrmSlaveSTR=String(newValfrmSlave);" is not actually converting the char to string.

No surprise there considering that newValfrmSlaveSTR is declared as a String.

If newValfrmSlaveSTR is already an array of chars terminated with '\0' then there is no need to convert it to a String. In any case, sprintf() works with strings not Strings. Hence the error message.

NOTE that a String is not the same as a string.

I'm not 100% clear what you want, but if you're receiving a single byte, a numeric value such as 283, and you want to convert it to a string, with ASCII digits representing the value, like "283", you could use itoa():-

    char buffer[4];
    byte num;
    if(Wire.available())
    {
        num=Wire.read();
        itoa(num,buffer,10);
        // Do the 'sprintf()' stuff here
    
    }

Hello again.

Two things I forgot to add to the original post were. "newValfrmSlave" is a char. The value returned is 2 whole numbers (11-49) and 4 decimal places (0-1024). ex 11.0 or 49.1024.

It doesn't look like I can use itoa unfortunately.

What I want the "sprintf" to send over rosserial in the end is some letters followed this value followed by ";" ex "oda49.1024;"

Thanks again.

ROVguy:
Hello again.

Two things I forgot to add to the original post were. “newValfrmSlave” is a char. The value returned is 2 whole numbers (11-49) and 4 decimal places (0-1024). ex 11.0 or 49.1024.

It doesn’t look like I can use itoa unfortunately.

What I want the “sprintf” to send over rosserial in the end is some letters followed this value followed by “;” ex “oda49.1024;”

Thanks again.

So you’re receiving 3 bytes? The value 49 in the first, then two bytes for the value to the right of the decimal point, a high byte and a low byte?

OldSteve:
So you're receiving 3 bytes? The value 49 in the first, then two bytes for the value to the right of the decimal point, a high byte and a low byte?

I guess, yes.

ROVguy:
I guess, yes.

Do you know the byte order? ie high byte first or low byte first?

And are there always 3 bytes sent, even when there are only two digits (or even none) to the right of the decimal point?
(I'm not at all familiar with "rosserial".)

Argg, sprintf is evil. Consider using a C string wrapper such as StaticString

Library download

If you must use C strings, at least use snprintf.

OldSteve:
Do you know the byte order? ie high byte first or low byte first?

And are there always 3 bytes sent, even when there are only two digits (or even none) to the right of the decimal point?
(I'm not at all familiar with "rosserial".)

The value will always be at least XX.X and can reach XX.XXXX . The 4 decimals will shift as it's the return of a 5VDC POT. If it's 5V the value will be "XX.1024" while 0V will be "XX.0" .

Camel:
Argg, sprintf is evil. Consider using a C string wrapper such as StaticString

Library download

If you must use C strings, at least use snprintf.

No I don't need to use string. I can use 'const char*'.
If I do use 'const char*' how do I convert from 'char' to 'const char*'?
Also if I do go the route of char* the message from the slave will be 2-3 letters followed by the numbers and a semi-clone ex. "oda49.1024;" or "oda11.0;"

Thanks

ROVguy:
The value will always be at least XX.X and can reach XX.XXXX . The 4 decimals will shift as it's the return of a 5VDC POT. If it's 5V the value will be "XX.1024" while 0V will be "XX.0" .

No I don't need to use string. I can use 'const char*'.
If I do use 'const char*' how do I convert from 'char' to 'const char*'?
Also if I do go the route of char* the message from the slave will be 2-3 letters followed by the numbers and a semi-clone ex. "oda49.1024;" or "oda11.0;"

Thanks

const char* and char* are just a pointers to (hopefully) arrays of chars. You can access the individual characters like this

const char* st = "A string";
char first = st[0]; //first now contains 'A'

The 'const' part means you can't accidentally modify the data. C strings are inherently unsafe and it's easy to write buggy code with them. Again, I'd suggest you use the StaticString library.

ROVguy:
The value will always be at least XX.X and can reach XX.XXXX . The 4 decimals will shift as it's the return of a 5VDC POT. If it's 5V the value will be "XX.1024" while 0V will be "XX.0" .

So what's the 'slave' device that sends the data? Is it a slave that you created yourself? (Sounds like it.)

Are you now saying that the slave sends a string, (a char array), including the decimal point, ranging from 4 ASCII characters to 7 ASCII characters in length? And is it null terminated? ie 5 to 8 characters long.
I noticed that you initially request 9 bytes from the slave.

Wire.requestFrom(SLAVE_ADDRESS, 9);

What does the data represent. I understand the 0 to 1023 bit now, the value from a pot, but what's the 11 to 49?
A pot won't produce the value 1024, either, only 0 to 1023, if you're using 'analogRead()' in the slave.

This is a guessing game, and one that will take forever at this rate.
And there's no need to use either the "String" class or Camel's own "StaticString" library. C strings, (char arrays), are fine.

If the slave is a device that you created yourself, posting it's code and that of the master will clarify things enormously.

OldSteve:
So what’s the ‘slave’ device that sends the data? Is it a slave that you created yourself? (Sounds like it.)

As I said in the original message there is rosserial communication between the computer and an arduino and then there is I2C communications between the Arduino’s. 38 Arduino slaves (addresses start at 11 and go to 49) and 1 master to be exact . Yes it is a char array coming from the slaves 'char tempStr[10]=“11.1024;” . This value is used for testing and will be the full range once I get everything talking to one another.

If I leave out the rosserial communications I receive the message and am able to print it using serial.print(newValfrmSlave).
ROS has char capabilities (#include <std_msgs/Char.h>) but since I haven’t figured out how to convert to const char* I have not used it.

As requested here is the code in the Master Arduino. Some parts are used for testing and they will be removed once all is working.

#include <ros.h>
#include <std_msgs/Float64.h>
#include <std_msgs/String.h>
#include <std_msgs/Char.h>
#include <Wire.h>

ros::NodeHandle nh;

float rawDataFromCPU=10.0; 
char newValToCPU[99];
char driverAddress[4];
char newPosValuePart[5]="1024";  //place where new extracted angle position is saved      
int MY_ADDRESS = 10;  //Master address constant 10
int SLAVE_ADDRESS = 0;  // Value received from computer 11-999
char newValfrmSlave;
String newValfrmSlaveSTR;

void messageCb( const std_msgs::Float64& msg)
{
  rawDataFromCPU = msg.data;
  digitalWrite(13, HIGH-digitalRead(13));   // blink the led
}

std_msgs::String str_msg;
std_msgs::Float64 test;
ros::Subscriber<std_msgs::Float64> subscribe("NewDriverAndValue", &messageCb);
ros::Publisher oldFbPos("oldFbPos", &str_msg);

void ExtractDecimalPart(float Value) 
{
  int tempVal1 = (int)(Value);
  int tempVal2 = 10000 * (Value - tempVal1 + 0.00005); //10000 b/c my float values always have exactly 4 decimal places
  String str=String(tempVal2); //converting integer into a string
  str.toCharArray(newPosValuePart,5); //passing the value of the string to the character array
}

void setup()
{
  pinMode(13, OUTPUT);
  nh.initNode();
  nh.advertise(oldFbPos);
  nh.subscribe(subscribe);

  Wire.begin(MY_ADDRESS);        // join i2c bus
  //TWBR = 12; // increase clock speed 4x

}

void loop()
{
  ExtractDecimalPart(rawDataFromCPU); /* This extracts the decimal number part from the float and saves as "newPosValuePart"   */
  dtostrf(rawDataFromCPU, 4, 0, driverAddress);  /* 4 is mininum width, 0 is # of decimal places; float value is copied onto str_temp.
                                                 This extracts the whole number part of the float and saves in driverAddress */
  //SLAVE_ADDRESS=(int)driverAddress;//int SLAVE_ADDRESS = (int) strtol(driverAddress, NULL, 10);   int i = (int) strtol("567", NULL, 10);
  SLAVE_ADDRESS = (int) strtol(driverAddress, NULL, 10);
  
  Wire.beginTransmission(SLAVE_ADDRESS); // transmit to slave
  Wire.write(newPosValuePart);              // sends  
  Wire.endTransmission();    // stop transmitting

  Wire.requestFrom(SLAVE_ADDRESS, 9);    // request 9 bytes from slave device 1st-3 address next-4 fbVar last-';'
  while (Wire.available())
  {
    newValfrmSlave = Wire.read(); // receive a byte as character
    //newValfrmSlaveSTR=string(newValfrmSlave);
  }



  
  sprintf(newValToCPU,"%s DA  %s NPV  %i SA     OPV",driverAddress,newPosValuePart,SLAVE_ADDRESS);//
  str_msg.data = newValToCPU;
  oldFbPos.publish( &str_msg );      // Publishes the str_msg on chatter
  nh.spinOnce();
  delay(500);
}

/*
rosrun rosserial_python serial_node.py /dev/ttyUSB0
rostopic echo oldFbPos
rostopic pub NewDriverAndValue std_msgs/Float64 11.0555
*/

That latest explanation helps, I think. Let me rephrase it: you are receiving character strings over I2C, which of course come in one character at a time. You need to put them back into a character array, just like they were on the sending unit (eg: char tempStr[10]="11.1024;) Once you have the complete string, then you need to pass it to a rosserial library function.

Does that sound like it?

There are two steps to this: getting the complete string, and then sending that string to rossserial.

the interesting piece of code is here:

 while (Wire.available())
  {
    newValfrmSlave = Wire.read(); // receive a byte as character
    //newValfrmSlaveSTR=string(newValfrmSlave);
  }

You are receiving individual characters, and putting them in newValfrmSlave (one at a time.) You need to create a character array (like you did on the sender side) and stick the characters into the array one at a time as you receive them. To do so, you will need a counter that keeps track of the current index.

This topic should help a lot: Serial Input Basics It’s a long thread, but read it all the way through as there are several updates later in the thread. While it specifically talks about reading data from a Serial port, the same concepts will apply to reading the strings from I2C port. The only real difference is that you are reading from Wire, not Serial. (Both Wire and Serial derive from the same Stream class, so all of the data access functions should be interchangeable between them.)

Now, once you have your string into a local character buffer (like the receivedChars array in Robin’s examples) then it becomes a matter of passing it to rosserial, which expects a const char* parameter. All that means is that it is expecting a pointer to the first character of a string of characters (the char* portion) and it is saying that it will not try to modify those characters (the const portion of it.) The string ends with a NULL character. If you define a buffer such as char receivedChars[max_chars]; that is already a string of characters with a NULL terminator. In C++, there is no difference between the name of an array, and a pointer to the first character of the array. so just referencing receivedChars (with no square brackets) is the same thing as a pointer to the first character of a character string (a char*.) While it is a variable string, you can still pass it to a function that is expecting a const string. So once you have the characters in your character buffer, all you need to do is pass the name of that buffer to the rosserial function.

ShapeShifter:
That latest explanation helps, I think.

I agree.

It would have been good to also see the slave’s code, but if it’s definitely a char array being sent, it’s fairly straightforward.
Something along these lines:-

   char buffer[10];
    byte index = 0;
    Wire.requestFrom(SLAVE_ADDRESS, 9);    // request 9 bytes from slave device
    while (Wire.available() && index < 9)
    {
        buffer[index] = Wire.read();
        index++;
    }
    buffer[index] = '\0';

OldSteve, ShapeShifter thanks for the latest update. Once I have a chance I will go through your suggestions and implement them. :grin:

The Serial Input Basics was definitely helpful. I’m not at 100% yet in understanding it but it is a good step forward.
I also got the sketch for the Master working well with the PC and with the Slaves though there is one line that is confusing.

I get this error “[ERROR] [WallTime: 1451642790.940456] Unable to sync with device; possible link problem or link software version mismatch such as hydro rosserial_python with groovy Arduino” when I remove a line that is not being used in the code. sprintf(newValToCPU,"%s ",driverAddress);

Here is the current code

#include <ros.h>
#include <std_msgs/Float64.h>
#include <std_msgs/String.h>
#include <Wire.h>

ros::NodeHandle nh;

float rawDataFromCPU=10.0000; 
char newValToCPU[15];
char driverAddress[4];
char newPosValuePart[5];  //place where new extracted angle position is saved      
int MY_ADDRESS = 10;  //Master address constant 10
int SLAVE_ADDRESS = 0;  // Value received from computer 11-999
char* newValfrmSlave="100000";

void messageCb( const std_msgs::Float64& msg)
{
  rawDataFromCPU = msg.data;
  digitalWrite(13, HIGH-digitalRead(13));   // blink the led
}

std_msgs::String str_msg;
std_msgs::Float64 test;
ros::Subscriber<std_msgs::Float64> subscribe("NewDriverAndValue", &messageCb);
ros::Publisher oldFbPos("oldFbPos", &str_msg);

void ExtractDecimalPart(float Value) 
{
  int tempVal1 = (int)(Value);
  int tempVal2 = 10000 * (Value - tempVal1 + 0.00005); //10000 b/c my float values always have exactly 4 decimal places
  String str=String(tempVal2); //converting integer into a string
  str.toCharArray(newPosValuePart,5); //passing the value of the string to the character array
}

void setup()
{
  pinMode(13, OUTPUT);
  nh.initNode();
  nh.advertise(oldFbPos);
  nh.subscribe(subscribe);

  Wire.begin(MY_ADDRESS);        // join i2c bus
  TWBR = 12; // increase clock speed 4x

}

void loop()
{
  ExtractDecimalPart(rawDataFromCPU); // This extracts the decimal number part from the float and saves as "newPosValuePart"
  dtostrf(rawDataFromCPU, 4, 0, driverAddress);  // 4 is mininum width, 0 is # of decimal places; float value is copied onto str_temp.
                                                 //This extracts the whole number part of the float and saves in driverAddress
  SLAVE_ADDRESS = (int) strtol(driverAddress, NULL, 10);
  
  Wire.beginTransmission(SLAVE_ADDRESS); // transmit to slave
  Wire.write(newPosValuePart);              // sends  
  Wire.endTransmission();    // stop transmitting
  
  char buffer[6];
  byte index = 0;

  Wire.requestFrom(SLAVE_ADDRESS, 6);    // request 9 bytes from slave device
  while (Wire.available() && index < 6)
  {
      buffer[index] = Wire.read();
      index++;
  }
  buffer[index] = '\0';
  newValfrmSlave=buffer;

  //sprintf(newValToCPU,"%s ",driverAddress);    // This is the line!
  str_msg.data = newValfrmSlave;
  oldFbPos.publish( &str_msg );      // Publishes the str_msg on oldFbPos
  nh.spinOnce();
  //delay(500);
}

/*
rosrun rosserial_python serial_node.py /dev/ttyUSB0
rostopic echo oldFbPos
rostopic pub NewDriverAndValue std_msgs/Float64 H15.0555
*/

Thanks again

It is set as a Slave with "Wire.begin(MY_ADDRESS)", but there is no onRequest or onReceive handler.
Please use Wire.setClock(400000L) instead of writing to the TWBR register.
Six bytes are requested, the buffer is six byte, but then you add the '\0' to the next index. Then you have used the seventh byte, which corrupts the memory.

Koepel:
It is set as a Slave with "Wire.begin(MY_ADDRESS)", but there is no onRequest or onReceive handler.
Please use Wire.setClock(400000L) instead of writing to the TWBR register.
Six bytes are requested, the buffer is six byte, but then you add the '\0' to the next index. Then you have used the seventh byte, which corrupts the memory.

Yep. In my example, I made the buffer one byte larger to accommodate the terminating null char.

ROVguy:
I get this error “[ERROR] [WallTime: 1451642790.940456] Unable to sync with device; possible link problem or link software version mismatch such as hydro rosserial_python with groovy Arduino” when I remove a line that is not being used in the code.

You are not setting up the Serial port, but are printing to it? Generally not a good idea.

While I’m not familiar at all with rosserial, I’m betting that it is using the Serial port to communicate its protocol with the remote computer. If that’s the case, you are sending data through that same port, but without going through the proper protocol. The remote computer is probably receiving that data, sees that it doesn’t fit the protocol’s rules, so it complains about it, making the assumption that you have mismatched software versions and not guessing that you are forcing data into the stream that is outside of the protocol.

About the buffer size issue: your code is making a request for six bytes, but your comment says nine bytes. I find that the best comments are ones that actually explain what is going on in the code, rather than simply parroting the code. A better comment in this case would simply to say “Get a sample of data from the remote system” or something along those lines. Repeating nine bytes in the comment doesn’t really help explain what’s going on (the size is easy enough to see in the code) but having it there means that now there are two places that need to be updated when the value changes. If you then update the code, but not the comment, it causes confusion: is it six bytes or nine? Having good comments is very helpful. Having an incorrect comment is worse than having no comment at all.