Anatomy of a module

An OST module is a shared library (.so), dynamically loaded by the server, that exposes structured data (properties, containing elements) and reacts to writes sent by clients.

Two base classes

  • Basemodule — the bare essentials: properties, elements, events, profile persistence. No dependency on INDI. This is the class to use for any module that doesn’t directly drive an INDI device (business logic, aggregation, processing…).
  • IndiModule (derives from Basemodule) — adds all the machinery for connecting to an INDI server, managing devices, and the commonly useful derived properties (optics, equipment, location…). See the INDI modules chapter.

This chapter only covers Basemodule — everything it describes also applies to IndiModule, which inherits from it.

The file triplet

A minimal module fits in three files, in its own subfolder:

  • <name>.h — declares the class (derived from Basemodule), its constructor/destructor, and the methods it overrides (most often onExternalEvent()).
  • <name>.cpp — implements the constructor (which loads the module’s structure) and the overridden methods. Nothing else is required: a module with no action to handle doesn’t need to override any method.
  • <name>.json — declares the module’s structure: its properties and their elements. Embedded in the binary via a <name>.qrc file (Qt resource) and loaded at startup.
// <nom>.h
#ifndef HELLO_H
#define HELLO_H
#include <basemodule.h>

class Hello : public Basemodule
{
        Q_OBJECT
    public:
        Hello(QString name, QString label, QString profile, QVariantMap availableModuleLibs);
        ~Hello();
};

extern "C" Hello* initialize(QString name, QString label, QString profile, QVariantMap availableModuleLibs);
#endif
// <nom>.cpp
#include "hello.h"

Hello* initialize(QString name, QString label, QString profile, QVariantMap availableModuleLibs)
{
    return new Hello(name, label, profile, availableModuleLibs);
}

Hello::Hello(QString name, QString label, QString profile, QVariantMap availableModuleLibs)
    : Basemodule(name, label, profile, availableModuleLibs)
{
    Q_INIT_RESOURCE(hello);
    loadOstPropertiesFromFile(":hello.json");
}

Hello::~Hello() {}
  • initialize() is the export function, with C linkage (extern "C") so it can be found by its raw name once the .so is dynamically loaded. Its signature is always the same; it merely instantiates the class.
  • loadOstPropertiesFromFile(":name.json") reads the embedded JSON file and builds the properties/elements it describes — see The smallest module.
A module’s git identity

A module usually reports its own identity (commit hash/date/message, version) via setMetadata() in its constructor, from the constants generated by the build (Version::GIT_SHA1, etc.):

setMetadata("description", "Ce que fait ce module");
setMetadata("thisGithash", QString::fromStdString(Version::GIT_SHA1));

setMetadata() is a free-form piece of metadata, with no prior JSON declaration needed.

The event chain

When a client writes a value (or triggers an action), the server calls, in this order, three virtual methods — never directly, always through a final entry point that a module developer never touches or overrides:

onExternalEventRoot()     (final — jamais surchargé)
  → onExternalEventBase()   (validation commune : propriété/élément existe,
                              permission, directedit, mise à jour automatique)
  → onExternalEventIndi()   (hook IndiModule — vide pour un Basemodule)
  → onExternalEvent()        (le module répond ici, s'il a besoin de réagir)

onExternalEventBase() already applies the written value to the datastore by itself if the element (or its property) has autoupdate:true — in that case, onExternalEvent() often has nothing to do at all, or just needs to re-read the applied value in order to react. See Reacting to a write for the details and a common pitfall.

A module with no action of its own to handle (like the hello example above) simply has nothing to override: the default empty implementation of onExternalEvent() is enough.