Quickstart
A real driver, from an empty directory to something installed on a controller. About ten minutes, and no code — the device in this walkthrough speaks RS-232, which the declarative runtime handles on its own.
The device is a projector. It has a serial port, discrete power commands, discrete input selection, no volume worth controlling, and it ignores everything for about twenty seconds while its lamp comes up.
1. Get the tools
One binary, junodrv. It validates a manifest against the real proxy contracts and builds the
.junodrv archive a controller installs.
cargo install --locked --features pack \
--git https://github.com/junohouse/driver-sdk driver-sdk
That is the only thing you need. junodrv carries the contracts compiled in, so it works
offline and there is no controller in the loop — which is the point: writing and checking a
Juno driver requires access to nothing private.
2. Declare what the device is
mkdir acme-projector && cd acme-projector
manifest.toml
[driver]
id = "acme.projector.serial"
name = "Acme Projector (RS-232)"
manufacturer = "Acme"
version = "1.0.0"
runtime = "declarative"
api = 1
# One binding: this device is a display, so it is a `tv`.
[[proxy]]
id = 1
type = "tv"
primary = true
# A sub-table rather than `capabilities = { … }` only because there are four of them: a TOML
# inline table has to fit on one line. Both forms parse to the same thing.
[proxy.capabilities]
has_discrete_power = true
has_discrete_input = true
has_volume = false
warmup_ms = 20000
# We need a serial port. We do not care whose — see Connections.
[[control]]
id = 1
kind = "serial"
name = "Projector RS-232"
required = true
# The two inputs on the back panel, so the pathfinder can route video to them.
[[connection]]
id = 1001
proxy = 1
dir = "consumer"
class = "HDMI"
name = "HDMI 1"
[[connection]]
id = 1002
proxy = 1
dir = "consumer"
class = "HDMI"
name = "HDMI 2"
Three of those capability lines do real work:
has_discrete_power = truegives youonandoffinstead of onlypower_toggle. A toggle is guesswork the first time something asks for a room to be on.has_volume = falseremovesvolume_up,volume_down,set_volumeandmute_togglefrom the callable set.has_volumedefaults to true, so leaving it out would advertise four commands this projector does not have.warmup_ms = 20000is the promise that makes AV routing work. Core waits that long after powering the projector on before sendingset_input, which is the difference between a working room and "the projector came on but it is on the wrong input".
Everything a tv may declare is in the tv contract.
3. Say what the bytes are
commands.toml
# The port settings. Core issues `configure` on the bound serial connection from this block
# when the driver binds; you never call it yourself.
[transport]
kind = "serial"
baud = 9600
databits = 8
parity = "none"
stopbits = 1
command.on = { tx = "PWR ON\r", expect = ":", timeout_ms = 3000 }
command.off = { tx = "PWR OFF\r", expect = ":" }
# The proxy passes `connection`, which is one of the ids from the manifest. The device wants
# its own numbering, so the mapping happens here rather than anywhere downstream.
command.set_input = { tx = "SOURCE {connection:04d}\r" }
# What the projector says back. Each frame is matched against every rule in order.
[receive]
delimiter = "\r"
parse = [
{ regex = "PWR=(\\d)", notify = "power_changed", args = { on = "$1 == 1" } },
{ regex = "SOURCE=(\\d+)", notify = "input_changed", args = { connection = "$1" } },
]
# It never volunteers anything, so ask.
[poll.power]
tx = "PWR?\r"
every_ms = 5000
4. Check it
junodrv check .
acme.projector.serial v1.0.0 ok
This is the same validation the certification pipeline runs, against the same contracts a controller enforces. It catches a capability that does not exist, a parameter out of range, a connection pointing at a proxy you did not declare, and a notification the capabilities you declared do not permit. Get in the habit of running it before you build.
Try breaking it on purpose. A manifest that fails here is one that would otherwise have failed at install time, in somebody's house:
error: acme.projector.serial:
proxy 1: capability `warmup_ms`: "slow" is not a U32
error: acme.projector.serial:
proxy 1: unknown capability `nope_not_real` for proxy `tv`
5. Build it
junodrv pack . --out dist
acme.projector.serial v1.0.0 -> dist/acme.projector.serial-1.0.0.junodrv
pack validates first and separately, so it never writes an archive that would not install.
6. Put it on a controller
Drag the .junodrv onto the configurator's driver pane, or drop it in the controller's
drivers/ directory and restart. Either way it is marked Third-party: nobody certified it,
and the controller will never auto-update it. That is the right state for a driver you are
still writing.
Then, in the configurator:
- Add a device using the driver.
- Draw a line from its Projector RS-232 control connection to any serial port in the project. A DB9 on the controller, a Global Caché iTach across the LAN, a USB dongle — the driver is byte-identical for all three and never learns which it got.
- Press On.
7. Watch what it is doing
The driver's pane shows every command in, every byte out, and every notification back. Two things are worth knowing while you are debugging:
- A command that never appears was never callable. Check the capability that gates it in the proxy reference — an absent command is nearly always an undeclared capability, not a broken driver.
- A device that appears stuck is usually one that is not notifying. Core's state, the UI and
every automation trigger read from your notifications, not from your commands, so a
PWR ONwith nopower_changedbehind it looks like nothing happened.
Where to go next
| Next | Why |
|---|---|
| Connections | Why the driver above never mentions a serial device, and how AV signal routing works. The part of this SDK worth understanding properly. |
| Runtimes | When declarative runs out, and what to write instead. |
| Discovery | Making a controller find the device on its own rather than being told an address. |
| Manifest reference | Everything the file in step 2 can contain. |
| Publishing | Shipping it — to your own users, or into the registry. |