Auto: ablegacy-11 — RSLogix 500/PLC-5 CSV symbol import

Closes #254
This commit is contained in:
Joseph Doherty
2026-04-26 04:13:13 -04:00
parent 4fdeef7a6c
commit 4e8df38bb2
19 changed files with 1644 additions and 0 deletions

View File

@@ -186,6 +186,52 @@ parse time — both combinations are semantically meaningless against a contiguo
For `B`-files the Rockwell convention is "one BOOL per word, not per bit": `B3:0,10`
returns `bool[10]` (one per word's non-zero state), not `bool[160]`.
### `import-rslogix`
ablegacy-11 / [#254](https://github.com/dohertj2/lmxopcua/issues/254) — bulk-import RSLogix
500 / 5 CSV symbol exports into an `appsettings.json` tag fragment. Avoids hand-typing every
`N7:0` / `F8:12` / `B3:0/5` row of a several-hundred-tag PLC. Binary `.RSS` / `.RSP` project
files are out of scope; export to CSV first.
```powershell
# Default: emit JSON fragment to stdout
otopcua-ablegacy-cli import-rslogix `
--file C:\plc\plc-export.csv `
--device ab://192.168.1.20/1,0
# Write the fragment to a file + print a summary line to stdout
otopcua-ablegacy-cli import-rslogix `
--file C:\plc\plc-export.csv `
--device ab://192.168.1.20/1,0 `
--output tags.json
# Filter by Scope column — only import Local:1 program-scoped tags
otopcua-ablegacy-cli import-rslogix `
--file C:\plc\plc-export.csv `
--device ab://192.168.1.20/1,0 `
--scope Local:1
# Summary mode — one-line counter for CI / health checks
otopcua-ablegacy-cli import-rslogix `
--file C:\plc\plc-export.csv `
--device ab://192.168.1.20/1,0 `
--emit summary
```
| Flag | Default | Purpose |
|---|---|---|
| `-f` / `--file` | **required** | RSLogix CSV path |
| `-d` / `--device` | **required** | `ab://host[:port]/cip-path` every imported tag binds to |
| `--emit` | `appsettings-fragment` | `appsettings-fragment` (JSON) or `summary` (one-line counter) |
| `-o` / `--output` | stdout | Optional output file path |
| `--scope` | none | Scope filter — `Global` / `Local:N` (case-insensitive); empty Scope counts as Global |
| `--max-rows` | unlimited | Defensive cap on rows imported |
| `--strict` | off | Fail-fast on first malformed row (default permissive: skip + log) |
See [drivers/AbLegacy-RSLogix-Import.md](drivers/AbLegacy-RSLogix-Import.md) for the full
column reference, file-letter → `AbLegacyDataType` mapping, and the API surface
(`IRsLogixImporter`, `AbLegacyDriverOptions.AddRsLogixImport`).
## Known caveat — ab_server upstream gap
The integration-fixture `ab_server` Docker container accepts TCP but its PCCC

View File

@@ -67,6 +67,19 @@ their flag values to the already-shipped driver.
then the other. The plausible result identifies the correct setting
for that device family. (Modbus, S7.)
## Family-specific commands
Most drivers ship the four shared verbs and nothing else. AB Legacy adds a
fifth family-specific verb for bulk symbol-table import:
| Driver | Extra verb | Doc |
|---|---|---|
| AB Legacy | `import-rslogix` — read RSLogix 500/5 CSV symbol exports + emit a JSON tag fragment | [drivers/AbLegacy-RSLogix-Import.md](drivers/AbLegacy-RSLogix-Import.md) |
Binary RSLogix project files (`.RSS` / `.RSP`) are out of scope for v1 — the
format is proprietary and undocumented; no parser ships in libplctag or any
community library. Export to CSV first.
## Known gaps
- **AB Legacy cip-path quirk** — libplctag's ab_server requires a

View File

@@ -0,0 +1,163 @@
# AB Legacy — RSLogix symbol & data-table import
ablegacy-11 / [#254](https://github.com/dohertj2/lmxopcua/issues/254) — bulk-import
RSLogix 500 / 5 symbol exports into the AB Legacy driver. Saves operators from
hand-typing every `N7:0` / `F8:12` / `B3:0/5` row of a several-hundred-tag PLC
into `appsettings.json`.
## Supported formats — v1
| Format | Status | Notes |
|---|---|---|
| `.CSV` "Database Export" | **supported** | Header columns `Symbol,Address,Description,DataType,Scope`; quoted fields, doubled-quote escapes, comment lines (`;` / `#`) all honoured |
| `.SLC` text export | **supported** | RSLogix 500's "Save As Text" emits the same column shape — point the importer at the file directly |
| `.RSS` (RSLogix 500 binary project) | **out of scope** | Proprietary; no parser ships in libplctag or any community project. Export to CSV first |
| `.RSP` (RSLogix 5 binary project) | **out of scope** | Same as `.RSS` |
The binary `.RSS` / `.RSP` non-goal isn't a "we don't have time" decision —
Rockwell's binary format is undocumented + tied to RSLogix's internal page
layout, and the only known parsers are commercial IDE plugins. v1 ships with
text/CSV only and a clean abstraction (`IRsLogixImporter`) so a binary parser
can slot in later without reshaping the call sites.
## CSV column reference
| Column | Required | Notes |
|---|---|---|
| `Symbol` | yes | OPC UA tag name. RSLogix symbols are already stable; the importer uses them verbatim |
| `Address` | yes | PCCC address. File letter implies `DataType` (see below); the importer's resolution wins over the CSV's `DataType` column |
| `Description` | no | Parsed but currently unused — `AbLegacyTagDefinition` has no `Description` field at the v2 schema layer (see [#248](https://github.com/dohertj2/lmxopcua/issues/248)). Held in the column contract for future schema bumps |
| `DataType` | no | RSLogix-supplied (`INT` / `REAL` / `BOOL` / `TIMER` / …). Ignored at import time; the importer derives the type from the file letter |
| `Scope` | no | `Global` (default when blank) or `Local:N` for ladder-file-N-scoped tags. Acts as a filter when `--scope` is set on the CLI |
### File-letter → `AbLegacyDataType` mapping
| Letter | Example | Maps to | Notes |
|---|---|---|---|
| `N` | `N7:0` | `Int` (signed 16-bit) | |
| `F` | `F8:0` | `Float` (32-bit IEEE-754) | |
| `B` | `B3:0/0` | `Bit` | Bit-within-word also forces Bit when `BitIndex` is set |
| `L` | `L9:0` | `Long` (signed 32-bit) | SLC 5/05+ only |
| `ST` | `ST10:0` | `String` | 82-byte fixed-length + length word |
| `T` | `T4:0.ACC` | `TimerElement` | Sub-element implied by `.ACC` / `.PRE` / `.EN` / `.DN` |
| `C` | `C5:0.ACC` | `CounterElement` | |
| `R` | `R6:0.LEN` | `ControlElement` | |
| `A` | `A14:0` | `AnalogInt` | Older hardware |
| `I` / `O` / `S` | `I:0/0` | `Int` (or `Bit` with bit suffix) | I/O + status files |
| `PD` / `MG` / `PLS` / `BT` | `PD9:0` | `PidElement` etc. | Family-gated; PD/MG common on SLC500 + PLC-5; PLS/BT PLC-5 only |
| `RTC` / `HSC` / `DLS` / … | `RTC:0.YR` | `MicroLogixFunctionFile` | MicroLogix 1100 / 1400 only |
A bit suffix (`/N`) on any file letter forces `Bit`, regardless of the file
letter's normal classification — `N7:0/3` parses as Bit, not Int.
## Scope filter
The `Scope` column distinguishes program-scoped tags (`Local:1`, `Local:2`, …)
from globals. RSLogix exports usually mix both. The CLI's `--scope` flag (and
`ImportOptions.ScopeFilter` at the API level) keeps only the rows whose
`Scope` value matches case-insensitively; rows with no `Scope` column count as
`Global`.
```powershell
# Import only the Global symbols
otopcua-ablegacy-cli import-rslogix `
--file plc-export.csv `
--device ab://192.168.1.20/1,0 `
--scope Global
# Import only the file-2 program-scope tags
otopcua-ablegacy-cli import-rslogix `
--file plc-export.csv `
--device ab://192.168.1.20/1,0 `
--scope Local:2
```
## CLI subcommand — `import-rslogix`
```powershell
otopcua-ablegacy-cli import-rslogix --help
```
| Flag | Default | Purpose |
|---|---|---|
| `-f` / `--file` | **required** | Path to the CSV export |
| `-d` / `--device` | **required** | Canonical AB Legacy gateway URI every imported tag binds to |
| `--emit` | `appsettings-fragment` | `appsettings-fragment` (JSON) or `summary` (one-line counter) |
| `-o` / `--output` | stdout | Optional path; when set the JSON fragment is written there + summary line goes to stdout |
| `--scope` | none | Optional Scope filter (case-insensitive) |
| `--max-rows` | unlimited | Defensive cap on rows imported |
| `--strict` | off | Fail-fast on the first malformed row (default permissive: skip + log) |
### `appsettings-fragment` output shape
The default `--emit appsettings-fragment` mode writes a JSON object whose
`Tags` array is shaped like the `AbLegacyDriverConfigDto.Tags` array — paste
straight into the driver-instance config under
`Drivers/<instance>/Config/Tags`.
```json
{
"Tags": [
{
"Name": "MotorSpeed",
"DeviceHostAddress": "ab://192.168.1.20/1,0",
"Address": "N7:0",
"DataType": "Int",
"Writable": true
},
]
}
```
### Summary line
`--emit summary` writes a single line:
```
Imported 142 tag(s), skipped 3, errors 0.
```
`Skipped` covers Scope-filter rejections + missing-required-field rows; `errors`
covers rows whose `Address` failed to parse as a PCCC address.
## API surface — `IRsLogixImporter` + `AddRsLogixImport`
For server-side / bootstrap use-cases the importer is also reachable via:
```csharp
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Import;
var options = new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://192.168.1.20/1,0")],
};
// Append imported tags onto an existing options object.
var updated = options.AddRsLogixImport(
path: @"C:\plc\plc-export.csv",
deviceHostAddress: "ab://192.168.1.20/1,0",
out var result);
// result.ParsedCount / SkippedCount / ErrorCount surface the import telemetry.
Console.WriteLine($"Imported {result.ParsedCount} tags");
```
For a hand-managed importer instance (e.g. supplying a custom `ILogger`) call
`new RsLogixSymbolImport(logger).Parse(stream, deviceHostAddress, opts)`
directly.
## Operational notes
- The importer is **additive**`AddRsLogixImport` concatenates onto the
existing `Tags` list rather than replacing it. Hand-rolled tags (system-status
variables, computed fields the operator added by hand) survive a re-import.
- Re-imports are not idempotent today — calling `AddRsLogixImport` twice will
produce duplicate tag rows. Operators are expected to either start from a
clean options object or de-duplicate themselves; a future schema rev may add
a `replace=true` switch.
- Description metadata is dropped on the floor — see the column reference
above. When [#248](https://github.com/dohertj2/lmxopcua/issues/248) lands a
`Description` field on `AbLegacyTagDefinition` the importer will start
populating it without further changes to the CSV contract.

View File

@@ -59,6 +59,28 @@ supplies a `FakeAbLegacyTag`.
`_Diagnostics/<host>/<name>` short-circuit returns the live snapshot through
`ReadAsync` without bumping `RequestCount`; two devices keep counters
independent.
- `RsLogixSymbolImportTests` — ablegacy-11 / #254 RSLogix CSV symbol-import parser:
canonical 8-row CSV (one row per N/F/B/L/ST/T/C/R) → 8 typed
`AbLegacyTagDefinition`s with the right `DataType`; header + comment-line
(`;` / `#`) skipping; malformed-row → log warning + skip (`IgnoreInvalid=true`
default) vs. `InvalidDataException` (`IgnoreInvalid=false`); empty stream →
empty result; UTF-8 BOM survival; embedded comma in quoted Description;
doubled-quote escape; `--scope` filter (Global vs. Local:N); `MaxRowsToImport`
cap; missing required header column → `InvalidDataException` regardless of
`IgnoreInvalid`; `TryResolveDataType` rejects garbage + bit-suffix overrides
the file letter (`N7:0/3` → Bit).
- `RsLogixSymbolImportGoldenTests` — golden-snapshot integration: loads
`Fixtures/rslogix-canonical.csv` (8-row canonical export covering every v1
file letter), serialises the resulting tag list, and compares to
`Fixtures/rslogix-canonical-expected.json`. On mismatch the actual JSON is
dumped to `%TEMP%/rslogix-canonical-actual.json` and the path printed in the
failure message so the dev can `cp` the golden after reviewing the diff.
- `AbLegacyDriverFactoryAddRsLogixImportTests` — covers the
`AbLegacyDriverFactoryExtensions.AddRsLogixImport` extension method:
appends imported tags onto an existing options object without dropping the
hand-rolled tags or the device list; mutates by-copy (immutability
guarantee); `AddRsLogixImportWithResult` tuple overload returns both the
modified options and the import counters.
- `AbLegacyDeadbandTests` — PR 8 per-tag deadband / change filter:
absolute-only suppression sequence `[10.0, 10.5, 11.5, 11.6] -> [10.0, 11.5]`,
percent-only suppression with a zero-prev short-circuit, both-set logical-OR
@@ -173,5 +195,10 @@ falsely marked Stopped just because the driver-wide probe timeout is tight.
— known-limitations write-up + resolution paths
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/FakeAbLegacyTag.cs`
in-process fake + factory
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/Fixtures/rslogix-canonical.csv`
— ablegacy-11 / #254 8-row canonical RSLogix CSV symbol export, one row per
v1 file letter (N/F/B/L/ST/T/C/R)
- `tests/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/Fixtures/rslogix-canonical-expected.json`
— golden snapshot the import tests compare against
- `src/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs` — scope remarks
at the top of the file