Environment
- OpenPLC Editor: OpenPLC.Editor-4.2.11.AppImage (Ubuntu)
- OpenPLC Runtime:
ghcr.io/autonomy-logic/openplc-runtime:v4.1.10, arm64
- Deployment: Docker container, Debian 13 host, ARM64 SBC
- OPC UA server: built-in Runtime OPC UA plugin, path
/openplc/opcua, port 4840
Summary
Any IEC STRING (or WSTRING) variable exposed via the OPC UA server always reads back as an empty string over OPC UA, regardless of its actual value inside the running PLC program, and any client write to such a variable silently fails to reach the program's memory. Non-string types (BOOL, INT family, REAL/LREAL, TIME family) are unaffected.
Root cause (found by reading the plugin source)
core/src/drivers/plugins/python/opcua/opcua_memory.py, _ctype_for():
def _ctype_for(datatype: str) -> Optional[Any]:
...
if t in ("STRING", "WSTRING"):
return None # variable-length, not yet supported by debug surface
Both debug_read_value() and debug_write_value() in the same file short-circuit on ctype is None:
debug_read_value() returns None immediately, with the comment # STRING/WSTRING — not supported yet (Phase 4a in debug_dispatch.hpp explicitly stubs string reads).
debug_write_value() returns False immediately, without ever calling args.debug_write.
In synchronization.py, the read path silently substitutes a default value on None:
@staticmethod
def _get_default_value(datatype: str) -> Any:
...
if dtype == "STRING":
return ""
return 0
...so every push to an OPC UA client for a STRING variable is unconditionally "", no matter what the program actually holds.
The write path logs a failure for every attempted write to a STRING variable:
ok = debug_write_value(self.args, addr[0], addr[1], datatype, plc_value)
if not ok:
log_error(f"debug_write({addr[0]}, {addr[1]}) failed")
This matches the runtime log output exactly: repeated [ERROR]: debug_write(0, N) failed lines for the (arr, elem) addresses of any STRING variable being written from an OPC UA client (or otherwise touched by the plugin's write path), recurring for as long as the variable keeps getting written.
Steps to reproduce
- In an OpenPLC Editor v4 project, add any
STRING variable to a PROGRAM (or a STRING field inside a user-defined STRUCT), and expose it via the OPC UA server device config.
- Compile and deploy to Runtime v4.1.10.
- In the program logic, assign a literal to the variable, e.g.
MY_STRING := 'hello';, every scan.
- Connect an OPC UA client (e.g. UaExpert) to the Runtime's OPC UA server and browse to the node.
Expected: the node's value reflects 'hello'.
Actual: the node's value is always an empty string.
- From the OPC UA client, write a non-empty string to the same node (assuming the program does not overwrite it, or comment out the assignment first).
Expected: the write succeeds and the program can read back the new value.
Actual: the write has no effect on the program's memory; the Runtime log shows [ERROR]: debug_write(<arr>, <elem>) failed for that variable's address.
Impact
Any project design that relies on STRING variables as part of the OPC UA data contract (telemetry fields, text status/log fields, or any scheme that packs multiple values into a delimited string to reduce OPC UA node count) is currently non-functional for both directions (server → client and client → server) — the value is unconditionally empty on read, and writes are unconditionally dropped. Non-string scalar and array-of-non-string types are unaffected, since _ctype_for() correctly maps them to a ctypes scalar.
Suggested fix direction
Implement the STRING/WSTRING case in the debug memory-access surface referenced by the comments (debug_dispatch.hpp, "Phase 4a"): a length-prefixed or null-terminated variable-length read/write path through debug_read/debug_write, analogous to the existing fixed-size scalar path, with a defined maximum length matching the Editor's STRING size limit (126 characters).
Environment
ghcr.io/autonomy-logic/openplc-runtime:v4.1.10, arm64/openplc/opcua, port 4840Summary
Any IEC
STRING(orWSTRING) variable exposed via the OPC UA server always reads back as an empty string over OPC UA, regardless of its actual value inside the running PLC program, and any client write to such a variable silently fails to reach the program's memory. Non-string types (BOOL, INT family, REAL/LREAL, TIME family) are unaffected.Root cause (found by reading the plugin source)
core/src/drivers/plugins/python/opcua/opcua_memory.py,_ctype_for():Both
debug_read_value()anddebug_write_value()in the same file short-circuit onctype is None:debug_read_value()returnsNoneimmediately, with the comment# STRING/WSTRING — not supported yet (Phase 4a in debug_dispatch.hpp explicitly stubs string reads).debug_write_value()returnsFalseimmediately, without ever callingargs.debug_write.In
synchronization.py, the read path silently substitutes a default value onNone:...so every push to an OPC UA client for a STRING variable is unconditionally
"", no matter what the program actually holds.The write path logs a failure for every attempted write to a STRING variable:
This matches the runtime log output exactly: repeated
[ERROR]: debug_write(0, N) failedlines for the(arr, elem)addresses of any STRING variable being written from an OPC UA client (or otherwise touched by the plugin's write path), recurring for as long as the variable keeps getting written.Steps to reproduce
STRINGvariable to aPROGRAM(or aSTRINGfield inside a user-definedSTRUCT), and expose it via the OPC UA server device config.MY_STRING := 'hello';, every scan.Expected: the node's value reflects
'hello'.Actual: the node's value is always an empty string.
Expected: the write succeeds and the program can read back the new value.
Actual: the write has no effect on the program's memory; the Runtime log shows
[ERROR]: debug_write(<arr>, <elem>) failedfor that variable's address.Impact
Any project design that relies on
STRINGvariables as part of the OPC UA data contract (telemetry fields, text status/log fields, or any scheme that packs multiple values into a delimited string to reduce OPC UA node count) is currently non-functional for both directions (server → client and client → server) — the value is unconditionally empty on read, and writes are unconditionally dropped. Non-string scalar and array-of-non-string types are unaffected, since_ctype_for()correctly maps them to actypesscalar.Suggested fix direction
Implement the STRING/WSTRING case in the debug memory-access surface referenced by the comments (
debug_dispatch.hpp, "Phase 4a"): a length-prefixed or null-terminated variable-length read/write path throughdebug_read/debug_write, analogous to the existing fixed-size scalar path, with a defined maximum length matching the Editor's STRING size limit (126 characters).