How to integrate a composite robot

This guide connects an Android tablet and robot controller as two DDI devices inside one customer-visible robot, forwards MCU health through the controller, and assigns OTA installation ownership for all three computers.

Read Composite robot terminal design first for the identity counts and trade-offs.

Prerequisites

  • one registrar-created robot_asset_id;

  • registered model and serial pairs for the tablet and robot controller;

  • separate bootstrap credentials for both devices;

  • the Android/Kotlin SDK on the tablet and the C SDK on the controller;

  • a protected key store on each directly connected device;

  • an AOA framing protocol with producer, component, sequence, and payload fields;

  • product-specific Android, controller, and MCU installers; and

  • Dev2M releases and OTAForge assignment policy for the two OTA targets.

The examples use placeholders. Never place bootstrap credentials, enrollment tokens, private keys, or certificate bodies in logs or source control.

1. Register the asset and component graph

Create one logical asset, then attach two registered DDI devices and one MCU component:

robot_asset_id: robot-00042
├── tablet serial: TAB-00042       role: tablet
└── controller serial: ROB-00042   role: controller
    └── MCU serial: IMU-00042      role: imu_mcu

Perform this through the platform registrar or manufacturing workflow, not an untrusted device assertion. Store robot_asset_id and component_role as inventory metadata that downstream telemetry consumers can join to each certificate-derived thing_id.

Verification: the platform shows one robot asset with two DDI identities. The MCU appears in component inventory but has no certificate or broker client.

2. Provision both DDI identities

On Android, use ProvisioningClient.identify, ProvisioningCsr.generate, and ProvisioningClient.provision, then build an MqttConfig from the returned tablet certificate and private key. Run blocking provisioning calls on Dispatchers.IO.

On the controller, use:

fgai_provisioning_client_t *provisioner =
    fgai_provisioning_client_new("https://things.flexgalaxy.ai", bootstrap_bearer);

fgai_provisioning_identity_t identity = {0};
fgai_provisioning_keypair_t keypair = {0};
fgai_provisioning_result_t issued = {0};

fgai_provisioning_identify(
    provisioner, "ROBOT-CONTROLLER", "ROB-00042",
    NULL, NULL, firmware_version, hardware_revision, &identity);
fgai_provisioning_build_csr(
    "ROB-00042", "ROBOT-CONTROLLER", identity.thing_id, 0, &keypair);
fgai_provisioning_provision(
    provisioner, identity.thing_id, identity.enrollment_token,
    keypair.csr_pem, NULL, &issued);

Check every C return value in production and free each result with its matching fgai_provisioning_*_free function. Persist the key, leaf, and CA atomically. The repository’s provision_robust.c is the complete checked implementation.

Verification: the tablet and controller have different thing_id values and certificate SANs. Neither device can read the other’s private key.

3. Connect two clients to the shared broker

The tablet creates one Android MQTT client:

val tabletClient = FgaiMqttClient(
    MqttConfig(
        host = "things.flexgalaxy.ai",
        port = 8883,
        deviceId = tabletThingId,
        caCertPem = caPem,
        clientCertPem = tabletCertPem,
        clientKeyPem = tabletKeyPem,
    )
)
tabletClient.connect()

The controller creates a separate C client:

fgai_mqtt_config_t *config = fgai_mqtt_config_new("things.flexgalaxy.ai", 8883);
fgai_mqtt_config_set_device_id(config, controller_thing_id);
fgai_mqtt_config_set_tls(config, "ca.crt", "device.crt", "device.key");
fgai_mqtt_client_t *controller = fgai_mqtt_client_new(config);
fgai_mqtt_client_connect(controller);

Both clients use the same broker hostname, not separate brokers. Their stable client IDs and permitted topic subtrees differ because their thing_id values differ.

If the controller uses the tablet’s network, expose ordinary IP connectivity such as tethering and let the controller establish mTLS itself. Do not terminate the controller’s DDI session in ClearJanitor or reuse the tablet certificate.

Verification: the broker observes two concurrent client IDs, fgai-<tablet_thing_id> and fgai-<controller_thing_id>.

4. Publish ClearJanitor cleaning-job telemetry

Publish numeric job outcomes from the tablet and identify the producer with tags:

tabletClient.publishTelemetry(
    metrics = mapOf(
        "job_duration_seconds" to job.durationSeconds.toDouble(),
        "cleaned_area_m2" to job.cleanedAreaM2,
        "water_used_ml" to job.waterUsedMl.toDouble(),
    ),
    tags = mapOf(
        "producer" to "clearjanitor",
        "applet_id" to "clearjanitor",
        "robot_asset_id" to robotAssetId,
        "job_id" to job.id,
    ),
)

Keep job descriptions, maps, and other structured records in an application API or object store. The current DDI telemetry helper accepts only numeric metrics and string tags. Treat robot_asset_id as query context; the platform must derive authorization and ownership from the certificate identity and registrar relationship.

Verification: the message arrives on device/<tablet_thing_id>/telemetry with the tablet certificate identity and the producer=clearjanitor tag.

5. Publish runnerchecker and MCU telemetry

runnerchecker publishes through the controller client:

fgai_mqtt_metric_t motor_metrics[] = {
    {"motor_current_a", motor_current_a},
    {"motor_temperature_c", motor_temperature_c},
    {"motor_health_score", motor_health_score},
};
fgai_mqtt_tag_t motor_tags[] = {
    {"producer", "runnerchecker"},
    {"component", "drive_motor_left"},
    {"robot_asset_id", robot_asset_id},
};
fgai_mqtt_telemetry_t motor_sample = FGAI_MQTT_TELEMETRY_INIT;
motor_sample.metrics = motor_metrics;
motor_sample.n_metrics = 3;
motor_sample.tags = motor_tags;
motor_sample.n_tags = 3;
fgai_mqtt_client_publish_telemetry(controller, &motor_sample);

Read raw IMU measurements locally, then publish bounded summaries through the same controller client:

fgai_mqtt_metric_t imu_metrics[] = {
    {"accel_rms_mps2", accel_rms_mps2},
    {"gyro_peak_dps", gyro_peak_dps},
    {"imu_temperature_c", imu_temperature_c},
};
fgai_mqtt_tag_t imu_tags[] = {
    {"producer", "mcu"},
    {"component_id", "imu-01"},
    {"robot_asset_id", robot_asset_id},
};
fgai_mqtt_telemetry_t imu_sample = FGAI_MQTT_TELEMETRY_INIT;
imu_sample.metrics = imu_metrics;
imu_sample.n_metrics = 3;
imu_sample.tags = imu_tags;
imu_sample.n_tags = 3;
fgai_mqtt_client_publish_telemetry(controller, &imu_sample);

The controller owns the DDI sequence and publish timestamp. Preserve the MCU sequence and measurement age in a future batch schema if exact source timing is required; the current convenience API cannot override observed_at.

Verification: both messages arrive on device/<controller_thing_id>/telemetry, separated by their producer and component tags.

6. Keep AOA local and identity-neutral

Define an AOA envelope that is independent of cloud credentials. At minimum it should carry a local producer ID, component ID, monotonically increasing sequence, payload type, payload length, and checksum. The receiver maps that local producer to its own DDI telemetry call.

Do not send DDI private keys, bootstrap credentials, or enrollment tokens over AOA. Do not treat “USB cable connected” as proof that two registered devices belong to the same customer asset; the platform registry is authoritative.

Verification: disconnecting AOA stops local cross-component data without changing either device’s certificate or cloud ownership relation.

7. Implement three installers behind two OTA targets

The tablet polls its certificate-bound OTA path and hands a verified artifact to a privileged Android installer. The Android SDK version 1.1.0 has a known gap: provisionDevice() does not populate the internal enrollment store required by DdiDeviceSdk.ota. Do not claim tablet OTA is complete until the binding has a public, tested provisioning-to-store hand-off.

The controller creates its OTA client with the same certificate files used for DDI:

fgai_ota_client_t *ota = fgai_ota_client_new(
    "https://things.flexgalaxy.ai",
    "ca.crt",
    "device.crt",
    "device.key");
fgai_ota_response_t root = {0};
fgai_ota_error_t result = fgai_ota_poll(ota, &root);

For a robot-runtime artifact, stage it in the inactive slot, verify compatibility, switch atomically, reboot, and report the running version. For an MCU artifact, the controller must:

  1. download it through fgai_ota_download with the expected SHA-256;

  2. verify model, hardware revision, image signature, and rollback policy;

  3. enter the MCU bootloader and flash a recoverable bank;

  4. read back or boot-test the new MCU version; and

  5. call fgai_ota_feedback with success or failure details.

The SDK preserves HawkBit’s HAL document. Application code must inspect the assignment metadata and dispatch each artifact to the correct installer; the SDK intentionally does not infer an installer from a filename.

Verification: run separate isolated assignments for tablet, robot runtime, and MCU firmware. Each must update only its intended component and produce feedback under the tablet or controller certificate that polled the assignment.

8. Exercise replacement and failure paths

Before production rollout, prove these cases:

  • replace the tablet while retaining the logical robot and controller history;

  • reject a certificate whose thing_id does not match its MQTT topic;

  • reconnect and re-subscribe both MQTT clients after broker interruption;

  • lose AOA while both DDI sessions remain correctly identified;

  • reject a foreign or non-OTA HAL link;

  • reject a bad SHA-256 without publishing the temporary artifact;

  • fail an MCU flash and recover its prior bank; and

  • prevent a controller artifact from reaching the Android or MCU installer.

Troubleshooting

Symptom

Cause

Fix

broker disconnects on publish

certificate identity and topic thing_id differ

use the device’s own registration and frozen topic tree

platform shows two customer robots

tablet and controller were not attached to one logical asset

correct the registrar relationship; do not merge certificates

MCU appears as a third online DDI device

it was registered as a direct client despite being controller-proxied

retain a component record but remove the unnecessary DDI credential

IMU traffic overwhelms the controller or broker

raw high-rate samples were published individually

filter, aggregate, and rate-limit locally; design a batch schema if raw data is required

tablet OTA accessor rejects the device as unenrolled

Android 1.1.0 provisioning does not populate its internal store

implement and test the provisioning-to-store hand-off before enabling tablet OTA

robot is online only when the tablet process is running

the controller’s DDI session was incorrectly terminated in the app

provide IP transport and let the controller establish its own mTLS connection

Related references: Device operations, SDK reference, Errors and recovery, and Runtime surface.