Auto: s7-d1 — TIA Portal CSV + STEP 7 Classic AWL symbol import

Closes #299
This commit is contained in:
Joseph Doherty
2026-04-26 06:32:18 -04:00
parent ac3fd45cc6
commit a908dff7b5
20 changed files with 2526 additions and 0 deletions

View File

@@ -136,3 +136,42 @@ otopcua-s7-cli subscribe -h 192.168.1.30 -a DB1.DBW0 -t Int16 -i 500
S7comm has no native push — the CLI polls through `PollGroupEngine` just like
Modbus / AB.
### `import-symbols`
PR-S7-D1 / [#299](https://github.com/dohertj2/lmxopcua/issues/299) — read a TIA
Portal CSV ("Show all tags" export) or STEP 7 Classic `.AWL` file and emit a
JSON tag fragment for `appsettings.json`, or a one-line summary. Mirrors the
AB Legacy `import-rslogix` CLI in shape.
```powershell
# TIA Portal CSV — emit JSON fragment to stdout
otopcua-s7-cli import-symbols --file plc-export.csv --format tia
# STEP 7 Classic AWL — emit summary line
otopcua-s7-cli import-symbols --file classic.awl --format awl --emit summary
# DE-locale CSV — auto-detected; output to file
otopcua-s7-cli import-symbols `
--file plc-de.csv `
--format tia `
--emit appsettings-fragment `
--output tags.json
# Strict mode — fail-fast on the first malformed row (CI lint)
otopcua-s7-cli import-symbols --file plc.csv --format tia --strict
```
| Flag | Default | Purpose |
|---|---|---|
| `-f` / `--file` | **required** | Path to the TIA CSV or `.AWL` file |
| `--format` | `tia` | `tia` (CSV) or `awl` (STEP 7 Classic) |
| `-d` / `--device` | none | Optional documentation tag (held for symmetry with `import-rslogix`) |
| `--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 |
| `--max-rows` | unlimited | Defensive cap on rows imported |
| `--strict` | off | Fail-fast on the first malformed row (default permissive: skip + log) |
UDT-typed rows import as placeholder tags (data type forced to `Byte`); see
[S7-TIA-Import.md](drivers/S7-TIA-Import.md) for the full format reference,
locale auto-detection, and AWL position-based addressing rules.

View File

@@ -0,0 +1,215 @@
# S7 — TIA Portal CSV & STEP 7 Classic AWL symbol import
PR-S7-D1 / [#299](https://github.com/dohertj2/lmxopcua/issues/299) — bulk-import
TIA Portal "Show all tags" CSV exports and STEP 7 Classic AWL declaration files
into the S7 driver. Saves operators from hand-typing every `%MW0` /
`%DB1.DBW0` row of a several-hundred-tag PLC into `appsettings.json`.
## Supported formats — v1
| Format | Status | Notes |
|---|---|---|
| TIA Portal `.CSV` ("Show all tags" export) | **supported** | Header columns `Name,Path,Data type,Logical address,Comment,Hmi accessible,…`; en-US (`,`) and DE-locale (`;` separator + `,` decimal) auto-detected |
| STEP 7 Classic `.AWL` (`VAR_GLOBAL` + `DATA_BLOCK`) | **supported, best-effort** | Position-based offset assignment (no exact byte offsets in hand-exported AWL — see below) |
| STEP 7 / TIA Portal native binary (`.s7p`, `.zap`) | **out of scope** | Proprietary; no community parser. Use TIA's "Show all tags" CSV export |
| TIA Portal Openness API | **out of scope** | Requires a licensed TIA install + OpenAPI license; future PR |
## TIA Portal CSV column reference
| Column | Required | Notes |
|---|---|---|
| `Name` | yes | OPC UA tag name. TIA symbols are stable across deployments; the importer uses them verbatim |
| `Logical address` (or `Address`) | yes | TIA-style address with leading `%` (e.g. `%MW0`, `%DB1.DBW10`, `%DB1.DBX2.3`). Stripped on import |
| `Data type` | recommended | TIA primitive type (`Int`, `Real`, `Bool`, `String`, …) — drives the imported `S7DataType` |
| `Comment` | no | Parsed but currently unused — `S7TagDefinition` 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 |
| `Hmi accessible` | no | Filter — rows with `False` / `FALSCH` / `nein` are skipped (internal symbols TIA shows in the editor but doesn't expose to client interfaces). Missing column defaults to `True` |
| `Hmi visible` / `Hmi writeable` | no | Currently unused — held for future Admin-UI-side metadata |
| `Length` | no | For `String` rows: max length. Default 254. Drives `StringLength` on the imported tag |
| `Path` | no | TIA tag-table path (`Default tag table`, custom names). Currently unused; held in the contract |
### TIA `Data type` → `S7DataType` mapping
| TIA type | Maps to | Notes |
|---|---|---|
| `Bool` | `Bool` | Bit access; address must include a `.bit` suffix |
| `Byte`, `SInt`, `USInt` | `Byte` | 1-byte unsigned/signed |
| `Int` | `Int16` | Signed 16-bit |
| `Word`, `UInt` | `UInt16` | Unsigned 16-bit |
| `DInt` | `Int32` | Signed 32-bit |
| `DWord`, `UDInt` | `UInt32` | Unsigned 32-bit |
| `LInt` | `Int64` | 64-bit signed (S7-1500 only) |
| `LWord`, `ULInt` | `UInt64` | 64-bit unsigned (S7-1500 only) |
| `Real` | `Float32` | IEEE-754 32-bit |
| `LReal` | `Float64` | IEEE-754 64-bit (S7-1500 only) |
| `String` | `String` | S7 STRING with 2-byte header; `Length` column drives `StringLength` |
| `WString` | `WString` | S7 WSTRING (UTF-16BE) |
| `Char` / `WChar` | `Char` / `WChar` | Single-character |
| `Date` | `Date` | UInt16 days since 1990-01-01 |
| `Time` | `Time` | Int32 ms |
| `TOD` / `Time_Of_Day` | `TimeOfDay` | UInt32 ms since midnight |
| `DT` / `Date_And_Time` | `DateAndTime` | 8-byte BCD |
| `DTL` | `Dtl` | 12-byte structured (S7-1200 / S7-1500) |
| `S5Time` | `S5Time` | 16-bit BCD duration |
| `Struct` / quoted UDT name | UDT placeholder | See below |
### UDT placeholders
UDT-typed symbols (TIA `Data type` = `"MyUdt"` quoted, or the literal `Struct`)
import as a **placeholder** — the resulting tag lands in the driver options so
it shows up in the Admin UI tag list, but its data type is forced to `Byte`
and the row is marked `Writable = false`. PR-S7-D2 will replace the placeholder
with proper UDT layout once the symbol table covers nested struct fields.
`S7ImportResult.UdtPlaceholderCount` tracks how many of the imported tags
landed in this bucket.
## DE locale handling
TIA Portal honours the Windows display locale when writing CSV. A DE-locale
install emits:
- Field separator `;` (because `,` is the decimal separator)
- Decimal-comma in addresses: `%MW0,5` rather than `%MW0.5` for bit addresses
- Boolean column values `WAHR` / `FALSCH` rather than `True` / `False`
The importer **auto-detects** the locale from the first non-blank line:
- Field-separator detection: counts `;` vs `,` occurrences in the header
- Decimal-comma detection: scans the first data row's address column for a
digit-comma-digit pattern
- Boolean column values: recognises both languages (`true/false/wahr/falsch/yes/no/ja/nein`,
case-insensitive) plus bare `0`/`1`
The address column is rewritten to en-US shape (`%MW0,5``MW0.5`) before the
strict `S7AddressParser` runs, so the rest of the driver pipeline sees a
single canonical address shape.
## STEP 7 Classic AWL — `VAR_GLOBAL` + `DATA_BLOCK`
Best-effort parser for legacy STEP 7 Classic projects:
- `VAR_GLOBAL … END_VAR` — global memory area declarations. Each entry maps to
a sequential `M{B|W|D}{offset}` address based on declaration order.
- `DATA_BLOCK DBn … END_DATA_BLOCK` — DB declarations. Each field maps to a
`DB{n}.DB{B|W|D}{offset}` address based on declaration order; the DB number
is parsed from the `DATA_BLOCK` line's `DBn` keyword.
### Position-based addressing — heuristic
Real STEP 7 Classic projects carry exact byte offsets in the symbol table /
.gr8 deployment artefact, but a hand-exported AWL file omits them. The
importer assumes:
| Type | Bytes |
|---|---|
| `BOOL` | 1 (rounded up to byte alignment) |
| `BYTE` / `SINT` / `USINT` / `CHAR` | 1 |
| `INT` / `WORD` / `UINT` | 2 |
| `DINT` / `DWORD` / `UDINT` / `REAL` | 4 |
| `LREAL` / `LINT` / `ULINT` / `LWORD` | 8 |
| `STRING[N]` | N + 2 (2-byte header) |
| `STRING` (no length) | 256 |
| `STRUCT` / `Array[…] of …` / quoted UDT name | UDT placeholder (8-bit Byte at next aligned offset) |
S7 alignment rule: offsets round up to a 2-byte boundary for any 16-bit-or-larger
type. Sites needing exact offsets should drive their symbol import from the
TIA Portal CSV path instead — the CSV carries the offsets verbatim.
Comments (`(* ... *)` block, `// ...` line) are stripped before declaration
parsing. Initial-value clauses (`:= 0`) are recognised and discarded.
## CLI subcommand — `import-symbols`
```powershell
otopcua-s7-cli import-symbols --help
```
| Flag | Default | Purpose |
|---|---|---|
| `-f` / `--file` | **required** | Path to the TIA CSV or `.AWL` file |
| `--format` | `tia` | `tia` (CSV) or `awl` (STEP 7 Classic) |
| `-d` / `--device` | none | Optional documentation tag (reserved for symmetry with `import-rslogix`) |
| `--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 |
| `--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 `S7DriverConfigDto.Tags` array — paste
straight into the driver-instance config under
`Drivers/<instance>/Config/Tags`.
```json
{
"Tags": [
{
"Name": "MotorSpeed",
"Address": "MW0",
"DataType": "Int16",
"Writable": true,
"StringLength": 254
},
]
}
```
### Summary line
`--emit summary` writes a single line:
```
Imported 142 tag(s), skipped 3, errors 0, udt-placeholders 5.
```
`Skipped` covers HMI-accessible-false rows + missing-required-field rows;
`errors` covers rows whose `Address` failed to parse as an S7 address;
`udt-placeholders` covers UDT-typed rows that imported as placeholders.
## API surface — `IS7SymbolImporter` + `AddTiaCsvImport` / `AddAwlImport`
For server-side / bootstrap use-cases the importer is reachable via:
```csharp
using ZB.MOM.WW.OtOpcUa.Driver.S7;
using ZB.MOM.WW.OtOpcUa.Driver.S7.SymbolImport;
var options = new S7DriverOptions { Host = "192.168.1.30", CpuType = CpuType.S71500 };
// Append imported tags onto an existing options object.
var updated = options.AddTiaCsvImport(
path: @"C:\plc\tia-export.csv",
out var result);
Console.WriteLine($"Imported {result.ParsedCount} tags ({result.UdtPlaceholderCount} placeholders)");
// AWL variant — same shape.
var withAwl = updated.AddAwlImport(
path: @"C:\plc\classic.awl",
out var awlResult);
```
For a hand-managed importer instance (e.g. supplying a custom `ILogger`) call
`new TiaCsvImporter(logger).Parse(stream, opts)` or
`new AwlImporter(logger).Parse(stream, opts)` directly.
## Operational notes
- The importers are **additive**`AddTiaCsvImport` / `AddAwlImport` concatenate
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 — calling `AddTiaCsvImport` twice will produce
duplicate tag rows. Operators are expected to start from a clean options
object or de-duplicate themselves; a future schema rev may add a
`replace=true` switch.
- UDT placeholders surface in the Admin UI as non-writable Byte tags. PR-S7-D2
will replace the placeholder rows with proper UDT layout (one tag per
primitive field); operators should not bind dependent client tags to
placeholder rows because the addresses will be rewritten when D2 lands.
- Description metadata is dropped on the floor today — see the column
reference above. When [#248](https://github.com/dohertj2/lmxopcua/issues/248)
lands a `Description` field on `S7TagDefinition` the importer will start
populating it without further changes to the CSV contract.

View File

@@ -940,6 +940,32 @@ project that rejects PG and accepts OP). That test is documented in
`tests/ZB.MOM.WW.OtOpcUa.Driver.S7.IntegrationTests` but only runs against
real firmware — the pymodbus-style "TSAP simulator" doesn't exist for S7.
## Symbol import
PR-S7-D1 / [#299](https://github.com/dohertj2/lmxopcua/issues/299) — bulk-import
TIA Portal "Show all tags" CSV exports and STEP 7 Classic AWL declaration files
into the S7 driver's tag list. Operators no longer hand-edit the
`Drivers/<instance>/Config/Tags` JSON for hundred-tag projects.
Two formats supported v1:
- **TIA Portal CSV** — `Name,Path,Data type,Logical address,Comment,Hmi accessible,…`.
en-US (`,`) and DE-locale (`;` separator + `,` decimal) auto-detected.
HMI-hidden symbols filter out automatically; UDT-typed rows import as
placeholders until PR-S7-D2 ships proper UDT layout.
- **STEP 7 Classic AWL** — `VAR_GLOBAL` + `DATA_BLOCK` declarations parsed
best-effort with position-based offset assignment.
Two surface options:
- **CLI**: `otopcua-s7-cli import-symbols --file foo.csv --format tia` emits
an `appsettings.json` JSON fragment for hand-merge.
- **API**: `S7DriverOptions.AddTiaCsvImport(path, out result)` /
`AddAwlImport(path, out result)` for server-side bootstrap paths.
Full reference: [`docs/drivers/S7-TIA-Import.md`](../drivers/S7-TIA-Import.md).
CLI flag table: [`docs/Driver.S7.Cli.md` "import-symbols"](../Driver.S7.Cli.md#import-symbols).
## References
1. Siemens Industry Online Support, *Modbus/TCP Communication between SIMATIC S7-1500 / S7-1200 and Modbus/TCP Controllers with Instructions `MB_CLIENT` and `MB_SERVER`*, Entry ID 102020340, V6 (Feb 2021). https://cache.industry.siemens.com/dl/files/340/102020340/att_118119/v6/net_modbus_tcp_s7-1500_s7-1200_en.pdf