# Create a builder2ibek converter for a support module When `builder2ibek xml2yaml` converts a builder XML file it applies a **converter** — a small Python module — for each XML component prefix it encounters. If no converter exists, the entity is passed through unchanged with whatever attributes the XML had. This how-to explains when and how to write a new converter. --- ## Why fix builder2ibek rather than editing ioc.yaml by hand It is tempting to run `xml2yaml`, fix up the resulting `ioc.yaml` manually, and move on. Resist this. Writing a converter instead has important advantages: - **Others benefit immediately.** Anyone converting the same module gets correct output without knowing about the quirk. - **Re-conversion is safe.** If the original builder XML is modified (a device address changes, a new instance is added) you can re-run `xml2yaml` and get a correct result. Manual edits to `ioc.yaml` would have to be re-applied by hand. - **The fix is tested.** Once you add a sample XML/YAML pair to `tests/samples/` the CI suite catches any future regressions. - **The knowledge is captured.** The converter documents exactly what builder.py was doing implicitly, in executable form. The rule of thumb: if you find yourself editing `ioc.yaml` to fix something that came from `xml2yaml`, ask yourself whether that fix belongs in a converter instead. --- ## When a converter is needed A converter is needed when the ibek entity requires different field names, values, or structure from the raw XML attributes. Common cases: | Situation | Converter action | |---|---| | An XML attribute has been renamed in ibek | `entity.rename("old", "new")` | | An XML attribute is GUI-only or builder-only and absent from ibek | `entity.remove("attr")` | | An XML string `"True"` / `"False"` should be a YAML boolean | Already handled by `@globalHandler`; check first | | A numeric value must be transformed (e.g. `valve × 10 = addr`) | Arithmetic on `entity.addr` | | The entity type itself must change (module renamed in ibek) | `entity.type = "newModule.NewClass"` | | An XML element maps to a different ibek module entirely | `entity.type = "dlsPLC.something"` | | One XML element should be deleted (e.g. replaced by an ibek default) | `entity.delete_me()` | | One XML element should expand to two ibek entities | `entity.add_entity(Entity({...}))` | --- ## Converter anatomy Every converter is a `.py` file in `src/builder2ibek/converters/`. `moduleinfos.py` discovers them automatically at import time — no registration step is needed. The minimal structure is: ```python from builder2ibek.converters.globalHandler import globalHandler from builder2ibek.types import Entity, Generic_IOC # Must match the XML element prefix: xml_component = "myModule" @globalHandler def handler(entity: Entity, entity_type: str, ioc: Generic_IOC): """Convert myModule XML entities to ibek YAML.""" if entity_type == "SomeClass": entity.rename("oldParam", "newParam") entity.remove("builderOnlyParam") ``` ### Required module-level names | Name | Type | Purpose | |---|---|---| | `xml_component` | `str` or `list[str]` | XML prefix(es) this converter handles | | `handler` | callable | Called once per matching entity during conversion | ### Optional module-level names | Name | Type | Purpose | |---|---|---| | `yaml_component` | `str` | Override the ibek module prefix in the output (if different from `xml_component`) | | `defaults` | `dict` | Default parameter values injected for specific entity types | | `schema` | `str` | URL of the ibek IOC schema for the Generic IOC (informational) | ### The `@globalHandler` decorator `@globalHandler` wraps your `handler` function so that the global pre-processing runs first. The global handler: - Removes `gda_name` and `gda_desc` from every entity - Converts string values `"True"` / `"False"` / `""` to Python `True` / `False` / `None` Always use `@globalHandler` unless you have a specific reason not to. --- ## The `Entity` API `Entity` is a `dict` subclass that also supports attribute-style access. All XML attributes arrive as string values; the global handler has already converted booleans and empty strings. ```python entity.rename("old", "new") # rename a key; no-op if absent entity.remove("key") # delete a key; no-op if absent entity.type = "mod.Class" # change the ibek entity type entity.delete_me() # mark this entity for removal from output entity.add_entity(new) # insert an extra entity after this one ``` Read-only access to the IOC-level context: ```python ioc.ioc_name # IOC name string ioc.entities # list of all entity dicts already processed ``` --- ## Example 1: simple field rename and removal The `autosave` XML element has several DLS-specific attributes that do not exist in the ibek entity model: ```xml ``` The ibek model uses `P` for the PV prefix (derived from `iocName` with a trailing colon added): ```python # src/builder2ibek/converters/autosave.py xml_component = "autosave" @globalHandler def handler(entity: Entity, entity_type: str, ioc: Generic_IOC): if entity_type == "Autosave": entity.rename("iocName", "P") # rename the field entity.P += ":" # append the colon entity.remove("bl") # DLS-only flags not in ibek model entity.remove("name") entity.remove("path") entity.remove("skip_1") entity.debug = bool(entity.debug) ``` --- ## Example 2: type change and value transformation `vacuumValve.vacuumValve` maps to `dlsPLC.vacValve` with the `valve` number multiplied by 10 to get `addr`, and the `crate` short name looked up to obtain the full `vlvcc` PV device name: ```python # src/builder2ibek/converters/vacuumValve.py xml_component = "vacuumValve" read100Objects = {} # shared state to resolve crate name → port @globalHandler def handler(entity: Entity, entity_type: str, ioc: Generic_IOC): if entity_type == "vacuumValveRead": read100Objects[entity.name] = entity.port entity.type = "dlsPLC.read100" entity.century = 0 entity.remove("name") elif entity_type in ["vacuumValve", "vacuumValve_callback"]: entity.type = "dlsPLC.vacValve" entity.rename("crate", "vlvcc") entity.addr = int(entity.valve) * 10 entity.remove("valve") entity.port = read100Objects[entity.vlvcc] ``` Note the module-level `read100Objects` dict: entities are processed in XML document order, so a `vacuumValveRead` entity always appears before any `vacuumValve` that references it. --- ## Example 3: deleting an entity `devIocStats.devIocStatsHelper` is superseded by an ibek default that auto-generates the equivalent entity. The converter simply removes it: ```python @globalHandler def handler(entity: Entity, entity_type: str, ioc: Generic_IOC): if entity_type == "devIocStatsHelper": entity.delete_me() return ``` --- ## Example 4: handling multiple XML prefixes One converter can handle several XML prefixes by setting `xml_component` to a list. This is useful for support modules that were renamed: ```python xml_component = ["vacuumValve", "oldVacuum"] @globalHandler def handler(entity: Entity, entity_type: str, ioc: Generic_IOC): ... ``` --- ## Development iteration loop Open the `builder2ibek` repository in the devcontainer (first run `git submodule update --init` to check out `ibek-support` and `ibek-support-dls`). The typical inner loop is: 1. Edit `src/builder2ibek/converters/.py` 2. Optionally edit the ibek support YAML in `ibek-support-dls/` or `ibek-support/` if the entity model also needs updating 3. Re-convert all sample XML files and update expected outputs: ```bash cd tests/samples && ./make_samples.sh ``` 4. Rebuild the global IOC YAML schema so VSCode validation reflects any support YAML changes: ```bash ./update-schema ``` 5. Open the generated `.yaml` files in `tests/samples/` in VSCode and check for schema validation errors (requires the Red Hat YAML extension). If the extension does not pick up schema changes immediately, toggle **Yaml: Validate** off and on in the extension settings. 6. Once the output looks correct, commit both the converter and the updated sample files. ## Adding a test Add a sample XML and the expected YAML to `tests/samples/` so the CI will catch regressions. ```bash cp MY-IOC.xml tests/samples/ cd tests/samples && ./make_samples.sh ``` Review the diff carefully — `make_samples.sh` overwrites the `.yaml` files from the current converter output, so only commit when the output is correct. The test suite runs all sample conversions automatically: ```bash uv run pytest tests/test_file_conversion.py ``` --- ## Submitting the converter Converters are part of the `builder2ibek` source tree, not the `ibek-support*` repositories. Open a pull request against [epics-containers/builder2ibek](https://github.com/epics-containers/builder2ibek) including: 1. `src/builder2ibek/converters/.py` 2. `tests/samples/.xml` and `tests/samples/.yaml`