IOOpenDeviceIO

Authoring guide

Producing a conformant .odio.json

This guide shows three ways to author an ODIO device file — by hand, with the TypeScript SDK, or via the Genie importer — and how to validate the result. A file is conformant if and only if it validates against the canonical schema for its kind.

What you are producing

One .odio.json file per orderable model. It is a single JSON object with a required odioVersion, id, device identity, and ports array, plus optional power, physical, standards, parameters, and provenance blocks. Start every file by pointing $schema at the canonical URL so editors and validators resolve it:

skeleton
{ "$schema": "https://opendeviceio.org/schema/v0.1/device.schema.json", "odioVersion": "0.1.0", "id": "acme/widget-100", "device": { "manufacturer": "Acme", "model": "Widget 100" }, "ports": [] }

The connector / link / signals model

Each port separates three things. Get this right and the rest follows:

  • connector — the physical jack only, from the controlled vocabulary (rj45, hdmi-type-a, usb-c, phoenix, xlr-3-f…). Unknown connectors use "connector": "other" plus free text so a missing term never blocks a file. A multi-pole terminal block records its physical poleCount.
  • link — the physical pipe: type, standard, speed/bandwidthGbps, and link-level facts such as PoE ({ standard, role, classWatts }) or USB powerDeliveryWatts. State it once per port.
  • signals — the concurrent logical flows. One connector may carry several: an HDMI port carries video + audio + control (CEC); one RJ45 can carry dante + aes67 + LAN. Keep the physical pole count (on the connector) separate from the number of logical circuits (signal.channels).

The canonical worked example of that last point: a 3-pole Phoenix RS-232 port is one connector with one control circuit, whereas an 8-pole Phoenix GPIO header is one connector carrying eight independent control circuits (channels: 8).

A worked example

An HDMI input that carries video and embedded audio, plus a phoenix GPIO header showing the pole-count-vs-channels distinction:

ports
"ports": [ { "id": "hdmi-in-1", "label": "HDMI INPUT 1", "direction": "input", "connector": "hdmi-type-a", "count": 1, "link": { "type": "hdmi", "standard": "hdmi-2.0", "bandwidthGbps": 18 }, "location": { "face": "rear", "group": "inputs", "order": 1 }, "signals": [ { "domain": "video", "transport": "hdmi", "maxResolution": "4096x2160", "maxRefreshHz": 60, "hdcp": "2.2" }, { "domain": "audio", "transport": "lpcm", "maxChannelsPerCircuit": 8 } ] }, { "id": "gpio", "label": "GPIO", "direction": "bidirectional", "connector": "phoenix", "poleCount": 8, "signals": [ { "domain": "control", "transport": "gpio", "channels": 8 } ] }, { "id": "lan", "label": "LAN", "direction": "bidirectional", "connector": "rj45", "link": { "type": "ethernet", "standard": "1000base-t", "speed": "1g", "poe": { "standard": "802.3at", "role": "pd", "classWatts": 30 } }, "signals": [ { "domain": "network", "transport": "control-network", "managed": true }, { "domain": "control", "transport": "ip-control" } ] } ]

The id slug rule

The id is the stable join key. It is derived as slug(manufacturer)/slug(model)[@slug(revision)]. The slug rule:

  • lowercase everything;
  • replace + with -plus;
  • collapse runs of any other character outside [a-z0-9._-] into a single -.

So Lightware / UCX-4x2-HC60D becomes lightware/ucx-4x2-hc60d, and Extron / DTP2 T 211 revision A becomes extron/dtp2-t-211@a.

The ^x- extension rule

The core schema sets additionalProperties: false, so an unrecognized non-extension key makes the file invalid — drift is caught, not silently accepted. To add vendor- or tool-specific data, use a key matching ^x- at any object level; validators MUST ignore unknown x- keys.

extension keys
{ "id": "acme/widget-100", "device": { "manufacturer": "Acme", "model": "Widget 100" }, "ports": [ /* ... */ ], "x-dtools": { "category": "Switchers" }, "x-note": "Free-form note for reviewers; ignored by validators." }

Authoring with the SDK

@opendeviceio/sdk ships the generated TypeScript types, an Ajv 2020 validator, and convenience accessors. Author the object with full type-checking, then validate:

typescript
import { type OdioDevice, validateDocument, formatErrors, inputPorts, poeBudget } from "@opendeviceio/sdk"; const device: OdioDevice = { $schema: "https://opendeviceio.org/schema/v0.1/device.schema.json", odioVersion: "0.1.0", id: "acme/widget-100", device: { manufacturer: "Acme", model: "Widget 100" }, ports: [ { id: "lan", label: "LAN", direction: "bidirectional", connector: "rj45", link: { type: "ethernet", standard: "1000base-t", speed: "1g" }, signals: [{ domain: "network", transport: "control-network" }] } ] }; // validateDocument routes on `kind` (device | bundle | cable). const result = validateDocument(device); if (!result.valid) { console.error(formatErrors(result.errors)); process.exit(1); } // Accessors derive useful facts straight from the document: console.log(inputPorts(device).length, "input-capable ports"); console.log(poeBudget(device), "W of PoE source budget");

Other accessors include outputPorts, signalsByDomain, signalsByTransport, estimatedBtuPerHour, rackUnits, and — for kits — flattenBundle and bundleBillOfMaterials.

Authoring with Genie

Genie is the reference importer: it reads a product PDF and emits a draft .odio.json plus a review report flagging low-confidence fields. The intended workflow is generate → human-review → publish, promoting the document's provenance.validation.status from draft to reviewed (or manufacturer-verified) as it is checked.

shell
# Generate a draft from a datasheet, with a review report: genie parse datasheet.pdf -o widget-100.odio.json --review-report review.md # The Claude API key is supplied at runtime via an env var; Genie never ships one.

Validating your file

Validation is the whole contract. Use the SDK's validateDocument programmatically (above), or the repo's conformance runner to validate a directory of examples against the schema with Ajv 2020:

shell
# From the repo root — validates every examples/*.odio.json and confirms the # examples/invalid/* fixtures fail, exactly as CI does: npm install npm run validate:examples # node tools/validate-examples.mjs

Once your file validates, you can browse the registry to see how published devices, bundles, and cables render, or fetch the canonical device, bundle, and cable schemas directly.