Plugins

Covers Engine Plugin SDK 2.0.0 · Interface Plugin SDK 0.2.0

Storage and Settings

Choose where settings, private device configuration, runtime files, and shared assets belong.

Plugins often need to remember settings, read device configuration, or save files such as calibration data and operator layouts. This page explains where that data belongs and which SDK helpers to use.

The main choice is whether the data should travel with the workspace, stay private to one Engine, or ship with the plugin. The examples below cover each case.

You want to save…Start with…
A plugin-wide default or project overrideLayered settings.
A device connection or task mappingModule/task configuration.
Private credentials, recordings, or generated dataInstance folders.
A shared calibration file, layout, or modelWorkspace assets.
Files shipped with your pluginBundled assets.

The examples use my_driver as the manifest ID. Interface calls use operation from useDartwic(); C++ calls use an SDK_API& api. The Interface and Engine storage references cover the full APIs.

Storage scopes

A workspace is the portable folder you commit and share. Keep machine-specific state outside it. In this guide, the installation is engine/, the workspace is workspace/, and the example project is default.

LocationWhat belongs there
InstallationExecutables, installed plugins, SDKs, and bundled assets.
InstanceOne Engine’s local credentials, operational data, and caches.
WorkspaceShared defaults in settings.json and reusable files under assets/.
ProjectModules, tasks, scripts, schematics, and that project’s settings.json overrides.
InterfacePersonal theme/layout preferences in desktop .dartwic/ or browser storage.

Instance folders

Under engine/instances/<instance>/, project data uses three directories:

DirectoryPurpose
settings/<project>/Durable private configuration, such as credentials.
runtime/<project>/Recordings, logs, resume values, and dataframe state.
cache/<project>/Regenerable files, such as native builds and temporary export parts.

Only cache data should be disposable. Keep needed recordings and private settings when clearing generated files. Configuration and Storage has the Engine paths; Tracking a Workspace in GitHub covers sharing the complete workspace.

Module configuration and private values

Device connections and task mappings already have a home in the project. A module’s connection parameters normally live in modules/<instance>.json, under parameters; task scheduling and mappings belong to task configuration. Use those resources so the plugin and its editors work with the same configuration. Building a Hardware Driver shows both, and Config Map explains how to edit them in the Interface.

If a device also needs credentials specific to one Engine, keep those in the instance’s private settings folder:

using Json = nlohmann::json;
const auto privateConfig = api.readStorageJson(
    "settings", "my_driver/device_credentials.json", Json::object(), "default");
const auto token = privateConfig.value("token", std::string{});
if (token.empty()) throw std::runtime_error("Configure this Engine's device token.");

Save with writeStorageJson("settings", path, value, project). This stores local JSON without encryption. The helpers do not detect secrets placed in workspace files.

Defaults and overrides

Plugin-wide options, such as a discovery interval or display precision, can have defaults in code, a shared workspace value, and an override for one project:

Code defaults → workspace/settings.json → workspace/<project>/settings.json
                  Shared choice             Project exception

Store plugin settings under plugins.<plugin-id>. Save only intentional changes, so later workspace/default changes can still reach projects that inherit them.

RuleEffect
Objects merge recursivelyA patch can change one field without replacing its neighbors.
Arrays replaceSaving an array replaces that entire array.
false, 0, "", and null are valuesUse reset to inherit again.

Read and change a setting

A read returns the effective settings after merging the layers, plus a revision identifying the version you read. Supply that revision when saving to prevent an older edit from overwriting newer changes. sources identifies each setting’s layer.

This example changes a plugin’s discovery interval for one project:

import {createStorageClient} from '@dartwic/interface-sdk/storage';

const storage = createStorageClient(operation);
const project = 'default'; // Capture the intended project before asynchronous work.
const defaults = {plugins: {my_driver: {discovery_interval_ms: 1000}}};
const snapshot = await storage.readEffectiveSettings(defaults, project);
const interval = snapshot.effective.plugins.my_driver.discovery_interval_ms;

await storage.writeSettingsOverride('project', {
    plugins: {my_driver: {discovery_interval_ms: 2000}},
}, {project, revision: snapshot.revision});

Use 'workspace' instead for a choice shared by all projects. Capture the selected project once before a load or delayed save; a project switch must not redirect work already in progress. Validate your plugin’s fields before applying them.

Engine plugins use the same model:

using Json = nlohmann::json;
const std::string project = "default";
const Json defaults = {{"plugins", {{"my_driver", {{"discovery_interval_ms", 1000}}}}}};
const auto snapshot = api.readEffectiveSettings(defaults, project);
api.writeSettingsOverride("project", {
    {"plugins", {{"my_driver", {{"discovery_interval_ms", 2000}}}}}
}, Json::array(), project, snapshot.at("revision").get<std::string>());

Reset an override

Reset an override to inherit from the layer below it again. Identify the setting with its JSON pointer:

const saved = await storage.readEffectiveSettings(defaults, project);
await storage.resetSettingsOverride('project', [
    '/plugins/my_driver/discovery_interval_ms',
], {project, revision: saved.revision});

Resetting a project override reveals the workspace value; resetting a workspace override reveals the code default unless a project overrides it. Resetting a section removes all its overrides at that scope. In pointer tokens, escape / as ~1 and ~ as ~0.

Read back with your defaults after saving to check the effective value; the write response does not retain defaults from an earlier read. On a revision conflict, reload the settings and reconcile the edit before saving again.

Apply settings to running code

Saving a setting makes it persistent, but does not automatically change running code. At load time, the Engine merges plugin settings into the configuration passed to the factory without rewriting plugin.json. For later changes, your plugin must reread and safely apply them, or require a restart. Keep file reads and JSON parsing outside high-frequency task loops.

Core policies also have their own timing; for example, recording configuration requires an Engine restart.

Autosaved settings panels

Autosave lets someone edit settings without pressing a Save button. The example plugin uses a 500 ms debounce for display precision and unit labels. Its interface/src/settings.mjs, settingsAutosave.mjs, and useDisplaySettings.jsx show the storage calls, save queue, and React integration.

When building a similar panel:

  • Validate drafts before queuing; an empty numeric input is not automatically zero.
  • Queue only changed fields and save them sequentially, using each returned revision.
  • Keep newer user edits when an older save response arrives.
  • Flush on blur/cleanup and show pending, saved, or failed state. Keep failed changes available for retry; reload and reconcile revision conflicts.

The queue is example code, not an SDK export. Use command APIs for device commands and Interface storage for personal display preferences.

Settings persistence checks

Before relying on a settings panel, read back through the same helper and captured project. Check that only intended overrides were saved, reset restores inheritance, and a stale revision or disconnect leaves the UI reporting an unsaved change.

Scoped file helpers

Calibration documents, generated previews, and other files can use scoped helpers to select their storage location. Each helper chooses a root and validates the relative path:

HelperRoot
installationPath / instancePathInstalled software / current Engine instance.
workspacePath / projectPathPortable workspace / selected project.
projectSettingsPathInstance settings/<project>/.
projectRuntimePathInstance runtime/<project>/.
projectCachePathInstance cache/<project>/.

For example, save project-specific calibration and a disposable preview separately:

import {createStorageClient, projectPath, projectCachePath} from '@dartwic/interface-sdk/storage';
const storage = createStorageClient(operation);
await storage.writeJson(projectPath('my_driver/calibration.json', 'default'), {
    version: 1, offset: 0.12,
});
const calibration = await storage.readJson(projectPath('my_driver/calibration.json', 'default'));
await storage.writeJson(projectCachePath('my_driver/preview.json', 'default'), {
    generated_from: 'my_driver/calibration.json',
});

Interface helpers return Engine file references, not paths on the browser’s computer. C++ path helpers return std::filesystem::path. Pass an explicit project for delayed operations; Interface instance-subdirectory helpers require it.

Engine code uses readStorageJson / writeStorageJson with scopes installation, instance, workspace, project, settings, runtime, or cache. A write atomically replaces one file; your plugin handles schema changes and coordination across files.

General storage/settings calls require admin access unless explicitly allowed. Relative-path checks help prevent misplaced files, but native plugins still run with the Engine process’s filesystem access.

Shared and bundled assets

An asset is a reusable file referenced by a project, such as a layout or model. Keep user-authored assets separate from files distributed with the plugin:

KindLocation and lifetime
Shared plugin-authored fileworkspace/assets/plugins/<plugin-id>/; travels with the workspace.
Other shared assetworkspace/assets/<namespace>/; reusable across projects.
Bundled fileInstalled plugin’s assets/; replaced by updates and removed on uninstall.

Shared plugin files

Workspace asset helpers are useful when several projects need the same layout, model, or calibration file. Use your manifest ID to group plugin-authored files, and save the returned reference in the schematic or resource:

import {createPluginWorkspaceAssetClient} from '@dartwic/interface-sdk/assets';
const authored = createPluginWorkspaceAssetClient(operation, 'my_driver');
const reference = await authored.saveJson('layouts/operator.json', {version: 1});
const layout = await authored.readJson('layouts/operator.json');
// reference: assets/plugins/my_driver/layouts/operator.json

Subdirectories are supported and filenames need an extension. Use createWorkspaceAssetClient for other shared namespaces. For example, the 3D model node stores a shared model package and keeps asset_path in its node data.

Asset helpers read and write UTF-8 text or JSON. Encode binary content in a validated text format, and validate loaded documents against your plugin’s schema. Reads are cached per operation callback; invalidate after external edits or use a new filename. Failed reads are evicted.

An asset may be used by several projects. Check those references before deleting a shared file.

Engine plugins can save and read the same assets. Here node_name is the target Engine node and layout is your JSON document:

const auto reference = api.savePluginWorkspaceAsset(
    node_name, "my_driver", "layouts/operator.json", layout.dump());
const auto text = api.readPluginWorkspaceAsset(
    node_name, "my_driver", "layouts/operator.json");

Bundled files

Files that ship with the plugin, such as labels or default layouts, belong in its source assets/ directory. The build copies them into each installed side. These files are replaced on update and removed on uninstall, so save user-authored changes through workspace helpers.

import {createBundledPluginAssetClient} from '@dartwic/interface-sdk/assets';
// Use after the Interface plugin runtime has registered its host adapter.
const bundled = createBundledPluginAssetClient('my_driver');
const labels = await bundled.readJson('labels.json');

Bundled readers are read-only, and installation leaves these files in the plugin directory. Engine plugins locate them with pluginAssetPath:

const auto bundled_labels = api.pluginAssetPath("my_driver", "labels.json");

C++ file/asset calls are synchronous and can block or throw. Interface helpers return Promises; awaiting a write confirms file-operation completion. Keep file/network work outside timing-sensitive callbacks.