dmroeder / pylogix

Read/Write data from Allen Bradley Compact/Control Logix PLC's
Apache License 2.0
600 stars 182 forks source link

Precision on real tags with decimals #34

Closed TheFern2 closed 5 years ago

TheFern2 commented 5 years ago

It looks like when a real value has decimal values we are gaining precision on the return value, or is it losing precision?

online values:

image

return values:

-1.0000000200408773e+20 0.9999989867210388 0.10000000149011612 0.009999999776482582 0.003329999977722764 1000000000.0 5.0

I did some debugging and returnvalue is already rounded off when it gets returned in L1201. I will debug some more tomorrow, and see what else I can find.

image

https://github.com/dmroeder/pylogix/blob/6dc0842a442148ac5952ce7a1863da3151f953a2/eip.py#L1195-L1203

I found this link which looks like our problem. https://stackoverflow.com/q/39619636/1013828

TheFern2 commented 5 years ago

Not sure how much help I can be without knowing how to decipher the bytes data but here's the retdata from the same above tags, which I am sure you can recreate on your setup but just in case. Let me know if you need any other data but I think the problem might be at the unpack_from, but that is a bit of a guess at this point:

I was trying to figure out if the value on retdata is losing precision before or after: def _parseReply(self, tag, elements, data):

-------------------------------
status:  0
retdata:  b'p\x00 \x00\x01\x00\x02H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xa1\x00\x04\x00\x01\x00\x00 \xb1\x00\x0c\x00\x02\x00\xcc\x00\x00\x00\xca\x00!<Z;'
0.003329999977722764
-------------------------------
status:  0
retdata:  b'p\x00 \x00\x01\x00\x02H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xa1\x00\x04\x00\x01\x00\x00 \xb1\x00\x0c\x00\x03\x00\xcc\x00\x00\x00\xca\x00\xef\xff\x7f?'
0.9999989867210388
-------------------------------
status:  0
retdata:  b'p\x00 \x00\x01\x00\x02H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xa1\x00\x04\x00\x01\x00\x00 \xb1\x00\x0c\x00\x04\x00\xcc\x00\x00\x00\xca\x00\xcd\xcc\xcc='
0.10000000149011612
-------------------------------
status:  0
retdata:  b'p\x00 \x00\x01\x00\x02H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xa1\x00\x04\x00\x01\x00\x00 \xb1\x00\x0c\x00\x05\x00\xcc\x00\x00\x00\xca\x00\n\xd7#<'
0.009999999776482582
-------------------------------
status:  0
retdata:  b'p\x00 \x00\x01\x00\x02H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xa1\x00\x04\x00\x01\x00\x00 \xb1\x00\x0c\x00\x06\x00\xcc\x00\x00\x00\xca\x00!<Z;'
0.003329999977722764
-------------------------------
status:  0
retdata:  b'p\x00 \x00\x01\x00\x02H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xa1\x00\x04\x00\x01\x00\x00 \xb1\x00\x0c\x00\x07\x00\xcc\x00\x00\x00\xca\x00(knN'
1000000000.0
-------------------------------
status:  0
retdata:  b'p\x00 \x00\x01\x00\x02H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xa1\x00\x04\x00\x01\x00\x00 \xb1\x00\x0c\x00\x08\x00\xcc\x00\x00\x00\xca\x00\x00\x00\xa0@'
5.0
dmroeder commented 5 years ago

Apologies for the delay.

The behavior you are seeing is the nature of floating point numbers. Not just with Rockwell or Python, in general. Rockwell is not showing you the exact representation of the number in memory. In believe they did in the early days of RSLogix, though I think it caused confusion when people would enter a value of 7.52 in the controller tags (See KB 8232), it would display 7.5199997. I’m guessing at some point they changed this to display the exact number they entered to save confusion. Or they added the “style” selection to display Exponent vs Float, I haven’t used the really old versions in many years so I cannot remember.

There are a couple of ways you can see this in action in Rockwell land. Create a new program, create a single tag that is a REAL, put the number 7.52 in it. Change the style field in the controller tags to exponent, you will see 7.52 change to the truer representation of 7.5199998e+000. Though it’s still not 100% accurate.

With that same program, save it as a L5K and check it out in your favorite text editor, it’ll probably look similar to above.

Now for fun, if you add the numbers 7.51 and 0.01, destination is the tag you created, you’ll see 7.5200005 as the result. Not 7.52.

Lastly, you can use a RTOS to convert the number 7.52 to a STRING, you will get “7.51999998”

TheFern2 commented 5 years ago

Yeah it was a bit strange. It makes sense, I will try to load the returned data into the PLC and see what happens. I guess it really doesn't matter how it looks as long as the plc knows how to handle those values.

By the way the same happens with EtherIP repo. Though it has multiple methods to retrieve data getInt, 7, getDouble creates the same scenario 7.5199997 and getFloat 7.52 so I figured something could be done in python as well. Nevertheless if the data loads correctly to the PLC then it should be OK.

dmroeder commented 5 years ago

Interesting info on the EtherIP repo. "getFloat" seems like it would be a single precision floating point value, producing 7.5199997, "getDouble" would be double precision, producing more decimal points. Neither seems like it would product 7.52. Though maybe "getFloat" is special.

For fun: https://www.exploringbinary.com/floating-point-converter/

TheFern2 commented 5 years ago

No worries on the delay, hope you had some good holidays! I was working until Sunday hence me working on a side project lol. Not sure how much useful wireshark data will be here, I did one tag read with etherip in the below example. After reading the tag the data goes to a buffer, all three intValue, doubleValue, and floatValue are abstract methods for Number which is an actual JDK class.

Below are implementations of those abstract methods in the JDK Float class:

 /**
     * Returns the value of this {@code Float} as an {@code int} (by
     * casting to type {@code int}).
     *
     * @return  the {@code float} value represented by this object
     *          converted to type {@code int}
     */
    public int intValue() {
        return (int)value;
    }
/**
     * Returns the {@code float} value of this {@code Float} object.
     *
     * @return the {@code float} value represented by this object
     */
    public float floatValue() {
        return value;
    }

    /**
     * Returns the {@code double} value of this {@code Float} object.
     *
     * @return the {@code float} value represented by this
     *         object is converted to type {@code double} and the
     *         result of the conversion is returned.
     */
    public double doubleValue() {
        return (double)value;
    }

Case "REAL" returns the data truncated or rounded depending on which method you call but the actual CIP data never changes.

final synchronized public Number getNumber(final int index)
            throws Exception, IndexOutOfBoundsException
    {
        switch (this.type)
        {
        case BOOL:
        case SINT:
            return new Byte(this.data.get(this.type.element_size * index));
        case INT:
            return new Short(
                    this.data.getShort(this.type.element_size * index));
        case DINT:
        case BITS:
            return new Integer(
                    this.data.getInt(this.type.element_size * index));
        case REAL:
            return new Float(
                    this.data.getFloat(this.type.element_size * index));
        default:
            throw new Exception("Cannot retrieve Number from " + this.type);
        }
    }

image

image

On another note I did read one real tag and wrote that same value to another online tag with pylogix and everything works as expected, so we probably shouldn't spend too much time on this one. I probably should have done that first.

TheFern2 commented 5 years ago

I could add a helper formatting function. I'll do some testing next week.

TheFern2 commented 5 years ago

Closing issue, since is not really an issue. 😄