339 lines
16 KiB
Markdown
339 lines
16 KiB
Markdown
# 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`.
|
|
|
|
`S7ImportResult.UdtPlaceholderCount` tracks how many of the imported tags
|
|
landed in this bucket.
|
|
|
|
#### Cooperation with `Udts` declarations (PR-S7-D2 / #300)
|
|
|
|
PR-S7-D2 ships UDT fan-out via `S7DriverOptions.Udts` + `S7TagDefinition.UdtName`.
|
|
The importer and the `Udts` declaration cooperate as follows:
|
|
|
|
1. The importer emits a placeholder row for each UDT-typed symbol — same as
|
|
today (data type forced to `Byte`, `Writable = false`).
|
|
2. The operator hand-edits the placeholder row in the resulting JSON / options
|
|
object and:
|
|
- Sets `UdtName` to the UDT type name from the TIA "Data type" column
|
|
- Removes the `Writable: false` marker (UDT leaves inherit the parent's
|
|
writability)
|
|
3. The operator declares the matching `S7UdtDefinition` in
|
|
`S7DriverOptions.Udts` (member offsets come from the TIA UDT definition
|
|
in the project file — TIA's "Show all tags" CSV does not export struct
|
|
field offsets, hence the manual layout step).
|
|
4. At driver init, the fan-out replaces the placeholder with one scalar leaf
|
|
per UDT member.
|
|
|
|
The importer does NOT auto-populate `Udts` — UDT layouts live in the project
|
|
file, not the symbol-table CSV. A future enhancement may parse the SCL UDT
|
|
declaration alongside the CSV; for now the cooperation is "importer flags it,
|
|
operator declares the layout, driver fans out at init".
|
|
|
|
See [`docs/v2/s7.md` "UDT / STRUCT support"](../v2/s7.md#udt--struct-support)
|
|
for the full fan-out semantics, the 4-level nesting cap, and the
|
|
Optimized-block-access prerequisite.
|
|
|
|
## Instance DBs / FB parameters
|
|
|
|
PR-S7-D3 / [#301](https://github.com/dohertj2/lmxopcua/issues/301) — multi-instance
|
|
Function-Block (FB) instances are addressed symbolically inside the PLC program
|
|
(`MyFB_Instance.MyParam`) but the runtime wire access still needs the absolute
|
|
`DBn.DBW_offset`. TIA Portal's "Show all tags" CSV export distinguishes these
|
|
rows from regular global DBs via the **`DB type`** column.
|
|
|
|
### `DB type` column convention
|
|
|
|
| `DB type` value | Meaning | Path |
|
|
|---|---|---|
|
|
| (empty) | Legacy export — no column at all (TIA pre-v15 / partial export). Treated as Global. | D1 (existing) |
|
|
| `Global DB` / `Global` / `Global Data Block` | Standalone DB declared in the project tree. | D1 (existing) |
|
|
| `Globaler Datenbaustein` | Same as above, DE locale. | D1 (existing) |
|
|
| `Instance DB` / `Instance` / `Instance Data Block` | Multi-instance FB instance. Member tags are the FB's `IN` / `OUT` / `IN_OUT` / `STAT` parameters. | **D3 (new)** |
|
|
| `Instance-DB` / `Instanz-DB` / `Instanz-Datenbaustein` | Same as above (locale + dashing variants). | **D3 (new)** |
|
|
|
|
The `DB type` column is matched case-insensitively; quoting and surrounding
|
|
whitespace are tolerated.
|
|
|
|
### `MyFB_Instance.MyParam` → `DBn.DBW_offset`
|
|
|
|
The TIA Portal export ships the **resolved absolute address** in the
|
|
`Logical address` column for every instance-DB member — TIA itself walks the FB
|
|
interface declaration at export time and writes out the byte-offset-anchored
|
|
address verbatim. The importer accepts these rows the same way as a Global-DB
|
|
row, with two differences:
|
|
|
|
1. The row counts under `S7ImportResult.InstanceDbCount` (a sub-counter of
|
|
`ParsedCount`) so the operator can see how much of the import depends on the
|
|
FB-interface layout.
|
|
2. The row is rejected from the UDT placeholder path even if the data type
|
|
column happens to match a UDT name pattern — instance-DB members always
|
|
import as fully-functional scalar tags.
|
|
|
|
Example fixture row:
|
|
|
|
```csv
|
|
Name,Path,Data type,Logical address,Comment,Hmi accessible,DB type
|
|
MotorFB_1.Speed,FB instances,Int,%DB7.DBW0,Speed setpoint,True,Instance DB
|
|
```
|
|
|
|
The imported `S7TagDefinition` ends up with:
|
|
|
|
```csharp
|
|
new S7TagDefinition(
|
|
Name: "MotorFB_1.Speed",
|
|
Address: "DB7.DBW0",
|
|
DataType: S7DataType.Int16,
|
|
Writable: true);
|
|
```
|
|
|
|
### Empty-`Logical address` fallback
|
|
|
|
When TIA exports an instance-DB row with an empty `Logical address` column
|
|
(rare in practice — happens when the export was generated against a
|
|
not-yet-compiled project), `InstanceDbResolver` can compute the absolute
|
|
address from explicit parent-DB / parent-base-offset / member-offset inputs.
|
|
This fallback is exposed at the resolver-class level for advanced bootstrap
|
|
scenarios; the CSV path itself does not currently parse interface declarations
|
|
out of the file (TIA's CSV doesn't carry them).
|
|
|
|
For now the operator workflow is: re-export from TIA after compiling the
|
|
project so every instance-DB row carries a resolved `Logical address`.
|
|
|
|
### Re-import on FB-interface edit — caveat
|
|
|
|
When the FB interface changes — a member is added, removed, or reordered in
|
|
TIA — the instance-DB layout shifts on the PLC side. Member byte offsets that
|
|
worked yesterday point at the wrong word today; absolute-offset addressing has
|
|
no in-band schema check.
|
|
|
|
**The driver does not auto-detect this.** Operators must:
|
|
|
|
1. Recompile the FB in TIA Portal.
|
|
2. Download the updated program to the PLC.
|
|
3. **Re-export "Show all tags" CSV** from the updated project.
|
|
4. Re-import the CSV via `AddTiaCsvImport` or the `import-symbols` CLI.
|
|
5. Restart the driver instance (Admin UI → Drivers → Reload).
|
|
|
|
A stale import will silently read / write the wrong byte offsets — the values
|
|
will look like valid PLC data but reference whichever member used to live at
|
|
that offset before the interface edit. There is no runtime guard; this is the
|
|
same caveat that applies to all absolute-offset DB addressing on S7-1200 /
|
|
1500 (see [`docs/v2/s7.md` "UDT / STRUCT support"](../v2/s7.md#udt--struct-support)
|
|
for the parallel UDT-edit story).
|
|
|
|
A future enhancement may add a project-fingerprint compare at driver init —
|
|
hashing the interface offsets at import time and re-checking against a known
|
|
PLC system function. Tracked as a follow-up; not in PR-S7-D3.
|
|
|
|
## 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, instance-db 9.
|
|
```
|
|
|
|
`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;
|
|
`instance-db` (PR-S7-D3) covers rows whose `DB type` column tagged them as
|
|
multi-instance FB-instance members.
|
|
|
|
## 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
|
|
added the runtime UDT fan-out (`S7DriverOptions.Udts` + `S7TagDefinition.UdtName`)
|
|
— operators upgrade a placeholder row by setting `UdtName` and declaring the
|
|
matching `S7UdtDefinition`; see "Cooperation with `Udts` declarations" above.
|
|
Placeholder-only rows still work as a Byte view of the first byte but
|
|
can't browse / read their members until the layout is declared.
|
|
- 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.
|