Phase 3 PR 52 -- Modbus exception-code -> OPC UA StatusCode translation #51

Merged
dohertj2 merged 1 commits from phase-3-pr52-dl205-exception-codes into v2 2026-04-18 22:35:34 -04:00
Owner

Summary

Before this PR every Modbus exception and every transport failure collapsed to BadInternalError, making field diagnosis from the OPC UA client impossible. Adds MapModbusExceptionToStatus:

Modbus exc OPC UA
01 Illegal Function BadNotSupported
02 Illegal Data Address BadOutOfRange
03 Illegal Data Value BadOutOfRange
04 Server Failure BadDeviceFailure
05/06 Busy BadDeviceFailure
0A/0B Gateway BadCommunicationError
unknown BadInternalError

Also distinguishes socket-layer failures (BadCommunicationError) from protocol-layer faults so operators know to check network vs. tag config.

Per docs/v2/dl205.md, DL205/DL260 returns only codes 01-04; exception 04 (CPU in PROGRAM mode) is operator-recoverable, so surfacing it as BadDeviceFailure (not BadInternalError) makes the fix obvious.

Validation

  • 115/115 Modbus.Tests pass
  • 11/11 DL205 integration tests pass (new one: FC03 at unmapped register returns BadOutOfRange against live pymodbus)

Test plan

  • Unit theory exercises every code in the table
  • ReadAsync exception path surfaces mapped code
  • WriteAsync exception path surfaces mapped code
  • Non-Modbus failure distinct from protocol failure
  • Live integration test against dl205 profile
## Summary Before this PR every Modbus exception and every transport failure collapsed to `BadInternalError`, making field diagnosis from the OPC UA client impossible. Adds `MapModbusExceptionToStatus`: | Modbus exc | OPC UA | |------------|--------| | 01 Illegal Function | BadNotSupported | | 02 Illegal Data Address | BadOutOfRange | | 03 Illegal Data Value | BadOutOfRange | | 04 Server Failure | BadDeviceFailure | | 05/06 Busy | BadDeviceFailure | | 0A/0B Gateway | BadCommunicationError | | unknown | BadInternalError | Also distinguishes socket-layer failures (`BadCommunicationError`) from protocol-layer faults so operators know to check network vs. tag config. Per docs/v2/dl205.md, DL205/DL260 returns only codes 01-04; exception 04 (CPU in PROGRAM mode) is operator-recoverable, so surfacing it as BadDeviceFailure (not BadInternalError) makes the fix obvious. ## Validation - 115/115 Modbus.Tests pass - 11/11 DL205 integration tests pass (new one: FC03 at unmapped register returns BadOutOfRange against live pymodbus) ## Test plan - [x] Unit theory exercises every code in the table - [x] ReadAsync exception path surfaces mapped code - [x] WriteAsync exception path surfaces mapped code - [x] Non-Modbus failure distinct from protocol failure - [x] Live integration test against dl205 profile
dohertj2 added 1 commit 2026-04-18 22:35:30 -04:00
Phase 3 PR 52 -- Modbus exception-code -> OPC UA StatusCode translation. Before this PR every server-side Modbus exception AND every transport-layer failure collapsed to BadInternalError (0x80020000) in the driver's Read/Write results, making field diagnosis 'is this a tag misconfig or a driver bug?' impossible from the OPC UA client side. PR 52 adds a MapModbusExceptionToStatus helper that translates per spec: 01 Illegal Function -> BadNotSupported (0x803D0000); 02 Illegal Data Address -> BadOutOfRange (0x803C0000); 03 Illegal Data Value -> BadOutOfRange; 04 Server Failure -> BadDeviceFailure (0x80550000); 05/06 Acknowledge/Busy -> BadDeviceFailure; 0A/0B Gateway -> BadCommunicationError (0x80050000); unknown -> BadInternalError fallback. Non-Modbus failures (socket drop, timeout, malformed frame) in ReadAsync are now distinguished from tag-level faults: they map to BadCommunicationError so operators check network/PLC reachability rather than tag definitions. Why per-DL205: docs/v2/dl205.md documents DL205/DL260 returning only codes 01-04 with specific triggers -- exception 04 specifically means 'CPU in PROGRAM mode during a protected write', which is operator-recoverable by switching the CPU to RUN; surfacing it as BadDeviceFailure (not BadInternalError) makes the fix obvious. Changes in ModbusDriver: Read catch-chain now ModbusException first (-> mapper), generic Exception second (-> BadCommunicationError); Write catch-chain same pattern but generic Exception stays BadInternalError because write failures can legitimately come from EncodeRegister (out-of-range value) which is a driver-layer fault. Unit tests: MapModbusExceptionToStatus theory exercising every code in the table including the 0xFF fallback; Read_surface_exception_02_as_BadOutOfRange with an ExceptionRaisingTransport that forces code 02; Write_surface_exception_04_as_BadDeviceFailure for CPU-mode faults; Read_non_modbus_failure_maps_to_BadCommunicationError with a NonModbusFailureTransport that raises EndOfStreamException. 115/115 Modbus.Tests pass. Integration test: DL205ExceptionCodeTests.DL205_FC03_at_unmapped_register_returns_BadOutOfRange reads HR[16383] which is beyond the seeded uint16 cells on the dl205.json profile; pymodbus returns exception 02 and the driver surfaces BadOutOfRange. 11/11 DL205 integration tests pass with MODBUS_SIM_PROFILE=dl205. cde018aec1
dohertj2 merged commit 476ce9b7c5 into v2 2026-04-18 22:35:34 -04:00
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#51