16 KiB
S7 — TIA Portal CSV & STEP 7 Classic AWL symbol import
PR-S7-D1 / #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). 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:
- The importer emits a placeholder row for each UDT-typed symbol — same as
today (data type forced to
Byte,Writable = false). - The operator hand-edits the placeholder row in the resulting JSON / options
object and:
- Sets
UdtNameto the UDT type name from the TIA "Data type" column - Removes the
Writable: falsemarker (UDT leaves inherit the parent's writability)
- Sets
- The operator declares the matching
S7UdtDefinitioninS7DriverOptions.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). - 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"
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 — 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:
- The row counts under
S7ImportResult.InstanceDbCount(a sub-counter ofParsedCount) so the operator can see how much of the import depends on the FB-interface layout. - 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:
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:
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:
- Recompile the FB in TIA Portal.
- Download the updated program to the PLC.
- Re-export "Show all tags" CSV from the updated project.
- Re-import the CSV via
AddTiaCsvImportor theimport-symbolsCLI. - 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"
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,5rather than%MW0.5for bit addresses - Boolean column values
WAHR/FALSCHrather thanTrue/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 bare0/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 sequentialM{B|W|D}{offset}address based on declaration order.DATA_BLOCK DBn … END_DATA_BLOCK— DB declarations. Each field maps to aDB{n}.DB{B|W|D}{offset}address based on declaration order; the DB number is parsed from theDATA_BLOCKline'sDBnkeyword.
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
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.
{
"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:
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/AddAwlImportconcatenate onto the existingTagslist 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
AddTiaCsvImporttwice 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 areplace=trueswitch. - 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 settingUdtNameand declaring the matchingS7UdtDefinition; see "Cooperation withUdtsdeclarations" 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
lands a
Descriptionfield onS7TagDefinitionthe importer will start populating it without further changes to the CSV contract.