[ablegacy] AbLegacy — DH+ via 1756-DHRIO bridging #400
@@ -32,6 +32,27 @@ Family ↔ CIP-path cheat sheet:
|
||||
with no backplane
|
||||
- **LogixPccc** — `1,0` (Logix controller accessed via the PCCC compatibility
|
||||
layer; rare)
|
||||
- **PLC-5 via 1756-DHRIO bridge** — `1,<slot>,2,<station-octal>` (PLC-5 only).
|
||||
See [drivers/AbLegacy-DH-Bridging.md](drivers/AbLegacy-DH-Bridging.md) for
|
||||
the full DH+ syntax, octal-station reference (00..77 = 0..63), and manual
|
||||
hardware smoke procedure.
|
||||
|
||||
#### DHRIO worked example (PR ablegacy-13 / #256)
|
||||
|
||||
PLC-5 on DH+ node 7 (octal 07), DHRIO module in chassis slot 3,
|
||||
EtherNet/IP gateway 192.168.1.10:
|
||||
|
||||
```powershell
|
||||
otopcua-ablegacy-cli read `
|
||||
-g ab://192.168.1.10/1,3,2,07 `
|
||||
-P Plc5 -a N7:10 -t Int
|
||||
```
|
||||
|
||||
The parser validates `1,<slot>,2,<station>`: port-1 must be the backplane,
|
||||
slot must be 0..16, port-3 must be `2` (DH+), station must be octal 0..77 (so
|
||||
`80`, `90`, etc. are rejected). Combining a DH+ bridge path with a non-PLC-5
|
||||
family at startup throws `InvalidOperationException("DHRIO bridging is
|
||||
PLC-5-only")`.
|
||||
|
||||
### Per-device timeout / retry tuning (#252, PR 9)
|
||||
|
||||
|
||||
141
docs/drivers/AbLegacy-DH-Bridging.md
Normal file
141
docs/drivers/AbLegacy-DH-Bridging.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# AB Legacy — DH+ via 1756-DHRIO bridging
|
||||
|
||||
PR ablegacy-13 / [#256](https://github.com/dohertj2/lmxopcua/issues/256). The AB
|
||||
Legacy driver can address a PLC-5 sitting on a DH+ link by routing CIP requests
|
||||
through a 1756-DHRIO module installed in a ControlLogix chassis. This is the
|
||||
canonical way to keep an installed-base PLC-5 fleet alive after the chassis-
|
||||
level migration to ControlLogix; the DHRIO module exposes a DH+ "side" that
|
||||
talks to the legacy PLC-5 / SLC-DH+ peers and a backplane "side" that the
|
||||
ControlLogix CPU + Ethernet bridge can route through.
|
||||
|
||||
## Wire layout
|
||||
|
||||
```
|
||||
OtOpcUa server ──EtherNet/IP──► 1756-EN2T (slot 0) ──backplane──► 1756-DHRIO (slot N) ──DH+──► PLC-5
|
||||
```
|
||||
|
||||
Two CIP hops:
|
||||
|
||||
1. **Backplane** — port `1`, slot `<N>` (the slot the DHRIO module lives in).
|
||||
2. **DH+** — port `2`, station `<S>` (the DH+ node address of the target PLC-5,
|
||||
in **octal**).
|
||||
|
||||
Resulting CIP path: `1,<N>,2,<S>`.
|
||||
|
||||
> The first port `1` is always the backplane; port `2` is the DH+ side of the
|
||||
> 1756-DHRIO module. This mirrors the convention Rockwell uses in RSLinx + RSLogix
|
||||
> 5.
|
||||
|
||||
## Octal station number
|
||||
|
||||
The DH+ network was specified with **octal** node addresses. Rockwell tooling
|
||||
displays them in octal too (RSLogix 5 → "DH+ Node Address" field on the
|
||||
controller properties dialog). The driver follows suit — the station segment
|
||||
of the CIP path **must be parsed as octal** (digits 0..7 only; `8`, `9`, and
|
||||
multi-byte garbage are rejected).
|
||||
|
||||
DH+ addresses run `0..77` octal == `0..63` decimal. Quick reference:
|
||||
|
||||
| Octal | Decimal | Octal | Decimal | Octal | Decimal | Octal | Decimal |
|
||||
|------:|--------:|------:|--------:|------:|--------:|------:|--------:|
|
||||
| 00 | 0 | 20 | 16 | 40 | 32 | 60 | 48 |
|
||||
| 01 | 1 | 21 | 17 | 41 | 33 | 61 | 49 |
|
||||
| 02 | 2 | 22 | 18 | 42 | 34 | 62 | 50 |
|
||||
| 03 | 3 | 23 | 19 | 43 | 35 | 63 | 51 |
|
||||
| 04 | 4 | 24 | 20 | 44 | 36 | 64 | 52 |
|
||||
| 05 | 5 | 25 | 21 | 45 | 37 | 65 | 53 |
|
||||
| 06 | 6 | 26 | 22 | 46 | 38 | 66 | 54 |
|
||||
| 07 | 7 | 27 | 23 | 47 | 39 | 67 | 55 |
|
||||
| 10 | 8 | 30 | 24 | 50 | 40 | 70 | 56 |
|
||||
| 11 | 9 | 31 | 25 | 51 | 41 | 71 | 57 |
|
||||
| 12 | 10 | 32 | 26 | 52 | 42 | 72 | 58 |
|
||||
| 13 | 11 | 33 | 27 | 53 | 43 | 73 | 59 |
|
||||
| 14 | 12 | 34 | 28 | 54 | 44 | 74 | 60 |
|
||||
| 15 | 13 | 35 | 29 | 55 | 45 | 75 | 61 |
|
||||
| 16 | 14 | 36 | 30 | 56 | 46 | 76 | 62 |
|
||||
| 17 | 15 | 37 | 31 | 57 | 47 | 77 | 63 |
|
||||
|
||||
Anything past `77` octal (i.e. decimal > 63) is invalid on a real DH+ network
|
||||
and rejected by the parser.
|
||||
|
||||
## PLC-5 only
|
||||
|
||||
DHRIO bridging is **PLC-5-only**. The driver enforces this at
|
||||
`AbLegacyDriver.InitializeAsync` time: a DH+ bridge path combined with
|
||||
`PlcFamily=Slc500 / MicroLogix / LogixPccc` throws
|
||||
`InvalidOperationException("DHRIO bridging is PLC-5-only")` immediately rather
|
||||
than letting reads silently fail with `BadCommunicationError` on the wire.
|
||||
|
||||
Background: the 1756-DHRIO module only speaks DH+ to PLC-5 / SLC-DH+ peers, and
|
||||
libplctag's PCCC stack only exposes the PLC-5 side. SLC 5/04 boxes on DH+
|
||||
**can** be physically reached through a DHRIO module, but the protocol stack
|
||||
needed to drive them isn't exposed by libplctag — out of scope for this driver.
|
||||
|
||||
## CLI worked example
|
||||
|
||||
PLC-5 at DH+ node `07` (octal == 7 decimal), DHRIO module in slot 3, gateway
|
||||
`192.168.1.10`:
|
||||
|
||||
```powershell
|
||||
otopcua-ablegacy-cli probe `
|
||||
-g ab://192.168.1.10/1,3,2,07 `
|
||||
-P Plc5 `
|
||||
-a N7:0
|
||||
```
|
||||
|
||||
```powershell
|
||||
# Read N7:10 from the PLC-5 across the DHRIO bridge
|
||||
otopcua-ablegacy-cli read `
|
||||
-g ab://192.168.1.10/1,3,2,07 `
|
||||
-P Plc5 `
|
||||
-a N7:10 `
|
||||
-t Int
|
||||
```
|
||||
|
||||
The driver surfaces the parsed bridge form on the host-address record:
|
||||
`BackplaneSlot=3`, `DhPlusPort=2`, `DhPlusStation=7` (decimal-translated). Use
|
||||
those values when reading driver-diagnostics output to confirm the bridge was
|
||||
recognised — a non-bridge CIP path leaves all three fields null.
|
||||
|
||||
## Manual smoke procedure
|
||||
|
||||
There is no automated end-to-end coverage for DH+ bridging because the only
|
||||
path to wire-level validation is real hardware (libplctag's `ab_server` Docker
|
||||
image doesn't simulate the DHRIO + DH+ + PLC-5 stack). The unit-test layer
|
||||
covers parser positive / negative cases.
|
||||
|
||||
Hardware smoke checklist:
|
||||
|
||||
1. Confirm the 1756-DHRIO module is present in the target ControlLogix chassis.
|
||||
RSLinx Classic should show `DH+, 1` under the chassis tree with the PLC-5
|
||||
nodes enumerated underneath.
|
||||
2. Note the DHRIO module's slot number (the `<N>` in `1,<N>,2,<S>`).
|
||||
3. Note the target PLC-5's DH+ node address — read it off the front-panel switch
|
||||
bank, or the controller properties in RSLogix 5. **Read it as octal**.
|
||||
4. From an OtOpcUa box that can reach the EtherNet/IP gateway:
|
||||
|
||||
```powershell
|
||||
otopcua-ablegacy-cli probe -g ab://<gateway>/1,<slot>,2,<station-octal> -P Plc5 -a S:0
|
||||
```
|
||||
|
||||
`S:0` (status file word 0) is non-destructive and present on every PLC-5.
|
||||
5. If the probe succeeds, exercise an N file read against a known
|
||||
non-zero address. Compare against the value displayed in RSLogix 5 →
|
||||
Online → Data → N7.
|
||||
|
||||
If the probe fails with `BadCommunicationError`:
|
||||
|
||||
- Wrong slot number — re-check via RSLinx.
|
||||
- Wrong octal node — convert from RSLogix 5's display value (already octal); a
|
||||
decimal-thinking conversion mistake is the most common smoke failure.
|
||||
- DHRIO module's DH+ baud rate doesn't match the PLC-5's switch setting (57.6k
|
||||
/ 115.2k / 230.4k) — driver-side problem this can't paper over.
|
||||
- A scanner on the DHRIO is in scheduled-mode and starving unscheduled
|
||||
PCCC traffic — bump the DHRIO's unscheduled-message slice in RSLogix 5000.
|
||||
|
||||
## See also
|
||||
|
||||
- [`Driver.AbLegacy.Cli.md`](../Driver.AbLegacy.Cli.md) — the family / CIP-path
|
||||
cheat sheet now carries a DHRIO row.
|
||||
- [`drivers/AbLegacy-Test-Fixture.md`](AbLegacy-Test-Fixture.md) — DH+ bridging
|
||||
is unit-only; no Docker fixture supports it.
|
||||
@@ -132,6 +132,17 @@ driver-side correctness depends on libplctag being correct.
|
||||
`IPerCallHostResolver` contract is verified; real PCCC wire routing across
|
||||
multiple gateways is not.
|
||||
|
||||
### 3a. DH+ via 1756-DHRIO bridging (PR ablegacy-13 / #256)
|
||||
|
||||
Unit-only — coverage lives in `AbLegacyDhPlusBridgingTests`. The CIP-path
|
||||
parser positive / negative cases (octal-station validation, slot bounds, port
|
||||
shape) and the PLC-5-only family guard at `InitializeAsync` are exercised
|
||||
against fakes. There is no Docker fixture for DH+ because libplctag's
|
||||
`ab_server` doesn't simulate the DHRIO + DH+ + PLC-5 stack — wire-level
|
||||
validation requires real hardware. See
|
||||
[`AbLegacy-DH-Bridging.md`](AbLegacy-DH-Bridging.md) for the manual smoke
|
||||
procedure.
|
||||
|
||||
### 4. Alarms / history
|
||||
|
||||
PCCC has no alarm object + no history object. Driver doesn't implement
|
||||
|
||||
@@ -54,6 +54,16 @@
|
||||
Failure threshold the server is configured with (default 3). The
|
||||
demote assertion writes/reads N+1 times against the killed simulator
|
||||
to guarantee the threshold trips even if some reads beat the kill.
|
||||
|
||||
.PARAMETER DhPlusStation
|
||||
PR ablegacy-13 / #256 — DH+ node address (octal 0..77 == decimal 0..63)
|
||||
of a PLC-5 reachable through a 1756-DHRIO module. **Documentation
|
||||
parameter only — there is no automated assertion**: libplctag's ab_server
|
||||
does not simulate the DHRIO + DH+ + PLC-5 stack, so wire-level coverage
|
||||
requires real hardware. When supplied alongside a `-Gateway` of the form
|
||||
`ab://<host>/1,<slot>,2,<station-octal>` and `-PlcType Plc5`, the value
|
||||
here is recorded in the run log so reproducibility is auditable. See
|
||||
docs/drivers/AbLegacy-DH-Bridging.md for the manual smoke procedure.
|
||||
#>
|
||||
|
||||
param(
|
||||
@@ -64,7 +74,11 @@ param(
|
||||
[Parameter(Mandatory)] [string]$BridgeNodeId,
|
||||
[string]$DiagnosticsRequestCountNodeId,
|
||||
[string]$DiagnosticsDemoteCountNodeId,
|
||||
[int]$FailureThresholdForDemote = 3
|
||||
[int]$FailureThresholdForDemote = 3,
|
||||
# PR ablegacy-13 / #256 — DH+ station via 1756-DHRIO bridging. Doc-only:
|
||||
# no automated assertion (no Docker fixture covers DH+). See script header
|
||||
# comment + docs/drivers/AbLegacy-DH-Bridging.md.
|
||||
[string]$DhPlusStation
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
@@ -163,6 +163,19 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
||||
var addr = AbLegacyHostAddress.TryParse(device.HostAddress)
|
||||
?? throw new InvalidOperationException(
|
||||
$"AbLegacy device has invalid HostAddress '{device.HostAddress}' — expected 'ab://gateway[:port]/cip-path'.");
|
||||
// PR ablegacy-13 / #256 — DHRIO DH+ bridging is PLC-5-only. SLC500 /
|
||||
// MicroLogix / LogixPccc cannot be addressed through a 1756-DHRIO module
|
||||
// (the module only speaks DH+ to PLC-5 and SLC-DH+ peers — and the SLC-DH+
|
||||
// path uses a different protocol stack libplctag's PCCC layer doesn't
|
||||
// expose). Catch the misconfiguration up front rather than waiting for
|
||||
// reads to return BadCommunicationError on the wire.
|
||||
if (addr.IsDhPlusBridge && device.PlcFamily != AbLegacyPlcFamily.Plc5)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"AbLegacy device '{device.HostAddress}' uses the 1756-DHRIO DH+ bridge " +
|
||||
$"path '1,{addr.BackplaneSlot},2,…' but PlcFamily='{device.PlcFamily}'. " +
|
||||
"DHRIO bridging is PLC-5-only.");
|
||||
}
|
||||
var profile = AbLegacyPlcFamilyProfile.ForFamily(device.PlcFamily);
|
||||
_devices[device.HostAddress] = new DeviceState(addr, device, profile);
|
||||
// PR ablegacy-10 / #253 — pre-allocate the diagnostic-counter slot so the
|
||||
|
||||
@@ -7,14 +7,38 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
|
||||
/// a direct-wired SLC 500 uses an empty path).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Parser duplicated from AbCipHostAddress rather than shared because the two drivers ship
|
||||
/// independently + a shared helper would force a reference between them. If a third AB
|
||||
/// driver appears, extract into Core.Abstractions.
|
||||
/// <para>Parser duplicated from AbCipHostAddress rather than shared because the two drivers
|
||||
/// ship independently + a shared helper would force a reference between them. If a third AB
|
||||
/// driver appears, extract into Core.Abstractions.</para>
|
||||
/// <para>PR ablegacy-13 / #256 — the optional <see cref="BackplaneSlot"/>,
|
||||
/// <see cref="DhPlusPort"/> and <see cref="DhPlusStation"/> fields are populated when the
|
||||
/// CIP path matches the canonical 1756-DHRIO bridge form <c>1,<slot>,2,<station></c>:
|
||||
/// port 1 (backplane) → DHRIO module slot → port 2 (DH+ side of the module) → DH+ node
|
||||
/// address. The DH+ station number is octal in PLC-5 firmware (0..77 = decimal 0..63);
|
||||
/// we parse it as octal and surface the decimal value for diagnostics. DHRIO bridging is
|
||||
/// PLC-5-only — the family-validation guard lives on the driver.</para>
|
||||
/// </remarks>
|
||||
public sealed record AbLegacyHostAddress(string Gateway, int Port, string CipPath)
|
||||
public sealed record AbLegacyHostAddress(
|
||||
string Gateway,
|
||||
int Port,
|
||||
string CipPath,
|
||||
int? BackplaneSlot = null,
|
||||
int? DhPlusPort = null,
|
||||
int? DhPlusStation = null)
|
||||
{
|
||||
public const int DefaultEipPort = 44818;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum chassis slot index accepted for the DHRIO bridge form. Real ControlLogix
|
||||
/// chassis are 4 / 7 / 10 / 13 / 17 slots; capping at 16 covers the largest standard
|
||||
/// 17-slot frame (slots 0..16) without rejecting anything an operator might legitimately
|
||||
/// configure. Tighter / family-specific bounds are enforced elsewhere.
|
||||
/// </summary>
|
||||
public const int MaxBackplaneSlot = 16;
|
||||
|
||||
/// <summary>True iff the CIP path was the canonical 1756-DHRIO DH+ bridge form.</summary>
|
||||
public bool IsDhPlusBridge => DhPlusStation is not null;
|
||||
|
||||
public override string ToString() => Port == DefaultEipPort
|
||||
? $"ab://{Gateway}/{CipPath}"
|
||||
: $"ab://{Gateway}:{Port}/{CipPath}";
|
||||
@@ -48,6 +72,73 @@ public sealed record AbLegacyHostAddress(string Gateway, int Port, string CipPat
|
||||
}
|
||||
if (string.IsNullOrEmpty(gateway)) return null;
|
||||
|
||||
// PR ablegacy-13 / #256 — optional DHRIO DH+ bridge path detection.
|
||||
// Shape: exactly four comma-separated decimal segments `port,slot,port,station` with
|
||||
// port[0]=1 (backplane), slot in [0..16], port[2]=2 (DH+), station octal 0..77.
|
||||
// Anything else is left as an opaque CIP path — direct-wired PLCs, MicroLogix empty
|
||||
// paths, longer multi-hop bridges all flow through unchanged.
|
||||
if (TryParseDhPlusBridge(cipPath, out var slot, out var dhPort, out var station))
|
||||
{
|
||||
return new AbLegacyHostAddress(gateway, port, cipPath,
|
||||
BackplaneSlot: slot,
|
||||
DhPlusPort: dhPort,
|
||||
DhPlusStation: station);
|
||||
}
|
||||
|
||||
return new AbLegacyHostAddress(gateway, port, cipPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <c>true</c> iff <paramref name="cipPath"/> is exactly the four-segment DHRIO
|
||||
/// bridge form <c>1,<slot>,2,<station></c>. Returns <c>false</c> for every
|
||||
/// other shape — including malformed near-misses (slot out of range, station out of
|
||||
/// octal range, port-1 ≠ backplane). A near-miss returns false rather than throwing so
|
||||
/// the caller can keep treating the CIP path as opaque.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is the single source of truth for the DHRIO octal-station range check.
|
||||
/// Reuses the same "octal accepts only 0..7" rule as <c>AbLegacyAddress</c>'s PLC-5
|
||||
/// I/O parser — a leading <c>8</c> or <c>9</c> in any digit is rejected.
|
||||
/// </remarks>
|
||||
private static bool TryParseDhPlusBridge(
|
||||
string cipPath, out int slot, out int dhPort, out int station)
|
||||
{
|
||||
slot = 0;
|
||||
dhPort = 0;
|
||||
station = 0;
|
||||
|
||||
if (string.IsNullOrEmpty(cipPath)) return false;
|
||||
|
||||
// Reject leading / trailing whitespace inside segments — `1, 3, 2, 07` (with spaces)
|
||||
// is plausibly user typo but we keep the parser strict to avoid false positives.
|
||||
var parts = cipPath.Split(',');
|
||||
if (parts.Length != 4) return false;
|
||||
|
||||
if (!int.TryParse(parts[0], out var firstPort) || firstPort != 1) return false;
|
||||
if (!int.TryParse(parts[1], out slot) || slot < 0 || slot > MaxBackplaneSlot) return false;
|
||||
if (!int.TryParse(parts[2], out dhPort) || dhPort != 2) return false;
|
||||
if (!TryParseOctal(parts[3], out station) || station < 0 || station > 63) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a DH+ station number written in octal (0..77 octal = 0..63 decimal). Mirrors
|
||||
/// the octal-rule in <c>AbLegacyAddress.TryParseIndex</c> — digits 0..7 only, no sign,
|
||||
/// no prefix. Returns <c>false</c> for empty input, illegal digits (8/9), or non-digit
|
||||
/// characters.
|
||||
/// </summary>
|
||||
private static bool TryParseOctal(string text, out int value)
|
||||
{
|
||||
value = 0;
|
||||
if (string.IsNullOrEmpty(text)) return false;
|
||||
var acc = 0;
|
||||
foreach (var c in text)
|
||||
{
|
||||
if (c < '0' || c > '7') return false;
|
||||
acc = (acc * 8) + (c - '0');
|
||||
}
|
||||
value = acc;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,14 @@ public sealed record AbLegacyPlcFamilyProfile(
|
||||
SupportsPlsFile: false,
|
||||
SupportsBlockTransferFile: false);
|
||||
|
||||
/// <remarks>
|
||||
/// PR ablegacy-13 / #256 — PLC-5 is the only family that supports the 1756-DHRIO DH+
|
||||
/// bridging form (CIP path <c>1,<slot>,2,<station-octal></c>). The DHRIO
|
||||
/// module only speaks DH+ to PLC-5 / SLC-DH+ peers, and libplctag's PCCC stack only
|
||||
/// exposes the PLC-5 side. SLC500 / MicroLogix / LogixPccc devices addressed through a
|
||||
/// DHRIO path are rejected at <c>AbLegacyDriver.InitializeAsync</c> time. See
|
||||
/// <c>docs/drivers/AbLegacy-DH-Bridging.md</c> for the full DH+ syntax + smoke procedure.
|
||||
/// </remarks>
|
||||
public static readonly AbLegacyPlcFamilyProfile Plc5 = new(
|
||||
LibplctagPlcAttribute: "plc5",
|
||||
DefaultCipPath: "1,0",
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// PR ablegacy-13 / #256 — coverage for the 1756-DHRIO DH+ bridge form on
|
||||
/// <see cref="AbLegacyHostAddress"/>: octal-station validation, slot bounds, port shape,
|
||||
/// and the PLC-5-only family guard at <c>AbLegacyDriver.InitializeAsync</c>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class AbLegacyDhPlusBridgingTests
|
||||
{
|
||||
[Theory]
|
||||
// station 07 octal → 7 decimal
|
||||
[InlineData("ab://10.0.0.1/1,3,2,07", 3, 2, 7)]
|
||||
// station 77 octal → 63 decimal (top of the DH+ address range)
|
||||
[InlineData("ab://10.0.0.1/1,3,2,77", 3, 2, 63)]
|
||||
// single-digit station 0..7
|
||||
[InlineData("ab://10.0.0.1/1,0,2,0", 0, 2, 0)]
|
||||
[InlineData("ab://10.0.0.1/1,16,2,77", 16, 2, 63)]
|
||||
// station 10 octal → 8 decimal
|
||||
[InlineData("ab://10.0.0.1/1,3,2,10", 3, 2, 8)]
|
||||
public void DhPlusBridge_parses_valid_octal_station(string input, int slot, int dhPort, int station)
|
||||
{
|
||||
var parsed = AbLegacyHostAddress.TryParse(input);
|
||||
parsed.ShouldNotBeNull();
|
||||
parsed.IsDhPlusBridge.ShouldBeTrue();
|
||||
parsed.BackplaneSlot.ShouldBe(slot);
|
||||
parsed.DhPlusPort.ShouldBe(dhPort);
|
||||
parsed.DhPlusStation.ShouldBe(station);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
// octal-illegal digits 8 / 9 in the station segment
|
||||
[InlineData("ab://10.0.0.1/1,3,2,80")]
|
||||
[InlineData("ab://10.0.0.1/1,3,2,90")]
|
||||
[InlineData("ab://10.0.0.1/1,3,2,08")]
|
||||
[InlineData("ab://10.0.0.1/1,3,2,79")]
|
||||
// port-1 not the backplane (must be exactly 1)
|
||||
[InlineData("ab://10.0.0.1/0,3,2,77")]
|
||||
[InlineData("ab://10.0.0.1/2,3,2,77")]
|
||||
// port-3 not the DH+ port (must be exactly 2)
|
||||
[InlineData("ab://10.0.0.1/1,3,3,77")]
|
||||
[InlineData("ab://10.0.0.1/1,3,1,77")]
|
||||
// slot out of range (max 16)
|
||||
[InlineData("ab://10.0.0.1/1,17,2,07")]
|
||||
[InlineData("ab://10.0.0.1/1,99,2,07")]
|
||||
[InlineData("ab://10.0.0.1/1,-1,2,07")]
|
||||
// wrong segment count
|
||||
[InlineData("ab://10.0.0.1/1,3,2")]
|
||||
[InlineData("ab://10.0.0.1/1,3,2,07,extra")]
|
||||
public void DhPlusBridge_rejects_invalid_form(string input)
|
||||
{
|
||||
// The host-address parser still succeeds (the CIP path stays opaque) — only the
|
||||
// DH+ accessors are absent. That keeps AB-Legacy compatible with paths shaped
|
||||
// similarly to a DHRIO bridge but not actually one (e.g. a future DHRIO-clone
|
||||
// module on a non-backplane-1 port).
|
||||
var parsed = AbLegacyHostAddress.TryParse(input);
|
||||
// For the wrong-segment-count and trailing-extra cases, parsing still produces a
|
||||
// generic record with an opaque CipPath and no DH+ surface. For the octal-illegal
|
||||
// and slot/port out-of-range cases the same applies — the CIP path is preserved
|
||||
// verbatim for libplctag, but the structured DH+ accessors are null because the
|
||||
// path didn't pass the strict bridge-form validator.
|
||||
parsed.ShouldNotBeNull();
|
||||
parsed.IsDhPlusBridge.ShouldBeFalse();
|
||||
parsed.BackplaneSlot.ShouldBeNull();
|
||||
parsed.DhPlusPort.ShouldBeNull();
|
||||
parsed.DhPlusStation.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DhPlusBridge_diagnostics_surface_all_three_fields()
|
||||
{
|
||||
var parsed = AbLegacyHostAddress.TryParse("ab://10.0.0.1/1,7,2,42");
|
||||
parsed.ShouldNotBeNull();
|
||||
parsed.IsDhPlusBridge.ShouldBeTrue();
|
||||
parsed.BackplaneSlot.ShouldBe(7);
|
||||
parsed.DhPlusPort.ShouldBe(2);
|
||||
// 42 octal == 4*8 + 2 == 34 decimal
|
||||
parsed.DhPlusStation.ShouldBe(34);
|
||||
// CIP path is preserved verbatim — libplctag receives the original octal-station
|
||||
// representation, not the decimal-translated one.
|
||||
parsed.CipPath.ShouldBe("1,7,2,42");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NonBridgePath_leaves_dhplus_fields_null()
|
||||
{
|
||||
// Direct-wired SLC 5/05 — empty path
|
||||
var direct = AbLegacyHostAddress.TryParse("ab://10.0.0.1/");
|
||||
direct.ShouldNotBeNull();
|
||||
direct.IsDhPlusBridge.ShouldBeFalse();
|
||||
direct.BackplaneSlot.ShouldBeNull();
|
||||
direct.DhPlusPort.ShouldBeNull();
|
||||
direct.DhPlusStation.ShouldBeNull();
|
||||
|
||||
// Standard ControlLogix backplane bridge — only two segments, not four
|
||||
var twoHop = AbLegacyHostAddress.TryParse("ab://10.0.0.1/1,0");
|
||||
twoHop.ShouldNotBeNull();
|
||||
twoHop.IsDhPlusBridge.ShouldBeFalse();
|
||||
twoHop.BackplaneSlot.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(AbLegacyPlcFamily.Slc500)]
|
||||
[InlineData(AbLegacyPlcFamily.MicroLogix)]
|
||||
[InlineData(AbLegacyPlcFamily.LogixPccc)]
|
||||
public async Task DhPlusBridge_with_non_plc5_family_throws(AbLegacyPlcFamily nonPlc5)
|
||||
{
|
||||
var options = new AbLegacyDriverOptions
|
||||
{
|
||||
Devices = new[]
|
||||
{
|
||||
new AbLegacyDeviceOptions(
|
||||
HostAddress: "ab://10.0.0.1/1,3,2,07",
|
||||
PlcFamily: nonPlc5),
|
||||
},
|
||||
};
|
||||
var driver = new AbLegacyDriver(options, "drv-test", new FakeAbLegacyTagFactory());
|
||||
var ex = await Should.ThrowAsync<InvalidOperationException>(async () =>
|
||||
await driver.InitializeAsync("{}", CancellationToken.None));
|
||||
ex.Message.ShouldContain("DHRIO");
|
||||
ex.Message.ShouldContain("PLC-5-only");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DhPlusBridge_with_plc5_family_initialises_cleanly()
|
||||
{
|
||||
var options = new AbLegacyDriverOptions
|
||||
{
|
||||
Devices = new[]
|
||||
{
|
||||
new AbLegacyDeviceOptions(
|
||||
HostAddress: "ab://10.0.0.1/1,3,2,07",
|
||||
PlcFamily: AbLegacyPlcFamily.Plc5),
|
||||
},
|
||||
// Probe disabled so InitializeAsync doesn't try to spin a probe loop against a
|
||||
// non-existent gateway; the only thing under test is the family validation.
|
||||
Probe = new AbLegacyProbeOptions { Enabled = false },
|
||||
};
|
||||
var driver = new AbLegacyDriver(options, "drv-test", new FakeAbLegacyTagFactory());
|
||||
await driver.InitializeAsync("{}", CancellationToken.None);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user