Packaging
PLC Packaging Machine Programming: Cam Sequencing, Reject Systems & OEE in Structured Text
High-speed packaging demands precise timing, reliable product tracking, and zero-tolerance reject systems. Build production-grade packaging machine logic with cam sequencing, tracking arrays, and real-time OEE in Structured Text.
How a packaging machine is put together
A typical packaging machine combines continuous motion (film transport, conveyors) with discrete events (seal, cut, fill, label) that have to happen at exact positions in the machine cycle. That is a different world from process control: you measure timing in degrees of machine rotation, not in seconds.
The machine cycle in degrees
Machine Cycle (0° to 360°)
│
├── 0°–30° : Film advance / index
├── 30°–60° : Product drop / fill
├── 60°–120° : Cross-seal (jaws closed)
├── 120°–150° : Seal cooling
├── 150°–180° : Jaw open / package release
├── 180°–270° : Label application
├── 270°–330° : Inspection / vision check
├── 330°–360° : Reject / accept gate
└── 360° = 0° : Cycle repeats
Sequencing with virtual cams
Packaging machines sequence with virtual cam switches instead of timers. A cam switch is an output that turns on and off at set encoder positions:
TYPE CamSwitch :
STRUCT
OnPosition : REAL; // Degrees where output turns ON
OffPosition : REAL; // Degrees where output turns OFF
Output : BOOL; // Current state
Enabled : BOOL; // Master enable
SpeedComp : REAL; // Advance ON position at high speed (degrees)
END_STRUCT;
END_TYPE
FUNCTION_BLOCK FB_CamController
VAR_INPUT
MachineAngle : REAL; // 0.0 to 360.0 from encoder
MachineSpeed : REAL; // RPM for speed compensation
Enable : BOOL;
END_VAR
VAR
Cams : ARRAY[1..16] OF CamSwitch;
NumCams : INT := 8;
END_VAR
VAR
i : INT;
adjustedOn : REAL;
END_VAR
IF NOT Enable THEN
FOR i := 1 TO NumCams DO
Cams[i].Output := FALSE;
END_FOR;
RETURN;
END_IF;
FOR i := 1 TO NumCams DO
IF NOT Cams[i].Enabled THEN
Cams[i].Output := FALSE;
ELSE
// Speed compensation: advance ON point at higher speeds
// to account for actuator response time
adjustedOn := Cams[i].OnPosition - (Cams[i].SpeedComp * MachineSpeed / 100.0);
IF adjustedOn < 0.0 THEN adjustedOn := adjustedOn + 360.0; END_IF;
// Handle wrap-around (e.g., ON=350°, OFF=10°)
IF adjustedOn < Cams[i].OffPosition THEN
Cams[i].Output := (MachineAngle >= adjustedOn) AND (MachineAngle < Cams[i].OffPosition);
ELSE
// Wrap-around case
Cams[i].Output := (MachineAngle >= adjustedOn) OR (MachineAngle < Cams[i].OffPosition);
END_IF;
END_IF;
END_FOR;
Cam setup for a VFFS machine
// Vertical Form Fill Seal (VFFS) cam configuration
CamCtrl.Cams[1].OnPosition := 0.0; CamCtrl.Cams[1].OffPosition := 30.0; // Film pull
CamCtrl.Cams[2].OnPosition := 35.0; CamCtrl.Cams[2].OffPosition := 55.0; // Product fill
CamCtrl.Cams[3].OnPosition := 60.0; CamCtrl.Cams[3].OffPosition := 120.0; // Cross-seal jaws
CamCtrl.Cams[4].OnPosition := 70.0; CamCtrl.Cams[4].OffPosition := 115.0; // Seal heater
CamCtrl.Cams[5].OnPosition := 180.0; CamCtrl.Cams[5].OffPosition := 260.0; // Labeler
CamCtrl.Cams[6].OnPosition := 270.0; CamCtrl.Cams[6].OffPosition := 320.0; // Vision trigger
CamCtrl.Cams[7].OnPosition := 330.0; CamCtrl.Cams[7].OffPosition := 355.0; // Reject gate
// Speed compensation: seal jaws need 5° advance per 100 RPM
CamCtrl.Cams[3].SpeedComp := 5.0;
CamCtrl.Cams[4].SpeedComp := 5.0;
Tracking products through the machine
On a high-speed line several products are in the machine at once, each at a different stage. A tracking shift register follows each one:
TYPE ProductRecord :
STRUCT
Present : BOOL; // Product exists at this station
Weight : REAL; // Filled weight
SealOK : BOOL; // Seal integrity check passed
LabelOK : BOOL; // Label present and readable
VisionPass : BOOL; // Vision inspection passed
RejectCode : INT; // 0=good, >0=reject reason
BatchID : DINT;
END_STRUCT;
END_TYPE
FUNCTION_BLOCK FB_ProductTracker
VAR
Track : ARRAY[1..20] OF ProductRecord; // 20 stations in machine
NumStations : INT := 12;
END_VAR
// Shift register: advance all products by one station
// Called once per machine cycle (at 0° or cycle-complete signal)
VAR_INPUT
AdvancePulse : BOOL;
NewProduct : BOOL; // Product entering station 1
NewWeight : REAL;
END_VAR
VAR
i : INT;
END_VAR
IF AdvancePulse THEN
// Shift everything forward (station N → station N+1)
FOR i := NumStations TO 2 BY -1 DO
Track[i] := Track[i-1];
END_FOR;
// Load new product at station 1
Track[1].Present := NewProduct;
Track[1].Weight := NewWeight;
Track[1].SealOK := FALSE;
Track[1].LabelOK := FALSE;
Track[1].VisionPass := FALSE;
Track[1].RejectCode := 0;
Track[1].BatchID := CurrentBatchID;
END_IF;
Quality checks at each station
// Station 4: Checkweigher
IF Track[4].Present THEN
IF Track[4].Weight < MinWeight OR Track[4].Weight > MaxWeight THEN
Track[4].RejectCode := 1; // Weight out of tolerance
END_IF;
END_IF;
// Station 7: Seal inspection
IF Track[7].Present THEN
Track[7].SealOK := SealInspectionSensor;
IF NOT Track[7].SealOK THEN
Track[7].RejectCode := 2; // Seal failure
END_IF;
END_IF;
// Station 9: Vision system
IF Track[9].Present THEN
Track[9].VisionPass := VisionSystem_Result;
Track[9].LabelOK := VisionSystem_LabelOK;
IF NOT Track[9].VisionPass THEN
Track[9].RejectCode := 3; // Vision reject
END_IF;
END_IF;
Reject station logic
The reject system has to work 100% of the time, because a missed reject means a bad product reaches the customer. So the logic below does not stop at firing the reject: it waits for a sensor to confirm the product left the line, and raises an alarm if it did not.
FUNCTION_BLOCK FB_RejectStation
VAR_INPUT
Product : ProductRecord;
StationIndex : INT; // Which tracking station is the reject at
RejectConfirm : BOOL; // Sensor confirming product was ejected
END_VAR
VAR_OUTPUT
RejectCmd : BOOL; // Activate reject mechanism
RejectActive : BOOL;
MissedReject : BOOL; // CRITICAL: product wasn't ejected
RejectCount : DINT;
RejectByCode : ARRAY[1..10] OF DINT; // Count per reject reason
END_VAR
VAR
tmrReject : TON;
tmrConfirm : TON;
WaitingConfirm: BOOL;
END_VAR
RejectCmd := FALSE;
MissedReject := FALSE;
IF Product.Present AND Product.RejectCode > 0 THEN
// Product needs rejecting
RejectCmd := TRUE;
RejectActive := TRUE;
WaitingConfirm := TRUE;
RejectCount := RejectCount + 1;
// Track reject reasons
IF Product.RejectCode >= 1 AND Product.RejectCode <= 10 THEN
RejectByCode[Product.RejectCode] := RejectByCode[Product.RejectCode] + 1;
END_IF;
END_IF;
// Verify reject actually happened
IF WaitingConfirm THEN
tmrConfirm(IN := TRUE, PT := T#500MS); // 500ms window to confirm
IF RejectConfirm THEN
// Product confirmed ejected
WaitingConfirm := FALSE;
tmrConfirm(IN := FALSE);
ELSIF tmrConfirm.Q THEN
// MISSED REJECT — critical alarm
MissedReject := TRUE;
WaitingConfirm := FALSE;
tmrConfirm(IN := FALSE);
// Stop the line or activate secondary reject
END_IF;
END_IF;
Calculating OEE in real time
OEE (Overall Equipment Effectiveness) is the standard KPI for packaging lines. It multiplies three ratios:
OEE = Availability × Performance × Quality
Availability = Run Time / Planned Production Time
Performance = (Ideal Cycle Time × Total Count) / Run Time
Quality = Good Count / Total Count
FUNCTION_BLOCK FB_OEECalculator
VAR_INPUT
MachineRunning : BOOL;
PlannedProduction : BOOL; // Shift is active
CycleComplete : BOOL; // One product completed
ProductGood : BOOL; // Product passed all inspections
IdealCycleTime : REAL; // Seconds per unit at rated speed
END_VAR
VAR_OUTPUT
OEE_Pct : REAL;
Availability_Pct : REAL;
Performance_Pct : REAL;
Quality_Pct : REAL;
GoodCount : DINT;
TotalCount : DINT;
RejectCount : DINT;
DowntimeMinutes : REAL;
END_VAR
VAR
PlannedTimeSec : REAL := 0.0;
RunTimeSec : REAL := 0.0;
scanTimeSec : REAL := 0.01;
END_VAR
// Accumulate time
IF PlannedProduction THEN
PlannedTimeSec := PlannedTimeSec + scanTimeSec;
IF MachineRunning THEN
RunTimeSec := RunTimeSec + scanTimeSec;
END_IF;
END_IF;
// Count products (rising edge)
IF CycleComplete THEN
TotalCount := TotalCount + 1;
IF ProductGood THEN
GoodCount := GoodCount + 1;
ELSE
RejectCount := RejectCount + 1;
END_IF;
END_IF;
// Calculate OEE components
IF PlannedTimeSec > 0.0 THEN
// Availability
Availability_Pct := (RunTimeSec / PlannedTimeSec) * 100.0;
DowntimeMinutes := (PlannedTimeSec - RunTimeSec) / 60.0;
// Performance
IF RunTimeSec > 0.0 THEN
Performance_Pct := ((IdealCycleTime * DINT_TO_REAL(TotalCount)) / RunTimeSec) * 100.0;
IF Performance_Pct > 100.0 THEN Performance_Pct := 100.0; END_IF;
END_IF;
// Quality
IF TotalCount > 0 THEN
Quality_Pct := (DINT_TO_REAL(GoodCount) / DINT_TO_REAL(TotalCount)) * 100.0;
END_IF;
// OEE
OEE_Pct := (Availability_Pct / 100.0)
* (Performance_Pct / 100.0)
* (Quality_Pct / 100.0)
* 100.0;
END_IF;
OEE benchmarks
| OEE Level | Rating | Typical For |
|---|---|---|
| 85%+ | World-class | Best-in-class automated lines |
| 60–85% | Good | Well-maintained production |
| 40–60% | Average | Opportunity for improvement |
| < 40% | Poor | Significant losses, investigate root causes |
The techniques at a glance
| Concept | Implementation |
|---|---|
| Cam sequencing | Position-based outputs with speed compensation |
| Product tracking | Shift register following products through stations |
| Quality inspection | Station-by-station checks updating product records |
| Reject system | Confirmed ejection with missed-reject alarm |
| OEE calculation | Real-time Availability × Performance × Quality |