Parsing SCL files (ICD, CID, SSD) without losing your mind
Every 61850 integration starts by reading a file the customer sent you: an ICD from the device vendor, a CID from the project, or an SSD from the design institute. The format looks like straightforward XML until you try to resolve a single data attribute, at which point you meet the reference graph — and the seven things that break naive parsers.
What is actually in the file
An SCL document has up to five top-level sections, and only two of them matter for 90% of work:
Header— identity, version, revision, and vendor tooling notes.Communication— subnetwork and address assignments (IP, MAC, VLAN).IED— the devices: access points, servers, logical devices, logical nodes, and the instantiated data objects with their values.DataTypeTemplates— the type definitions that everything inIEDrefers to. This is where the real structure lives.Substation— the primary equipment hierarchy (SSD content), linked to logical nodes by reference.
The reference graph, in one paragraph
An LN in IED has an lnClass and an lnType. The lnType points to an LNodeType in DataTypeTemplates. That LNodeType lists DO elements, each with a type pointing to a DOType. A DOType lists DA elements (with functional constraints) whose type may point to a DAType or a basic type, and may contain nested SDO elements that point to further DOTypes. Actual values live in DOI/SDI/DAI elements inside the LN. Everything is a reference by string; nothing is nested inline.
A minimal walker
This is deliberately small — build an index of types first, then walk instances. Doing it in the other order is what produces the "why is my attribute empty" class of bug.
- Use a namespace-agnostic iterator (
{*}in lxml, or a local-name comparison) — files in the wild mix namespaces, and some tools emit no namespace at all. - Attach the functional constraint (
fc) to every leaf. An attribute without its FC is not addressable over MMS in a predictable way.
# 1) index the templates once
ln_types = {e.get("id"): e for e in root.iter("{*}LNodeType")}
do_types = {e.get("id"): e for e in root.iter("{*}DOType")}
da_types = {e.get("id"): e for e in root.iter("{*}DAType")}
ns = "{http://www.iec.ch/61850/2003/SCL}"
# 2) walk instances and resolve
def walk_ln(ln, path=""):
lnt = ln_types[ln.get("lnType")]
for do in lnt.findall(f"{ns}DO"):
dot = do_types[do.get("type")]
for da in dot.findall(f"{ns}DA"):
# functional constraint decides whether this is a value you can read over MMS
fc = da.get("fc")
print(f"{path}.{do.get('name')}.{da.get('name')} bType={da.get('bType')} fc={fc}")
for sdo in dot.findall(f"{ns}SDO"):
walk_sdo(sdo, path=f"{path}.{do.get('name')}")
Seven gotchas that break naive parsers
- Vendor elements.
Privateelements carrying vendor data appear insideIED,DOTypeand elsewhere. Ignore them structurally, but log them — they often explain why a device behaves in a way the file does not describe. - Non-standard LN classes. Extended classes (starting with a letter, or vendor-defined) are legal. A parser that hard-codes the standard list silently drops data.
- Nested SDO depth. Sub-data-objects can nest more than one level. A walker that assumes a single level loses whole branches without erroring.
- Duplicate names across logical devices. Data object names are only unique within their logical node. Key your map by the full path, not the leaf name.
- Missing templates. Files edited by hand regularly reference a type that was deleted. Fail loudly with the missing id; do not skip.
- Size. Station-wide CIDs reach tens of megabytes. Parse as a stream or at least avoid building three copies of the tree, and expect the type index to dominate memory.
- Edition differences in structure. Elements such as equipment hierarchy and logical device naming appear in later editions. An unknown element is not automatically an error.
Validation before you trust a file
Cheap checks that catch most real problems before they reach a device:
- Every reference resolves (no dangling
typeorlnType). - Every leaf has an FC, and the FC is one you can serve.
- Dataset members point to existing attributes, with the FC appended (
LD/LN.DO.DA[FC]). - Report control blocks reference existing datasets, and the count is within the device's declared
ConfReportControllimit. - Control objects have a
ctlModelyour stack implements.
Questions that come up
Should we parse SCL ourselves or use a vendor tool?
Use a vendor tool for engineering and for generating artefacts, but keep a parser of your own for validation — it is the only way to answer "is this file self-consistent" before it reaches hardware.
How do we handle a customer who sends a modified CID?
Treat every incoming file as untrusted input: validate references, diff against the previous revision, and record the hash. Version drift between what the customer edited and what was commissioned is a recurring source of on-site surprises.
Does the same file work for both editions?
The schema family is shared, but the capability declarations and structural details differ. Parse defensively and key behaviour off the Services block rather than the edition number.
Keep reading
IEC 61850 Edition 1 vs Edition 2: read the ICD before you argue
Standards · 8 min
Replacing libiec61850 without rewriting your device
Migration · 8 min
Related products
EmbedCore-v5
Turn a conventional protection or bay controller into a compliant IEC 61850 IED in one to two weeks.
SSD Studio
Draw the primary single-line diagram; get a valid IEC 61850 SSD model out of the same file.
Ask about your specific case
Describe the device, the platform and the acceptance criteria. You get an answer from an engineer, not a form letter.