Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Decoding a hex string

This is part of a bigger problem where we needed to port an old application to a new device (and of course to another programming language/framework). After a lot of effort ( sniffing the comm lines, reverse engineering the transmitted data for about a 2-3 weeks ) I managed to narrow in on what I think are the 4 bytes that contains 2 numbers. One of the number is a temperature reading like 30.51, 30.46 etc as shown in the first column (of Table 01). The other value is a float that can be in the range of something like 3.9 to 3.6 ( and even lower, with 4 decimals ). The 4 hex bytes (col 2 of Table 01) should contain both these values. I had to do some reverse engineering because no documentation is available nor the source code. I managed to narrow in on the part of java code that I think decodes the hex strings into 2 number as stated. Will someone be able to check if the code is what I think that it is? I am not a java programmer and I mostly deal with other programming languages. So I will need couple of things

  1. Is the attached code responsible for decoding the hex into the 3 float numbers? This is the most important
  2. If possible refactor a bit of that code so that it can run on (https://www.compilejava.net/). This is so that I can try test the algo with different sets of hex numbers.
  3. "If possible" add some comments

Table 01

30.51 => 01:53:4e:98
30.46 => 01:53:8e:94
30.43 => 01:53:8e:91
30.39 => 01:53:8e:8e
30.39 => 01:53:4e:8e

12.36 => 01:52:88:b1
16.01 => 01:52:c9:cf
18.65 => 01:52:ca:a5
21.14 => 01:52:8b:74

If there is any information needed please let me know because I have spent a lot of time trying to figure this out. I can record more numbers if needed.

Btw the number on the left is a temperature reading ( in celsius ). So that "may" involve a multiplication in the end to arrive at the number? I am not so sure about that but I thought I'd mention what I know about this.

I don't have the time to learn java to fix this ( it is super rare for us to deal with java ) and I have already spent close to a month on this. Really appreciate any help to just clear this hurdle. Here is the java code that decodes the hex to 2 numbers. I got this by reverse engineering the legacy app. Please note that I deleted the decompiled code that I posted earlier because I was just made aware of the fact that it is covered under an NDA.

Oops, I made a mistake of not mentioning that finally this needed to be plugged into a Python program - as I was mentioning it was part of a much bigger Python project, nearly all of which I've coded and works great. And a huge apology for not mentioning that (and forgetting to add the Python tag). I will have to rewrite this in Python.

like image 362
R.W Avatar asked Mar 13 '18 12:03

R.W


4 Answers

Decompiled Code does calculate temperature:

As you suspected, the decompiled code class a is capable of calculating the temperature. The method double a() has the algorithm. The decompiled code does not compile, however after a fair bit of work, it has been corrected (see below) and does calculate the temperature accurately based on your inputs and expected values.

Results: (using mapKey=77)

30.506 => 01:53:4e:98
30.460 => 01:53:8e:94
30.425 => 01:53:8e:91
30.391 => 01:53:8e:8e
30.391 => 01:53:4e:8e
12.338 => 01:52:88:b1
15.990 => 01:52:c9:cf
18.636 => 01:52:ca:a5
21.127 => 01:52:8b:74

Calibration:

Method a() - now called calculateTemperature() seems to have calibration built into it if you choose to use it. Of the 4 arguments, the first is the hex string (of which only two octets are used by the algorithm), and the other three can calibrate the result. It is hard to know how to use these values. But if you leave them as default values as I have shown in main() then the resulting temperatures are calculated correctly. Perhaps you know why you might want to calibrate the result. One of the calibrations requires passing a value between 65 and 85 (77 is what I used). There is another similar argument which causes multiplication of the result (zero will ignore the argument). I found that using mapKey=77 produces results slightly too low (but 78 was slightly too high). I found using a scale of 100175 made the results even closer to the expected values. And the third calibration is a +/- type of calibration (zero will ignore the argument).

Possible calculation of voltage

After the temperature is calculated, there is some code which assigns a value of around 0.2-2.0 when the temperature is between 33.2 and 36.0. I have called this voltage, but have no idea what it is. For your input values, the temperature is always below 33.2, so this part of the code is effectively useless.

Unknown Meaning of method c()

There is a small method c(). It returns a value around 4.16 - 4.18 for the values you have provided. Does this mean anything to you?

Working code:

The main() method demonstrates how to get the temperature from a hex string, and prints the results of the input data. The code runs on the website you mentioned in your question. Of note is that the 3 calibration arguments do not need to be byte[] arrays, and may be simpler understood and used as type int. If you have any questions about the code, lets work in the comments (or chat) and I'll explain it to you.

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Meter {

private List<Double> parameterList = new ArrayList<>();     // Used by the temperature algorithm
private Map<Integer, Double> scaleMap = new HashMap<>(21);  // Used to scale (not necessarily required)

public static void main(String[] args) {
    Meter meter = new Meter();
    meter.initData();

    //30.51 => 01:53:4e:98
    //30.46 => 01:53:8e:94
    //30.43 => 01:53:8e:91
    //30.39 => 01:53:8e:8e
    //30.39 => 01:53:4e:8e

    //12.36 => 01:52:88:b1
    //16.01 => 01:52:c9:cf
    //18.65 => 01:52:ca:a5
    //21.14 => 01:52:8b:74

    // Test each of the provided hex values

    int mapKey = 77;    // 77 seemed the best value; 78 was a bit too high

    String[] values = { "01:53:4e:98", "01:53:8e:94", "01:53:8e:91", "01:53:8e:8e",
            "01:53:4e:8e", "01:52:88:b1", "01:52:c9:cf", "01:52:ca:a5", "01:52:8b:74" };

    ByteBuffer key = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(mapKey);
    ByteBuffer scale = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(0);     // A number around 100175 perhaps provides better results
    ByteBuffer offset = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(0);    // No offset

    for (int i=0; i<values.length; i++) {
        double tempC = meter.calculateTemperature(hexStringToByteArray(values[i]), key.array(), scale.array(), offset.array());
        System.out.printf("%2.3f => %s\n", tempC, values[i]);
    }

}

/**
 * Convert a hex string (which may contain the `:` character) to a byte array
 * @param hexString 4 octets of the form xx:xx:xx:xx
 * @return The byte array
 */
static byte[] hexStringToByteArray(String hexString) {
    hexString = hexString.replaceAll(":", "");
    int len = hexString.length();
    byte[] data = new byte[len / 2];
    for (int i = 0; i < len; i += 2) {
        data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4)
                             + Character.digit(hexString.charAt(i+1), 16));
    }
    return data;
}

/**
 * Populate the algorithm parameters (required), and the scaling factors (not necessarily required)
 */
private void initData() {

    scaleMap.put(65, 29.629);
    scaleMap.put(66, 29.660);
    scaleMap.put(67, 29.691);
    scaleMap.put(68, 29.722);
    scaleMap.put(69, 29.753);
    scaleMap.put(70, 29.784);
    scaleMap.put(71, 29.815);
    scaleMap.put(72, 29.846);
    scaleMap.put(73, 29.877);
    scaleMap.put(74, 29.908);
    scaleMap.put(75, 29.939);
    scaleMap.put(76, 29.970);
    scaleMap.put(77, 30.001);
    scaleMap.put(78, 30.032);
    scaleMap.put(79, 30.063);
    scaleMap.put(80, 30.094);
    scaleMap.put(81, 30.125);
    scaleMap.put(82, 30.156);
    scaleMap.put(83, 30.187);
    scaleMap.put(84, 30.218);
    scaleMap.put(85, 30.249);

    parameterList.add(52.94);
    parameterList.add(49.61);
    parameterList.add(46.51);
    parameterList.add(43.62);
    parameterList.add(40.94);
    parameterList.add(38.44);
    parameterList.add(36.12);
    parameterList.add(33.95);
    parameterList.add(31.93);
    parameterList.add(30.05);
    parameterList.add(28.29);
    parameterList.add(26.61);
    parameterList.add(25.05);
    parameterList.add(23.59);
    parameterList.add(22.23);
    parameterList.add(20.96);
    parameterList.add(19.76);
    parameterList.add(18.65);
    parameterList.add(17.60);
    parameterList.add(16.63);
    parameterList.add(15.71);
    parameterList.add(14.84);
    parameterList.add(14.02);
    parameterList.add(13.25);
    parameterList.add(12.53);
    parameterList.add(11.86);
    parameterList.add(11.22);
    parameterList.add(10.63);
    parameterList.add(10.07);
    parameterList.add(9.541);
    parameterList.add(9.046);
    parameterList.add(8.572);
    parameterList.add(8.126);
    parameterList.add(7.706);
    parameterList.add(7.311);
    parameterList.add(6.938);
    parameterList.add(6.588);
    parameterList.add(6.257);
    parameterList.add(5.946);
    parameterList.add(5.651);
    parameterList.add(5.374);
    parameterList.add(5.109);
    parameterList.add(4.859);
    parameterList.add(4.623);
    parameterList.add(4.400);
    parameterList.add(4.189);
    parameterList.add(3.990);
    parameterList.add(3.801);
    parameterList.add(3.623);
    parameterList.add(3.454);
    parameterList.add(3.294);
    parameterList.add(3.141);
    parameterList.add(2.996);
    parameterList.add(2.858);
    parameterList.add(2.728);
    parameterList.add(2.604);
    parameterList.add(2.487);
    parameterList.add(2.376);
    parameterList.add(2.270);
    parameterList.add(2.170);
    parameterList.add(2.075);
    parameterList.add(1.984);
    parameterList.add(1.897);
    parameterList.add(1.815);
    parameterList.add(1.737);
    parameterList.add(1.662);
    parameterList.add(1.591);
    parameterList.add(1.524);
    parameterList.add(1.459);
    parameterList.add(1.398);
    parameterList.add(1.340);
    parameterList.add(1.284);
    parameterList.add(1.231);
    parameterList.add(1.180);
    parameterList.add(1.132);
    parameterList.add(1.086);
    parameterList.add(1.042);
    parameterList.add(1.000);
    parameterList.add(0.9599);
    parameterList.add(0.9216);
    parameterList.add(0.8851);
    parameterList.add(0.8501);
    parameterList.add(0.8168);
    parameterList.add(0.7849);
    parameterList.add(0.7545);
    parameterList.add(0.7254);
    parameterList.add(0.6974);
    parameterList.add(0.6707);
    parameterList.add(0.6451);
    parameterList.add(0.6207);
    parameterList.add(0.5973);
    parameterList.add(0.5743);
    parameterList.add(0.5523);
    parameterList.add(0.5313);
    parameterList.add(0.5112);
    parameterList.add(0.4920);
    parameterList.add(0.4736);
    parameterList.add(0.4560);
    parameterList.add(0.4392);
    parameterList.add(0.4230);
    parameterList.add(0.4076);
    parameterList.add(0.3925);
    parameterList.add(0.3781);
    parameterList.add(0.3642);
    parameterList.add(0.3510);
    parameterList.add(0.3383);
    parameterList.add(0.3261);
    parameterList.add(0.3144);
    parameterList.add(0.3032);
    parameterList.add(0.2925);
    parameterList.add(0.2822);
    parameterList.add(0.2722);
    parameterList.add(0.2626);
    parameterList.add(0.2534);
    parameterList.add(0.2445);
    parameterList.add(0.2360);
    parameterList.add(0.2279);
    parameterList.add(0.2201);
    parameterList.add(0.2126);
    parameterList.add(0.2054);
    parameterList.add(0.1984);
    parameterList.add(0.1917);
    parameterList.add(0.1852);
    parameterList.add(0.1790);
    parameterList.add(0.1731);
    parameterList.add(0.1673);
    parameterList.add(0.1618);
    parameterList.add(0.1564);
    parameterList.add(0.1513);
    parameterList.add(0.1464);
    parameterList.add(0.1416);
    parameterList.add(0.1370);
    parameterList.add(0.1326);
    parameterList.add(0.1283);
    parameterList.add(0.1242);
    parameterList.add(0.1203);
    parameterList.add(0.1164);
    parameterList.add(0.1128);
    parameterList.add(0.1092);
    parameterList.add(0.1058);
    parameterList.add(0.1026);

}

/**
 * 
 * @param b1array The hex number b1:b2:b3:b4 (as a byte array)
 *            - The only bits used are b2 (6 low bits & x3f) and b3
 *            (all 8 bits & xff)
 * @param mapKey
 *            - Value from 65 to 85; if 77 then scale is 1.0; otherwise < 77
 *            or > 77 causes scaling
 * @param byte1Scale
 *            - Equal to zero (scale=1), or units in micro (10E6) where
 *            scale=value/10E6
 * @param byte1Offset
 *            - Measured in 10E6 (micro) - offset amount
 * @return The temperature in degrees Celsius
 */
public double calculateTemperature(byte[] b1array, byte[] mapKey, byte[] byte1Scale, byte[] byte1Offset) {

    double scale;
    int scaleMicroValue = ByteBuffer.wrap(byte1Scale).order(ByteOrder.LITTLE_ENDIAN).getInt();
    if (scaleMicroValue == 0) {
        scale = 1.0D;
    } else {
        scale = scaleMicroValue / 1000000.0D;
    }

    double offsetValue = ByteBuffer.wrap(byte1Offset).order(ByteOrder.LITTLE_ENDIAN).getInt() / 1000000.0D;

    /* 14 bits: b2_5 b2_4 ... b2_0 b3_7 .. b3_0 */
    byte byte2 = b1array[2];
    byte byte3 = b1array[3];
    int bitValue = (byte3 & 0xFF | (byte2 & 0x3F) << 8);
    double scaledBitValue = bitValue * scale - offsetValue;

    int key = (byte) (mapKey[0] & 0xFF);
    double mapValue = scaleMap.containsKey(key) ? scaleMap.get(key) : scaleMap.get(77);

    double param1 = 0.0;
    double param2 = parameterList.get(0);
    double result = 33.0D / scaledBitValue * (8191.0D - scaledBitValue) / mapValue;

    int i = 0;
    int j = parameterList.size();
    double minParameter = parameterList.get(j - 1);

    if (param2 < result || minParameter > result)
        return 0;

    int index = 0;
    boolean process = true;
    while (i < j && process) {
        if (result >= parameterList.get(i)) {
            if (i == 0) {
                param1 = parameterList.get(i);
                param2 = parameterList.get(i + 1);
                index = i;
                process = false;
            }
            if (process) {
                param1 = parameterList.get(i - 1);
                param2 = parameterList.get(i);
                index = i - 1;
            }
            process = false;
        }
        if (process)
            i++;
    }
    if (process) {
        index = 0;
        param2 = 0;
    }

    double voltage = 0.0;   // I don't even know if this is voltage (but it is only calculated if temp between 33.2 and 36
    double tempC = index + (result - param1) / (param2 - param1) - 40.0D;

    if ((tempC < 34.0D) && (tempC >= 33.2D)) {
        voltage = 1.95D;
    }

    while (true) {
        if ((tempC < 34.1D) && (tempC >= 34.0D)) {
            voltage = 1.881D;
        } else if ((tempC < 34.2D) && (tempC >= 34.1D)) {
            voltage = 1.805D;
        } else if ((tempC < 34.3D) && (tempC >= 34.2D)) {
            voltage = 1.71D;
        } else if ((tempC < 34.4D) && (tempC >= 34.3D)) {
            voltage = 1.615D;
        } else if ((tempC < 34.5D) && (tempC >= 34.4D)) {
            voltage = 1.52D;
        } else if ((tempC < 34.6D) && (tempC >= 34.5D)) {
            voltage = 1.4249999999999998D;
        } else if ((tempC < 34.7D) && (tempC >= 34.6D)) {
            voltage = 1.3299999999999998D;
        } else if ((tempC < 34.8D) && (tempC >= 34.7D)) {
            voltage = 1.2349999999999999D;
        } else if ((tempC < 34.9D) && (tempC >= 34.8D)) {
            voltage = 1.14D;
        } else if ((tempC < 35.0D) && (tempC >= 34.9D)) {
            voltage = 1.045D;
        } else if ((tempC < 35.1D) && (tempC >= 35.0D)) {
            voltage = 0.95D;
        } else if ((tempC < 35.2D) && (tempC >= 35.1D)) {
            voltage = 0.855D;
        } else if ((tempC < 35.3D) && (tempC >= 35.2D)) {
            voltage = 0.76D;
        } else if ((tempC < 35.4D) && (tempC >= 35.3D)) {
            voltage = 0.6649999999999999D;
        } else if ((tempC < 35.5D) && (tempC >= 35.4D)) {
            voltage = 0.57D;
        } else if ((tempC < 35.6D) && (tempC >= 35.5D)) {
            voltage = 0.475D;
        } else if ((tempC < 35.7D) && (tempC >= 35.6D)) {
            voltage = 0.38D;
        } else if ((tempC < 35.8D) && (tempC >= 35.7D)) {
            voltage = 0.285D;
        } else if ((tempC < 35.9D) && (tempC >= 35.8D)) {
            voltage = 0.19D;
        } else {
            if (tempC >= 36.0D) {
                break;
            }
            if (tempC < 35.9D) {
                break;
            }
            voltage = 0.095D;
        }
    }
    return tempC;
}

/**
 * I don't know what this function is:  It always calculates around 4.16 - 4.18
 * @param bArray The hex number b1:b2:b3:b4 (as a byte array)
 * Uses: 
 * byte0:  Low 2 bits
 * byte1:  all bits
 * byte2:  high 2 bits
 * @return I don't know the significance of this value
 */
public double m(byte[] bArray) {
    int byte2 = bArray[2];
    int byte1 = bArray[1];
    int byte0 = bArray[0];
    return 0.003077674645823156D * (((byte0 & 0x3) << 2 | (byte1 & 0xC0) >> 6) << 8
            | (byte2 & 0xC0) >> 6 | (byte1 & 0x3F) << 2);
}
}

EDIT: Method to accept a hex string as a parameter, and output temperature

/**
 * Arg[0] is expected to be the input (a hex string)
 * The output is the temperature (printed to the console)
 * @param args One value; The hex string
 */
public static void main(String[] args) {

    Meter meter = new Meter();
    meter.initData();
    int mapKey = 77;    // Scaling factor
    ByteBuffer key = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(mapKey);
    ByteBuffer scale = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(1000180);       // No scaling
    ByteBuffer offset = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(0);    // No offset

    byte[] hexString = hexStringToByteArray(args[0]);

    double tempC = meter.calculateTemperature(hexString, key.array(), scale.array(), offset.array());
    System.out.printf("%.2f", tempC);

}
like image 94
Ian Mc Avatar answered Oct 21 '22 05:10

Ian Mc


The C, 8 and 4 values in the 4th column from left don't affect the values. That makes me think that there are only 12 bits of information being sent for each value. Perhaps it is the integer raw output from an ADC? If that's the case, then you can decode it with something like:

double valueOne(long input) {
    long x = input & 0xFFF;

    double m = (30.51 - 12.36) / (0xE98 - 0x8B1);
    int c = 0xE91;
    double b = 30.43;

    return m*(x-c)+b;
}

The m*(x-c+b) linear approximation is to make it as accurate as possible around what seems to be your operating point; you can adjust the m, c, b values as needed.

To handle the other values, create a similar routine that masks and shifts to get the right 12 bits and provide the needed m, c and b calibration values.

Here's what that computes for Table 1's values:

3:4e:98 30.514
3:8e:94 30.466
3:8e:91 30.430
3:4e:8e 30.393
3:4e:8e 30.393

2:88:b1 12.364
2:c9:cf 15.799
2:ca:a5 18.370
2:8b:74 20.856

The lowest values aren't quite right; maybe it would be better to extrapolate from a different value than the 12.36 point (see formula for the m value). Or maybe this is thermocouple data, which isn't quite linear. Would need to know more about the actual circuit to comment on that.

like image 26
Bob Jacobsen Avatar answered Oct 21 '22 06:10

Bob Jacobsen


I had to do some reverse engineering because no documentation is available nor the source code.

What I love about Java is that it can "easily" be decompiled.

Since you're porting something to a new device, I'm going to assume that you have the rights to go around decompiling and poking around the source code.

There are a few websites like this one, which can help you a lot.

This might not directly solve the problem, but hopefully you'll be able to figure out how they encoded it :)

like image 2
bob zhang Avatar answered Oct 21 '22 05:10

bob zhang


If you wanna decompile the bytecode, just use a decompiler. You can use this page or use a JD plug-in in your IDE. IntelliJ IDEA or Eclipse have a built-in Decompiler, just open the .class file in it and read the decompiledcode.

If you just wanna decode some hexstring, not the bytecode, use this website

Btw, in case you really want to build your own hex string decoder, I think using a bunch of if else statement is not a good idea. Hex encoding/decoding is just a process of moving some bits around. Using << or >> operator will make your code pretty shorter and easier to read. May this article will help you.

like image 2
Aperture Prometheus Avatar answered Oct 21 '22 05:10

Aperture Prometheus