Phase 3 PR 45 -- DL205 string byte-order quirk #44

Merged
dohertj2 merged 1 commits from phase-3-pr45-dl205-string-byte-order into v2 2026-04-18 22:12:16 -04:00
Owner

Summary

First of 5 stacked PRs landing the DL205/DL260 quirks documented in docs/v2/dl205.md. This one tackles the headline string-byte-order quirk.

  • Adds ModbusStringByteOrder { HighByteFirst, LowByteFirst } enum + StringByteOrder field on ModbusTagDefinition (default = standard Modbus packing).
  • DecodeRegister / EncodeRegister String branches respect per-tag byte order. LowByteFirst packs the first char in the low byte of each register -- the AutomationDirect DirectLOGIC pack-order.
  • Without the flag the driver decodes eHllo instead of Hello from HR[1040..1042] -- same wire bytes, different interpretation.

Validation

  • 56/56 Modbus.Tests pass
  • Integration test DL205_string_low_byte_first_decodes_Hello_from_HR1040 passes against the live pymodbus dl205 profile
  • Control assertion proves HighByteFirst != Hello on the same wire (flag is not a no-op)

Test plan

  • Unit tests for both byte orders (symmetric encode/decode)
  • Nul-truncation still honored under LowByteFirst
  • Integration test against pymodbus dl205 profile
  • Modbus driver full suite green (no regression)
## Summary First of 5 stacked PRs landing the DL205/DL260 quirks documented in docs/v2/dl205.md. This one tackles the headline string-byte-order quirk. - Adds `ModbusStringByteOrder { HighByteFirst, LowByteFirst }` enum + `StringByteOrder` field on `ModbusTagDefinition` (default = standard Modbus packing). - `DecodeRegister` / `EncodeRegister` String branches respect per-tag byte order. `LowByteFirst` packs the first char in the low byte of each register -- the AutomationDirect DirectLOGIC pack-order. - Without the flag the driver decodes `eHllo` instead of `Hello` from HR[1040..1042] -- same wire bytes, different interpretation. ## Validation - 56/56 Modbus.Tests pass - Integration test `DL205_string_low_byte_first_decodes_Hello_from_HR1040` passes against the live pymodbus dl205 profile - Control assertion proves `HighByteFirst != Hello` on the same wire (flag is not a no-op) ## Test plan - [x] Unit tests for both byte orders (symmetric encode/decode) - [x] Nul-truncation still honored under LowByteFirst - [x] Integration test against pymodbus dl205 profile - [x] Modbus driver full suite green (no regression)
dohertj2 added 1 commit 2026-04-18 22:12:04 -04:00
Phase 3 PR 45 -- DL205 string byte-order quirk (low-byte-first ASCII packing). Adds ModbusStringByteOrder enum {HighByteFirst, LowByteFirst} + StringByteOrder field on ModbusTagDefinition (default HighByteFirst, the standard Modbus convention). DecodeRegister + EncodeRegister String branches now respect per-tag byte order. Under LowByteFirst each register packs the first char in the low byte instead of the high byte -- the AutomationDirect DirectLOGIC DL205/DL260/DL350 family's headline string quirk. Without the flag the driver decodes 'eHllo' garbage from HR[1040..1042] even though wire bytes are identical. Unit tests: String_LowByteFirst_decodes_DL205_packed_Hello (5 chars across 3 regs with nul pad), String_LowByteFirst_decode_truncates_at_first_nul, String_LowByteFirst_encode_round_trips_with_decode (asserts exact DL205-documented byte sequence {0x65,0x48,0x6C,0x6C,0x00,0x6F} + symmetric encode->decode), String_HighByteFirst_and_LowByteFirst_differ_on_same_wire (control: same wire, different flag => different decode). 56/56 Modbus.Tests pass. Integration test: DL205StringQuirkTests.DL205_string_low_byte_first_decodes_Hello_from_HR1040 against the dl205.json pymodbus profile; reads HR[1040..1042] with both flags on the same tag map and asserts LowByteFirst='Hello' + HighByteFirst!='Hello'. Gated on MODBUS_SIM_PROFILE=dl205 since the standard profile doesn't seed HR[1040..1042]. Verified 2/2 integration tests pass against running pymodbus dl205 simulator. Baseline for PR 46 (BCD decoder), PR 47 (V-memory octal helper), PR 48 (CDAB float order), PR 49 (FC03/FC16 per-device caps) -- each lands its own DL205_<behavior> test class in tests/.../DL205/. cd19022d19
dohertj2 merged commit 635f67bb02 into v2 2026-04-18 22:12:16 -04:00
dohertj2 referenced this issue from a commit 2026-04-19 16:59:47 -04:00
AB CIP PR 4 — IWritable implementation. LibplctagTagRuntime.EncodeValue fills in the switch for every atomic Logix type the driver currently surfaces — Bool (standalone BOOL via SetInt8 0/1), SInt/USInt (SetInt8/SetUInt8), Int/UInt (SetInt16/SetUInt16), DInt/UDInt (SetInt32/SetUInt32), LInt/ULInt (SetInt64/SetUInt64), Real (SetFloat32), LReal (SetFloat64), String (SetString 0), Dt (epoch DINT via SetInt32). BOOL-within-DINT writes throw NotSupportedException with a code comment matching the Modbus BitInRegister pattern at ModbusDriver.cs line 640 — the read-modify-write logic + lock-per-DINT discipline is a follow-up PR rather than squeezing it into the initial wire plumbing. Structure writes throw NotSupportedException pointing at PR 6 when UDT support lands. AbCipDriver now implements IWritable. WriteAsync iterates writes preserving order, short-circuits on unknown reference → BadNodeIdUnknown, on non-writable tag definition → BadNotWritable, on unknown device → BadNodeIdUnknown. Happy path materialises the cached runtime via EnsureTagRuntimeAsync (shares PR 3's lazy-init path so read+write on the same tag hits one native handle), EncodeValue into the tag's buffer, WriteAsync flushes, GetStatus confirms the wire status, maps libplctag error codes via AbCipStatusMapper.MapLibplctagStatus, sets health Healthy on success. Per plan decisions #44, #45, #143 the driver does NOT auto-retry writes — that's a resilience-layer concern (Polly pipeline sitting above) keyed on the tag's WriteIdempotent flag. Exception-mapping table — OperationCanceledException rethrows (honors cancellation), NotSupportedException → BadNotSupported (bit-in-DINT, Structure, future unsupported types), FormatException → BadTypeMismatch (Convert.ToInt32 of a non-numeric string), InvalidCastException → BadTypeMismatch (caller passed an object incompatible with the conversion target), OverflowException → BadOutOfRange (value exceeds target type range, e.g. Int16 write of 1_000_000), any other Exception → BadCommunicationError (wire drop, libplctag-internal failure). Health surface updates Degraded on every non-Cancellation exception path, Healthy on success. Introduces AbCipStatusMapper.BadTypeMismatch (0x80730000). 10 new unit tests in AbCipDriverWriteTests covering — unknown ref → BadNodeIdUnknown, non-writable tag → BadNotWritable, successful DInt write encodes + flushes the value + marks WriteCount=1, BOOL-in-DINT rejected as BadNotSupported (separate ThrowingBoolBitFake mirrors LibplctagTagRuntime's runtime check), non-zero libplctag status after write mapped via AbCipStatusMapper (timeout -5 → BadTimeout), FormatException from non-numeric-string write → BadTypeMismatch (RealConvertFake exercises real Convert.ToInt32), OverflowException from Int16 write of 1_000_000 → BadOutOfRange, generic exception during write → BadCommunicationError + health Degraded, batch with mixed success+failure preserves order across four request types, cancellation propagates as OperationCanceledException. FakeAbCipTag's test-fake base class methods made virtual so override hooks work correctly through the IAbCipTagRuntime interface (new-shadow was silently falling through to the base implementation). Total AbCip unit tests now 98/98 passing; Modbus + other existing tests untouched; full solution builds 0 errors.
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#44