Ignition Gateway Scripts Keep FSM State by Writing It to a Tag

Daniel Price7 min read
HMI / SCADAOther ManufacturerTechnical Reference
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

A finite state machine (FSM) instantiated in an Ignition script module lives in exactly one process. It is not a shared object. The working design for a gateway timer script (300 ms period, about 1000 tag reads and 100 tag writes per scan) is to rebuild the transition tables on every run and keep only the current state in a gateway memory tag. That removes any dependence on how long the interpreter keeps a module-level object alive.

Which process holds the FSM instance?

Trace where the object is created and who can reach it. A top-level Python object in a script module is created separately in each runtime that loads the project. No memory is shared between these runtimes, and there is no network path for an object reference between them.

Runtime Own copy of module-level objects? Shares memory with
Gateway (timer, tag change, message scripts) Yes, one instance Nothing else
Each Designer Yes, one per open Designer Nothing else
Each Client Yes, one per running Client Nothing else

The consequence: fsm1 created in a client script is a different object from fsm1 created in a gateway script, even if both call project.fsm.FiniteStateMachine(). No script location makes one instance visible to all gateway and client scripts. A state value visible everywhere has to travel through a shared medium: a database, a file, or a gateway memory tag that each scope reads on demand.

Must clients see the FSM, or only the gateway?

This decision splits the design in two.

  • Clients must read or drive the state: Treat the gateway as the single owner. Store the state in a memory tag (or database row) and let clients read it as a normal tag binding. Route operator input to the gateway with a message script or a tag write, and let the gateway FSM evaluate it. Message scripts and tag change scripts can mimic synchronization between scopes, but they do not guarantee it. Two scopes that each run their own FSM copy can diverge.
  • Only gateway scripts need the FSM: Keep the FSM entirely in gateway scope. This is the case for a PLC simulator driven by a gateway timer script. Go to the next check.

If the honest answer is "clients need a live copy of the same object," the requirement cannot be met with script objects. Move the state to a tag.

What resets a module-level object inside the gateway scope?

Objects instantiated at the top level of a script module persist in their scope until a Designer saves or publishes the containing project, or any Designer saves a global change such as a shared script. At that point the scripting modules reload and module-level objects are re-created from their initializers. Observed behavior on a gateway timer script matches this: some script variables keep their values between runs. That is a side effect of the module staying loaded, not a contract.

Event Module-level FSM object Value stored in a tag or database
Timer script runs again Usually still present Present
Project save or publish Re-created from initializer Present
Global or shared-script save from any Designer Re-created from initializer Present
Gateway restart Gone Present if the store is durable; confirm memory tag retention in your version, or use a database if you need guaranteed durability

Any of the first three rows can occur mid-sequence during a commissioning session, so a design that depends on the object surviving will lose state at an arbitrary scan.

Does the posted FSM code run correctly as written?

The logic works. The posted trace shows trig3 firing the registered f1To3 transition from s1 to s3 (state 2), then trig1 moving row 2, column 0 to state 1. Output 2 then 1 matches the table [[0,0,2],[0,2,0],[1,1,2]]. Four defects still need fixing before it goes into a timer script:

Defect Effect Fix
Typographic quotes (“ ”) around dictionary keys and the print string Syntax error when pasted into the script editor Use straight ASCII quotes
stateXition = [], states = {}, triggers = {} declared at class level Mutable containers shared by every instance that mutates them in place instead of rebinding them; registerStateTransition mutates in place Create the containers in __init__
0 used as the "no function" sentinel (if func != 0) Ambiguous; any falsy value slips through the comparison differently Use None and test if func is not None
print statements Valid only in the Python 2 syntax that Ignition scripting uses; use system.util.getLogger for gateway diagnostics Log to the gateway logger, not stdout

Note also that the transition function runs only when the new state differs from the current one (if newState != self.state). Self-transitions never call their function. If a state must execute an action on re-entry, remove that condition.

Does a 300 ms scan tolerate rebuilding the tables every run?

Yes. The gateway script reads about 1000 tags and writes about 100 per cycle and typically finishes in under 6 ms. The tag I/O dominates. Building three 3x3 lists and two dictionaries adds negligible time next to that.

Quantity Value
Timer period 300 ms
Typical run time < 6 ms
Duty (6 / 300) < 2 %

Rebuild-per-scan removes the persistence question for the tables, which are constants. The single mutable datum is the current state integer. Persist that and the FSM is reconstructible at any scan.

Is a script FSM the right tool, or does SFC fit better?

Use these checks to decide:

  1. Is the sequence a step-and-transition process with operator confirmations and step timing? The SFC module implements that natively, with persisted chart state managed by the platform, and is the better fit than a hand-built transition matrix.
  2. Is the FSM a small simulation of PLC behavior tied to a fixed timer scan? Keep it as a gateway script with the state in a tag.
  3. Do you need PLC-style communication from the gateway itself? A separate Ethernet/IP module exists for gateway-side PLC-style work; evaluate it against your licensing and version before committing.

Reserve script FSMs for logic that is easy to restate and where losing the object costs nothing. If the scripting approach requires client visibility or multi-scope coordination, it is doing too much in scripting.

How do you restructure the gateway timer script so only the state persists?

  1. Create a memory tag to hold the FSM state integer. The path below is a placeholder: [default]FSM/State. Initialize it to the state you want after a cold start.
  2. Rewrite the class so containers are created per instance and the state can be injected:
    class FiniteStateMachine(object):
        def __init__(self, initialState=0):
            self.triggers = {}
            self.states = {}
            self.stateXition = []
            self.stateXitionFunction = []
            self.state = initialState
    
        def newState(self, triggerEvent):
            trig = self.triggers[triggerEvent]
            nxt = self.stateXition[self.state][trig]
            if nxt != self.state:
                func = self.stateXitionFunction[self.state][trig]
                if func is not None:
                    func()
                self.state = nxt
            return self.state
  3. In the timer script, read the state tag first, build the FSM, apply the scan's trigger events, then write the state back only if it changed:
    
    

    In Ignition versions that predate system.tag.readBlocking and system.tag.writeBlocking, use the older system.tag.read and system.tag.write calls with the same logic.

  4. Register transition functions inside scan (or a helper it calls) on every run, since the object is rebuilt each time.
  5. Source the events list from tag reads already performed in the scan or from operator-input tags. Clear one-shot operator triggers after they are consumed so a trigger is not applied twice on consecutive scans.

How do you confirm the state survives a save and a rebuild?

  1. Run the timer script and drive the FSM to a non-initial state, for example send trig3 from s1. Confirm [default]FSM/State reads 2.
  2. Save the project from the Designer, which forces the script modules to reload. Confirm the tag still reads 2 and that the next scan resumes from state 2, not from the initializer.
  3. Send trig1 and confirm the tag moves to 1, matching row 2, column 0 of the transition table.
  4. Watch the script run time in the gateway diagnostics or a logged timestamp delta across a full 1000-read, 100-write cycle. Confirm it stays well inside 300 ms (about 6 ms typical).
  5. Repeat step 2 while a client window shows the state tag, and confirm the client displays the tag value with no script involvement.

FAQ

What happens if I save the project while my gateway script holds an FSM object?

The scripting modules reload and the module-level object is re-created from its initializer, so the current state returns to its initial value. The same reset occurs when any Designer saves a global change such as a shared script. State held in a tag or database is unaffected.

What happens if a client script instantiates the same FSM class?

The client gets its own independent object with no link to the gateway instance, and so does every Designer and every other client. State changes in one copy are invisible to the others, so read the shared state tag instead of keeping a client-side FSM.

What happens if the gateway restarts mid-sequence?

Every module-level object is lost, and the timer script rebuilds the FSM on its first scan. It resumes from the state stored in the tag or database. Confirm your version retains memory tag values across a restart, or use a database row when the state must survive with certainty.

Back to blog