This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
pyINDI is a pure-Python implementation of the INDI protocol (Instrument Neutral Distributed Interface) for astronomical instrument control. It provides a Python device driver framework (server-side) and a Tornado-based web client (browser-side). Used in production at MMT Observatory and Steward Observatory.
# Install in development mode
pip install -e .
# Run tests
pytest --pyargs pyindi
# Run a single test
pytest --pyargs pyindi -k "test_name"
# Code style
flake8 pyindi --count --max-line-length=100
# Or via tox
tox -e codestyle
# Run an example driver with indiserver
indiserver -vv ./example_drivers/skeleton.py
# Inspect running driver output (port 7624)
echo "<getProperties version='1.7'>" | nc localhost 7624pyindi/
├── device.py # Base class for INDI device drivers (stdin/stdout XML with indiserver)
├── client.py # Async TCP client connecting to indiserver
├── webclient.py # Tornado web app: WebSocket bridge between browser and indiserver
├── utils.py # INDIEvents - event-driven client with property watching/callbacks
├── data/indi.dtd # INDI protocol DTD used to generate def/set XML elements
└── www/ # Static JavaScript frontend (vanilla JS, no frameworks) + HTML templates
INDI Driver (device.py) ←stdin/stdout→ indiserver:7624 ←TCP→ INDIClient/INDIWebClient
↕ WebSocket
Browser (JS)
Subclass device and override these methods:
ISGetProperties(device)— called when a client requests property definitionsinitProperties()— define vector properties; typically callsself.buildSkeleton("file.xml")ISNewNumber(device, name, values, names)— handle incoming number updatesISNewText(device, name, values, names)— handle incoming text updatesISNewSwitch(device, name, values, names)— handle incoming switch updates
Key device methods:
IDDef(vec)— announce a new property to clientsIDSet(vec)— send updated property values to clientsIDSetBLOB(blob)— send BLOB dataIDMessage(msg)— send a log messageIUFind(name)— look up a registered vector propertyIUUpdate(device, name, values, names, Set=False)— update a vector propertybuildSkeleton(skelfile)— build properties from an XML skeleton file@device.repeat(millis)— decorator to schedule a method to run repeatedly
Start a driver with device.start() (sync) or await device.astart() (async).
All live in device.py. Each "vector" groups one or more "properties":
| Vector Class | Elements | XML Tag Context |
|---|---|---|
INumberVector |
INumber (float) |
NumberVector |
ITextVector |
IText (str) |
TextVector |
ISwitchVector |
ISwitch (On/Off) |
SwitchVector |
ILightVector |
ILight (IPState) |
LightVector |
IBLOBVector |
IBLOB (bytes) |
BLOBVector |
Enums: IPState (Idle/Ok/Busy/Alert), IPerm (ro/wo/rw), ISRule (OneOfMany/AtMostOne/AnyOfMany), ISState (On/Off).
XML for def/set elements is auto-generated using indi.dtd — the DTD attribute names are mapped to class attributes.
INDIWebApp is the main entry point. It:
- Starts a Tornado IOLoop
- Creates
INDIWebClient(singleton) which connects to indiserver via TCP - Serves a WebSocket at
/indi/websocketthat bridges browser ↔ indiserver - Serves static JS/CSS at
/indi/static/ - Serves the default GUI at
/indi/index.html
To add custom routes, pass handlers to build_app(). Subclass INDIHandler and use self.indi_render() for templates that need pyINDI JS/CSS injected via {% raw pyindi_head %}.
Protected routes that cannot be overridden: /indi/websocket, /indi/static/(.*), /indi/templates/(.*).
BLOBs are handled separately: BlobClient feeds raw XML through BlobHandler (SAX parser) to extract base64 data and convert it to binary, then calls the user-supplied handle_blob callback.
INDIEvents (subclasses INDIClientSingleton) provides a higher-level API:
watch(device, name, callback)— callcallback(xml_element)when the property changes@INDIEvents.handle_property(device, name)— decorator to register a method as a property handlergetProperties(device, name)— send a getProperties XML request
INDIClientSingleton ensures only one TCP connection per process. INDIWebClient uses this singleton via INDIWebClient() anywhere in the codebase (see websocket handler, blob handler).
- Naming mirrors the INDI C++ library (
ISNew*,IDSet*,IUFind, etc.) - All driver I/O is asyncio-based; use
self.mainloopto schedule CPU-bound work viarun_in_executor device._registrantsanddevice._NewPropertyMethodsare class-level (shared), so subclasses must be careful not to cross-contaminate- Line length: 100 characters (flake8)
- Python 3.8+ required