4

I am reading data from a meter over dlms protocol and I am getting hex values from there. I want to convert that hex string to signed decimal value. For example - ff9d denotes a value of -99 [reference].

My hex to decimal code is:

unsigned int hexToDec(String hexString)
{
  unsigned int decValue = 0;
  int nextInt;

  for (int i = 0; i < hexString.length(); i++)
  {
    nextInt = int(hexString.charAt(i));
    if (nextInt >= 48 && nextInt <= 57) nextInt = map(nextInt, 48, 57, 0, 9);
    if (nextInt >= 65 && nextInt <= 70) nextInt = map(nextInt, 65, 70, 10, 15);
    if (nextInt >= 97 && nextInt <= 102) nextInt = map(nextInt, 97, 102, 10, 15);
    nextInt = constrain(nextInt, 0, 15);
    decValue = (decValue * 16) + nextInt;
  }
  return decValue;
}

Can anyone please help me how to do it in Arduino IDE?

6
  • Have you tried googling something like "C++ convert hex string to int"?
    – chrisl
    Oct 1, 2020 at 7:36
  • Yes, I have converted hex string to decimal. But can't figure out the signed part. I can share that code if you want? Oct 1, 2020 at 7:38
  • Yes, please share the code. You can edit your question for including it.
    – chrisl
    Oct 1, 2020 at 8:18
  • I have edited my question @chrisl Oct 1, 2020 at 8:51
  • 1
    Any code snippet you have on how to do this in Arduino @EdgarBonet ? Oct 1, 2020 at 9:09

2 Answers 2

2

For example – ff9d denotes a value of -99

The function you wrote returns an unsigned integer which, being unsigned, cannot be negative. The simple fix is to declare it as

int hexToDec(String hexString)

That should be enough to get it working as expected. Note that the local variable decValue still has to be unsigned, otherwise you would get a signed overflow, which is undefined behavoir (i.e. “forbidden”) in C++. Returning an unsigned number from a function that is supposed to return a signed number is perfectly fine: you get an implicit conversion which is guaranteed to do the right thing.

Now, if I may provide some more feedback about this function:

  • Use, if you can, plain C strings (char *) instead of String objects, as the latter are not very friendly to your Arduino's memory.
  • Use fixed-length integers (uint16_t if your protocol dictates 16-bit integers) instead of the generic int, otherwise your code will fail if you move to a 32-bit platform, such as an ARM-powered Arduino.
  • Use character literals ('0' instead of 48) instead of hard-coding ASCII code points, as that makes the code more readable.
  • Use a simple subtraction (nextInt - '0') instead of the quite expensive map() function.

Here is my take at it. Note that it has a “classic C” accent you do not need to replicate. You may prefer using indices to crawl the string.

int16_t hex2int(const char *hex)
{
    uint16_t value;  // unsigned to avoid signed overflow
    for (value = 0; *hex; hex++) {
        value <<= 4;
        if (*hex >= '0' && *hex <= '9')
            value |= *hex - '0';
        else if (*hex >= 'A' && *hex <= 'F')
            value |= *hex - 'A' + 10;
        else if (*hex >= 'a' && *hex <= 'f')
            value |= *hex - 'a' + 10;
        else
            break;  // stop at first non-hex digit
    }
    return value;
}

Example usage:

Serial.println(hex2int("ff9d"));  // prints "-99"

Edit: As revealed by the comments, the whole question is an XY problem. You are asking help about your misguided solution rather than your actual problem. The data you have is not hex: it's binary. You have a 16-bit signed integer stored in a byte array, most significant byte first. You figured out you can convert that to an integer by converting it first to a string representation as hexadecimal, then converting that representation back to a binary number. Of course you can do that, but it's an overly complicated and expensive way of doing something trivial.

The canonical way to recover an integer from a byte array is by assembling the bytes together using bitwise logic and bit shifts. In this case:

int16_t reactive_power = (uint16_t) data[dataSize - 5] << 8
                       | data[dataSize - 4];
10
  • How should I pass ff9d as string in the hex2int function? Oct 1, 2020 at 9:42
  • @Prateek: See the example I added. Oct 1, 2020 at 9:50
  • I am doing this: String reacP = String(data[dataSize - 5], HEX) + String(data[dataSize - 4], HEX); Serial.println(hexToInt(reacP)); but I am getting the error: cannot convert 'String' to 'const char*' for argument '1' to 'int16_t hexToInt(const char*)' Oct 1, 2020 at 9:56
  • @Prateek: Looks to me like you are converting binary data to hex string, only to convert it back to binary. Please edit your question showing how you get the data. Most importantly: what is the data type of data? Oct 1, 2020 at 9:59
  • That datatype of data is unsigned char as in void ReactivePower(unsigned char data[], int dataSize). Oct 1, 2020 at 10:08
1

Which mode of DLMS you are you using?

IEC 61107 or currently IEC 62056-21, was an international standard for a computer protocol to read utility meters. It is designed to operate over any media, including the Internet. A meter sends ASCII (in modes A..D) or HDLC (mode E) data to a nearby hand-held unit (HHU) using a serial port. IEC 62056

In your question you mentioned:

hex string

which means a string of hex characters using ASCII (modes A to D).

However, in the comments you mentioned:

void ReactivePower(unsigned char data[], int dataSize) String reacP = String(data[dataSize - 5], HEX) + String(data[dataSize - 4], HEX);

which means an array of bytes obtained from a stream of bytes using HDLC (mode E).

ASCII (Modes A to D)

For ASCII (modes A to D), there is a simple transformation to get the digit value from the hex character.

              ASCII
 Hex   --------------------                            Digit
 char  Decimal  Hexadecimal    Op1    Low nibble  Op2  Base 10
-----  -------  -----------  -------  ----------  ---  -------
 '0'      48        30        & 0x0F       0              0
 '1'      49        31        & 0x0F       1              1
 '2'      50        32        & 0x0F       2              2
 '3'      51        33        & 0x0F       3              3
 '4'      52        34        & 0x0F       4              4
 '5'      53        35        & 0x0F       5              5
 '6'      54        36        & 0x0F       6              6
 '7'      55        37        & 0x0F       7              7
 '8'      56        38        & 0x0F       8              8
 '9'      57        39        & 0x0F       9              9
 'A'      65        41        & 0x0F       1       +9    10
 'B'      66        42        & 0x0F       2       +9    11
 'C'      67        43        & 0x0F       3       +9    12
 'D'      68        44        & 0x0F       4       +9    13
 'E'      69        45        & 0x0F       5       +9    14
 'F'      70        46        & 0x0F       6       +9    15
 'a'      97        61        & 0x0F       1       +9    10
 'b'      98        62        & 0x0F       2       +9    11
 'c'      99        63        & 0x0F       3       +9    12
 'd'     100        64        & 0x0F       4       +9    13
 'e'     101        65        & 0x0F       5       +9    14
 'f'     102        66        & 0x0F       6       +9    15
  • For characters '0' to '9', simply AND with 0x0F.
  • For characters 'A' to 'F' and 'a' to 'f', AND with 0x0F and add 9.
char ch = '1';
byte b = 0;
if (ch >= '0' && ch <= '9')
{
  b = ch & 0x0F;
}
else if ((ch >= 'A' && ch <= 'F') || (ch >= 'a' && ch <= 'f'))
{
  b = (ch & 0x0F) + 9;
}

It's also important to know whether the transformation was successful, e.g. the first character could be bad (either a null terminator or non hex character), so this needs to be tested for during the transformation. Also, there are four nibbles in an int16 so the transformation needs to quit when a maximum of four hex characters have been processed:

void setup()
{
  pinMode(13, OUTPUT);
  Serial.begin(115200);
}

void loop()
{
  char string[] = "A";
  //char string[] = "Waffle";
  //char string[] = "ff9d";
  int16_t the_int = 0;
  int count = AsciiToInt16(string, the_int);

  Serial.print(string);
  Serial.print(",  ");
  if (count)
  {
    Serial.print("Ok,  ");
    Serial.print(count);
    Serial.print(",  ");
    Serial.println(the_int);
  }
  else
  {
    Serial.println("Bad");
  }
}

int AsciiToInt16(const char* ch, int16_t& value)
{
  int count = 0;
  byte nibble = 0;

  // Stop after 4th nibble or null terminator.
  for(int i = 0; i < 4 && ch[i] != 0; i++)
  {
    if (AsciiToNibble(ch[i], nibble))
    {
      value <<= 4;
      value |= nibble;
      count++;
    }
    else
    {
      break;  // Stop after non hex digit.
    }
  }
  return count;
}

bool AsciiToNibble(const char ch, byte& b)
{
  if (ch >= '0' && ch <= '9')
  {
    b = ch & 0x0F;
    return true;
  }
  else if ((ch >= 'A' && ch <= 'F') || (ch >= 'a' && ch <= 'f'))
  {
    b = (ch & 0x0F) + 9;
    return true;
  }
  return false;
}

Output

A,  Ok,  1,  10
Waffle,  Bad
ff9d,  Ok,  4,  -99

HDLC (Mode E)

For HDLC (mode E), simply do:

void ReactivePower(byte data[], int dataSize)
{
  uint16_t reacP = (data[dataSize - 5] << 8) | data[dataSize - 4];
  Serial.println(dataSize);
  Serial.println(reacP);
}
. . .
  byte data[] = { 1, 3, 5, 2, 4, 8 };
  ReactivePower(data, sizeof(data));

Output

6
773

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.