Phase 3 PR 46 -- DL205 BCD decoder #45

Merged
dohertj2 merged 1 commits from phase-3-pr46-dl205-bcd into v2 2026-04-18 22:13:26 -04:00
Owner

Summary

Stacked on PR 45. Adds ModbusDataType.Bcd16 + Bcd32 for DirectLOGIC binary-coded-decimal numerics.

  • DL205/DL260 store timers/counters/operator-display values as BCD (register 0x1234 = decimal 1234, NOT 4660).
  • DecodeBcd walks nibbles MSB→LSB; rejects any nibble > 9 with InvalidDataException (prevents silent data corruption on transient garbage).
  • Bcd32 respects ModbusByteOrder so word-swap families can use it.
  • Caller must opt in per tag; default decoding stays binary.

Validation

  • 66/66 Modbus.Tests pass (including Bcd16_decodes_DL205_register_1234_as_decimal_1234 with control vs Int16)
  • 3/3 DL205 integration tests pass (HR[1072]=0x1234 decodes 1234 under BCD, 0x1234 under Int16)

Test plan

  • Nibble overflow detection
  • Encode/decode round-trip (both widths)
  • Word-swap for BCD32
  • Integration test against pymodbus dl205 profile
## Summary Stacked on PR 45. Adds `ModbusDataType.Bcd16` + `Bcd32` for DirectLOGIC binary-coded-decimal numerics. - DL205/DL260 store timers/counters/operator-display values as BCD (register `0x1234` = decimal 1234, NOT 4660). - `DecodeBcd` walks nibbles MSB→LSB; rejects any nibble > 9 with `InvalidDataException` (prevents silent data corruption on transient garbage). - `Bcd32` respects `ModbusByteOrder` so word-swap families can use it. - Caller must opt in per tag; default decoding stays binary. ## Validation - 66/66 Modbus.Tests pass (including `Bcd16_decodes_DL205_register_1234_as_decimal_1234` with control vs Int16) - 3/3 DL205 integration tests pass (HR[1072]=0x1234 decodes 1234 under BCD, 0x1234 under Int16) ## Test plan - [x] Nibble overflow detection - [x] Encode/decode round-trip (both widths) - [x] Word-swap for BCD32 - [x] Integration test against pymodbus dl205 profile
dohertj2 added 1 commit 2026-04-18 22:13:22 -04:00
Phase 3 PR 46 -- DL205 BCD decoder (binary-coded-decimal numeric encoding). Adds ModbusDataType.Bcd16 and Bcd32 to the driver. Bcd16 is 1 register wide, Bcd32 is 2 registers wide; Bcd32 respects ModbusByteOrder (BigEndian/WordSwap) the same way Int32 does so the CDAB-style families (including DL205/DL260 themselves) can be configured. DecodeRegister uses the new internal DecodeBcd helper: walks each nibble from MSB to LSB, multiplies the running result by 10, adds the nibble as a decimal digit. Explicitly rejects nibbles > 9 with InvalidDataException -- hardware sometimes produces garbage during write-in-progress transitions and silently returning wrong numeric values would quietly corrupt the caller's data. EncodeRegister's new EncodeBcd inverts the operation (mod/div by 10 nibble-by-nibble) with an up-front overflow check against 10^nibbles-1. Why this matters for DL205/DL260: AutomationDirect DirectLOGIC uses BCD as the default numeric encoding for timers, counters, and operator-display numerics (not binary). A plain Int16 read of register 0x1234 returns 4660; the BCD path returns 1234. The two differ enough that silently defaulting to Int16 would give wildly wrong HMI values -- the caller must opt in to Bcd16/Bcd32 per tag. Unit tests: DecodeBcd (theory: 0,1,9,10,1234,9999), DecodeBcd_rejects_nibbles_above_nine, EncodeBcd (theory), Bcd16_decodes_DL205_register_1234_as_decimal_1234 (control: same bytes as Int16 decode to 4660), Bcd16_encode_round_trips_with_decode, Bcd16_encode_rejects_out_of_range_values, Bcd32_decodes_8_digits_big_endian, Bcd32_word_swap_handles_CDAB_layout, Bcd32_encode_round_trips_with_decode, Bcd_RegisterCount_matches_underlying_width. 66/66 Modbus.Tests pass. Integration test: DL205BcdQuirkTests.DL205_BCD16_decodes_HR1072_as_decimal_1234 against dl205.json pymodbus profile (HR[1072]=0x1234). Asserts Bcd16 decode=1234 AND Int16 decode=0x1234 on the same wire bytes to prove the paths are distinct. 3/3 DL205 integration tests pass with MODBUS_SIM_PROFILE=dl205. 8248b126ce
dohertj2 merged commit 1a60470d4a into v2 2026-04-18 22:13:26 -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.