@@ -57,12 +57,39 @@ into the S7 driver. Saves operators from hand-typing every `%MW0` /
|
||||
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.
|
||||
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.
|
||||
|
||||
## DE locale handling
|
||||
|
||||
TIA Portal honours the Windows display locale when writing CSV. A DE-locale
|
||||
@@ -206,9 +233,11 @@ For a hand-managed importer instance (e.g. supplying a custom `ILogger`) call
|
||||
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.
|
||||
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
|
||||
|
||||
@@ -90,8 +90,10 @@ not differentiated at test time.
|
||||
|
||||
### 5. Data types beyond the scalars
|
||||
|
||||
UDT fan-out, `STRING` with length-prefix quirks, `DTL` / `DATE_AND_TIME`,
|
||||
arrays of structs — not covered.
|
||||
`STRING` with length-prefix quirks, `DTL` / `DATE_AND_TIME`, arrays of
|
||||
structs — not covered. UDT fan-out IS covered (PR-S7-D2 / #300) via the
|
||||
`udt_layout` meta-seed in `Docker/profiles/s7_1500.json` and the
|
||||
`Driver_fans_out_udt_into_member_tags` integration test.
|
||||
|
||||
## When to trust the S7 tests, when to reach for a rig
|
||||
|
||||
@@ -101,7 +103,7 @@ arrays of structs — not covered.
|
||||
| "Does the driver lifecycle hang / crash?" | yes | yes |
|
||||
| "Does a real read against an S7-1500 return correct bytes?" | no | yes (required) |
|
||||
| "Does mailbox serialization actually prevent PG timeouts?" | no | yes (required) |
|
||||
| "Does a UDT fan-out produce usable member variables?" | no | yes (required) |
|
||||
| "Does a UDT fan-out produce usable member variables?" | yes (Snap7 + `udt_layout` meta-seed) | yes |
|
||||
|
||||
## Follow-up candidates
|
||||
|
||||
|
||||
@@ -966,6 +966,84 @@ Two surface options:
|
||||
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).
|
||||
|
||||
## UDT / STRUCT support
|
||||
|
||||
PR-S7-D2 / #300 — UDT-typed DBs are exposed via per-member fan-out at driver
|
||||
init time. The driver reads / writes / subscribes only ever target scalar
|
||||
leaves; the parent UDT pointer never reaches the wire. This keeps the rest of
|
||||
the driver pipeline (address parser, block-coalescing planner, scan-group
|
||||
partitioner, deadband filter) UDT-unaware.
|
||||
|
||||
### `S7UdtDefinition`
|
||||
|
||||
A UDT is declared once in `S7DriverOptions.Udts` and referenced by tags whose
|
||||
`UdtName` is set:
|
||||
|
||||
```csharp
|
||||
new S7UdtDefinition(
|
||||
Name: "Pump",
|
||||
Members: [
|
||||
new S7UdtMember("Pressure", Offset: 0, S7DataType.Float32),
|
||||
new S7UdtMember("Status", Offset: 4, S7DataType.Int16),
|
||||
new S7UdtMember("Enabled", Offset: 6, S7DataType.Bool),
|
||||
],
|
||||
SizeBytes: 7);
|
||||
```
|
||||
|
||||
Tags adopt the UDT layout via `UdtName`:
|
||||
|
||||
```csharp
|
||||
new S7TagDefinition("Pump1", "DB1.DBX0.0", S7DataType.Byte, UdtName: "Pump");
|
||||
```
|
||||
|
||||
### Fan-out semantics
|
||||
|
||||
At `InitializeAsync` time the driver:
|
||||
|
||||
1. Walks `_options.Tags`. For each tag with `UdtName`, looks up the UDT in
|
||||
`_options.Udts` (case-insensitive).
|
||||
2. For each UDT member, computes `parent.Address.ByteOffset + member.Offset`
|
||||
and emits one scalar `S7TagDefinition` per leaf with name
|
||||
`Parent.Member` (dot-separated).
|
||||
3. Array members emit `Member[0]`, `Member[1]`, ... at stride `elementBytes`.
|
||||
4. Nested UDT members recurse — array-of-UDT walks at stride `inner.SizeBytes`.
|
||||
5. The fanned-out leaves replace the parent UDT tag in the driver's tag map.
|
||||
|
||||
Reads / writes / subscribes that target the parent name surface
|
||||
`BadNodeIdUnknown` — clients must address the leaves directly.
|
||||
|
||||
### 4-level nesting cap
|
||||
|
||||
UDT-of-UDT is supported up to 4 levels deep. Anything deeper throws
|
||||
`InvalidOperationException("UDT nesting depth exceeds 4 levels…")` at Init.
|
||||
This catches accidentally-recursive declarations early; real industrial UDTs
|
||||
rarely go beyond 2 layers.
|
||||
|
||||
### Optimized block access — must be off
|
||||
|
||||
The static-offset model assumes member byte offsets in the declaration match
|
||||
the runtime layout exactly. TIA Portal's "Optimized block access" flag lets
|
||||
the runtime reorder members for memory alignment, breaking that assumption.
|
||||
Same prerequisite as general absolute-offset DB addressing on S7-1200 / 1500:
|
||||
**Optimized block access must be disabled** on any DB that the driver
|
||||
addresses by absolute offset, including UDT-typed DBs.
|
||||
|
||||
If a customer can't disable Optimized access (e.g., shared-DB constraints),
|
||||
the workaround is to expose the UDT through the symbolic-tag path once that
|
||||
ships — not in PR-S7-D2.
|
||||
|
||||
### Validation
|
||||
|
||||
The fan-out rejects, with clear errors:
|
||||
|
||||
- UDT name not found in `Udts` collection
|
||||
- Member offsets not in ascending order
|
||||
- Member offsets that overlap (a primitive's `[offset, offset+width)` range
|
||||
intersects the next member's offset)
|
||||
- Total members extending past `SizeBytes`
|
||||
- Tag with `UdtName` AND `ElementCount > 1` (array-of-UDT belongs in the UDT
|
||||
layout, not at the parent-tag level)
|
||||
|
||||
## 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
|
||||
|
||||
Reference in New Issue
Block a user