Skip to content

Microstop Thresholds for Conveyor Motor Jams

A conveyor motor jam looks nothing like a CNC feed hold: instead of one clean cycle-time metric drifting, you get two independent signals moving at once — drive current climbing toward stall while belt speed collapses toward zero — and a third failure class, starvation, that produces an identical speed drop with no current rise at all. This page is the conveyor-specific case of threshold tuning for microstops: how to fuse current and speed into a single jam signature, debounce the VFD current ripple that would otherwise trip false stops on every belt, and route the result into Downtime Classification & OEE Calculation without conflating a jam with a machine that simply has nothing to convey. Get the signal fusion wrong and a conveyor line either buries real jams inside “normal” Performance loss or floods the reason-code table with phantom microstops every time an upstream cell goes idle.

The three-way ambiguity is the whole problem: a stalled belt, a starved belt, and a backed-up belt all report belt_speed_pct ≈ 0. Only the accompanying current trace tells them apart, and only the dwell time tells a true jam apart from a bounce in the current sensor.

Conveyor speed and current traces isolating a real jam from a transient slip Two aligned panels over the same 90-second window. Belt speed drops briefly to 65 percent for 1.5 seconds (filtered by a 2.0 second debounce hold) and later drops fully to 0 percent for 12 seconds while current spikes from a 2.1 amp baseline to 4.8 amps of stall current, the combined signature of a genuine jam inside the 4-to-45-second microstop band that counts as Performance loss rather than Availability loss. Belt speed (% of nominal) debounce_hold 2.0 s · min 4 s · max 45 s 100% 0% Motor current (A) 5.0A 0A 2.1A nominal 4.8A stall Brief slip (1.5 s) < debounce_hold (2.0 s) → filtered as bounce Jam (12 s stall) within [4 s, 45 s) → MICROSTOP → Performance loss current + speed both move — starvation shows speed drop alone

Conveyor jam signature: current draw and belt speed Permalink to this section

Treating belt_speed_pct in isolation cannot separate a jam from starvation, so the detector must fuse two channels: drive current from the VFD (or a clamp-on current transducer on older fixed-speed motors) and belt speed from a shaft encoder or photoeye tachometer. A jam stalls the motor against a mechanical obstruction, so current climbs toward the drive’s stall limit while speed collapses; starvation — no product reaching the belt — drops speed with current sitting at or below its unloaded baseline; a downstream blockage backs product up against a guard rail, which reads as a partial current rise with speed near zero but without the sharp stall-current spike a true jam produces. These three regimes must be resolved before any duration threshold is applied, the same discipline PLC tag standardization requires upstream so motor_current_a and belt_speed_pct always mean the same physical quantity across every conveyor on the line.

from __future__ import annotations

import pandas as pd

STALL_CURRENT_RATIO = 1.8  # multiple of nominal current that indicates stall


def classify_conveyor_signature(
    df: pd.DataFrame,
    *,
    nominal_current_a: float,
    speed_floor_pct: float = 8.0,
) -> pd.DataFrame:
    """Resolve each low-speed sample into JAM, STARVED, or BLOCKED.

    Requires `motor_current_a` and `belt_speed_pct` columns sampled from the
    same scan. Rows above the speed floor are left unclassified (running).
    """
    required = {"motor_current_a", "belt_speed_pct"}
    missing = required - set(df.columns)
    if missing:
        raise KeyError(f"classify_conveyor_signature: missing columns {sorted(missing)}")

    stalled_current = nominal_current_a * STALL_CURRENT_RATIO
    low_speed = df["belt_speed_pct"] <= speed_floor_pct

    df = df.copy()
    df["signature"] = pd.NA
    df.loc[low_speed & (df["motor_current_a"] >= stalled_current), "signature"] = "JAM"
    df.loc[low_speed & (df["motor_current_a"] <= nominal_current_a * 0.5), "signature"] = "STARVED"
    df.loc[
        low_speed
        & (df["motor_current_a"] > nominal_current_a * 0.5)
        & (df["motor_current_a"] < stalled_current),
        "signature",
    ] = "BLOCKED"
    return df

Nominal current is per-motor, not per-line: a 0.75 kW take-up conveyor draws roughly 2.1 A at 480 V three-phase, while a 3.7 kW incline section on the same line runs closer to 7 A, so nominal_current_a must be looked up from the asset registry, never hard-coded. Only the JAM signature is a downtime candidate; STARVED intervals are unscheduled no-demand time and must never enter the Performance-loss accounting, the same distinction the parent threshold-tuning page draws between a genuine microstop and a machine with nothing to run.

Debounce and the minimum-duration threshold Permalink to this section

VFD current telemetry carries switching ripple that a single noisy sample can misread as a stall, and a belt encoder loses lock for a scan or two during a genuine slip that self-corrects. Both call for a debounce hold before a JAM signature is trusted, followed by the same half-open [min_threshold_sec, max_threshold_sec) classification used across the threshold tuning for microstops topic — but with conveyor-appropriate values. Conveyor jams cascade fast: product piles up against the stalled section within seconds and starves or blocks neighboring conveyors, so plants typically escalate to logged downtime sooner than a CNC cell does. A min_threshold_sec of 4 s and max_threshold_sec of 45 s is a common starting point, well below the 60–120 s ceiling used on machining centers.

def apply_debounce_and_threshold(
    df: pd.DataFrame,
    *,
    debounce_hold_sec: float = 2.0,
    min_threshold_sec: float = 4.0,
    max_threshold_sec: float = 45.0,
    timestamp_col: str = "timestamp",
) -> pd.DataFrame:
    """Debounce JAM signatures, then classify surviving spans as microstops.

    A JAM span shorter than `debounce_hold_sec` is discarded as sensor
    noise. Surviving spans are windowed and classified by the same
    half-open interval used elsewhere in threshold tuning.
    """
    if min_threshold_sec >= max_threshold_sec:
        raise ValueError("min_threshold_sec must be < max_threshold_sec")

    df = df.sort_values(timestamp_col).copy()
    is_jam = df["signature"] == "JAM"
    span_id = (is_jam != is_jam.shift()).cumsum()

    spans = (
        df[is_jam]
        .groupby(span_id[is_jam])
        .agg(start=(timestamp_col, "min"), end=(timestamp_col, "max"))
    )
    spans["dwell_sec"] = (spans["end"] - spans["start"]).dt.total_seconds()

    # Drop spans that never persisted past the debounce hold.
    spans = spans[spans["dwell_sec"] >= debounce_hold_sec].copy()
    spans["is_microstop"] = (spans["dwell_sec"] >= min_threshold_sec) & (
        spans["dwell_sec"] < max_threshold_sec
    )
    spans["is_downtime"] = spans["dwell_sec"] >= max_threshold_sec
    return spans.reset_index(drop=True)

A jam that opens at 4.9 A and self-clears in 1.3 s is almost always a current-sensor artifact riding through a VFD switching transient, not a mechanical stall — the debounce hold exists to discard exactly that case before the duration comparison ever runs.

Classifying microstop vs logged downtime Permalink to this section

Once a span survives debounce, its dwell time routes it to Performance loss or Availability loss, and its signature determines the reason code that reaches the ledger described in event-to-downtime mapping. A jam between 4 s and 45 s is MICROSTOP_CONVEYOR_JAM, absorbed into the Performance factor:

P=Ideal Cycle Time×Total CountRun TimeP = \frac{\text{Ideal Cycle Time} \times \text{Total Count}}{\text{Run Time}}

A jam that persists past 45 s escalates to DOWNTIME_CONVEYOR_JAM instead, moving out of Run Time and into the Availability term,

A=Planned Production TimeDowntimePlanned Production TimeA = \frac{\text{Planned Production Time} - \text{Downtime}}{\text{Planned Production Time}}

and is handed to the same state-machine design for RUN/IDLE/DOWN event classification that governs every other asset on the line, so a conveyor and a CNC cell feeding the same shift report use one consistent DOWN semantics.

def emit_reason_codes(spans: pd.DataFrame) -> pd.DataFrame:
    """Map dwell-classified spans to auditable OEE reason codes."""
    spans = spans.copy()
    spans["reason_code"] = "IGNORED_NOISE"
    spans.loc[spans["is_microstop"], "reason_code"] = "MICROSTOP_CONVEYOR_JAM"
    spans.loc[spans["is_downtime"], "reason_code"] = "DOWNTIME_CONVEYOR_JAM"
    return spans

Reconcile the emitted spans against the raw current trace weekly; a healthy conveyor should show a stable count of short jams (belt-splice snags, product misalignment) and a near-zero rate of full downtime escalations.

SELECT time_bucket('1 day', ts) AS day,
       asset_id,
       count(*) FILTER (WHERE reason_code = 'MICROSTOP_CONVEYOR_JAM') AS microstops,
       count(*) FILTER (WHERE reason_code = 'DOWNTIME_CONVEYOR_JAM') AS downtimes,
       round(avg(dwell_sec) FILTER (WHERE reason_code = 'MICROSTOP_CONVEYOR_JAM')::numeric, 2) AS avg_jam_dwell_s
FROM conveyor_jam_spans
WHERE ts >= now() - interval '7 days'
GROUP BY day, asset_id
ORDER BY day, asset_id;

A rising avg_jam_dwell_s without a matching rise in downtimes usually means the belt tension or guide rail is degrading — the jams are getting harder to clear but still self-resolve inside the microstop window, which is exactly the leading indicator a threshold-tuned pipeline should surface before it becomes an unplanned stoppage.

Gotchas & anti-patterns Permalink to this section

  • Thresholding speed alone. Speed-only detection cannot distinguish a jam from starvation; both read near-zero. Skipping the current channel guarantees false microstops every time an upstream cell empties out.
  • One nominal_current_a for the whole line. Motors of different frame sizes on the same conveyor system have different stall ratios; a shared constant either misses small-motor jams or false-trips large ones.
  • Debouncing the duration threshold instead of the signature. Filtering short spans out of the is_microstop result set after the fact still lets a single noisy sample open and immediately close a span in the ledger. Debounce the JAM signature itself before any span is materialized.
  • Ignoring BLOCKED as a distinct code. Rolling BLOCKED into JAM hides a downstream problem behind an upstream asset’s reason-code history, sending maintenance to the wrong conveyor section.
  • Copying the CNC page’s 60 s ceiling verbatim. Conveyor product pile-up compounds fast; a 45 s (or tighter) max_threshold_sec is usually correct even though a machining center’s cycle-time thresholds tolerate a full minute.

Quick reference Permalink to this section

Signal combination Speed Current Classification OEE treatment
Nominal running ~100% ~nominal RUNNING Availability + Performance
Brief current ripple dips <2 s bumps <2 s filtered (debounce) none
Stall, 4–45 s ~0% ≥1.8× nominal MICROSTOP_CONVEYOR_JAM Performance loss
Stall, ≥45 s ~0% ≥1.8× nominal DOWNTIME_CONVEYOR_JAM Availability loss
No product arriving ~0% ≤0.5× nominal STARVED excluded (no-demand)
Downstream backed up ~0% 0.5–1.8× nominal BLOCKED Availability loss (upstream cause)