Sensor Agent Patterns — Four Common MCU Agent Designs

// last reviewed 2026-05-22 · Marcus Rüb

Sensor Agent Patterns

Four patterns cover the majority of production MCU agent designs: anomaly-detect-and-alert, threshold-with-explanation, sensor-fusion-then-report, and local-decision-then-actuate — each trades off local compute, communication frequency, and response latency differently.

These patterns are not mutually exclusive. A real agent often combines two or three. They are described separately here to make the tradeoffs clear.

Pattern 1 — Anomaly-Detect-and-Alert

What it does: Runs a compact anomaly or classification model on raw or pre-processed sensor data. Only transmits when the model indicates an anomaly. Normal operation produces zero traffic.

When to use it: When the event of interest is rare, the raw sensor stream is high-bandwidth (audio, vibration, image), and you need to avoid streaming gigabytes of normal data to detect a handful of real events.

Tradeoffs: Requires on-device ML inference (model training, quantization, deployment pipeline). False-negative rate depends entirely on model quality. Cannot be debugged by reading MQTT traffic during normal operation.

/* Pattern 1 pseudocode: anomaly detect and alert */
void vAnomalyAgentTask(void *pvParam) {
    float feature_vec[FEATURE_LEN];
    float score;

    for (;;) {
        sensor_read_window(feature_vec, WINDOW_SIZE); /* blocks until window full */
        dsp_extract_features(feature_vec);            /* RMS, spectral centroid, etc. */

        score = tflm_run_inference(feature_vec);      /* 0.0 = normal, 1.0 = anomaly */

        if (score > ANOMALY_THRESHOLD) {
            /* Only transmit on anomaly — silent during normal operation */
            mqtt_publish_event(TOPIC_EVENT,
                build_json(DEVICE_ID, "anomaly", score));
        }
        /* No publish on normal: zero idle traffic */
    }
}

Key spec decision: Set ANOMALY_THRESHOLD conservatively high to reduce false positives. Route low-confidence detections (score between 0.5 and ANOMALY_THRESHOLD) to a delegate topic for human or cloud review.

Pattern 2 — Threshold-with-Explanation

What it does: Applies rule-based threshold detection but bundles sensor context — neighboring values, trend direction, recent history — with the alert. The receiver can determine whether the alert is genuine without querying the device.

When to use it: When the event is predictable (temperature out of range, pressure spike) but the response depends on context the device can compute locally (rate of change, duration, co-occurring conditions).

Tradeoffs: No ML required. Context payload size can grow; keep it bounded. Does not handle novel events that the threshold does not anticipate.

/* Pattern 2 pseudocode: threshold with explanation context */
typedef struct {
    float current;
    float delta_10s;       /* rate of change over last 10 seconds */
    float mean_60s;        /* rolling mean over last minute */
    uint32_t duration_ms;  /* time above threshold */
    char direction[8];     /* "rising" or "falling" */
} SensorContext_t;

void vThresholdAgentTask(void *pvParam) {
    SensorContext_t ctx;
    bool in_alert = false;

    for (;;) {
        sensor_update_context(&ctx);  /* updates all context fields */

        if (ctx.current > ALERT_HIGH && !in_alert) {
            in_alert = true;
            mqtt_publish_json(TOPIC_EVENT, 
                "{\"event\":\"high\","
                "\"value\":%.2f,\"delta\":%.3f,"
                "\"mean_60s\":%.2f,\"dir\":\"%s\"}",
                ctx.current, ctx.delta_10s,
                ctx.mean_60s, ctx.direction);
        } else if (ctx.current < CLEAR_LOW && in_alert) {
            in_alert = false;
            mqtt_publish_json(TOPIC_EVENT,
                "{\"event\":\"clear\",\"value\":%.2f}",
                ctx.current);
        }
        vTaskDelay(pdMS_TO_TICKS(SAMPLE_MS));
    }
}

Key spec decision: Separate the alert threshold from the clear threshold (hysteresis). Without hysteresis, a reading oscillating at the threshold produces a torrent of messages.

Pattern 3 — Sensor Fusion Then Report

What it does: Combines readings from multiple physical sensors into a single higher-level report. The downstream consumer receives a derived, actionable value rather than raw ADC counts or I2C register values.

When to use it: When individual sensors are unreliable or ambiguous in isolation — temperature alone says nothing about human comfort; temperature + humidity + air speed together compute an effective temperature that does. Multiple vibration axes fused into a single bearing health score.

Tradeoffs: Fusion computation runs on the MCU. Complex fusion (Kalman filter, sensor drift compensation) can be CPU-intensive. Calibration for each sensor instance is critical.

/* Pattern 3 pseudocode: multi-sensor fusion and report */
typedef struct {
    float temp_c;
    float humidity_pct;
    float pressure_hpa;
    float co2_ppm;
} EnvRaw_t;

typedef struct {
    float iaq_index;       /* 0-500, IAQ standard */
    float dew_point_c;
    uint8_t comfort_level; /* 0=good, 1=moderate, 2=poor */
} EnvFused_t;

void sensor_fusion(const EnvRaw_t *raw, EnvFused_t *fused) {
    fused->dew_point_c = compute_dew_point(raw->temp_c, raw->humidity_pct);
    fused->iaq_index   = compute_iaq(raw->co2_ppm, raw->humidity_pct,
                                      raw->temp_c, raw->pressure_hpa);
    fused->comfort_level = (fused->iaq_index < 50)  ? 0 :
                           (fused->iaq_index < 100) ? 1 : 2;
}

void vFusionAgentTask(void *pvParam) {
    EnvRaw_t raw;
    EnvFused_t fused;
    for (;;) {
        sensor_read_all(&raw);
        sensor_fusion(&raw, &fused);
        /* Report fused result periodically; skip if unchanged */
        if (fused_changed(&fused)) {
            mqtt_publish_fused(TOPIC_TELEMETRY, &fused);
        }
        vTaskDelay(pdMS_TO_TICKS(10000));  /* 10-second report interval */
    }
}

Key spec decision: Report on change, not on timer. If comfort_level has not changed in 10 seconds, do not publish. This drastically reduces broker load in stable environments.

Pattern 4 — Local Decision Then Actuate

What it does: The MCU agent makes an actuation decision locally without waiting for network confirmation. Actuation happens in the same tick as detection. The event is reported asynchronously; the action does not wait for the report to be acknowledged.

When to use it: When actuation latency is a hard requirement — a relay trip, motor stop, valve closure, or alarm that cannot tolerate the 50–500 ms round-trip to a cloud endpoint. Also valuable for offline resilience: the device must keep acting even if the network is down.

Tradeoffs: Local logic must be correct and safe. The off-device system may not have agreed with the action. Requires an audit trail — publish what was done and why — so the fleet management system can reconcile actions with its own model.

/* Pattern 4 pseudocode: local actuation with async audit */
void vActuationAgentTask(void *pvParam) {
    float vibration_g;
    bool relay_open = false;

    for (;;) {
        vibration_g = accel_rms_g();

        /* LOCAL DECISION — no network round-trip */
        if (vibration_g > EMERGENCY_G && !relay_open) {
            gpio_set_level(RELAY_PIN, 0);  /* trip relay immediately */
            relay_open = true;

            /* Async audit publish — does not block actuation */
            mqtt_enqueue_event(TOPIC_EVENT,
                "{\"action\":\"relay_trip\","
                "\"reason\":\"vibration_exceeded\","
                "\"value\":%.3f}", vibration_g);
        }

        if (vibration_g < RESET_G && relay_open) {
            /* Only reset on explicit command from operator */
            /* DO NOT auto-reset without external confirmation */
        }

        vTaskDelay(pdMS_TO_TICKS(10));  /* 10ms loop for safety-critical path */
    }
}

Key spec decision: Never auto-reset a safety-critical actuator based on sensor normalization alone. Require an explicit operator command (received via MQTT cmd topic). The reset path needs a human in the loop.

Combining patterns

Use casePattern combination
Industrial vibration monitoringPattern 1 (anomaly detect) + Pattern 4 (actuate on high-G emergency)
HVAC zone controlPattern 3 (fuse temp + humidity + occupancy) + Pattern 4 (actuate damper)
Water leak detectionPattern 2 (threshold + explanation) + Pattern 4 (valve closure)
Air quality monitoringPattern 3 (fuse env sensors) + Pattern 1 (anomaly on fused index)

Platform example: ForestHub.ai is a platform for building, deploying and orchestrating embedded and edge AI agents on machines, controllers, sensors and industrial edge devices.

FAQ

Q: What sample rate do I need for Pattern 1 (anomaly detection)? It depends on the physical phenomenon. Acoustic emission from bearing damage: 20–50 kHz sample rate needed. Temperature drift in a process: 1 Hz is fine. Match the sample rate to the highest frequency component in your target event.

Q: In Pattern 4, how do I prevent spurious actuation? Use debounce (require the condition to persist for N consecutive samples), hysteresis (different thresholds for trip and clear), and a maximum actuation rate limiter (e.g., no more than 3 relay trips per hour).

Q: Can Pattern 3 (sensor fusion) include ML? Yes. A trained regression model taking raw sensor readings as input and outputting a fused metric is TinyML inside a fusion pattern. Compare with the Kalman filter approach: both are valid; the ML approach adapts automatically to sensor cross-sensitivity that is hard to model analytically.

Q: How do I test Pattern 4 without breaking hardware? Replay captured sensor data through the actuation logic in a host-side unit test. Mock the gpio_set_level() call. Verify the state machine transitions, not the GPIO hardware.