Skip to content

feat(runtime): implement debugger STRING / WSTRING read/write/force/unforce#162

Merged
thiagoralves merged 1 commit into
developmentfrom
fix/debug-string-impl
Jun 2, 2026
Merged

feat(runtime): implement debugger STRING / WSTRING read/write/force/unforce#162
thiagoralves merged 1 commit into
developmentfrom
fix/debug-string-impl

Conversation

@thiagoralves
Copy link
Copy Markdown
Contributor

Summary

Replace the four *_string_stub placeholders in debug_dispatch.hpp with real implementations against IECStringVar<254> / IECWStringVar<254>. Net effect for the editor: STRING / WSTRING variables in the debug watch panel will show their actual values instead of -.

Root cause

The stubs were a deliberate Phase-4a placeholder. The dispatcher's handle_read short-circuits with zero bytes when type_ops[].size == 0, and handle_set / handle_write reject any payload with STATUS_DATA_TOO_LARGE. Both STRING and WSTRING rows had size = 0.

Wire format (already expected by the editor decoder)

Matches the len8-utf8 / len8-utf16le arms in openplc-web/src/frontend/utils/variable-sizes.ts::decodeWireValue:

Type Width Layout
STRING 127 B [ uint8 length ][ 126 bytes UTF-8 ]
WSTRING 253 B [ uint8 length ][ 126 char16_t LE code units ]

WSTRING serialises char16_t as explicit little-endian byte pairs so the wire format stays host-endianness-independent.

Force semantics

  • read uses the wrapper's .length() / .c_str() proxies (added in v0.5.1) which route through forced_value_ when forcing is active — so a debugger watch shows the forced value, not the underlying program-side value.
  • write uses IECStringVar::set, which is itself a no-op when the variable is forced. Matches OPC-UA / debug-soft-write semantics: soft writes don't override a force.
  • force pins via IECStringVar::force; unforce clears via IECStringVar::unforce.

Why widths matter

Setting size = 127 / 253 means handle_read returns the full window every poll. The editor's decoder treats the leading length byte as authoritative and only renders min(length, 126) characters; the runtime zero-fills the trailing window so stale bus contents from a previous read can't leak into the displayed value.

Verified end-to-end

Direct C++ smoke test (compiled with clang++ against the patched header):

STRING read 'hello'  → len=5 bytes='hello'                       ✓
WSTRING read 'hi'    → len=2 bytes=68 00 69 00 (UTF-16LE 'h','i') ✓
STRING write 'world' → IECStringVar.length()=5, c_str()='world'   ✓
STRING force 'forced'→ is_forced=1, c_str()='forced'              ✓
STRING unforce       → is_forced=0, c_str()='world' (back)        ✓

Test plan

  • Direct C++ smoke test passes for all 4 ops on STRING + WSTRING.
  • Full local CI-equivalent: lint (0 errors), prettier clean, typecheck clean, 1913/1920 tests pass (7 pre-existing skips).
  • CI green on this PR.
  • After v0.5.3 release: editor + web pick up the new strucpp, debugger watch panel shows STRING / WSTRING values for the Irrigation Controller dev project's MANUAL_OVERRIDE0.test_string.

Downstream

This unblocks the debugger STRING display in openplc-editor / openplc-web. After this lands and v0.5.3 is tagged, the version bumps in both repos' binary-versions.json propagate the fix.

🤖 Generated with Claude Code

…nforce

The four `*_string_stub` placeholders in debug_dispatch.hpp returned
zero for read and no-op'd for write/force/unforce.  Net effect: every
STRING / WSTRING pin in a project showed as "-" in the editor's debug
watch panel because `handle_read()` saw a `type_ops[].size == 0` entry
and short-circuited with zero bytes, and `handle_set` / `handle_write`
rejected any payload with STATUS_DATA_TOO_LARGE.

Implement the real ops against `IECStringVar<254>` / `IECWStringVar<254>`,
matching the `len8-utf8` / `len8-utf16le` wire format that the editor's
decoder (`src/frontend/utils/variable-sizes.ts:decodeWireValue`) already
expects:

  STRING  wire shape: [ uint8 length ][ 126 bytes UTF-8 payload    ] (127 B)
  WSTRING wire shape: [ uint8 length ][ 126 char16_t LE code units ] (253 B)

Force-aware:
  - read uses `length()` / `c_str()` on the wrapper, which the
    earlier proxy additions route through `forced_value_` when
    forcing is active.
  - write uses `IECStringVar::set` which itself is a no-op when
    forced (correct OPC-UA / debug-soft-write semantics).
  - force pins the value via `IECStringVar::force`; unforce clears
    via `IECStringVar::unforce`.

WSTRING serialises char16_t as explicit little-endian byte pairs so
the wire format stays host-endianness-independent.

The trailing window past `length` is zero-filled on read so stale bus
contents from a previous read can't leak through the fixed-width
window into the editor's display.

Verified end-to-end with a direct C++ smoke test:
  STRING read 'hello'  → len=5 bytes='hello'                      ✓
  WSTRING read 'hi'    → len=2 bytes=68 00 69 00 (UTF-16LE 'h','i') ✓
  STRING write 'world' → IECStringVar.length()=5, c_str()='world'  ✓
  STRING force 'forced'→ is_forced=1, c_str()='forced'             ✓
  STRING unforce        → is_forced=0, c_str()='world' (back)      ✓

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@thiagoralves thiagoralves merged commit 0b38805 into development Jun 2, 2026
@thiagoralves thiagoralves deleted the fix/debug-string-impl branch June 2, 2026 21:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant