Skip to content

[Bug] OPC UA write to REAL/LREAL variables stores incorrect values (double type conversion in convert_value_for_plc) #164

Description

@alkoval72

Writing a value to a REAL or LREAL PLC variable via OPC UA client results in a completely wrong value being stored. The INT type is not affected.

Steps to Reproduce

I am using: OpenPLC Runtime v4(latest) and OpenPLC.Editor-4.2.10. Running OpenPLC Runtime v4 in Docker.

docker pull ghcr.io/autonomy-logic/openplc-runtime:latest && \
docker run -d \
  --name openplc-runtime \
  --restart unless-stopped \
  --publish 8443:8443 \
  --publish 1502:502 \
  --publish 14840:4840 \
  --cap-add SYS_NICE \
  --cap-add SYS_RESOURCE \
  --memory 2g \
  --memory-swap 2g \
  --pids-limit 256 \
  --security-opt no-new-privileges:true \
  --log-driver json-file \
  --log-opt max-size=5m \
  --log-opt max-file=2 \
  --volume /mnt/data/openplc/runtime:/var/run/runtime \
  "$(docker image inspect ghcr.io/autonomy-logic/openplc-runtime:latest --format='{{index .RepoDigests 0}}')"
  1. Create a PLC program with three variables:
    my_int  : INT;
    my_real : REAL;
    my_lreal: LREAL;
    
  2. Assign 42 to each variable in the program body.
  3. Start the PLC in OpenPLC Runtime v4.
  4. Connect an OPC UA client and write the value 42 to each variable node.
  5. Read back the values.

Expected Behavior

Variable Written Read back
INT 42 42
REAL 42.0 42.0
LREAL 42.0 42.0

Actual Behavior

Variable Written Read back
INT 42 42 ✅
REAL 42.0 ~1.109918e+09 ❌
LREAL 42.0 ~4.631e+18 ❌

Environment

  • OpenPLC Runtime v4 (latest)
  • OpenPLC Editor 4.2.10

Root Cause Analysis

The bug is in core/src/drivers/plugins/python/opcua/opcua_utils.py, function convert_value_for_plc().

The write path is:

OPC UA client writes float 42.0
  → synchronization.py: calls convert_value_for_plc("REAL", 42.0)
  → opcua_memory.py:   calls debug_write_value(..., result)
  → C layer:           stores raw bytes in PLC variable

The bug is a double conversion. convert_value_for_plc() reinterprets the IEEE 754 bit pattern of 42.0 as an integer:

# convert_value_for_plc() current broken code
elif datatype.upper() in ["FLOAT", "REAL"]:
    if isinstance(value, float):
        return struct.unpack('I', struct.pack('f', value))[0]
        # 42.0  →  b'\x00\x00\x28\x42'  →  1109917696

Then debug_write_value() in opcua_memory.py treats that integer numerically when constructing the ctypes value:

encoded = ctypes.c_float(1109917696)
# Numeric int→float conversion:  1109917696.0  ≈  1.109918e+09  ❌

The same pattern affects LREAL:

# 42.0 → struct.unpack('Q', struct.pack('d', 42.0))[0] → 4638355772470722560
# ctypes.c_double(4638355772470722560) ≈ 4.638e+18  ❌

INT is unaffected because its branch does not use struct.pack/unpack it passes the integer value directly.

The read path (PLC -> OPC UA) works correctly because debug_read_value() returns an actual Python float via ctypes.c_float.value, and convert_value_for_opcua() simply does return float(value) for that case.

The LREAL bug was introduced in commit 3df551aa by copying the same broken pattern from the REAL branch.

Proposed Fix

In convert_value_for_plc(), simply return float(value) for REAL and LREAL. The debug_write_value() function already handles correct IEEE 754 encoding via ctypes internally:

# convert_value_for_plc()  fix
elif datatype.upper() in ["FLOAT", "REAL"]:
    return float(value)   # debug_write_value will call ctypes.c_float(42.0) → correct bytes

elif datatype.upper() == "LREAL":
    return float(value)   # debug_write_value will call ctypes.c_double(42.0) → correct bytes

Forum topic: https://edge.autonomylogic.com/forum/thread/error-changing-values-via-opc-ua

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions