indi 0.1.3
indi: ^0.1.3 copied to clipboard
A pure Dart client for the INDI astronomy protocol. Control telescopes, cameras, focusers and more from Flutter apps, with automatic reconnect.
indi #
A pure Dart client for INDI, the protocol used to
control astronomy equipment: telescope mounts, cameras, focusers, filter
wheels, domes and more. Build Flutter apps (or command-line tools) that talk
to any INDI server, such as the one running on an Astroberry, StellarMate or
any Linux box with indiserver.
Guides and examples: dindi.torresj.es. API reference: pub.dev.
final client = IndiClient(host: 'astroberry.local');
await client.connect();
final mount = Telescope(await client.waitForDevice('Telescope Simulator'));
await mount.connect();
await mount.slewTo(EquatorialCoordinates.parse('05:35:17', '-05:23:28'));
final camera = Camera(await client.waitForDevice('CCD Simulator'));
final image = await camera.expose(const Duration(seconds: 30));
Features #
- Complete INDI 1.7 protocol: every message type, all five property types
(number, text, switch, light, BLOB), sexagesimal number formats, BLOB
policies, and the INDI 2
pingRequest/pingReplyextension. - Automatic reconnection and session resume: when the connection drops,
the client reconnects with exponential backoff, requests the properties
again, restores BLOB settings and removes whatever disappeared. Your
IndiDeviceobjects stay valid throughout. - Dead-link detection: keep-alive pings detect connections that die silently, like a Wi-Fi drop.
- Awaitable commands:
await mount.slewTo(...)completes when the driver reports the slew finished, and throws with the driver's message if it fails. - Typed device wrappers for mounts, cameras, guiders, focusers, filter wheels, domes, rotators, weather stations, GPS, dust caps, light boxes, I/O boxes, sky quality meters and polar alignment correctors, on top of a fully generic API for anything else.
- Built for Flutter: immutable property snapshots and streams that emit
the current value first, ready for
StreamBuilder. - Fast with large images: BLOBs are base64-decoded while they stream in (about 200 MB/s), without huge intermediate strings.
- Every platform: Android, iOS, macOS, Windows, Linux and the web (through a WebSocket bridge).
- Testable: an in-memory
FakeIndiServerlets you test your app without hardware. - Pure Dart, no native code, three small dependencies.
Getting started #
dart pub add indi # or: flutter pub add indi
You need an INDI server. To try the package without hardware, run the simulators on any Linux machine (or a Raspberry Pi):
sudo add-apt-repository ppa:mutlaqja/ppa && sudo apt install indi-bin
indiserver indi_simulator_telescope indi_simulator_ccd indi_simulator_focus
Usage #
Connecting #
import 'package:indi/indi.dart';
final client = IndiClient(host: '192.168.1.20'); // port defaults to 7624
await client.connect();
// Devices appear as the server defines them.
final camera = await client.waitForDevice('CCD Simulator');
// Or find devices by kind, using the interfaces their drivers declare.
final mounts = client.devicesWith(DeviceInterface.telescope);
connect() throws an IndiConnectionException if the server can't be
reached. Once connected, the client stays connected: see
Reconnection. Call close() when done.
Devices, properties and elements #
Each device has properties (INDI "vectors"), and each property has elements with values. Properties are immutable snapshots: every update from the driver creates a new one.
final coords = mount.getNumber('EQUATORIAL_EOD_COORD');
print(coords?.state); // PropertyState.ok
print(coords?['RA']?.value); // 5.5916…
print(coords?['RA']?.formattedValue); // " 5:35:30", using the driver's format
final connected = mount.getSwitch('CONNECTION')?.isOn('CONNECT');
StandardProperties and StandardElements hold the names of the
standard INDI properties.
Watching values #
watch emits the current snapshot and then every change, which makes it a
natural fit for Flutter:
StreamBuilder<NumberProperty>(
stream: camera.watch<NumberProperty>('CCD_TEMPERATURE'),
builder: (context, snapshot) => Text(
snapshot.data?['CCD_TEMPERATURE_VALUE']?.formattedValue ?? '…',
),
);
Everything that happens is also available as a stream of sealed events:
client.events.listen((event) {
switch (event) {
case DeviceAdded(:final device):
print('New device: ${device.name}');
case PropertyUpdated(:final property):
print('${property.name} is now ${property.state}');
case MessageReceived(:final message):
print('[${message.level.name}] ${message.body}');
default:
break;
}
});
Sending commands #
Commands are checked before sending (element names, permissions, switch rules and ranges), and the returned future completes when the driver confirms:
await mount.sendNumbers('EQUATORIAL_EOD_COORD', {'RA': 5.5, 'DEC': -5.4});
await mount.setSwitch('TELESCOPE_TRACK_STATE', 'TRACK_ON');
await camera.sendNumber('CCD_TEMPERATURE', 'CCD_TEMPERATURE_VALUE', -10);
- When the driver rejects a change, the property goes to the
Alertstate and the future fails with anIndiPropertyAlertExceptionthat carries the driver's message. - Pass
timeout:to limit the wait, or setIndiClientOptions.commandTimeout. completion: CommandCompletion.sentreturns immediately, andCommandCompletion.afterBusywaits for the property to goBusyand back, which avoids mistaking a periodic update for the answer.- Commands are never queued or replayed across a reconnect, because a slew
executed minutes late could be dangerous. Sending while disconnected throws
an
IndiNotConnectedException.
Device wrappers #
The wrappers give standard devices meaningful, typed methods:
| Wrapper | Highlights |
|---|---|
Telescope |
slewTo, syncTo, abort, park, tracking and rates, motion, site, time |
Camera |
expose, images, frame type, binning, ROI, cooling, gain, offset, streaming |
Focuser |
moveTo, moveBy, abort, sync, temperature |
FilterWheel |
selectFilter('Ha'), selectSlot, filter names |
Dome |
shutter, moveTo azimuth, park, slaving |
Rotator |
moveTo, sync, abort, isReversed and setReversed |
Guider |
pulseGuide (also available on Telescope and Camera) |
Weather |
isSafe, status per parameter, readings |
Gps |
location and time, refresh |
DustCap |
open, close, isMoving, abort |
LightBox |
light on and off, brightness |
IoBox |
digital inputs and outputs with their names, analog inputs, setOutput, pulse mode |
SkyQualityMeter |
skyBrightness in mag/arcsec², sensor temperature and readings |
PolarAligner |
moveBy azimuth and altitude, abort, speed, position, reverse |
final wheel = FilterWheel(await client.waitForDevice('Filter Simulator'));
await wheel.selectFilter('Ha');
final focuser = Focuser(await client.waitForDevice('Focuser Simulator'));
await focuser.moveBy(-150);
final relays = IoBox(await client.waitForDevice('Simulator IO'));
await relays.setOutput(1, true);
Wrappers keep no state and expose the underlying IndiDevice as device, so
driver-specific properties are always one step away.
Images and other BLOBs #
INDI servers send binary data (BLOBs), such as images, only to clients that
enable it. Camera.expose does this for you. With the generic API:
await camera.setBlobMode(BlobMode.also, property: 'CCD1');
camera.watch<BlobProperty>('CCD1').listen((property) {
final blob = property['CCD1']?.blob;
if (blob != null) File('image${blob.format}').writeAsBytesSync(blob.decompress());
});
For imaging apps, set IndiClientOptions(separateBlobConnection: true). Images
then arrive on a second connection, so a 50 MB download never delays other
commands.
Reconnection and resume #
final client = IndiClient(
host: 'astroberry.local',
options: const IndiClientOptions(
reconnect: ReconnectPolicy(maxDelay: Duration(seconds: 30)),
heartbeat: HeartbeatOptions(interval: Duration(seconds: 10)),
),
);
client.connectionStates.listen((state) => print(state));
// IndiConnected → IndiReconnecting(attempt: 1, delay: 1s) → IndiConnecting → IndiConnected
client.eventsOf<SessionResumed>().listen((e) => print('Back after ${e.downtime}'));
When the connection drops:
- Pending commands fail with
IndiConnectionLostException, and devices reportisAvailable == false. - The client reconnects with exponential backoff and jitter.
- Properties are requested again, and BLOB settings and watched devices are restored.
- Once the definitions settle, anything that was not defined again is
removed, and a
SessionResumedevent is emitted. The sameIndiDeviceobjects are reused.
Flutter web #
Browsers can't open TCP connections, so on the web the client connects through a WebSocket. Put a bridge such as websockify next to indiserver:
websockify 8624 localhost:7624
final client = IndiClient(host: 'astroberry.local', port: 8624); // ws:// on the web
// or explicitly, on any platform:
final ws = IndiClient.withTransport(
WebSocketTransport(Uri.parse('wss://observatory.example.com/indi')),
);
Platform setup #
| Platform | What to do |
|---|---|
| Android | Add <uses-permission android:name="android.permission.INTERNET"/> to AndroidManifest.xml. |
| iOS | Add NSLocalNetworkUsageDescription to Info.plist to reach servers on the local network. |
| macOS | Add com.apple.security.network.client to DebugProfile.entitlements and Release.entitlements. |
| Web | Use a WebSocket bridge (see above). |
Logging #
The package logs through package:logging
under the indi logger. Set the level to Level.FINEST to see every command
sent and received:
Logger.root.level = Level.FINEST;
Logger.root.onRecord.listen((r) => print('${r.loggerName}: ${r.message}'));
Testing your app #
package:indi/testing.dart provides FakeIndiServer, an in-memory server
that behaves like indiserver and its drivers:
final server = FakeIndiServer()
..define(const DefNumberVector(
device: 'Focuser',
name: 'ABS_FOCUS_POSITION',
elements: [DefNumber(name: 'FOCUS_ABSOLUTE_POSITION', value: 1000, max: 50000)],
));
final client = IndiClient.withTransport(server.transport);
await client.connect();
server.update(...); // push changes from the "driver"
server.dropConnections(); // simulate a network failure
Low-level protocol access #
package:indi/protocol.dart exposes the protocol layer: a sealed class per
INDI message, a streaming IndiDecoder and an IndiEncoder, and the number
and timestamp formats. Use it to build proxies, loggers or test tools.
Compatibility #
- INDI protocol 1.7, as implemented by indiserver 1.8 and later. Keep-alive pings need indiserver 2.0 or later; with older servers they are turned off automatically.
- Dart 3.5 or later; Flutter 3.24 or later.
Contributing #
Contributions are welcome. See CONTRIBUTING.md.
License #
MIT. See LICENSE.