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:
- 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.
- 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.
- 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?
- 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. - 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 - 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.readBlockingandsystem.tag.writeBlocking, use the oldersystem.tag.readandsystem.tag.writecalls with the same logic. - Register transition functions inside
scan(or a helper it calls) on every run, since the object is rebuilt each time. - Source the
eventslist 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?
- Run the timer script and drive the FSM to a non-initial state, for example send
trig3froms1. Confirm[default]FSM/Statereads2. - Save the project from the Designer, which forces the script modules to reload. Confirm the tag still reads
2and that the next scan resumes from state2, not from the initializer. - Send
trig1and confirm the tag moves to1, matching row 2, column 0 of the transition table. - 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).
- 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.