Public API: iDryer::Link¶
iDryer::Link is the single entry point for the embedded developer. The facade hides the entire SDK stack: WiFi/Improv, cloud state machine, HTTP claim, MQTT, local WebSocket, NVS. The product only needs to fill telemetry/status fields, register callbacks, and call begin()/loop().
Lifecycle¶
Typical main.cpp skeleton:
#include <iDryer.h>
#include <runtime/idryer_runtime.h> // only needed if setCommandHandler is used
static const iDryer::Config CFG = {
.deviceType = iDryer::DeviceType::StorageLink,
.unitsCount = 1,
.hasAirTemp = true,
.hasAirHumidity = true,
.hasHeaterTemp = false,
.hasHeaterPower = false,
.hasFanStatus = false,
.hasScales = false,
.hasRfid = false,
.telemetryPeriodMs = 10000,
.statusPeriodMs = 0,
.hardwareVersion = "1.0",
.firmwareVersion = "1.0.0",
};
static iDryer::Link link(CFG);
void setup() {
link.begin();
// setCommandHandler — strictly AFTER begin(): begin() installs its own dispatcher
link.runtime()->setCommandHandler(handleCommand);
}
void loop() {
link.loop();
link.telemetry.airTempC[0] = sensor.readTemp();
link.telemetry.airHumidityPct[0] = sensor.readHumidity();
}
Configuration: iDryer::Config¶
Filled once in main.cpp, passed to the Link constructor. All fields use aggregate init (C++ designated initializers).
| Field | Type | Purpose | Note |
|---|---|---|---|
deviceType |
DeviceType |
Device type | required |
unitsCount |
uint8_t |
Number of units (chambers), 1..MAX_UNITS (4) |
required |
hasAirTemp |
bool |
Air temperature sensor present | false = field omitted from JSON |
hasAirHumidity |
bool |
Humidity sensor present | false = field omitted from JSON |
hasHeaterTemp |
bool |
Heater temperature sensor present | — |
hasHeaterPower |
bool |
Heater power sensor present | — |
hasFanStatus |
bool |
Fan status present | — |
hasScales |
bool |
Scales present | — |
hasRfid |
bool |
RFID reader present | — |
telemetryPeriodMs |
uint32_t |
Auto-publish period for Telemetry (ms) |
0 = do not publish |
statusPeriodMs |
uint32_t |
Auto-publish period for Status (ms) |
0 = do not publish |
hardwareVersion |
const char* |
Hardware version string | required |
firmwareVersion |
const char* |
Firmware version string | required |
Which integrations go into the image is set by
IDRYER_WITH_HA,IDRYER_WITH_BAMBUandIDRYER_WITH_MOONRAKERin the product'splatformio.ini. A disabled integration is absent from the image and fromintegrations/status; a portal command for it is rejected.
Class iDryer::Link¶
Constructor¶
Takes the configuration by const reference. CFG must exist for the full object lifetime (typically static const).
Methods¶
begin()¶
Brings up the entire SDK stack: WiFi/Improv, cloud state machine, HTTP claim, MQTT, local WebSocket, NVS persistence.
Call once in setup(). Returns true on successful initialization.
loop()¶
The only required tick. Services WiFi/MQTT/LocalAccess, and auto-publishes telemetry and status on their timers.
Call every iteration of loop(). Without this call the connection is not maintained.
Source: iDryer-Storage/src/main.cpp:253, iHeater-link/src/main.cpp:381.
publishTelemetryNow()¶
Immediately publishes the current state of link.telemetry, regardless of the telemetryPeriodMs timer.
publishStatusNow()¶
Immediately publishes the current state of link.status. Use after processing a command when the new state must be reflected in the portal right away.
raiseEvent()¶
Publishes an event to the topic idryer/{serial}/events. Sent immediately.
| Parameter | Type | Purpose |
|---|---|---|
severity |
EventKind |
Info / Warning / Error |
event |
const char* |
Event code, e.g. "OVERHEAT", "SESSION_COMPLETE" |
message |
const char* |
Arbitrary debug text |
unitId |
uint8_t |
Unit index (0..unitsCount-1) or 0xFF for device-wide |
onCommand()¶
Registers a handler for a command by name. It fires when commands/{name} with
an arbitrary JSON payload arrives over MQTT or Local WS.
CommandCallback = void (*)(JsonObjectConst data)
The name is arbitrary: drying, stop, storage, get_config, set,
profile, led.pulse — anything goes. Returns false on overflow (no more
than 12 commands) or invalid arguments.
The built-in commands link_integration, bambu_apply and ping are handled
by the library itself and cannot be intercepted. You may still register a
handler under the same name — it runs after the library's own.
Important: if runtime()->setCommandHandler(...) is set, the full
dispatcher intercepts everything and handlers registered here are not called.
onIntegrationStatus()¶
Called when an integration connection state changes (HA, Bambu, Moonraker). Optional callback.
IntegrationStatusCallback = std::function<void(const iDryer::IntegrationStatus&)>
onClaimComplete()¶
Called once, when the backend has confirmed the binding — before MQTT comes up.
ClaimCompleteCallback = void (*)(const char* deviceId)
isBound()¶
true — the device holds a permanent secret and is bound to an account.
false — setup mode: the device is waiting for a pairing token from the app or
the flasher. A product needs this to show the state to the user.
handleRevoke()¶
Handles the REVOKE command from the portal: wipes the secret and returns the
device to waiting for a token. Called from onCommand("revoke").
mqttKey()¶
The key the device publishes MQTT under: the module serial before binding, the controller serial after it.
isOnline()¶
Returns true if the device is registered and the MQTT session is active.
serial()¶
Device serial number (string from NVS, assigned during claim). Empty string before claim completes.
seedWifiCredentialsIfEmpty()¶
Writes WiFi credentials to NVS only if they are not yet set. Call before begin(). Used in dev environments with hardcoded credentials.
setWifiCredentials()¶
Always overwrites WiFi credentials in NVS. Dev helper and forced re-provisioning.
eraseClaimAndRestart()¶
Removes the device token from NVS and reboots the chip. After reboot the device is unclaimed — the auto-claim flow starts again. This function does not return.
integrationsManager()¶
Outlet to the integrations manager — for product-side wiring (Moonraker chamber target callbacks, Bambu printer status, etc.).
Requires #include <integrations/common/link_integrations_manager.h>.
// iHeater-link/src/main.cpp:337
device().integrationsManager()->setVirtualChamberCallback(onVirtualChamberUpdate);
mqttClient()¶
Outlet to the SDK MQTT client — for components that publish their own topics or integrate into command routing (e.g., MenuBridge).
Requires #include <mqtt/mqtt_client.h>.
devicePublisher()¶
Outlet to the dual-publish helper — sends one payload to both MQTT and Local WS simultaneously. Use for product responses that must reach the LAN client the same way auto-published telemetry does.
runtime()¶
Outlet to the SDK runtime — used to set a full command handler instead of the facade dispatcher. After setCommandHandler(...) the handlers registered via onCommand() are no longer called via the MQTT path.
Important: call strictly after begin() — begin() installs its own dispatcher, which must be overwritten.
// iDryer-Storage/src/main.cpp:249
link.runtime()->setCommandHandler(handleCommand);
// Handler signature:
// void handleCommand(const char* cmd, JsonObjectConst data);
Requires #include <runtime/idryer_runtime.h>.
Telemetry fields¶
Filled by the product in loop(). The SDK reads them on the telemetryPeriodMs timer and publishes to MQTT and Local WS.
| Field | Type | Config flag | Purpose |
|---|---|---|---|
telemetry.airTempC[unitId] |
float |
hasAirTemp |
Air temperature, °C |
telemetry.airHumidityPct[unitId] |
float |
hasAirHumidity |
Humidity, % |
telemetry.heaterTempC[unitId] |
float |
hasHeaterTemp |
Heater temperature, °C |
telemetry.heaterPower01[unitId] |
float |
hasHeaterPower |
Heater power, 0.0..1.0 |
telemetry.fanOn[unitId] |
bool |
hasFanStatus |
Fan status |
telemetry.weightG[unitId] |
uint16_t |
hasScales |
Weight, grams |
// iDryer-Storage/src/main.cpp:267
link.telemetry.airTempC[0] = r.temperature;
link.telemetry.airHumidityPct[0] = r.humidity;
unitId = 0 for the first (or only) unit. The index must be < Config.unitsCount.
Status fields — same structure, but for operational state:
| Field | Type | Purpose |
|---|---|---|
status.mode[unitId] |
UnitMode |
Current unit mode |
status.targetTempC[unitId] |
float |
Target temperature |
status.durationS[unitId] |
uint32_t |
Requested duration, s (0 = indefinite) |
status.elapsedS[unitId] |
uint32_t |
Time elapsed since session start, s |
// iHeater-link/src/main.cpp:229
device().status.mode[0] = iDryer::UnitMode::Drying;
device().status.targetTempC[0] = cmd.targetTempC;
device().publishStatusNow();
Callback registration via runtime¶
If full control over incoming commands is needed (e.g., the product handles get_config, set, non-standard invoke):
// Signature — from idryer_runtime.h
void handleCommand(const char* cmd, JsonObjectConst data);
// Registration — strictly after link.begin()
link.runtime()->setCommandHandler(handleCommand);
cmd — command string ("set", "invoke", "ping", "get_config").
data — ArduinoJson JsonObjectConst with payload.
With this approach, handlers from onCommand() are not called from the MQTT path — the product parses commands itself.
Enumerations¶
iDryer::DeviceType¶
| Value | Numeric | Purpose |
|---|---|---|
Unknown |
0 | None / undefined |
Dryer |
1 | Dryer (iDryer LINK) |
Heater |
2 | Heater |
StorageLink |
4 | Storage Link (ESP32-C3 + LED) |
IHeaterLink |
5 | iHeater Link |
iDryer::UnitMode¶
Idle, Drying, Storage, Profile, Fault, Unknown
iDryer::EventKind¶
Info, Warning, Error
iDryer::RequestKind¶
Start, Stop, Storage, Find, ClearErrors
iDryer::IntegrationKind¶
Ha, Bambu, Moonraker
iDryer::IntegrationState¶
Disabled, Idle, Connecting, Online, ConfigMissing, Error
When to go deeper¶
The facade is sufficient for most tasks. If you need to work below the facade level — with idryer::IdryerRuntime, idryer::MqttClient, idryer::cloud::LinkIntegrationsManager — see the Architecture section.