The 7:43 AM problem: show operators the last 60 minutes, not the clock hour

A production-rate number that resets at the top of every hour can look fine for most of an hour after the line has stopped. Here's how to build a true rolling 60-minute count in a Logix PLC or in Ignition, and the traps to avoid.

Why clock-hour numbers mislead

It's 7:43 AM and an operator looks at the HMI to see how the line is doing. Most screens give one of two answers, and both are misleading:

  • "This hour so far" only covers 7:00 to 7:43. At 7:05 it shows a handful of parts and looks terrible. At 7:55 it looks great. The number climbs all hour and drops back to zero at the top of the next hour, so nobody can tell at a glance whether the line is running well.
  • "Last full hour" covers 6:00 to 7:00. It's a steady number, but at 7:43 it's already 43 minutes out of date. If the line stopped at 7:20, this display still shows a normal hour until 8:00.

What the operator actually wants is the last 60 minutes, so at 7:43 the window covers 6:43 to 7:43. It moves forward every minute, it never resets, and a stoppage shows up as a number that steadily falls.

Three ways to count production at 7:43 AM A timeline from 6:00 to 8:00. The last full hour covers 6:00 to 7:00. This hour so far covers 7:00 to 7:43. The rolling 60 minutes covers 6:43 to 7:43. The line is stopped from 7:20 to 7:43. line stopped 7:20 Last full hour 6:00–7:00 · 43 min old This hour so far 7:00–7:43 · resets at 8:00 Rolling 60 minutes 6:43–7:43 6:00 6:30 7:00 8:00 now 7:43
At 7:43, with the line down since 7:20, the last full hour still looks normal and "this hour so far" is only a partial count. Only the rolling window always covers a full 60 minutes, stoppage included.

The idea: 60 one-minute buckets

Keeping a timestamp for every part and counting the ones newer than 60 minutes would work, but it's wasteful. Instead, keep 60 buckets, one per minute, plus a running total:

  1. Every part that's made adds 1 to the current minute's bucket and 1 to the running total.
  2. When the minute changes, move to the next bucket. That bucket holds the count from 60 minutes ago, so subtract it from the running total, then zero it.
  3. The running total is always the count for the current minute plus the previous 59. Multiply by 60 and divide by the minutes collected to get parts per hour.

The bucket array is used as a circular buffer: a pointer walks 0 to 59 and wraps back to 0, so nothing ever gets shifted or re-added. Each scan costs one addition, and each minute change costs one subtraction.

In the PLC (Studio 5000 / Logix)

This runs in any periodic or continuous task. It reads the line's existing part counter, so you don't need to touch the logic that already counts parts.

Tags (program scope)
NameTypePurpose
BucketDINT[60]Parts made in each of the last 60 minutes
PtrDINTIndex of the current minute's bucket, 0–59
Sum60DINTRunning total of all 60 buckets
MinutesFilledDINTMinutes of data collected so far, capped at 60
LastAcc, DeltaDINTCounter value last scan, and parts made since then
ClockDINT[7]Controller clock: year, month, day, hour, minute, second, µs
LastMinuteDINTClock minute seen on the last scan (when the pointer last moved)
ClearWindowBOOLLatched by the first-scan rung to start a fresh window (see gotchas)
iDINTLoop index for clearing the buckets
RatePerHourREALWhat the HMI shows
(* Rolling 60-minute part count: circular buffer + running sum *)

(* 0. Fresh start after power-up or Program-to-Run (S:FS rung latches ClearWindow) *)
IF ClearWindow THEN
    FOR i := 0 TO 59 DO
        Bucket[i] := 0;
    END_FOR;
    Sum60 := 0;
    Ptr := 0;
    MinutesFilled := 1;
    LastAcc := PartCounter.ACC;
    GSV(WallClockTime, , DateTime, Clock[0]);
    LastMinute := Clock[4];
    ClearWindow := 0;
END_IF;

(* 1. Parts made since last scan, from the line's existing CTU *)
Delta := PartCounter.ACC - LastAcc;
IF Delta < 0 THEN          (* counter was reset: count from zero *)
    Delta := PartCounter.ACC;
END_IF;
LastAcc := PartCounter.ACC;
Bucket[Ptr] := Bucket[Ptr] + Delta;
Sum60 := Sum60 + Delta;

(* 2. Once per clock minute, step to the next bucket *)
GSV(WallClockTime, , DateTime, Clock[0]);    (* UTC on v16+; only the minute is used *)
IF Clock[4] <> LastMinute THEN
    LastMinute := Clock[4];
    Ptr := Ptr + 1;
    IF Ptr > 59 THEN
        Ptr := 0;
    END_IF;
    Sum60 := Sum60 - Bucket[Ptr];  (* drop the minute from an hour ago *)
    Bucket[Ptr] := 0;              (* this slot is now the current minute *)
    IF MinutesFilled < 60 THEN
        MinutesFilled := MinutesFilled + 1;
    END_IF;
END_IF;

(* 3. Rate for the HMI: scale up while the first hour is still filling *)
RatePerHour := Sum60 * 60.0 / MinutesFilled;    (* REAL math: 60.0 forces it *)

MinutesFilled starts at 1 because the current, partial minute already counts as data.

First-scan rung

Add this one ladder rung in the same program, ahead of the structured text routine:

XIC S:FS OTL ClearWindow

S:FS is on for the first scan after the controller enters Run (power-up in Run, or a switch from Program to Run), so ClearWindow latches once and the structured text clears it after resetting the window.

Doing it all in ladder

If the plant standard is ladder, here's how each part of the structured text maps to ladder instructions:

  • Delta: SUB PartCounter.ACC LastAcc Delta, then LES Delta 0 → MOV PartCounter.ACC Delta, then MOV PartCounter.ACC LastAcc.
  • Accumulate: ADD Bucket[Ptr] Delta Bucket[Ptr] and ADD Sum60 Delta Sum60.
  • Minute tick: GSV WallClockTime DateTime Clock[0], then NEQ Clock[4] LastMinute drives the minute-change branch.
  • Advance: on that branch, MOV Clock[4] LastMinute, ADD Ptr 1 Ptr, GRT Ptr 59 → CLR Ptr, then SUB Sum60 Bucket[Ptr] Sum60 and CLR Bucket[Ptr].
  • Fresh start: the first-scan rung above, then XIC ClearWindow drives a branch with FLL 0 Bucket[0] 60 (zeros the array in one instruction), CLR Sum60, CLR Ptr, MOV 1 MinutesFilled, MOV PartCounter.ACC LastAcc, the same GSV and MOV Clock[4] LastMinute, and OTU ClearWindow.
  • Rate: CPT RatePerHour Sum60*60.0/MinutesFilled.

If you'd rather not depend on the controller clock, a free-running 60-second TON that resets itself with its own .DN bit can drive the minute step instead. It works, but it drifts slightly over a long run and won't line up with the minutes on the trend screen.

What about FFL and FFU?

A FIFO built from FFL/FFU is the textbook answer, and it works: load the finished minute's count, unload the oldest once the FIFO is full. The catch is that FFU shifts every element down one position each time it unloads, and you still need a running sum or a FAL/FOR loop to total the array. At once a minute, neither will hurt a modern controller, but the pointer and running sum above do less work, are easier to read in a trend, and are easier to clear in one place.

In Ignition (no PLC changes)

When the PLC is locked down, or you want the same number on lines with different controllers, Ignition can calculate it from tag history. Historize the line's part-counter tag (on change, with a small or zero deadband), then run a Gateway Timer Event script every 10–15 seconds that writes the result to a memory tag your screens can bind to:

# Gateway Event Scripts > Timer, fixed delay 10000 ms
COUNTER = "[default]Line1/PartCount"        # historized cumulative counter
OUT     = "[default]Line1/Rolling60/Parts"   # memory tag for the HMI

end   = system.date.now()
start = system.date.addMinutes(end, -60)

# Raw, on-change values for the last 60 minutes, plus the value at the start
ds = system.tag.queryTagHistory(paths=[COUNTER], startDate=start, endDate=end,
                                returnSize=-1, includeBoundingValues=True,
                                ignoreBadQuality=True)

total, prev = 0, None
for row in range(ds.getRowCount()):
    v = ds.getValueAt(row, 1)
    if v is None:
        continue
    if prev is not None:
        step = v - prev
        total += step if step >= 0 else v   # reset: count up from zero
    prev = v

system.tag.writeBlocking([OUT], [total])

Summing the step-by-step increases, rather than taking last value minus first value, is what keeps a counter reset in the middle of the window from producing a negative or wildly wrong number. For the same reason, avoid the "Range" aggregate in system.tag.queryTagHistory or system.tag.queryTagCalculations for counters: it returns maximum minus minimum, which is only right if the counter never resets inside the window.

If you already have a per-minute count tag (for example, the PLC's Bucket[Ptr] or a tag that resets each minute), it's even simpler: query it with returnSize=1 and aggregationMode="Sum" over the last 60 minutes. Keep in mind that a tag-history query is heavier than reading one PLC tag, so run it on the gateway once for everyone instead of from a binding on every open screen.

Gotchas

Counter rollover and resets

Never take "counter now minus counter an hour ago." A shift-change reset, an operator reset, or a wrap makes it negative. Both versions above work from small steps and treat a negative step as a reset. Older 16-bit counters (SLC-style INT accumulators) wrap at 32,767, which a fast line can reach in a single shift.

PLC power cycle

Logix tag values normally survive a power cycle, so after an outage the array still holds counts from before the outage, and the rate looks like the line never stopped. That's why S:FS sets ClearWindow. Starting fresh and letting the window refill is better than showing old parts as current ones.

Clock changes and DST

The logic only compares the clock minute, and a daylight-saving change moves the hour, not the minute, so it never adds or skips buckets. On v16 and later firmware, DateTime is UTC and LocalDateTime is the time-zone and DST-adjusted local time (before v16, DateTime returned local time). Either works here. An NTP or manual clock correction can make one minute slightly long or short, which is close enough for an operator display.

The first hour

For the first hour after startup or a clear, the window holds less than 60 minutes of data. Report the rate scaled by MinutesFilled as shown, and show something like "warming up, 12 of 60 min" on screen so a low raw count doesn't alarm anyone. Expect the rate to be noisy for the first couple of minutes after a restart: with only a partial minute of data, a few parts get multiplied up into a big swing in parts per hour. It settles as the window fills.

The short version

Sixty one-minute buckets, a pointer that wraps, and a running total. It costs a few instructions per scan, and it gives operators a production number they can trust at any minute of the hour.

← All tutorials · Next: Run hours you can trust →

Want this on your line?

We can add a rolling-hour rate to your existing PLC and Ignition screens, usually as a small fixed-price job. It's also a good thing to bring up during a free PLC/SCADA health check.