# 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//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.