Skip to main content
Version: 0.1

Modules

Every driver and plugin in Scadex is a module: an optional package with its own version, its own licence and its own log, which the gateway can start, stop, install and remove while it runs. This page is for engineers and administrators who operate a gateway. The module pages that follow describe each first-party module.

What a module is​

A module is a folder with a project (a .NET assembly), a LICENSE and a module.json:

{
"id": "io.scadex.driver.mqtt",
"name": "MQTT Driver",
"version": "1.0.0",
"vendor": "scadex.io",
"license": "LicenseRef-Scadex-Proprietary",
"sdkVersion": "1.2",
"requiresLicense": false,
"assembly": "Scadex.Modules.Mqtt.dll"
}
FieldMeaning
idStable identity, reverse-DNS by convention. io.scadex.* is first-party; the scadex. prefix is reserved. Must match the id in the module's code.
nameThe name the dashboard shows.
versionThe module's own semantic version. It also becomes the assembly version.
vendorWho made it.
licenseAn SPDX expression such as Apache-2.0, or a LicenseRef-* for proprietary terms.
sdkVersionThe major.minor of Scadex.Sdk the module was built against. It loads on a gateway with the same major and the same or a newer minor.
requiresLicenseOptional. When true, the module loads only as a signed package with a valid signed grant. See Licensing.
assemblyThe DLL to load.

The gateway reads module.json before loading any code, so a disabled or incompatible module never executes. A module that fails to load is reported as failed with its error; the gateway and the other modules keep running.

Every module is optional. The gateway runs with any combination of them, including none, with the tags it already has.

First-party modules​

IdConnects toContract
io.scadex.driver.opcua (OPC UA)OPC UA servers, with optional security and user credentialsSDK 1.2
io.scadex.driver.mqtt (MQTT)MQTT brokers over mqtt, mqtts, ws and wss, with optional credentialsSDK 1.2
io.scadex.historian (Historian)Nothing external: keeps accepted values in a file in its own data directorySDK 1.2
io.scadex.cron (Cron jobs)Nothing external: runs scripts and tag writes on a scheduleSDK 1.3

All four are at version 1.0.0, vendor scadex.io, licence LicenseRef-Scadex-Proprietary, and none requires a licence grant.

States​

GET /api/v1/runtime/modules reports each module's state:

StateMeaning
starting, stoppingA change is in progress. Controls wait until it settles.
runningLoaded and started. Its routes are live.
stoppedAn operator stopped it. It stays stopped when the gateway restarts, until it is started again (enabled is false).
failedIt could not load or start, or threw while starting. error says why. Starting it again reads its folder afresh.
disabledSwitched off in configuration. An operator cannot override that; lockedByConfiguration is true.
incompatibleIts sdkVersion is not one this gateway provides. Refused before any code ran.
unlicensedA licence is required and no valid grant was found.
unsignedA licence is required and the package has no module.sig.
tamperedThe package has a module.sig that does not verify.

Each module also reports its origin (built-in when it ships in the image, installed otherwise), whether it is removable, when it startedAt, how many restarts it has had, and a log summary with error and warning counts.

Lifecycle​

  • Stop cancels the module's work through its own StopAsync, with 30 seconds to finish before the gateway moves on without it. Its routes and value reader are taken away, and every tag it owns is marked bad, keeping the last value, with a detail saying why. A stopped module stays stopped across gateway restarts.
  • Start and restart load the module's code into a fresh, collectible load context, so they always run the files on disk now. The module is expected to publish fresh values rather than assume the old ones are current.
  • A module that throws, hangs or will not load ends up failed or stopped and nothing else. The gateway and the other modules carry on.
  • Operations on one module are serialised: a start cannot overlap a stop.

Choosing modules​

At build time​

SCADEX_MODULES picks which module folders go into the image:

docker compose build # all of them
SCADEX_MODULES="mqtt" docker compose build # just these folder names
SCADEX_MODULES="" docker compose build # core only

As a drop-in folder​

Each subfolder of the /modules mount is one installed module. In compose.yaml that mount is ./.local/modules. Copy a module's published folder there and restart the gateway:

mkdir -p .local/modules
docker run --rm -u "$(id -u):$(id -g)" -e HOME=/tmp -v "$PWD:/src" -w /src \
mcr.microsoft.com/dotnet/sdk:10.0.403 scripts/stage-modules.sh Release .local/modules mqtt
docker compose restart gateway

A module there replaces a built-in one with the same id, including its stored connections and credentials, so treat write access to this folder as admin access. A broken folder never replaces a working built-in.

In configuration​

SCADEX_DISABLED_MODULES (Modules__Disabled outside compose) is a comma-separated list of module ids to leave unloaded. Stored connections and tag bindings are kept for when the module is enabled again. A module disabled this way shows as disabled and cannot be started from the dashboard; only the configuration can change it.

At run time​

Open Gateway, then System, then Modules (the Gateway group is shown to administrators; engineers reach the page at /modules). The page lists every module with its state, version, where it came from (Built in or Installed), how long it has been running and its error and warning counts. Only engineers and administrators can open it.

  • Each row has Start, Restart and Stop buttons.
  • Selecting a module opens a side panel with Overview, Logs and Settings. Overview shows version and vendor, where it came from, uptime, restarts, licence, kind, the SDK it was built for, its log summary, its value-stream position where it has one, and its capabilities.
  • Settings has the Log level and, for installed modules, Remove module and Remove and delete its data.
  • Logs shows the module's own log, with filters and a download of the kept log files.
  • Administrators see Add module, which opens a panel to drop or pick a .zip package. The option Start it as soon as it is installed is on by default; leave it off to look at the module first. When the id is already installed, the dashboard asks before replacing it.

Old links to /settings/modules redirect to /modules.

Installing and removing​

A package is a zip of the module's folder with module.json at its top, or inside one folder. The upload is unpacked beside the installed modules and held to every rule a module on disk is held to, and nothing in it runs until it passes: a valid manifest that matches the code, an SDK version this gateway provides, and, when the module declares a licence or carries a module.sig, a valid signature.

Refused with 400: paths that climb out of the folder, symbolic links, more than 5,000 files, more than 400 MB unpacked, and an upload over 200 MB. A refused package leaves nothing behind.

RequestRolePurpose
GET /api/v1/runtime/modulesviewerEvery installed module and its state
GET /api/v1/runtime/modules/{id}viewerOne module
POST /api/v1/runtime/modules/{id}/startengineerStarts a stopped or failed module; answers with the module, so check its state
POST /api/v1/runtime/modules/{id}/stopengineerStops it and keeps it stopped across restarts
POST /api/v1/runtime/modules/{id}/restartengineerStops, reloads the code and starts
POST /api/v1/runtime/modules?replace=&start=adminInstalls a module from a zip, sent as the multipart field file
DELETE /api/v1/runtime/modules/{id}?purgeData=adminRemoves an installed module
  • Installing an id that is already installed answers 409 unless replace=true. With it the running module is stopped, its folder swapped for the new one, and the new one started; its connections, settings and tags are untouched.
  • start=false installs without starting; the module stays stopped until someone starts it.
  • An installed module shadows a built-in one with the same id, as a drop-in folder does.
  • Removing deletes the package and stops the module. Built-in modules can be stopped but not removed (409). Removing an installed module that shadowed a built-in hands over to the built-in again.
  • What the module stored (connections, settings, credentials, data directory and log) is kept so that installing it again picks up where it left off, unless purgeData=true.
warning

Tags are never deleted by removing a module. They stay bound to the module, marked bad, until someone deletes them, and the reply says how many are still bound.

Example upload from the command line:

(cd .local/modules && zip -r ../mqtt.zip io.scadex.driver.mqtt)
curl -b cookies.txt -H "Origin: https://app.example.com" -F file=@.local/mqtt.zip \
"https://app.example.com/api/v1/runtime/modules?replace=true"

Module logs​

Every module writes through the logger the gateway gives it, and the gateway keeps that log apart from its own.

  • The last 2,000 lines of each module are held in memory for the dashboard and the API.
  • Each module also writes rotating files at DATA_DIR/logs/modules/{id}/module.log, up to 2 MB each with the newest five kept. They survive restarts.
  • Each line has a time, level, message, optional exception, and the instance it concerns (a connection, a broker) when the module wrote it under a scope or with an instance name. Lines the gateway wrote about the module (started, stopped, failed, who asked, which tags it marked bad) are in the same log with source: "host".
  • Each module has its own level, information by default. Changing it takes effect at once and is remembered in DATA_DIR/modules.json, which also remembers which modules an operator stopped. Delete that file to forget both.
  • Credentials are masked before a line is kept, shown or written: password=, secret=, token=, api-key= and authorization: values, the password in scheme://user:password@host, and bearer tokens. Only these common shapes are recognised.
  • The gateway's own log (Diagnostics, then Logs) does not carry module lines. They still reach the container's console under the category Module.{id}.
RequestRolePurpose
PUT /api/v1/runtime/modules/{id}/log-levelengineerBody {"level": ...}: debug, information, warning, error (also trace, critical)
GET /api/v1/runtime/modules/{id}/logsengineerNewest first, filtered by level, search, instance, before, after, limit; nextBefore pages
GET /api/v1/runtime/modules/{id}/logs/stream?level&afterengineerServer-sent events: the backlog since after, then each line as it is written, with a keepalive every 15 seconds
GET /api/v1/runtime/modules/{id}/logs/downloadengineerThe kept log files, oldest first, as text

Module routes​

A module may expose its own HTTP routes. The gateway maps them beneath /api/v1/modules/{id}, applies the role policies (a GET needs viewer, other methods need engineer unless the module marks them as writing tag values), and reports them in GET /api/v1/runtime/modules. Drivers also get a baseline set of connection routes mapped for them; see Writing a module.

A module's routes exist while it runs. While a module is stopped, starting or failed, its URLs answer 503 "Module unavailable" rather than 404, so a caller can tell a stopped module from a missing one. A module that was never loaded (disabled in configuration, unlicensed, refused) has no routes and answers 404.

In this section​

  • OPC UA: connect OPC UA servers and bind their nodes to tags.
  • MQTT: connect brokers and turn topics into tags.
  • Historian: keep every accepted value for 90 days.
  • Cron jobs: run scripts and tag writes on a schedule.
  • Writing a module: the SDK contracts for developers.
  • Licensing: per-component licences, grants and signed packages.