State Machines¶
PyAutomation’s state-machine runtime executes all control logic, sequencing, and supervisory workflows. It is built on top of the statemachine library and wrapped by the Machine singleton so every machine shares the same scheduler, database connections, and observability layer.
Architecture at a Glance¶
- Machine (singleton): Owns the
StateMachineManager, loads persisted configuration, wires alarm/logging engines, and exposesappend_machine,start,join, anddrop. - Scheduler:
StateMachineWorkerruns machines on a fixed interval. Default mode is asynchronous; synchronous mode is available for tightly coupled cycles. - Base classes:
StateMachineCoredefines the standard lifecycle (start → wait → run → reset/restart).AutomationStateMachineextends it with extratestandsleepstates for simulation/low-power modes. - Data plane: Process variables are
ProcessTypeinstances. Read-only variables subscribe to CVT tags; writable variables can be exposed as tags automatically viacreate_tag_internal_process_type, enabling logging and alarming. - Observability: Each machine can emit state changes through SocketIO, is persisted by
MachinesLoggerEngine, and can be serialized for UIs or tests with.serialize(). - Interop: Machines interact with
CVTEnginefor data,AlarmManagerfor protection logic,DataLoggerEnginefor history, and the OPC UA server/client stack for field connectivity.
Lifecycle¶
- Define a class deriving from
AutomationStateMachine(orStateMachineCoreif you only need the core lifecycle). - Declare states and transitions as class attributes using the
statemachine.StateDSL. - Implement behaviors in
while_<state>handlers and transition hooks (e.g.,on_run_to_reset). - Register the instance with the
Machinesingleton viaappend_machine(machine, interval, mode). - Start scheduling with
machine.start()(PyAutomation calls this from higher-level entry points). - Operate: The scheduler cycles through states, buffering subscribed inputs until the machine is ready to run.
- Stop: Call
machine.stop()for a safe shutdown.
State Machine Lifecycle Diagram¶
stateDiagram-v2
[*] --> start: Initialize
start --> wait: start_to_wait
wait --> run: wait_to_run (buffers ready)
run --> reset: run_to_reset
run --> restart: run_to_restart
reset --> start: reset_to_start
restart --> wait: restart_to_wait
wait --> reset: wait_to_reset
wait --> restart: wait_to_restart
[*] --> test: Testing mode
[*] --> sleep: Sleep mode
test --> restart: test_to_restart
test --> reset: test_to_reset
sleep --> restart: sleep_to_restart
sleep --> reset: sleep_to_reset
State Machine Core Architecture¶
graph TB
subgraph "StateMachineCore"
Start[Start State]
Wait[Wait State]
Run[Run State]
Reset[Reset State]
Restart[Restart State]
end
subgraph "AutomationStateMachine"
Test[Test State]
Sleep[Sleep State]
end
subgraph "Data Flow"
CVT[CVT Tags]
Buffer[Input Buffers]
PV[Process Variables]
end
Start --> Wait
Wait -->|Buffers Full| Run
Run --> Reset
Run --> Restart
Reset --> Start
Restart --> Wait
Run --> Test
Run --> Sleep
CVT -->|Subscribe| Buffer
Buffer -->|Fill| Wait
Run -->|Read| PV
PV -->|Write| CVT
Implementing a Machine¶
from automation import PyAutomation
from automation.state_machine import AutomationStateMachine, State
from automation.models import FloatType
class Mixer(AutomationStateMachine):
idle = State('idle', initial=True)
running = State('running')
start = idle.to(running)
stop = running.to(idle)
def while_running(self):
# Main logic loop
level = self.get_process_variables().get("tank_level")
if level and level["value"] > 80:
self.send('stop')
app = PyAutomation()
mixer = Mixer(name="Mixer", interval=1.0)
app.machine.append_machine(mixer, interval=FloatType(1.0), mode="async")
app.machine.start()
Scheduling and Inputs¶
- Interval & mode:
append_machineacceptsinterval(seconds) andmode(asyncorsync). Intervals are persisted when a database is configured. - Buffers: Each subscribed tag keeps a rolling buffer (
buffer_size,buffer_roll_type) managed byStateMachineCore. The default behavior waits for buffers to fill before enteringrun. - Process variables: Use
add_process_variable(name, tag, read_only=True)to bind CVT tags as inputs. Internal process variables are exposed as CVT tags to make diagnostics, alarms, and logging first-class.
Interaction with the Rest of the Stack¶
- Alarms: Machines can raise alarms via
PyAutomation.create_alarmor rely on IAD alarms auto-created for out-of-range, frozen data, and outliers. - Data logging: When CVT tags are bound to a machine,
DataLoggerEnginepersists values through theLoggerWorkerwithout extra code. - OPC UA: Machines can expose or consume OPC UA nodes through the embedded server/client managers (
OPCUAServer,OPCUAClientManager).
Best Practices¶
- Keep
while_*handlers short and non-blocking; prefer timers or counters overtime.sleep. - Treat CVT as the single source of truth for inputs and outputs—avoid hidden globals.
- Use explicit state transitions (
send(...)) instead of conditionally mutating state flags. - Version your machine configuration (name, classification, priority, threshold/on-delay) in the database so restarts restore the expected behavior.
- Emit meaningful descriptions for process variables; they surface in the UI, logs, and OPC UA address space.
Troubleshooting Checklist¶
- Machine never enters
run: confirm subscribed tag buffers are filling and thatwait_to_runis reachable. - No state changes in UI: ensure SocketIO is configured (
define_dash_app) and the machine hasset_socketioset. - Scheduler idle: verify
machine.start()was called and there is at least one registered machine inStateMachineManager. - Unexpected restarts: check
while_runningexceptions—decorators (logging_error_handler) capture and log them, but long blocking work can trigger resets.