Skip to content

Writing a driver

A driver is a Device subclass: properties declared in setup(), periodic work in @every methods, client writes handled by @on_new methods. The SDK carries the standard INDI machinery so your driver is only its own behavior.

Start from a working file:

indi-nexus new my_driver.py

The shape of a driver

from indi_nexus.driver import Device, every, on_new
from indi_nexus.protocol import IPState, ISState, Number, Switch, SwitchVector

class Mount(Device):
    name = "Mount"

    async def setup(self) -> None:
        self.define_connection()
        self.define_number(
            "EQUATORIAL_EOD_COORD",
            [Number(name="RA", format="%9.6m"), Number(name="DEC", format="%9.6m")],
        )

    async def on_connect(self) -> None:
        await self.open_serial_link()

    @every(seconds=1, when_connected=True)
    async def poll(self) -> None:
        ra, dec = await self.read_mount()
        self["EQUATORIAL_EOD_COORD"].set(RA=ra, DEC=dec, state=IPState.OK)

    @on_new("EQUATORIAL_EOD_COORD")
    async def _goto(self, vector) -> None:
        if not self.require_connected():
            return
        await self.slew_to(vector.get("RA", 0.0), vector.get("DEC", 0.0))

if __name__ == "__main__":
    Mount.run()

The pieces

Properties - define_number/text/switch/light/blob(...) each return a BoundProperty handle and emit the def to clients. Later, self["NAME"].set(ELEMENT=value, state=...) updates elements and notifies every client in one call; OneOfMany switch rules are honoured automatically.

Connection - define_connection() adds the standard CONNECTION switch. The built-in handler flips it, calls your on_connect() / on_disconnect() hooks (open and close your hardware link there), and announces the transition. self.connected reads the state; self.require_connected() is the one-line guard for handlers; @every(..., when_connected=True) pauses polling while disconnected.

Client writes - an @on_new("NAME") method receives the parsed, typed vector the client sent. Two accessors answer the questions every handler asks:

  • vector.selected() - which switch member did the client turn On? (A OneOfMany write often carries only the selected member.)
  • vector.get(name, default) - what value did they send? (Tolerates partial writes; never raises.)

Robustness for free - a raising handler or a failing poll tick is reported to the client as an INDI message and isolated: one bad write or bug never kills the driver. Writes addressed to other devices are ignored, matching libindi semantics.

Messages - self.message("...") and self.log_error("...") send INDI messages; they land in the panel's log and every connected client.

Testing your driver

Drive the runtime over in-memory pipes, exactly as indiserver would - no sockets, no processes (see tests/test_driver.py and tests/test_examples.py for the patterns used to test the built-in examples):

runtime = DriverRuntime(MyDriver(), read, write)   # your in-memory callables

Full API

See the driver SDK reference.