📌 4.1.1 Start Production Run
📌

4.1.1 Start Production Run

1. Overview

The Start Production Run page is the operator-facing interface for initiating a production run on a selected production line. It supports two distinct workflows:

  1. Scheduled Production Run — selecting from a list of pre-planned Work Orders.

  2. Quick Start (Ad-hoc Production Run) — creating an ad-hoc Work Order + Schedule + Run using QR/barcode scanning or manual input, without modifying the scheduled list.

All production runs (scheduled or ad-hoc) are started only after operator login and only when the operator confirms the Start Run pop-up.

This page does not allow operators to modify planned production data.


2. Page Structure and Workflow

2.1 Line Selection

Operators select the target production line (AAA, BBB, CCC, etc.).

This becomes the line_id used for queries and run creation.

Line dropdown (Line Name selector)

Schema reference

  • Table: line

    • id (bigserial)

    • name (text)

    • short_name (text)

    • disable (boolean)

SQL – mes_line_list

SELECT id, name, short_name FROM line WHERE disable = FALSE ORDER BY name;

Use this to populate the Line Name dropdown (AAA, BBB, etc., in real data).


2.2 Scheduled Work Orders Table (Left Panel)

The table displays only scheduled/planned Work Orders created by planners.

Columns shown (UI):

  • Work Order

  • Product Code

  • Remaining Qty

  • Scheduled Start

  • Scheduled Finish

The operator can click a row to load full details into the right-hand panel.

Why Remaining Qty only?

This minimizes cognitive load and shows what matters:

How much is left to produce.

Back-end Data Logic:

  • scheduled_qty from schedule.quantity

  • produced_qty = SUM(run.totalcount) where run.closed = true

  • remaining_qty = scheduled_qty – produced_qty

Table Data Source: Named Query

mes_scheduledWorkOrdersByLine(:line_id)

SQL - Scheduled Work Orders table (left panel) + detail panel data

  • produced_qty = SUM(run.total_count) for all closed runs on that schedule.

  • remaining_qty = schedule.quantity – produced_qty.

Schema reference

  • schedule

    • id (bigserial)

    • line_id (int8)

    • work_order_id (int8)

    • schedule_type (text)

    • note (text)

    • schedule_start_datetime (timestamp)

    • schedule_finish_datetime (timestamp)

    • quantity (integer)

    • entered_by (text)

    • timestamp (timestamp)

    • actual_start_datetime (timestamp)

    • actual_finish_datetime (timestamp)

    • actual_quantity (integer)

    • run_start_datetime (timestamp)

  • work_order

    • id (bigserial)

    • product_code_id (int8)

    • work_order (text)

    • quantity (integer)

    • closed (boolean)

    • hide (boolean)

    • timestamp (timestamp)

  • run

    • id (bigserial)

    • schedule_id (int8)

    • total_count (integer)

    • closed (boolean)

    • … (other columns not directly used here)

  • product_code

    • id (bigserial)

    • product_code (text)

    • description (text)

    • disable (boolean)

  • product_code_line

    • id (bigserial)

    • line_id (int8)

    • product_code_id (int8)

    • idealcycletime (real)

    • enable (boolean)

2.1 Named Query – mes_scheduled_workorders_by_line

Parameters

  • :line_id (int8)

SQL

SELECT s.id AS schedule_id, wo.id AS work_order_id, wo.work_order AS work_order_no, pc.product_code AS product_code, pc.description AS product_description, pcl.idealcycletime AS ideal_cycle_time_s, s.quantity AS scheduled_qty, COALESCE(SUM(r.total_count), 0) AS produced_qty, s.quantity - COALESCE(SUM(r.total_count), 0) AS remaining_qty, s.schedule_start_datetime, s.schedule_finish_datetime, s.note AS planner_note, CASE WHEN s.actual_finish_datetime IS NOT NULL THEN 'Completed' WHEN s.actual_start_datetime IS NOT NULL THEN 'In Progress' ELSE 'Planned' END AS status FROM schedule s JOIN work_order wo ON s.work_order_id = wo.id JOIN product_code pc ON wo.product_code_id = pc.id LEFT JOIN product_code_line pcl ON pcl.product_code_id = pc.id AND pcl.line_id = s.line_id LEFT JOIN run r ON r.schedule_id = s.id AND r.closed = TRUE WHERE s.line_id = :line_id -- Optional: only show schedules that still have something to produce -- AND s.quantity - COALESCE(SUM(r.total_count), 0) > 0 GROUP BY s.id, wo.id, wo.work_order, pc.product_code, pc.description, pcl.idealcycletime, s.quantity, s.schedule_start_datetime, s.schedule_finish_datetime, s.note, s.actual_start_datetime, s.actual_finish_datetime ORDER BY s.schedule_start_datetime;

Usage

  • Scheduled Work Orders table (left)

    • Show:

      • work_order_no

      • product_code

      • remaining_qty

      • schedule_start_datetime

      • schedule_finish_datetime

  • Work Order Information panel (right)

    • Bind details from selected row:

      • Work Order ID → work_order_no

      • Scheduled Quantity → scheduled_qty

      • Produced Quantity → produced_qty

      • Remaining Quantity → remaining_qty

      • Start/Finish Times → schedule_start_datetime, schedule_finish_datetime

      • Product Code → product_code

      • Ideal Cycle Time → ideal_cycle_time_s

      • Product Description → product_description

      • Planner Note → planner_note


2.3 Work Order Information Panel (Right Panel)

When a row is selected OR a Quick Start WO is in progress, the panel shows:

  • Work Order ID

  • Scheduled Qty

  • Produced Qty

  • Remaining Qty

  • Scheduled Start / Finish time

  • Product Code

  • Ideal Cycle Time (from product_code_line)

  • Product Description

  • Planner Note

This panel represents the actual Work Order that will be started.


3. Operator Login (Required for All Production Actions)

The Operator Login modal requires:

  • Operator ID

  • Password

Login unlocks:

  • Quick Start button

  • Start Work Order button

Without login:

  • Both buttons are disabled

  • Status message reads: Operator not logged in…

Logout reverses these states.

4. Quick Start (Ad-hoc Work Order Creation)

Important: No database operations occur during Quick Start form submission.

This is by design.

Why?

To prevent unapproved data from appearing in the scheduled/planned list.

Quick Start Workflow

  1. Operator clicks Quick Start (only when logged in).

  2. Modal opens with:

    • QR/Barcode scan field

    • Work Order ID

    • Product Code

    • Quantity

    • Start/Finish Time

    • Planner Note

  3. Operator scans or fills data.

  4. Click Apply to Work Order:

    • A temporary QuickStartWO object is created in memory.

    • The Work Order Information panel displays this data.

    • No new row is added to the Scheduled table.

When the operator then clicks Start Work Order, the backend logic detects whether the WO came from:

  • A scheduled record, or

  • A Quick Start form

…and executes the appropriate SQL transaction.

Product list for Quick Start (Product Code dropdown)

Schema reference

  • product_code

  • product_code_line

We want only enabled product codes for the selected line.

Named Query – mes_product_master_by_line

Parameters

  • :line_id (int8)

SQL

SELECT pc.id AS product_code_id, pc.product_code, pc.description, pcl.idealcycletime AS ideal_cycle_time_s FROM product_code pc LEFT JOIN product_code_line pcl ON pcl.product_code_id = pc.id AND pcl.line_id = :line_id WHERE pc.disable = FALSE AND (pcl.enable IS NULL OR pcl.enable = TRUE) ORDER BY pc.product_code;

Use this to populate the Product Code select in the Quick Start modal, and to fill Ideal Cycle Time + description in the detail panel.

Start Run – Quick Start (Ad-hoc WO + Schedule + Run)

This is used when the Quick Start form is filled and Start Work Order is pressed. Here we:

  1. Create a work_order if it doesn’t exist.

  2. Create a schedule for that line + WO.

  3. Create a run for that schedule.

  4. Link schedule.run_id.

Parameters (from Quick Start panel & tags)

  • :line_id (int8)

  • :work_order_no (text)

  • :product_code_id (int8)

  • :target_qty (integer) → goes into schedule.quantity and optionally work_order.quantity

  • :schedule_start (timestamp) – from Quick Start Start Time

  • :schedule_finish (timestamp) – from Quick Start Finish Time

  • :note (text)

  • :operator_id (text)

  • :start_infeed (integer)

  • :start_outfeed (integer)

  • :start_waste (integer)

Named Query – mes_quick_start_run

WITH existing_wo AS ( SELECT id FROM work_order WHERE work_order = :work_order_no ), ins_wo AS ( INSERT INTO work_order ( product_code_id, work_order, quantity, closed, hide, timestamp ) SELECT :product_code_id, :work_order_no, :target_qty, FALSE, FALSE, NOW() WHERE NOT EXISTS (SELECT 1 FROM existing_wo) RETURNING id ), wo AS ( SELECT id FROM existing_wo UNION ALL SELECT id FROM ins_wo ), ins_schedule AS ( INSERT INTO schedule ( line_id, run_id, work_order_id, schedule_type, note, schedule_start_datetime, schedule_finish_datetime, quantity, entered_by, timestamp, actual_start_datetime, actual_finish_datetime, actual_quantity, run_start_datetime ) SELECT :line_id, NULL::bigint, wo.id, 'ADHOC', :note, COALESCE(:schedule_start, NOW()), :schedule_finish, :target_qty, :operator_id, NOW(), NOW(), -- actual_start_datetime NULL, -- actual_finish_datetime 0, -- actual_quantity NOW() -- run_start_datetime FROM wo RETURNING id ), ins_run AS ( INSERT INTO run ( schedule_id, run_start_datetime, start_infeed, current_infeed, start_outfeed, current_outfeed, start_waste, current_waste, total_count, waste_count, good_count, availability, performance, quality, oee, setup_start_datetime, setup_end_datetime, runtime, unplanned_downtime, planned_downtime, total_time, timestamp, closed, estimated_finish_time ) SELECT s.id, NOW(), :start_infeed, :start_infeed, :start_outfeed, :start_outfeed, :start_waste, :start_waste, 0, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NOW(), FALSE, NULL FROM ins_schedule s RETURNING id, schedule_id ) UPDATE schedule s SET run_id = r.id FROM ins_run r WHERE s.id = r.schedule_id RETURNING r.id AS run_id, s.id AS schedule_id;

This query returns run_id and schedule_id so you can:

  • Navigate to the Run Monitoring / Setup view with run_id.

  • Optionally refresh the Scheduled Work Orders list if you later want ad-hoc work to appear there (currently, in your design, it doesn’t).


5. Start Work Order Logic

When the operator presses Start Work Order, the system evaluates:

if quickStartWO exists → Start ad-hoc run
else → Start scheduled run

The confirmation popup shows:

  • Work Order ID

  • Line Name

  • Operator ID

When confirmed, the backend executes the start run transaction.

Before executing the start run transaction (Quick Start or Existing Schedule), the Start Work Order button script performs a pre-start validation on the line:

  • Read the tag [Line]/OEE/RunID.

  • A new Production Run can only be started when RunID is -1 or NULL, meaning there is no active run on this line.

  • If RunID has any other value, the system shows an error message (e.g. “Line already has an active run. Please stop or close the current run first.”) and does not call the database start run Named Query.

This check keeps the tag layer and database layer consistent and prevents multiple active runs on the same line.


“The following sections define the UI script, database logic, and MES tag initialization required to correctly start any Production Run.”

5.1 Perspective Start Work Order Button – Pseudo-code

The Start Work Order button in the Start Production Run view implements the following logic in its onActionPerformed script:

  1. Operator login check

    • Read the current operator ID from the session (e.g. session.custom.operatorId).

    • If no operator is logged in, show an error message and abort.

  2. Resolve line context

    • Read the selected line_id from the view.

    • Use a helper function (e.g. project.mes_start_run.getLinePath(lineId)) to resolve the UDT tag path for the line, such as

      [mes]Enterprise/Site/Area/Line 1/Line.

  3. Pre-start validation from tag

    • Read [Line]/OEE/RunID.

    • Only allow a new Production Run to start if RunID is -1 or NULL.

    • If another value is present, display an error message (e.g. “Line already has an active Run. Please stop or close the current Run first.”) and do not call any Named Query.

  4. Read live counters as start snapshot

    • Read the current counter tags from the Dispatch UDT:

      • [Line]/Dispatch/OEE Infeed/Count

      • [Line]/Dispatch/OEE Outfeed/Count

      • [Line]/Dispatch/OEE Waste/Count

    • Map these values into the :start_infeed, :start_outfeed, :start_waste parameters for the start-run Named Queries.

  5. Branch: Quick Start vs Existing Scheduled Work Order

    • If a quickStartWO object exists in the view (created by the Quick Start modal), call the mes_quick_start_run Named Query with:

      line_id, work_order_no, product_code_id, target_qty, schedule_start, schedule_finish, note, operator_id, and the three start_* values.

    • Otherwise, ensure that a row is selected in the Scheduled Work Orders table and call mes_start_run_existing_schedule with:

      schedule_id, operator_id, and the three start_* values.

    • Both variants return at least run_id (and optionally schedule_id).

  6. Initialise MES tags for the new run

    • Call a project script helper, e.g.

      project.mes_start_run.initRunTags(linePath, lineId, runId, scheduleId, workOrderNo, targetQty, idealCycleTimeS, operatorId).

    • This helper writes to the Line and OEE UDTs to set:

      • OEE/RunID, OEE/Schedule ID, OEE/WorkOrder, OEE/Quantity, OEE/Target Count

      • Reset counts and downtime fields to zero

      • Set OEE/Standard Rate based on ideal_cycle_time_s

      • Set OEE/Start Time, Line/Start Time, Line/Run Enabled, Line/Collect Data

      • Set the RunID and Enable tags inside the Dispatch UDT (Infeed, Outfeed, Waste).

  7. Navigation

    • Navigate to the Run Monitoring / Setup view, passing lineId and runId as view parameters.

The implementation details of getLinePath and initRunTags are encapsulated in a project script library (mes_start_run) so that mes_core remains unchanged and reusable.

Start Run – Existing Scheduled Work Order

This is the case where the operator selects a row in the Scheduled Work Orders table and presses Start Work Order.

Concept

  • Ensure schedule.actual_start_datetime is set (first time only).

  • Create a new run row.

  • Optionally set schedule.run_id & run_start_datetime.

  • Use tag counters as starting values.

Parameters (from Perspective)

  • :schedule_id (int8) – from selected row

  • :operator_id (text) – from Operator Login

  • :start_infeed (integer)

  • :start_outfeed (integer)

  • :start_waste (integer)

Named Query – mes_start_run_existing_schedule

(Shown as a transactional block – implementation in Ignition depends on how you handle multi-statement queries in PostgreSQL; you can also put this logic in a stored procedure.)

-- 1) Set actual_start_datetime if this is the first time we start this schedule UPDATE schedule SET actual_start_datetime = COALESCE(actual_start_datetime, NOW()) WHERE id = :schedule_id; -- 2) Insert new run INSERT INTO run ( schedule_id, run_start_datetime, start_infeed, current_infeed, start_outfeed, current_outfeed, start_waste, current_waste, total_count, waste_count, good_count, availability, performance, quality, oee, setup_start_datetime, setup_end_datetime, runtime, unplanned_downtime, planned_downtime, total_time, timestamp, closed, estimated_finish_time ) VALUES ( :schedule_id, NOW(), :start_infeed, :start_infeed, :start_outfeed, :start_outfeed, :start_waste, :start_waste, 0, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NOW(), FALSE, NULL ) RETURNING id;

If you want to link the latest run back to schedule.run_id:

WITH new_run AS ( INSERT INTO run ( ... ) -- same as above VALUES ( ... ) RETURNING id, schedule_id ) UPDATE schedule s SET run_id = r.id, run_start_datetime = COALESCE(s.run_start_datetime, NOW()) FROM new_run r WHERE s.id = r.schedule_id RETURNING r.id AS run_id;


Machine state is always driven by the PLC.

  • The PLC (or other field device) writes to the line state tag, e.g. [Line]/State/Code or an equivalent state tag in the Line UDT.

  • MES Starter does not force the machine into Running. It only:

    • Knows which run_id is active for the line, and

    • Optionally sets a permissive such as [Line]/Control/RunEnable that the PLC can use in its own logic.

A separate state-history script (outside of this Start Production Run page) subscribes to the state tag and writes changes into the state_history table, which is then used for Availability and Downtime analytics.

Therefore, the Start Work Order logic:

  • Creates the new run in the database.

  • Initialises MES tags (RunID, ScheduleID, etc.).

  • Optionally enables a PLC permissive (Run Enable).

  • Does not write “Running” or any other machine state code. The state transition to Running must be generated by the machine/PLC program.


1) Perspective Start Work Order button – pseudo-code

This is the script on the Confirm Start / Start Work Order button (onActionPerformed).

# PSEUDOCODE – Perspective Start Work Order button # 1) Guard: operator must be logged in operatorId = session.custom.operatorId # example – depends on your login wiring if not operatorId: system.perspective.sendMessage( "mes_error", {"message": "Operator not logged in."}, scope="session" ) return # 2) Resolve line context lineId = self.view.custom.selectedLineId # bound from Line dropdown. Adjust this to match your actual binding. linePath = project.mes_start_run.getLinePath(lineId) # e.g. returns "[mes]Enterprise/Site/Area/Line 1/Line" # 3) Pre-start validation – ensure no active Run on this line runIdPath = linePath + "/OEE/RunID" currentRunId = system.tag.readBlocking([runIdPath])[0].value if currentRunId is not None and currentRunId != -1: system.perspective.sendMessage( "mes_error", {"message": "Line already has an active Run. Please stop or close the current Run first."}, scope="session" ) return # 4) Read live counters from Dispatch UDT as start_* snapshot tagPaths = [ linePath + "/Dispatch/OEE Infeed/Count", linePath + "/Dispatch/OEE Outfeed/Count", linePath + "/Dispatch/OEE Waste/Count", ] infeed, outfeed, waste = [q.value for q in system.tag.readBlocking(tagPaths)] startInfeed = infeed or 0 startOutfeed = outfeed or 0 startWaste = waste or 0 # 5) Decide path: Quick Start vs Scheduled quickStartWO = self.view.custom.quickStartWO # None if not using Quick Start if quickStartWO is not None: # --- Quick Start path --- params = { "line_id": lineId, "work_order_no": quickStartWO["work_order_no"], "product_code_id": quickStartWO["product_code_id"], "target_qty": quickStartWO["target_qty"], "schedule_start": quickStartWO["schedule_start"], # can be null → COALESCE in SQL "schedule_finish": quickStartWO["schedule_finish"], "note": quickStartWO["planner_note"], "operator_id": operatorId, "start_infeed": startInfeed, "start_outfeed": startOutfeed, "start_waste": startWaste, } resultDS = system.db.runNamedQuery("mes_quick_start_run", params) row = resultDS[0] runId = row["run_id"] scheduleId = row["schedule_id"] workOrderNo = quickStartWO["work_order_no"] targetQty = quickStartWO["target_qty"] idealCycle = quickStartWO["ideal_cycle_time_s"] else: # --- Existing Scheduled Work Order path --- selectedRow = self.view.custom.selectedScheduleRow # from table selection if selectedRow is None: system.perspective.sendMessage( "mes_error", {"message": "Please select a Scheduled Work Order."}, scope="session" ) return scheduleId = selectedRow["schedule_id"] workOrderNo = selectedRow["work_order_no"] targetQty = selectedRow["scheduled_qty"] idealCycle = selectedRow["ideal_cycle_time_s"] params = { "schedule_id": scheduleId, "operator_id": operatorId, "start_infeed": startInfeed, "start_outfeed": startOutfeed, "start_waste": startWaste, } # this Named Query returns run_id (and optionally schedule_id, depending on variant) resultDS = system.db.runNamedQuery("mes_start_run_existing_schedule", params) row = resultDS[0] runId = row["run_id"] # 6) Initialise MES tags for this line/run project.mes_start_run.initRunTags( linePath=linePath, lineId=lineId, runId=runId, scheduleId=scheduleId, workOrderNo=workOrderNo, targetQty=targetQty, idealCycleTimeS=idealCycle, operatorId=operatorId ) # 7) Navigate to Run Monitoring / Setup view system.perspective.navigate( view="MES/RunMonitoring", params={"lineId": lineId, "runId": runId} )

2) Project script: mes_start_run.initRunTags

You keep mes_core untouched and add a small helper in a project script library, e.g. project.mes_start_run.

# project script: mes_start_run.py def getLinePath(lineId): """ Resolve a numeric lineId to its UDT tag path. Implementation is project-specific (could be a mapping table or a naming convention). """ # EXAMPLE ONLY – replace with your real mapping return "[mes]Enterprise/Site/Area/Line %d/Line" % lineId def initRunTags(linePath, lineId, runId, scheduleId, workOrderNo, targetQty, idealCycleTimeS, operatorId): """ Initialise the Line / OEE / Dispatch tags for a newly started Run. """ import system now = system.date.now() # 1) Core OEE context oeePaths = [ linePath + "/OEE/RunID", linePath + "/OEE/Schedule ID", linePath + "/OEE/WorkOrder", linePath + "/OEE/Quantity", # or Target Count, depending on final UDT linePath + "/OEE/Target Count", linePath + "/OEE/Good Count", linePath + "/OEE/Bad Count", linePath + "/OEE/Total Count", linePath + "/OEE/Planned Downtime", linePath + "/OEE/Unplanned Downtime", linePath + "/OEE/Run Time", linePath + "/OEE/Total Time", linePath + "/OEE/Standard Rate", linePath + "/OEE/OEE Availability", linePath + "/OEE/OEE Performance", linePath + "/OEE/OEE Quality", linePath + "/OEE/OEE", linePath + "/OEE/Start Time", ] # simple conversion: parts / minute from ideal cycle time in seconds if idealCycleTimeS and idealCycleTimeS > 0: standardRate = 60.0 / float(idealCycleTimeS) else: standardRate = 0 oeeValues = [ runId, scheduleId, workOrderNo, targetQty, targetQty, 0, 0, 0, 0, 0, 0, 0, standardRate, 0, 0, 0, 0, now, ] system.tag.writeBlocking(oeePaths, oeeValues) # 2) Line-level control / flags linePaths = [ linePath + "/Start Time", linePath + "/Run Enabled", linePath + "/Collect Data", ] lineValues = [now, True, True] system.tag.writeBlocking(linePaths, lineValues) # 3) Dispatch UDT context (RunID for each counter family) dispatchPaths = [ linePath + "/Dispatch/RunID", linePath + "/Dispatch/OEE Infeed/RunID", linePath + "/Dispatch/OEE Outfeed/RunID", linePath + "/Dispatch/OEE Waste/RunID", linePath + "/Dispatch/Enable", ] dispatchValues = [runId, runId, runId, runId, True] system.tag.writeBlocking(dispatchPaths, dispatchValues)

This uses your existing mes_core UDT structure (Line / OEE / Dispatch) from the Bootcamp JSON.