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 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:
A jam that persists past 45 s escalates to DOWNTIME_CONVEYOR_JAM instead, moving out of Run Time and into the Availability term,
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_afor 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_microstopresult set after the fact still lets a single noisy sample open and immediately close a span in the ledger. Debounce theJAMsignature itself before any span is materialized. - Ignoring BLOCKED as a distinct code. Rolling
BLOCKEDintoJAMhides 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_secis 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) |
Related Permalink to this section
- Threshold Tuning for Microstops — the parent subsystem and shared parameter contract
- Microstop Thresholds for Packaging Line Changeovers — the sibling case for state-gated planned stops
- Adjusting Performance Thresholds for Legacy CNC Machines — the single-signal, rolling-median variant of this problem
- State-Machine Design for RUN/IDLE/DOWN Event Classification — the state model these spans feed into
- Event-to-Downtime Mapping — turning classified spans into an auditable ledger