Writing plugins#
OpenOHL has three plugin tiers (ADR-0003): WebAssembly components (stable interface, sandboxed, any language), Python (scripts, reports, import/export) and native Rust inside this repository.
Every plugin has an ohl-plugin.toml manifest:
[plugin]
name = "hello-wasm"
version = "0.1.0"
description = "Example plugin"
sdk = "0.3" # plugin interface version targeted
tier = "wasm" # "wasm" or "python"
entry = "target/wasm32-wasip2/release/hello_wasm.wasm"
extension-points = []
[permissions] # everything denied by default
network = false
filesystem = []
Table columns#
A plugin may add columns to the app's tables, declared in its manifest (any tier; nothing to implement):
[[column]]
table = "circuits" # structures, sections, circuits, systems,
# jumpers, alignments or routes
name = "insulation_level" # unique in the plugin: letters, digits, _ -
title = "insulation level" # header (default: name)
help = "…" # shown on the header
values = ["low", "high"] # a dropdown; none: typed
default = "high" # shown, dimmed, where not set
The values are kept on the objects, in their fields under
plugin.name (e.g. code.insulation_level; a circuit's on all its
sections), saved with the project and in OHL XML (<field key value>).
The plugin, or the check that belongs to it, reads them there. Without
the plugin, a project keeps the values and the tables still show them
in a column marked "(unused)", typed like text. A check plugin can
declare a circuit's value its check needs this way (e.g. an insulation
level read by the distance between conductors).
Where plugins are found, and the shared settings#
Both desktop applications — the line application ohl-design (OHL-Design)
and OHL-Structure ohl-structure — list the plugins installed for the
user (the line application's data folder, plugins/:
eframe::storage_dir("OLS CAD"); the storage id keeps its name from
before the rename to OpenOHL, so settings, recent files and plugins stay
where they were) and
those of the folders added. The folders and the plugins turned off are
one settings file, settings.toml in that plugin folder
(ohl_plugins::PluginSettings: folders, disabled; written through
a temporary file renamed into place), read and written by both: a plugin
added or turned off in one is so in the other.
What applies in OHL-Structure: design-code (Load cases › Load cases
from a design code…: its load cases become recipes taking the line's
values); check is listed (member and joint code checks will be a
structure world later); io plugins take the line's export model, not a
structure (listed as not applicable); terrain, project and Python
plugins are the line application's. See
OHL-Structure.
WebAssembly plugins#
The interface is defined in WIT: crates/ohl-plugins/wit/plugin.wit. A Rust example lives in plugins/examples/hello-wasm:
rustup target add wasm32-wasip2
cd plugins/examples/hello-wasm && cargo build --release --target wasm32-wasip2
cargo run -p ohl-cli -- plugin info plugins/examples/hello-wasm
ohl plugin info DIR works for a plugin of any world: it loads the
plugin in the world of each extension point its manifest lists
(design-code: code-plugin, io, terrain, project, check; none
of these: the minimal plugin world) and prints its describe (a check
plugin, which has none, by its manifest), the world, the extension
points, tier, interface version and entry file, then what else it
offers: its formats (id, description, extension, importer or exporter),
its checks, or the length of a design code's parameter template.
name geojson
version 0.2.0
description GeoJSON export (structures, wires) and import (structures)
world io-plugin
extensions io
tier wasm
sdk 0.3
entry plugins/examples/geojson/target/wasm32-wasip2/release/geojson.wasm
format geojson: Structures and wires as GeoJSON features (line CRS) (.geojson, exporter)
format geojson-structures: Structures from GeoJSON Point features (.geojson, importer)
Plugins run sandboxed — no file system, network, environment variables or standard I/O — with exit clauses on every call: a time limit (default 5 s; a runaway plugin is interrupted and reported as a timeout) and a memory cap (default 256 MiB). plugins/tests/runaway is a deliberately endless plugin used to test this.
File-format plugins#
A plugin that adds import or export formats implements the io-plugin world of the same WIT file and lists the extension point io in its manifest:
extension-points = ["io"]
formats()lists its formats: id, description, extension and direction (exporter or importer).export-model(format, model, options)receives the solved model as JSON and returns the file's bytes. The model is{"format": "ohl-export-model", "version": 1, "crs": …, "line": <line file>, "cases": [{"name", "summary", "wires": [{"section", "span", "circuit", "phase", "color", "system", "voltage_kv", "horizontal_tension_n", "max_tension_n", "sag_m", "points": [[x, y, z], …]}], "strings": [{"structure", "label", "by", "force_n", "swing_deg", "swing_across_deg", "swing_along_deg", "max_load_n", "max_swing_deg", "max_swing_across_deg", "max_swing_along_deg"}]}]}(coordinates in the line's coordinate system; jumpers have sectionjumper:<id>and no horizontal tension; tensions per wire, N; sag in m).stringslists each structure's string as solved in the case (once;by: the section that reported it,jumper:<id>for a jumper's helping string) with its limits — see String limits.import-file(format, data, options)returns a line file (JSON), which the host checks like any line file. When the user gives a line to update, it arrives as optioninto(its JSON).- Errors are returned as strings and shown to the user as the format's error.
plugins/examples/geojson is a complete example (GeoJSON export of structures and wires; structures imported from GeoJSON points). Its formats appear next to the built-in ones in ohl formats --plugin plugins/examples/geojson; see Import and export.
Terrain plugins#
A plugin that reads terrain surfaces implements the terrain-plugin world and lists the extension point terrain:
extension-points = ["terrain"]
# Large files: more than the default 5 s and 256 MiB per call (granted up
# to 600 s and 4 GiB).
[limits]
seconds = 60
memory-mib = 1024
formats()lists the formats it reads (directionimporter).import-surfaces(format, data, options)returns surfaces: a name, the points asx0, y0, z0, x1, …(metres, x east, y north, z up) and the triangles as three point indices each. The host checks them like any mesh and keeps them in the project.
plugins/examples/esri-grid reads ESRI ASCII grids (.asc): a point per cell centre, two triangles per square of cells with data. Its format appears in ohl surface formats --plugin plugins/examples/esri-grid and in Terrain › Terrain surfaces once the plugin is installed or its folder added (Plugins › Plugin manager).
A plugin also suits formats that should not be in the open-source code — e.g. one read from a private specification — kept and installed only where it may be used.
Project plugins#
A plugin that reads another program's project files implements the project-plugin world and lists the extension point project (with [limits] for large files, as above):
formats()lists the formats it reads.import-project(format, data, options)returns a project: the line as a line file (JSON), its library files (kind—cables,weather,loadcases,structures,insulators,rules,galloping,ambient,costs,poles,femodels,sections—nameand TOMLtext, as OpenOHL writes them) and notes: what was read, what was left out, what was assumed. The host checks the line and creates a new project file with it and its libraries (File › New project from another program,ohl project import-from), and shows the notes.
plugins/examples/project-bundle reads a JSON bundle {"line": …, "libraries": [{"kind", "name", "text"}]} — the shape such a reader returns. Readers of formats that should stay out of the open-source code are installed only where they may be used.
Design-code plugins#
A plugin for a design code implements the code-plugin world
(crates/ohl-plugins/wit/code.wit) and lists the extension point
design-code:
extension-points = ["design-code"]
describe()returns its name, version, a one-line description and a parameters template: a TOML parameter file with its schema in comments and placeholder values.generate(parameters)reads the user's parameter file (TOML text) and returns generated:weather-cases(an.ohlwcfile's text),load-cases(an.ohllcfile's text, each case naming one of those weather cases),notes(what was computed, assumed or left out) andclearance-presets(may be empty). A wrong file is an error string, shown to the user.- A clearance preset (
clearance-preset) fills in a clearance window in one click: itsname(unique among the plugin's), itsmeasure(verticalorradial: the window), itsrules— a clearance rule file's text (.ohlr, formatohl-clearance-rules: the rules with their classes, voltages, distances and, vertical,vertical_band(the zone's half width of each rule);[wind]steps and sides;[check]circuits, calculationmethodandsearch_distance) — and itsweather-cases, an.ohlwcfile's text with the cases its rules name.
The host (ohl_io::codes, WasmHost::load_code) checks the files as
OpenOHL reads them before anything is written — each preset's rule file
read (an error names it, <plugin>-<name>.ohlr), a rule of its
measure, every weather case its rules name among its own, the names
unique; ohl code generate and Criteria › Design code write them (the
presets as <plugin>-<name>.ohlr and .ohlwc, in the project among
its rule and weather case files). Plugin interface version 0.2 (sdk = "0.2") brought the presets: a plugin built for 0.1 must be rebuilt. Version 0.3
(2026-10-18) renamed the interface ols:plugin → ohl:plugin and the
manifest ols-plugin.toml → ohl-plugin.toml (still read): a plugin
built for 0.2 must be rebuilt (its sources need the new package name
only). A plugin ships no values of the
standard: it implements the code's method, every number comes from the
parameter file, and the template's numbers are invented placeholders,
marked as such.
plugins/codes/* are such plugins (EN 50341 with a national annex overlay,
IEC 60826, NESC, CSA C22.3, ASCE MOP 74, GO 95); they share
plugins/codes/common (reading parameter files, writing the case files,
writing clearance presets — Preset, RuleFile —, the code_plugin!
macro implementing the world). A plugin for the Swiss LeV exists
separately; it is not part of the public code. See
Design codes.
Check plugins#
A plugin that runs a check implements the check interface
(crates/ohl-plugins/wit/code.wit: the check-plugin world, or
code-check-plugin for a design-code plugin that also runs checks) and
lists the extension point check. OpenOHL measures, the plugin holds
the values and the rules: the measuring stays in OpenOHL (it is
geometry, and fast), the code's numbers and its formulas stay out of it.
checks()lists the checks it runs: the id of what OpenOHL measures for it (todayphase-distance, the distance between conductors in the span), its title and its Checks submenu (the app builds the Checks menu from the plugins in use: a check's entry is there only while its plugin is; a check OpenOHL does not measure for is listed, greyed), an explanation of the computation (shown in its window), its values file as shipped (TOML) with its extension, the choices made before a run (e.g. the k diagram), and the plugin columns of the circuits its window shows to be set there.value-tables(check, values)shows a values file as tables (shown locked in the window, for transparency);edit-value(check, values, table, row, column, text)returns the file with that cell changed (an admin's edit, kept as the project's copy), or what is wrong.evaluate(check, values, choices, pair, stations): for one pair of conductors (conductor-pair: same system, same span, each one's kV, plugin columns, diameter and mass, the mean height, the largest sags between spacers when spacers join them) and its stations (6 numbers each: distance, β, each local sag, each l_k), the numbers required at each station — the distance first, then the check's own (kept with the worst station) — and the rule applied.
The host (ohl_io::checks, WasmHost::load_check) measures
(ohl_clearance::phase_distance), calls the plugin once per pair, one
instance per core (WasmHost::another_check, compiled once), and keeps
each pair's worst station and the length below — see Distance between
conductors: the core's part. A national-code
plugin kept outside the repository runs such a check. On an
existing line (930 conductors, 5593 pairs) a plugin's check takes about
as long as the same computation built into OpenOHL (0.8 s against
0.65–0.9 s; with nearby pairs, about 10 500, 0.9 s against 0.65–0.7 s), plus
about 0.3 s to load the plugin once.
String limits: what a plugin sees#
A string (insulator) may carry limits that OpenOHL does not check — the tension it is rated for and the swings it may take (String limits). Absent means not given: nothing checks it. Given, nothing in OpenOHL checks it either: a plugin (or a report, or a script) does, where its rules need it. What it gets:
- Export model (
ioplugins,export-model): thelineholds every string with its ownmax_load(N),max_swing_deg,max_swing_across_deg,max_swing_along_deg(degrees) — structure types placed and the limits of the insulator a string names filled in where it gives none (absent keys: not given). Each case'sstringsgives, per string, the solved axial forceforce_n(whole bundle, tension positive), its swingswing_deg(from straight down),swing_across_degandswing_along_deg(the angles of its projections on the structure's planes across and along the line, positive to the right and ahead), and its limits beside them asmax_load_n,max_swing_deg,max_swing_across_deg,max_swing_along_deg(null: not given). A plugin compares, e.g.abs(swing_across_deg) > max_swing_across_deg. The format stays version 1: keys were only added. - Check plugins (
check): the WIT records pass no strings (a check measures conductor pairs), so the interface is unchanged (stillohl:plugin@0.1.0); a check that needs the limits reads them from the line as above. - Rust (built-in checks, reports, the server):
ohl_line::InsulatorString::limits()→StringLimits,Line::string_limits(structure, label), and next to each solved stringsolve::StringState::{force, limits, swing}andjumper::JumperStringShape::{force, limits, swing};ohl_line::swing(orientation_deg, top, bottom)for any other position. Library side:InsulatorAssembly::limits(),TypeString::limits()(ohl_structures::InsulatorLimits). - Python: the line's JSON (the console's
ohlmodule) carries the samemax_*keys on its strings.
Python plugins#
Python plugins are scripts (tier = "python", entry the script). Outside the application they use the ohl module built from crates/ohl-py (package openohl); inside it, the Python console runs them (Plugins › Python console › Plugin scripts) with the ohl module of the application, which also reads and edits the open project (Python console). See plugins/examples/hello-python.
The desktop application embeds Python through its python feature (on by default): building it needs a shared Python 3.10+ (PYO3_PYTHON chooses which; a static one, such as Anaconda's, cannot be embedded). Without the feature (--no-default-features), the console says so.