Type Reference

This chapter describes every element type a definition can declare, with its options and an example. The examples come from the SensorEcu running example, a sensor acquisition function of a body ECU; the complete files are in the examples folder of this manual and are validated by the test suite.

Every element consists of exactly one type keyword (entity, struct, union, typeRef, ref, or a scalar such as u8) plus the common element keys: title, description, $comment, multiplicity, scope, typeName, inherits, embeddedTypeHint, allowPropertyNameDeviation, and _-prefixed annotations.

There is no null type: an absent value is an omitted optional element, never a null in the configuration (see the multiplicity section of the definition system).

Boolean

bool with an optional default:

sensorSupplyVoltage: {
    description: "Nominal supply voltage of the sensor rail.",
    f64: {
        min: 4.5,
        max: 5.5,
        unit: "V",
        default: 5.0
    }
},
selfTest: {
    description: "Run the sensor self test during ECU startup.",
    bool: {
        default: true
    }
},

Integers

Eight integer types select the storage width and signedness the embedded software expects: u8, i8, u16, i16, u32, i32, u64, i64.

Option Meaning

min, max

Value range, defaults to the full type range

default

Value applied when the configuration omits the parameter

unit

Physical unit of the stored value, see Units

display

UI hint, decimal (default) or hex

adcBaseAddress: {
    title: "ADC register base address",
    multiplicity: "1..1",
    u32: {
        min: 0x40000000,
        max: 0x4FFFFFFF,
        display: "hex"
    }
},
sampleTime: {
    description: "Time between two acquisition cycles.",
    u16: {
        min: 1,
        max: "10 s",
        unit: "ms",
        default: 100
    }
},

On the configuration side, integer values can be written as plain numbers (sampleTime: 50), as strings in decimal or hex notation, both with _ separators for readability, a decimal one also with an exponent when the value is whole ("1e3"), or with a unit:

adcBaseAddress: "0x4000_1000",

display: "hex" only affects how tooling presents the value; it does not change what the configuration author may write. It cannot be combined with a unit, as a hex value takes none.

Float

f64 is the floating point type, with the same min, max, default, unit, and display options as the integers (see the sensorSupplyVoltage example above). There is no f32 type; if the embedded software uses a narrower float, document it with embeddedTypeHint. A float value may also be written as a string ("1.5e3").

Units

unit names the unit the value is stored in. A built-in unit takes prefixes, so a configuration may write "2 MHz" for a parameter stored in kHz, and Confµgami normalizes it to 2000:

Unit Accepted prefixes

Hz

Hz, kHz, MHz, GHz

s

ps to s, plus min and h

Bytes, bit

SI (kB, MB, …​) or binary (KiB, MiB, …​), one family per parameter

  • unit: "kHz" stores kilohertz. For Bytes and bit the base prefix picks the family ("kB" is SI, "KiB" and a bare "Bytes" binary).

  • The object form { symbol, basePrefix, prefixFamily } gives a custom unit the SI prefixes p to P, which its basePrefix opts into: unit: { symbol: "V", basePrefix: "m" } stores millivolts, basePrefix: "" the bare volt. Without a basePrefix a custom symbol is a literal unit, like unit: "V". A symbol spelling a prefixed unit of a built-in, such as "ms", is an error, the short form unit: "ms" declares it. prefixFamily (SI or Binary) picks the family of Bytes and bit, and is required with a non-empty basePrefix on Bytes.

  • A short form that reads like a built-in unit but is none, a built-in symbol behind a prefix in another case ("KHz") or a prefix it does not take ("ks"), is a literal unit with a warning. The object form { symbol: "ks" } declares such a literal unit on purpose.

  • min, max and default accept the same strings as a configuration value, unit strings such as max: "10 s" above as well as hex such as max: "0xFFFF".

An integer must be convertible to a whole number in the stored unit, so "1.5 ms" is an error for a parameter stored in ms. Tooling shows each value with the best-fitting prefix.

String

Option Meaning

minLength, maxLength

Length bounds, in characters (Unicode code points)

pattern

Regular expression the value must match

literalsExhaustive

Closed set of allowed values, nothing else is accepted

literalsNonExhaustive

Suggested values offered in completion, other strings remain valid

default

Default value

literalsExhaustive and literalsNonExhaustive are mutually exclusive, and every literal must itself satisfy pattern if one is set.

shortName: {
    title: "ECU short name",
    description: "Short name of the ECU as visible in diagnostics.",
    multiplicity: "1..1",
    string: {
        minLength: 1,
        maxLength: 32,
        pattern: "^[A-Za-z][A-Za-z0-9_]*$"
    }
},
mountingPosition: {
    description: "Mounting position of the ECU in the vehicle.",
    string: {
        literalsExhaustive: [
            "front-left",
            "front-right",
            "rear-left",
            "rear-right"
        ],
        default: "front-left"
    }
},

A pattern is a JavaScript (ECMA-262) regular expression, because that is what JSON Schema’s pattern keyword is. It is validated in that dialect and written unchanged into the generated JSON Schema, so the validator and an editor checking against that schema agree.

  • \d, \w and \s are ASCII, not Unicode: \d is [0-9].

  • Syntax only the Rust regex crate accepts is rejected, above all inline flags such as (?i). ECMA-262 has no flags inside a pattern, so use explicit character classes.

  • Look-arounds and backreferences are legal ECMA-262 but rejected, because they cannot be enforced.

  • Generated Python validators use Python re semantics, a third dialect this does not cover.

Patterns are also unanchored: [0-9]+ matches abc123def. Always anchor with ^…​$ unless you really mean a substring match.

Choose between enum and string literals deliberately: enum literals must be identifiers and become real enum types in generated code; string literals may be arbitrary strings ("front-left") and stay strings in generated code.

Enum

An enum declares a closed set of identifier literals, here from the TurnIndicator example:

comfortMode: {
    description: "Behavior when the driver tips the indicator stalk.",
    enum: {
        literals: [
            "OFF",
            "THREE_FLASHES",
            "FIVE_FLASHES"
        ],
        default: "THREE_FLASHES"
    }
}

Literals are SCREAMING_SNAKE_CASE identifiers (they become enum constants in generated Rust and Python code) and must be unique, ignoring case. A literal off the convention is an error, allowLiteralNameDeviation: true on the enum allows it for literals spelled by an external format. default must be one of the literals.

Linker Symbol

linkerSymbol types a parameter as the name of a symbol resolved by the embedded linker, for example a callback function:

errorHook: {
    description: "C function called when a sensor failure is detected, e.g. to report a DTC.",
    embeddedTypeHint: "void (*errorHook)(uint8 sensorId)",
    linkerSymbol: {
        language: "C"
    }
},

language is required and currently supports "C"; values are checked against the C identifier rules. minLength, maxLength, and default are available like on strings. The embeddedTypeHint shown here is a documentation hint rendered in hover and inlay hints; it carries no semantics but tells the integrator the expected C signature. It applies to value types only and is rejected on an entity and a struct.

File Path

filePath types a parameter as the path to a file or folder. Validation resolves the path and checks it on disk:

linkerScript: {
    title: "Linker script",
    description: "The linker script, relative to the configuration file naming it.",
    multiplicity: "1..1",
    filePath: {
        pathKinds: ["File"],
        globPattern: "*.ld",
        baseResolutionFolder: "SourceFolder",
    },
},
outputDir: {
    title: "Output folder",
    description: "Where the build writes, created by the first build.",
    filePath: {
        pathKinds: ["Folder", "NonExisting"],
        baseResolutionFolder: "CfgSpace",
        support: ["AbsPath", "RelPath", "VariableResolution"],
    },
},
Option Meaning

pathKinds (required)

What the path may point to: File an existing file, Folder an existing folder, NonExisting nothing yet, such as a file a tool generates.

baseResolutionFolder

The folder a relative path starts from: Workspace the workspace root, CfgSpace the CfgSpace root, SourceFolder the folder of the configuration file writing the value. Required unless support lacks RelPath.

support

The forms a value may use, at least one of AbsPath and RelPath, default ["RelPath", "VariableResolution"]. AbsPath allows an absolute path such as /opt/tools or C:/tools. RelPath allows a path relative to the base folder. VariableResolution expands ${env:NAME} and ${env:NAME:default} before the check, as in a .confu. Without it, a ${ in a value warns and stays text.

globPattern

A pattern the value must match, see below.

resolveFilePathOnExport

What the merged model holds, default true, see below.

default

Default value, checked for its form and against globPattern, never on disk.

globPattern reads like a .gitignore line. *, ?, {a,b}, [0-9] and [!0-9] match within one path segment.

  • Without a /, the pattern matches the file name, and an existing folder is not checked: .ld, .{png,gif,jpg}.

  • With a /, it matches the path relative to the base folder, files and folders alike: src//*.{c,h}. stands for any number of folders and only as a whole segment. A path outside the base folder never matches.

  • Without RelPath in support, every value is absolute, so a pattern with a / matches the absolute path, a leading / standing for the root: /etc/logs/*.log.

The merged model holds the resolved path relative to the CfgSpace root, starting with .. when the target lies outside it. The merge, constraints and the export all see this form, so two files naming the same target through different relative paths agree. resolveFilePathOnExport: false keeps the value as written instead, validation still checks it on disk.

A default has no file to resolve against, so it always stays as written. A constraint therefore matches it with a resolved value only when it is written relative to the CfgSpace root.

Entity

An entity aggregates children and has identity (name/id) in the configuration. Entities are the only elements that can be referenced by ref and merged across files:

sensors: {
    title: "Sensors",
    description: "The sensors acquired by this ECU.",
    typeName: "Sensor",
    multiplicity: "1..*",
    inherits: "SensorEcu::SensorBase",
    entity: {
        input: {
            description: "The electrical input of the sensor.",
            multiplicity: "1..1",
            union: {
                typeRefs: [
                    "SensorEcu::AnalogInput",
                    "SensorEcu::DigitalInput"
                ]
            }
        },
        inputFilter: {
            description: "Filter applied before scaling.",
            typeRef: "SensorEcu::Filter"
        },
        outputFilter: {
            description: "Filter applied after scaling.",
            typeRef: "SensorEcu::Filter"
        },
        calibration: {
            description: "Two point calibration of the sensor.",
            struct: {
                offset: {
                    multiplicity: "1..1",
                    f64: {}
                },
                factor: {
                    multiplicity: "1..1",
                    f64: {}
                }
            }
        }
    }
},

This also demonstrates inherits: the sensor entity extends the reusable SensorEcu::SensorBase type, see Reusable types.

A configured list of sensors then looks like this:

{
    name: "OilTemp",
    input: {
        $type: "SensorEcu::AnalogInput",
        channel: 3,
        voltageRange: "RANGE_10V"
    },
    inputFilter: {
        cutoff: "10 Hz",
        order: 2
    },
    calibration: {
        offset: -40.0,
        factor: 0.125
    }
},

Struct

A struct groups parameters without identity. Use it when the group is pure data and never needs to be referenced or merged on its own:

calibration: {
    description: "Two point calibration of the sensor.",
    struct: {
        offset: {
            multiplicity: "1..1",
            f64: {}
        },
        factor: {
            multiplicity: "1..1",
            f64: {}
        }
    }
}

Structs cannot contain entities, cannot inherit, and have no name/id in the configuration.

Choosing Between Entity and Struct

Both aggregate children; the difference is identity. An entity is a named object boundary in the domain model, a struct is local composition under its parent.

Choose entity when:

  • Other elements need to point at the object (refTargetTypes only accepts entity types).

  • The object is a thing in the domain with its own identity, a sensor, a CAN channel, an NM node, rather than a bundle of settings.

  • Contributions from several configuration files should merge into one object (cross-file merging matches entities by identity).

Choose struct when:

  • The fields are just a grouped value under one parent, like a calibration or a filter setting.

  • Nothing ever needs to reference the group on its own.

  • You want to keep the model compact and local.

The choice is visible in the configuration and the generated schemas: an entity accepts the common keys name, id, $description, $comment, and (as a union member) the $type discriminator. name identifies the instance among its siblings and id globally; an instance takes one or the other, never both, and the id doubles as the name. A struct accepts none of these, it is anonymous by construction.

Reuse is not the differentiator: both entity and struct types can be declared under types and shared via typeRef and scope: "public", like the SensorEcu::Filter struct in the running example. When in doubt, start with a struct; turning it into an entity later is a smaller change than taking identity away from something other files already reference.

Union

A union lets the configuration author choose between several types:

input: {
    description: "The electrical input of the sensor.",
    multiplicity: "1..1",
    union: {
        typeRefs: [
            "SensorEcu::AnalogInput",
            "SensorEcu::DigitalInput"
        ]
    }
},

The members are named type paths (entities or structs, at least two). By default unions are discriminated: the configuration states the chosen type with $type:

input: {
    $type: "SensorEcu::AnalogInput",
    channel: 3,
    voltageRange: "RANGE_10V"
},

When to Use a Union

Use a union when one property must accept one of several structured types: mutually exclusive variants with different payload fields, like the analog and digital input above. If the variants carry no fields of their own and you only need a mode switch, use an enum instead; it is simpler to configure and generates a plain enum type.

How to Model It

  • Declare each member as a reusable type under types and list the members with fully qualified type paths in typeRefs.

  • Use entity or struct members. A discriminated union rejects scalar (parameter) and ref members, because they cannot carry a $type discriminator.

  • Do not make a list of a union that mixes entity and struct members. Entities merge across files by path and structs by index, so such a list has no merge rule and is an error.

  • Leave allowTypeAmbiguity at its default false unless you intentionally need overlapping shapes.

Type Ambiguity in Practice

A union is ambiguous when the same configuration object could match more than one member, for example two structs that differ only in optional fields.

With the default allowTypeAmbiguity: false, ambiguous input is rejected, and the generated schemas require the $type discriminator as a constant per member, so deserialization is always unambiguous.

With allowTypeAmbiguity: true, values match structurally: $type is still generated as a constant but the configuration author may omit it. Overlapping members are then accepted, which makes the behavior dependent on matching order and harder to maintain; different consumers can parse the same untagged value differently. Avoid it unless the overlap is the point. Number members stored in different units are a warning: the exported number no longer tells which unit it is in.

Reference

A ref points at an entity configured elsewhere:

monitoredSensorRef: {
    description: "The sensor supervised by the plausibility check.",
    ref: {
        refTargetTypes: [
            "SensorEcu::Ecu::Sensor"
        ]
    }
},

refTargetTypes lists the type paths the target entity may have. In the configuration, the reference value takes one of two forms:

Form Example

Absolute path of entity names

"/SensorEcu/FrontBodyEcu/OilTemp"

Id reference

"id:ignition"

monitoredSensorRef: "/SensorEcu/FrontBodyEcu/OilTemp",

The path segments are the configured entity identities (name, else id), starting at the definition name. They are not the definition’s relation names. A reference can also be written as an object to attach documentation; this one addresses its target with the id: form:

failsafeSensorRef: {
    refTarget: "id:ignition",
    $description: "Fall back to the ignition signal when the monitored sensor fails."
}

References are validated: the target must exist, be an entity, and its type must be one of the refTargetTypes.

Named Type Reference

typeRef is not a data type but a reuse mechanism: the element takes the shape of a named type declared elsewhere. See Reusable types and inheritance, which also covers the recursive case.

inputFilter: {
    description: "Filter applied before scaling.",
    typeRef: "SensorEcu::Filter"
},
outputFilter: {
    description: "Filter applied after scaling.",
    typeRef: "SensorEcu::Filter"
},