├── LICENSE
├── MANIFEST.in
├── README.md
├── README.rst
├── scm
├── FrameMover.py
├── Parallel.py
├── State.py
├── StateMachine.py
├── StateMachineManager.py
└── __init__.py
├── scm_tutorial.py
├── setup.py
├── test-CandyMachine.py
├── test-HistoryMachine.py
└── test-StateMachine.py
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 | include README.*
3 | global-exclude *.pyc
4 | global-exclude __pycache__
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pyscm
2 | A python state machine framework based on statecharts (scxml).
3 |
4 | This is a python library supporting statechart (in scxml format) originated by David Harel.
5 | Features like hierarchical states, parallel states, and history are ready for you to command.
6 | You won't find another lib as easy and flexible to use as SCM.
7 |
8 | Just take a look and run scm_tutorial.py.
9 | The code is listed here:
10 | ```
11 | from scm import StateMachineManager
12 |
13 |
14 | client_scxml = """\
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | """
29 |
30 | class Life:
31 | def __init__(self):
32 | self.mach_ = StateMachineManager.instance().getMach('the life')
33 | self.mach_.set_do_exit_state_on_destroy(True)
34 | self.mach_.register_state_slot("appear", self.onentry_appear, self.onexit_appear)
35 | self.mach_.register_state_slot("live", self.onentry_live, self.onexit_live)
36 | self.mach_.register_state_slot("eat", self.onentry_eat, self.onexit_eat)
37 | self.mach_.register_state_slot("move", self.onentry_move, self.onexit_move)
38 | self.mach_.register_state_slot("dead", self.onentry_dead, self.onexit_dead)
39 | self.mach_.register_action_slot('say_hello', self.say_hello)
40 | self.mach_.StartEngine()
41 |
42 | def test(self):
43 | self.mach_.enqueEvent("born")
44 | #self.mach_.frame_move(0) # state change to 'live'
45 | StateMachineManager.instance().pumpMachEvents()
46 | self.mach_.enqueEvent("hp_zero")
47 | #self.mach_.frame_move(0) # state change to 'dead'
48 | StateMachineManager.instance().pumpMachEvents()
49 |
50 | def onentry_appear(self):
51 | print("come to exist")
52 |
53 | def onexit_appear(self):
54 | print("we are going to...")
55 |
56 | def onentry_live(self):
57 | print("start living")
58 |
59 | def onexit_live(self):
60 | print("no longer live")
61 |
62 | def onentry_eat(self):
63 | print("start eating")
64 |
65 | def onexit_eat(self):
66 | print("stop eating")
67 |
68 | def onentry_move(self):
69 | print("start moving")
70 |
71 | def onexit_move(self):
72 | print("stop moving")
73 |
74 | def onentry_dead(self):
75 | print("end")
76 |
77 | def onexit_dead(self):
78 | assert (0 and "should not exit final state");
79 | print("no, this won't get called.")
80 |
81 | def say_hello(self):
82 | print("\n*** Hello, World! ***\n")
83 |
84 | if __name__ == '__main__':
85 | StateMachineManager.instance().set_scxml("the life", client_scxml)
86 | life = Life()
87 | life.test()
88 | StateMachineManager.instance().pumpMachEvents()
89 |
90 | ```
91 |
92 | The output you should see:
93 |
94 | """
95 | come to exist
96 | we are going to...
97 |
98 | *** Hello, World! ***
99 |
100 | start living
101 | start eating
102 | start moving
103 | stop eating
104 | stop moving
105 | no longer live
106 | end
107 | """
108 |
109 | Simply
110 | 1. you load the scxml from external file or from a string defined in your code.
111 | 2. you connect these onentry_ onexit_, etc. slots
112 | 3. you start the engine, call the framemove in main loop.
113 | Done.
114 |
115 | It's that easy!
116 |
117 | Read the tutorials at
118 | [English] (http://zen747.blogspot.tw/2017/07/a-scm-framework-tutorial-statechart.html)
119 |
120 | [Traditional Chinese] (http://zen747.blogspot.tw/2017/07/scm-framework.html)
121 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | This is a python library supporting statechart (in scxml format) originated by David Harel.
2 | Features like hierarchical states, parallel states, and history are ready for you to command.
3 | You won't find another lib as easy and flexible to use as SCM.
4 |
5 | Just take a look and run scm_tutorial.py please.
6 | The code is listed here:
7 |
8 | .. code:: python
9 |
10 | from scm import StateMachineManager
11 |
12 | client_scxml = """
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | """
27 |
28 | class Life:
29 | def __init__(self):
30 | self.mach_ = StateMachineManager.instance().getMach('the life')
31 | self.mach_.set_do_exit_state_on_destroy(True)
32 | self.mach_.register_state_slot("appear", self.onentry_appear, self.onexit_appear)
33 | self.mach_.register_state_slot("live", self.onentry_live, self.onexit_live)
34 | self.mach_.register_state_slot("eat", self.onentry_eat, self.onexit_eat)
35 | self.mach_.register_state_slot("move", self.onentry_move, self.onexit_move)
36 | self.mach_.register_state_slot("dead", self.onentry_dead, self.onexit_dead)
37 | self.mach_.register_action_slot('say_hello', self.say_hello)
38 | self.mach_.StartEngine()
39 |
40 | def test(self):
41 | self.mach_.enqueEvent("born")
42 | #self.mach_.frame_move(0) # state change to 'live'
43 | StateMachineManager.instance().pumpMachEvents()
44 | self.mach_.enqueEvent("hp_zero")
45 | #self.mach_.frame_move(0) # state change to 'dead'
46 | StateMachineManager.instance().pumpMachEvents()
47 |
48 | def onentry_appear(self):
49 | print("come to exist")
50 |
51 | def onexit_appear(self):
52 | print("we are going to...")
53 |
54 | def onentry_live(self):
55 | print("start living")
56 |
57 | def onexit_live(self):
58 | print("no longer live")
59 |
60 | def onentry_eat(self):
61 | print("start eating")
62 |
63 | def onexit_eat(self):
64 | print("stop eating")
65 |
66 | def onentry_move(self):
67 | print("start moving")
68 |
69 | def onexit_move(self):
70 | print("stop moving")
71 |
72 | def onentry_dead(self):
73 | print("end")
74 |
75 | def onexit_dead(self):
76 | assert (0 and "should not exit final state");
77 | print("no, this won't get called.")
78 |
79 | def say_hello(self):
80 | print("\n*** Hello, World! ***\n")
81 |
82 | if __name__ == '__main__':
83 | StateMachineManager.instance().set_scxml("the life", client_scxml)
84 | life = Life()
85 | life.test()
86 | StateMachineManager.instance().pumpMachEvents()
87 |
88 |
89 |
90 | The output you should see
91 |
92 | ::
93 |
94 | come to exist
95 | we are going to...
96 |
97 | *** Hello, World! ***
98 |
99 | start living
100 | start eating
101 | start moving
102 | stop eating
103 | stop moving
104 | no longer live
105 | end
106 |
107 | Simply
108 |
109 | 1. you load the scxml from external file or from a string defined in your code.
110 | 2. you connect these **onentry_** **onexit_**, etc. slots
111 | 3. you start the engine, call the framemove in main loop.
112 |
113 | Done.
114 |
115 | It's that easy!
116 |
117 | Read the tutorials at:
118 |
119 | - (English) http://zen747.blogspot.tw/2017/07/a-scm-framework-tutorial-statechart.html
120 | - (Traditional Chinese) http://zen747.blogspot.tw/2017/07/scm-framework.html
121 |
--------------------------------------------------------------------------------
/scm/FrameMover.py:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | class Signal:
5 | "Google signals and slots to learn the concept."
6 | def __init__(self):
7 | self.slots_ = []
8 |
9 | def connect(self, func):
10 | self.slots_.append(func)
11 | return Connection(self, func)
12 |
13 | def disconnect(self, func):
14 | self.slots_.remove(func)
15 |
16 | def emit(self):
17 | for func in self.slots_:
18 | func()
19 |
20 | class Connection:
21 | "The purpose of this class is to avoid the need to keep track of the origin of func"
22 | def __init__(self, signal, func):
23 | self.signal_ = signal
24 | self.func_ = func
25 |
26 | def disconnect(self):
27 | self.signal_.disconnect(self.func_)
28 |
29 | class FrameMover:
30 | def __init__(self):
31 | self.pause_ = False
32 | self.total_elapsed_time_ = 0
33 | self.signal_on_frame_move_ = Signal()
34 | self.signal_on_pause_ = Signal()
35 | self.signal_on_resume_ = Signal()
36 |
37 | def onFrameMove(self, t):
38 | pass
39 |
40 | def onPause(self):
41 | pass
42 |
43 | def onResume(self):
44 | pass
45 |
46 | def connect_signal_on_frame_move(self, slot):
47 | return self.signal_on_frame_move_.connect(slot)
48 |
49 | def connect_signal_on_pause(self, slot):
50 | return self.signal_on_pause_.connect(slot)
51 |
52 | def connect_signal_on_resume(self, slot):
53 | return self.signal_on_resume_.connect(slot)
54 |
55 | def frame_move(self, t):
56 | self.total_elapsed_time_ += t
57 | self.signal_on_frame_move_.emit()
58 | self.onFrameMove(t)
59 |
60 | def pause(self):
61 | if not self.pause_:
62 | self.pause_ = True
63 | self.signal_on_pause_.emit()
64 | self.onPause()
65 |
66 | def resume(self):
67 | if self.pause_:
68 | self.pause_ = False
69 | self.signal_on_resume_.emit()
70 | self.onResume()
71 |
72 | def toggle_pause(self):
73 | if self.pause_:
74 | self.resume()
75 | else:
76 | self.pause()
77 |
78 | def is_paused(self):
79 | return self.pause_
80 |
81 | def total_elapsed_time(self):
82 | return self.total_elapsed_time_
83 |
84 | def reset_time(self):
85 | self.total_elapsed_time_ = 0
86 |
--------------------------------------------------------------------------------
/scm/Parallel.py:
--------------------------------------------------------------------------------
1 | from .State import State
2 |
3 |
4 | done_state_prefix = "done.state."
5 |
6 |
7 | class Parallel(State):
8 |
9 | def __init__(self, state_id, parent, machine):
10 | State.__init__(self, state_id, parent, machine)
11 | self.finished_substates_ = set()
12 |
13 | def clone(self, parent, m):
14 | pa = Parallel(self.state_id_, parent, m)
15 | pa.clone_data(self)
16 | return pa
17 |
18 | def makeSureEnterStates(self):
19 | self.enterState(True)
20 | for substate in self.substates_:
21 | substate.makeSureEnterStates()
22 |
23 | def onEvent(self, e):
24 | if self.isLeavingState():
25 | return
26 |
27 | if self.done_:
28 | return
29 |
30 | if e[0:len(done_state_prefix)] == done_state_prefix:
31 | state_uid = e[len(done_state_prefix):]
32 | if state_uid:
33 | self.finished_substates_.add(state_uid)
34 |
35 | for tran in self.transitions_:
36 | if tran.attr_.event_ == e:
37 | if self.trig_cond(tran):
38 | self.changeState(tran)
39 | return
40 |
41 | for substate in self.substates_:
42 | substate.onEvent(e)
43 |
44 | if len(self.finished_substates_) == len(self.substates_):
45 | self.done_ = True
46 | self.signal_done_.emit()
47 | self.machine_().enqueEvent(done_state_prefix + self.state_uid_)
48 |
49 |
50 | def enterState(self, enter_substate=True):
51 | if self.active_:
52 | return
53 |
54 | # if not self.substates_:
55 | # raise Exception("Parallen have no substates!")
56 |
57 | if self.parent_() and self.machine_().history_type(self.parent_().state_uid()):
58 | self.parent_().history_state_id_ = self.state_uid_
59 |
60 | self.finished_substates_ = set()
61 |
62 | self.done_ = False
63 | self.active_ = True
64 | self.reset_time()
65 |
66 | self.machine_().current_leaf_state_ = self
67 | self.signal_onentry_.emit()
68 |
69 | if self.parent_() and not self.parent_().inStateObj(self, False):
70 | return
71 |
72 | if enter_substate:
73 | for substate in self.substates_:
74 | substate.enterState(enter_substate)
75 |
76 |
77 | def doEnterState(self, vps):
78 | state = vps[-1]
79 | if state.depth_ <= self.depth_:
80 | return
81 |
82 | vps.pop()
83 |
84 | for substate in self.substates_:
85 | if state is substate:
86 | state.enterState(False)
87 | if vps:
88 | state.doEnterState(vps)
89 | return
90 |
91 |
92 | def exitState(self):
93 | if not self.active_:
94 | return
95 |
96 | for substate in self.substates_:
97 | substate.exitState()
98 |
99 | self.active_ = False
100 |
101 | self.signal_onexit_.emit()
102 |
103 |
104 | def inState(self, state_id, recursive=True):
105 | for substate in self.substates_:
106 | if substate.state_uid_ == state_id:
107 | return True
108 |
109 | if recursive:
110 | for substate in self.substates:
111 | if substate.inState(state_id, recursive):
112 | return True
113 |
114 | return False
115 |
116 |
117 | def inStateObj(self, state, recursive=True):
118 | for substate in self.substates_:
119 | if substate is state:
120 | return True
121 |
122 | if recursive:
123 | for substate in self.substates_:
124 | if substate.inStateObj(state, recursive):
125 | return True
126 |
127 | return False
128 |
129 |
130 | def onFrameMove(self, t):
131 | if self.isLeavingState():
132 | if self.check_leaving_state_finished(t):
133 | return
134 |
135 | for substate in self.substates_:
136 | substate.frame_move(t)
137 | if not self.active_:
138 | return
139 |
140 | self.pumpNoEvents()
141 |
142 | if not self.active_:
143 | return
144 |
145 | for slot in self.frame_move_slots_:
146 | slot(t)
147 |
148 |
149 |
--------------------------------------------------------------------------------
/scm/State.py:
--------------------------------------------------------------------------------
1 | from .FrameMover import FrameMover, Signal, Connection
2 | from random import randint
3 | import weakref
4 |
5 | class TransitionAttr:
6 | def __init__(self, event, target):
7 | self.event_ = event
8 | self.transition_target_ = target
9 | self.not_ = False # to support "!In(state)"
10 | self.random_target_ = []
11 | self.cond_ = '' # for later connecting slot
12 | self.ontransit_ = ''# for later connecting slot
13 | self.in_state_ = [] # for inState check if set. You can use '|' to specify multiple states, ex. "In(state1|state2)"
14 |
15 |
16 | class Transition:
17 | def __init__(self, attr):
18 | self.attr_ = attr
19 | self.cond_functor_ = None
20 | self.signal_transit_ = Signal()
21 |
22 | def setAttr(self, attr):
23 | self.attr_ = attr
24 |
25 |
26 | class State(FrameMover):
27 |
28 | done_state_prefix = "done.state."
29 |
30 |
31 | def __init__(self, state_id, parent, machine):
32 | FrameMover.__init__(self)
33 | self.parent_ = weakref.ref(parent) if parent else None
34 | self.machine_ = weakref.ref(machine) if machine else None
35 | self.substates_ = []
36 | self.no_event_transitions_ = []
37 | self.transitions_ = []
38 | self.frame_move_slots_ = []
39 | self.slots_ready_ = False
40 | if self.parent_:
41 | self.depth_ = self.parent_().depth_ + 1
42 | else:
43 | self.depth_ = 0
44 | self.active_ = False
45 | self.done_ = False
46 | self.is_a_final_ = False
47 | self.current_state_ = None
48 | self.history_state_id_ = ''
49 | self.leaving_delay_ = 0.0
50 | self.leaving_target_transition_ = None
51 | self.leaving_elapsed_seconds_ = 0.0
52 | self.state_id_ = ''
53 | self.is_unique_state_id_ = True
54 | self.set_state_id(state_id)
55 | self.signal_done_ = Signal()
56 | self.signal_onentry_ = Signal()
57 | self.signal_onexit_ = Signal()
58 | #print(f'new {self.state_uid_}')
59 |
60 | def __del__(self):
61 | #print(f'delete {self.state_uid_}')
62 | pass
63 |
64 | def set_state_id(self, state_id):
65 | if self.slots_ready_:
66 | raise Exception("slots already ready, can't change state id!")
67 |
68 | if self.state_id_ and self.machine_() is not self:
69 | self.machine_().removeState(self)
70 |
71 | self.state_id_ = state_id
72 |
73 | if not self.state_id_: # anonymous state, generate one
74 | if self.machine_() is self:
75 | self.state_id_ = '_root'
76 | else:
77 | self.state_id_ = '_st' + str(self.machine_().num_of_states())
78 | else:
79 | self.is_unique_state_id_ = self.machine_().is_unique_id(self.state_id_)
80 |
81 | if self.is_unique_state_id_:
82 | self.state_uid_ = self.state_id_
83 | else:
84 | self.state_uid_ = self.parent_().state_uid_ + '.' + self.state_id_
85 |
86 | if self.machine_() is not self: # delay addState to after machine_() construction complete.
87 | self.machine_().addState(self)
88 |
89 |
90 | def state_id(self):
91 | return self.state_id_
92 |
93 |
94 | def state_uid(self):
95 | return self.state_uid_
96 |
97 |
98 | def depth(self):
99 | return self.depth_
100 |
101 |
102 | def active(self):
103 | return self.active_
104 |
105 |
106 | def done(self):
107 | return self.done_
108 |
109 |
110 | def leavingDelay(self):
111 | return self.leaving_delay_
112 |
113 |
114 | def isLeavingState(self):
115 | return self.leaving_target_transition_
116 |
117 |
118 | def setLeavingDelay(self, delay):
119 | self.leaving_delay_ = delay
120 |
121 |
122 | def initial_state(self):
123 | initstate = self.machine_().initial_state_of_state(self.state_uid_)
124 | if initstate:
125 | return inits;
126 | elif self.substates_:
127 | return substates_[0].state_uid()
128 | else:
129 | return "" # a leaf state has no initial.
130 |
131 |
132 | def machine_clear_substates(self):
133 | for substate in self.substates_:
134 | substate.machine_clear_substates ()
135 |
136 | self.machine_ = None
137 | self.substates_ = []
138 |
139 |
140 | def clone(self, parent, machine):
141 | state = State(self.state_id_, parent, machine)
142 | state.clone_data(self)
143 | return state
144 |
145 |
146 | def clone_data(self, rhs):
147 | for substate in rhs.substates_:
148 | self.substates_.append(substate.clone(self, self.machine_()))
149 |
150 | self.depth_ = rhs.depth_
151 | self.is_a_final_ = rhs.is_a_final_
152 | self.is_unique_state_id_ = rhs.is_unique_state_id_
153 | self.leaving_delay_ = rhs.leaving_delay_
154 |
155 |
156 | def reset_history(self):
157 | if not self.machine_:
158 | return
159 |
160 | if self.machine_().history_type(self.state_uid_):
161 | self.clearHistory()
162 |
163 | for substate in self.substates_:
164 | substate.reset_history()
165 |
166 |
167 | def clearHistory(self):
168 | self.history_state_id_ = ''
169 |
170 |
171 | def clearDeepHistory(self):
172 | self.history_state_id_ = ''
173 | for substate in self.substates_:
174 | substate.clearDeepHistory()
175 |
176 |
177 | def trig_cond(self, tran):
178 | if tran.cond_functor_:
179 | change = tran.cond_functor_()
180 | if tran.attr_.not_:
181 | change = not change
182 | return change
183 | elif tran.attr_.in_state_:
184 | change = False
185 | for s in tran.attr_.in_state_:
186 | change |= self.machine_().inState(s)
187 | if tran.attr_.not_:
188 | change = not change
189 | return change
190 | else:
191 | return True
192 |
193 |
194 | def enterState(self, enter_substate=True):
195 | if self.active_:
196 | return # already in state
197 |
198 | if self.parent_ and self.machine_().history_type(self.parent_().state_uid()):
199 | self.parent_().history_state_id_ = self.state_uid_
200 |
201 | self.reset_time()
202 |
203 | self.done_ = False
204 | self.active_ = True
205 |
206 | self.machine_().current_leaf_state_ = self
207 | self.signal_onentry_.emit()
208 |
209 | if not self.active_: # in case state changed immediately at last signal_onentry.
210 | return
211 |
212 | if enter_substate:
213 | self.doEnterSubState()
214 |
215 | if self.is_a_final_ and self.parent_():
216 | self.parent_().done_ = True
217 | self.parent_().signal_done_.emit()
218 | self.machine_().enqueEvent(State.done_state_prefix + self.parent_().state_uid())
219 |
220 |
221 | def exitState(self):
222 | if not self.active_:
223 | return
224 |
225 | if self.current_state_:
226 | self.current_state_.exitState()
227 |
228 | self.active_ = False
229 | self.current_state_ = None
230 |
231 | self.signal_onexit_.emit()
232 |
233 |
234 | def onEvent(self, e):
235 | if self.done_:
236 | return
237 |
238 | if self.isLeavingState():
239 | return
240 |
241 | for tran in self.transitions_:
242 | if tran.attr_.event_ == e:
243 | change = self.trig_cond(tran)
244 | if change:
245 | self.changeState(tran)
246 | return
247 |
248 | if self.current_state_:
249 | self.current_state_.onEvent(e)
250 |
251 |
252 | def findState(self, state_id, exclude=None, check_parent=True):
253 | for substate in self.substates_:
254 | if substate.state_id() == state_id:
255 | return substate
256 |
257 | for substate in self.substates_:
258 | if substate is not exclude:
259 | st = substate.findState(state_id, exclude, False)
260 | if st:
261 | return st
262 |
263 | if check_parent and self.parent_():
264 | st = self.parent_().findState(state_id, self, True)
265 | if st:
266 | return st
267 |
268 | return None
269 |
270 |
271 | def changeState(self, transition):
272 | if transition.attr_.random_target_:
273 | index = randint(0, len(transition.attr_.random_target_)-1)
274 | target = transition.attr_.random_target_[index]
275 | else:
276 | target = transition.attr_.transition_target_
277 |
278 | corresponding_state = self.machine_().state_id_of_history(target)
279 | if corresponding_state:
280 | st = self.machine_().getState(corresponding_state)
281 | if st.history_state_id_:
282 | target = st.history_state_id_
283 | else:
284 | target = st.initial_state()
285 |
286 | self.machine_().transition_source_state_ = self.state_uid_
287 | self.machine_().transition_target_state_ = target
288 |
289 | #print(f"change state from '{self.state_uid_}' to '{target}'")
290 |
291 | if not self.leaving_target_transition_:
292 | if self.leaving_delay_ != 0:
293 | attr = TransitionAttr(transition.attr_.event_, target)
294 | self.leaving_target_transition_ = Transition(attr)
295 | self.leaving_target_transition_.setAttr(attr)
296 | if transition.attr_.ontransit_:
297 | slot = self.machine_().GetActionSlot(transition.attr_.ontransit_)
298 | if slot:
299 | self.leaving_target_transition_.signal_transit_.connect(slot)
300 | return
301 |
302 | newState = []
303 | if ',' not in target:
304 | st = self.machine_().getState(target)
305 | if st:
306 | newState.append(st)
307 | else:
308 | raise Exception(f"can't find transition target '{target}'!")
309 | else:
310 | targets = target.split(',')
311 | for t in targets:
312 | st = self.machine_().getState(t)
313 | if st:
314 | newState.append(st)
315 | else:
316 | raise Exception("can't find transition target state!")
317 |
318 | if not newState:
319 | raise Exception("can't find transition target state!")
320 |
321 | lcaState = self.findLCA(newState[0])
322 |
323 | if not lcaState:
324 | raise Exception("can't find common ancestor state!")
325 |
326 | # make sure they are in the same parallel state?
327 | if len(newState) > 1:
328 | for st in newState:
329 | lca = self.findLCA(st)
330 | if lca is not lcaState:
331 | raise Exception("multiple targets but have no common ancestor.");
332 |
333 | # exit old states
334 | if len(newState) == 1 and lcaState is newState[0]:
335 | self.machine_().transition_source_state_ = lcaState.state_uid()
336 | lcaState.exitState()
337 | transition.signal_transit_.emit()
338 | lcaState.enterState()
339 | return
340 | elif lcaState.current_state_:
341 | self.machine_().transition_source_state_ = lcaState.current_state_.state_uid()
342 | lcaState.current_state_.exitState()
343 |
344 | transition.signal_transit_.emit()
345 |
346 | # enter new state
347 | vps = []
348 | for st in newState:
349 | vps.append(st)
350 | parentstate = st.parent_()
351 | while parentstate and parentstate is not lcaState:
352 | vps.append(parentstate)
353 | parentstate = parentstate.parent_()
354 |
355 | lcaState.doEnterState(vps)
356 | lcaState.makeSureEnterStates()
357 |
358 |
359 | def doEnterState(self, vps):
360 | state = vps[-1]
361 | if state.depth_ <= self.depth_:
362 | return
363 |
364 | vps.pop()
365 | self.current_state_ = state
366 | self.current_state_.enterState(False)
367 | if vps:
368 | self.current_state_.doEnterState(vps)
369 |
370 |
371 | def doEnterSubState(self):
372 | if self.history_state_id_ and self.machine_().history_type(self.state_uid_):
373 | self.current_state_ = self.machine_().getState(self.history_state_id_)
374 | self.current_state_.enterState()
375 | else:
376 | if self.substates_:
377 | initial_state = self.machine_().initial_state_of_state(self.state_uid_)
378 | if not initial_state:
379 | self.current_state_ = self.substates_[0]
380 | self.current_state_.enterState()
381 | else:
382 | attr = TransitionAttr('', initial_state)
383 | tran = Transition(attr)
384 | self.changeState(tran)
385 |
386 |
387 | def findLCA(self, ots):
388 | if not ots:
389 | raise Exception("findLCA on None object")
390 | if self is ots:
391 | return self
392 | elif self.depth_ > ots.depth_:
393 | return self.parent_().findLCA(ots)
394 | elif self.depth_ < ots.depth_:
395 | return self.findLCA(ots.parent_())
396 | else:
397 | if self.parent_() is ots.parent_():
398 | return self.parent_()
399 | else:
400 | return self.parent_().findLCA(ots.parent_())
401 |
402 | return None
403 |
404 |
405 | def inState(self, state_id, recursive=True):
406 | if not self.current_state_:
407 | return False
408 | if self.current_state_.state_id_ == state_id:
409 | return True
410 | elif recursive:
411 | return self.current_state_.inState(state_id, recursive)
412 | else:
413 | return False
414 |
415 |
416 | def inStateObj(self, state, recursive=True):
417 | if not self.current_state_:
418 | return False
419 | if self.current_state_ is state:
420 | return True
421 | elif recursive:
422 | return self.current_state_.inStateObj(state, recursive)
423 | else:
424 | return False
425 |
426 |
427 | def makeSureEnterStates(self):
428 | self.enterState(True)
429 | if not self.current_state_ and self.substates_:
430 | self.doEnterSubState()
431 | if self.current_state_:
432 | self.current_state_.makeSureEnterStates()
433 |
434 |
435 | def prepareActionCondSlots(self):
436 | if self.slots_ready_:
437 | return
438 |
439 | self.slots_ready_ = True
440 |
441 | tran_attrs = self.machine_().transition_attr(self.state_uid_)
442 | size = len(tran_attrs)
443 | for i in range(size):
444 | ptr = Transition(tran_attrs[i])
445 | if not tran_attrs[i].event_:
446 | self.no_event_transitions_.append(ptr)
447 | else:
448 | self.transitions_.append(ptr)
449 |
450 | self.machine_().setActionSlot(f'clh({self.state_uid_}*)', self.clearDeepHistory)
451 | self.machine_().setActionSlot(f'clh({self.state_uid_})', self.clearHistory)
452 |
453 | for substate in self.substates_:
454 | substate.prepareActionCondSlots()
455 |
456 |
457 | def connectCondSlots(self):
458 | self.connect_transitions_conds(self.transitions_)
459 | self.connect_transitions_conds(self.no_event_transitions_)
460 |
461 | for substate in self.substates_:
462 | substate.connectCondSlots()
463 |
464 |
465 | def connect_transitions_conds(self, transitions):
466 | """ """
467 | size = len(transitions)
468 | for i in range(size):
469 | if transitions[i].attr_.cond_:
470 | cond = transitions[i].attr_.cond_
471 | instate_check = cond[0:3]
472 | if instate_check == "In(" or instate_check == "in(":
473 | endmark = cond.find(')', 4)
474 | states = cond[3:endmark].split("|")
475 | for st in states:
476 | if not self.machine_().is_unique_id(st):
477 | s = self.findState(st)
478 | if not s:
479 | raise Exception("can't find state for In() check")
480 | st = s.state_uid()
481 | else:
482 | s = self.findState(st)
483 | if not s:
484 | raise Exception("can't find state for In() check")
485 | transitions[i].attr_.in_state_.append(st)
486 | else:
487 | s = self.machine_().GetCondSlot(cond)
488 | if s:
489 | transitions[i].cond_functor_ = s
490 | else:
491 | raise Exception("can't connect cond slot")
492 |
493 |
494 | def connectActionSlots(self):
495 | onentry = self.machine_().onentry_action(self.state_uid_)
496 | if onentry:
497 | s = self.machine_().GetActionSlot(onentry)
498 | if s:
499 | self.signal_onentry_.connect(s)
500 | elif onentry != f'onentry_{self.state_uid_}':
501 | raise Exception("can't connect onentry slot")
502 |
503 | onexit = self.machine_().onexit_action(self.state_uid_)
504 | if onexit:
505 | s = self.machine_().GetActionSlot(onexit)
506 | if s:
507 | self.signal_onexit_.connect(s)
508 | elif onexit != f'onexit_{self.state_uid_}':
509 | raise Exception("Can't connect onexit slot")
510 |
511 | frame_move = self.machine_().frame_move_action(self.state_uid_)
512 | if frame_move:
513 | sf = self.machine_().GetFrameMoveSlot(frame_move)
514 | if sf:
515 | self.frame_move_slots_.append(sf)
516 | elif frame_move != self.state_uid_:
517 | raise Exception("Can't connect frame_move slot")
518 |
519 | self.connect_transitions_signal(self.transitions_)
520 | self.connect_transitions_signal(self.no_event_transitions_)
521 |
522 | for substate in self.substates_:
523 | substate.connectActionSlots()
524 |
525 |
526 | def connect_transitions_signal(self, transitions):
527 | for tran in transitions:
528 | if tran.attr_.ontransit_:
529 | tr = tran.attr_.ontransit_
530 | s = self.machine_().GetActionSlot(tr)
531 | if s:
532 | tran.signal_transit_.connect(s)
533 | else:
534 | raise Exception("can't connect on_transit slot")
535 |
536 |
537 | def doLeaveAferDelay(self):
538 | if self.leaving_target_transition_:
539 | self.changeState(self.leaving_target_transition_)
540 | self.leaving_target_transition_ = None
541 | self.leaving_elapsed_seconds_ = 0
542 |
543 | def check_leaving_state_finished(self, t):
544 | if self.leaving_delay_ < 0:
545 | return False
546 | self.leaving_elapsed_seconds_ += t
547 | if self.leaving_elapsed_seconds_ >= self.leaving_delay_:
548 | self.doLeaveAferDelay()
549 | return True
550 | return False
551 |
552 |
553 | def onFrameMove(self, t):
554 | if self.isLeavingState():
555 | if self.check_leaving_state_finished(t):
556 | return
557 |
558 | if self.current_state_:
559 | self.current_state_.frame_move(t)
560 | if not self.active_:
561 | return
562 |
563 | self.pumpNoEvents()
564 |
565 | if not self.active_:
566 | return
567 |
568 | for slot in self.frame_move_slots_:
569 | slot(t)
570 |
571 |
572 | def pumpNoEvents(self):
573 | if self.isLeavingState():
574 | return
575 |
576 | for tran in self.no_event_transitions_:
577 | if self.trig_cond(tran):
578 | self.changeState(tran)
579 | return
580 |
581 |
--------------------------------------------------------------------------------
/scm/StateMachine.py:
--------------------------------------------------------------------------------
1 | from .State import State
2 | from .FrameMover import Signal
3 | import gc
4 | import weakref
5 |
6 |
7 | class TimedEventType:
8 | def __init__(self, time, strEvent, cancelable):
9 | self.time_ = time
10 | self.event_ = strEvent
11 | self.cancelable_ = cancelable
12 |
13 |
14 | class StateMachine(State):
15 |
16 | def __init__(self, manager):
17 | self.manager_ = weakref.ref(manager)
18 | self.slots_prepared_ = False
19 | self.slots_connected_ = False
20 | self.scxml_loaded_ = False
21 | self.engine_started_ = False
22 | self.on_event_ = False
23 | self.with_history_ = False
24 | self.current_leaf_state_ = None
25 | self.frame_move_slots_ = dict()
26 | self.cond_slots_ = dict()
27 | self.action_slots_ = dict()
28 | self.allow_nop_entry_exit_slot_ = False
29 | self.do_exit_state_on_destroy_ = False
30 | self.transition_source_state_ = self.transition_target_state_ = ''
31 | self.timed_events_ = []
32 | self.queued_events_ = []
33 | self.states_map_ = dict()
34 | self.signal_prepare_slots_ = Signal();
35 | self.signal_connect_cond_slots_ = Signal();
36 | self.signal_connect_action_slots_ = Signal();
37 |
38 | State.__init__(self, '', None, self)
39 |
40 | self.addState(self)
41 |
42 |
43 | def __del__(self):
44 | self.clearTimedEvents()
45 | self.destroy_machine(self.do_exit_state_on_destroy_)
46 | State.__del__(self)
47 |
48 | def set_do_exit_state_on_destroy(self, yes_no):
49 | self.do_exit_state_on_destroy_ = yes_no
50 |
51 | def clone(self):
52 | mach = StateMachine(self.manager_())
53 | mach.state_id_ = self.state_id_
54 | mach.scxml_id_ = self.scxml_id_
55 | mach.scxml_loaded_ = self.scxml_loaded_
56 |
57 | mach.machine_ = weakref.ref(mach)
58 | mach.clone_data(self)
59 |
60 | return mach
61 |
62 |
63 | def is_unique_id(self, state_id):
64 | if not self.manager_():
65 | print('no manager')
66 | return True
67 | else:
68 | return self.manager_().is_unique_id(self.scxml_id_, state_id)
69 |
70 |
71 | def getState(self, state_uid):
72 | if state_uid in self.states_map_:
73 | return self.states_map_[state_uid]
74 | else:
75 | return None
76 |
77 |
78 | def addState(self, state):
79 | if not state:
80 | return
81 | self.states_map_[state.state_uid()] = state
82 |
83 |
84 | def removeState(self, state):
85 | if not state or state is self:
86 | return
87 | if self.states_map_[state.state_uid()] is state:
88 | del self.states_map_[state.state_uid()]
89 |
90 |
91 | def inState(self, state_uid):
92 | if state_uid in self.states_map_:
93 | return self.states_map_[state_uid].active()
94 | else:
95 | return False
96 |
97 | def elapsed_time_of_current_state(self):
98 | return self.current_leaf_state_.total_elapsed_time_
99 |
100 | def onFrameMove(self, t):
101 | if not self.slots_connected_:
102 | return
103 | State.onFrameMove(self, t)
104 | self.pumpTimedEvents()
105 | while len(self.queued_events_):
106 | self.pumpQueuedEvents()
107 |
108 | def pumpQueuedEvents(self):
109 | queued_events = self.queued_events_
110 | self.queued_events_ = []
111 | for event in queued_events:
112 | self.onEvent(event)
113 |
114 | def enqueEvent(self, e):
115 | self.queued_events_.append(e)
116 | self.manager_().addToActiveMach(self)
117 |
118 |
119 | def onEvent(self, e):
120 | if not self.slots_connected_:
121 | raise Exception('Slots not connected yet!')
122 | if self.on_event_:
123 | self.enqueEvent(e)
124 | return
125 | #print(f'onEvent({e})')
126 | self.on_event_ = True
127 | State.onEvent(self, e)
128 | self.on_event_ = False
129 |
130 |
131 | def prepareEngine(self):
132 | if not self.scxml_loaded_:
133 | raise Exception('no scxml loaded')
134 | self.prepare_slots()
135 | self.connect_slots()
136 |
137 |
138 | def StartEngine(self):
139 | if not self.scxml_loaded_:
140 | raise Exception('no scxml loaded')
141 | if self.engine_started_:
142 | return
143 | self.prepareEngine()
144 | self.engine_started_ = True
145 | self.enterState()
146 |
147 |
148 | def ReStartEngine(self):
149 | if not self.scxml_loaded_:
150 | raise Exception('no scxml loaded')
151 | if self.engine_started_:
152 | self.ShutDownEngine()
153 | self.StartEngine()
154 |
155 |
156 | def engineStarted(self):
157 | return engine_started_
158 |
159 |
160 | def ShutDownEngine(self, do_exit_state):
161 | if do_exit_state:
162 | self.exitState()
163 | self.engine_started_ = False
164 |
165 |
166 | def engineReady(self):
167 | return self.scxml_loaded_;
168 |
169 |
170 | def isLeavingState(self):
171 | return self.getEnterState().isLeavingState()
172 |
173 |
174 | def re_enter_state(self):
175 | return self.transition_source_state_ == self.transition_target_state_
176 |
177 |
178 | def getEnterState(self):
179 | return self.current_leaf_state_
180 |
181 |
182 | def prepare_slots(self):
183 | if self.slots_prepared_:
184 | return
185 |
186 | self.slots_prepared_ = True
187 | self.prepareActionCondSlots()
188 | self.onPrepareActionCondSlots()
189 | self.signal_prepare_slots_.emit()
190 |
191 |
192 | def connect_slots(self):
193 | if self.slots_connected_:
194 | return
195 |
196 | self.slots_connected_ = True
197 | self.connectCondSlots()
198 | self.onConnectCondSlots()
199 | self.signal_connect_cond_slots_.emit()
200 |
201 | self.connectActionSlots()
202 | self.onConnectActionSlots()
203 | self.signal_connect_action_slots_.emit()
204 |
205 |
206 | def clear_slots(self):
207 | self.slots_connected_ = False
208 | self.action_slots_ = dict()
209 | self.cond_slots_ = dict()
210 | self.frame_move_slots_ = dict()
211 | self.transitions_ = []
212 |
213 |
214 | def destroy_machine(self, do_exit_state):
215 | if self.slots_connected_:
216 | if do_exit_state:
217 | self.exitState()
218 | self.scxml_loaded_ = False
219 | self.queued_events_ = []
220 | self.states_map_ = dict()
221 | self.machine_clear_substates()
222 | self.clear_slots()
223 | self.reset_history()
224 |
225 | self.engine_started_ = False
226 |
227 |
228 | def loadSCXMLString(self, xmlstr):
229 | if self.scxml_loaded_:
230 | self.destroy_machine()
231 | self.scxml_loaded_ = self.manager_().loadMachFromString(self, xmlstr)
232 | if not self.scxml_loaded_:
233 | self.destroy_machine()
234 | self.onLoadScxmlFailed()
235 |
236 |
237 | def GetCondSlot(self, name):
238 | if not self.cond_slots_:
239 | return None
240 | if name in self.cond_slots_:
241 | return self.cond_slots_[name]
242 | return None
243 |
244 |
245 | def GetActionSlot(self, name):
246 | if not self.action_slots_:
247 | return None
248 | if name in self.action_slots_:
249 | return self.action_slots_[name]
250 | return None
251 |
252 |
253 | def GetFrameMoveSlot(self, name):
254 | if not self.frame_move_slots_:
255 | return None
256 | if name in self.frame_move_slots_:
257 | return self.frame_move_slots_[name]
258 | return None
259 |
260 |
261 | def setCondSlot(self, name, s):
262 | self.cond_slots_[name] = s
263 |
264 |
265 | def setActionSlot(self, name, s):
266 | self.action_slots_[name] = s
267 |
268 |
269 | def setFrameMoveSlot(self, name, s):
270 | self.frame_move_slots_[name] = s
271 |
272 |
273 | def registerTimedEvent(self, after_t, event_e, cancelable):
274 | p = TimedEventType(after_t + self.total_elapsed_time_, event_e, cancelable)
275 | idx = 0
276 | for element in self.timed_events_:
277 | if p.time_ > element.time_:
278 | idx = i
279 | break
280 | idx += 1
281 | self.timed_events_.insert(idx, p)
282 | return p
283 |
284 |
285 | def clearTimedEvents(self):
286 | self.timed_events_.clear()
287 |
288 |
289 | def pumpTimedEvents(self):
290 | idx = 0
291 | for p in self.timed_events_:
292 | if p.time_ <= self.total_elapsed_time_:
293 | if not p.cancelable_ or len(gc.get_referrers(p)) > 1:
294 | self.machine_.enqueEvent(p.event_)
295 | del self.timed_events_[idx]
296 | else:
297 | break
298 | idx += 1
299 |
300 |
301 | def state_id_of_history(self, history_id):
302 | return self.manager_().history_id_resided_state(self.scxml_id_, history_id)
303 |
304 |
305 | def history_type(self, state_uid):
306 | return self.manager_().history_type(self.scxml_id_, state_uid)
307 |
308 |
309 | def initial_state_of_state(self, state_uid):
310 | return self.manager_().initial_state_of_state(self.scxml_id_, state_uid)
311 |
312 |
313 | def onentry_action(self, state_uid):
314 | return self.manager_().onentry_action(self.scxml_id_, state_uid)
315 |
316 |
317 | def onexit_action(self, state_uid):
318 | return self.manager_().onexit_action(self.scxml_id_, state_uid)
319 |
320 |
321 | def frame_move_action(self, state_uid):
322 | return self.manager_().frame_move_action(self.scxml_id_, state_uid)
323 |
324 |
325 | def transition_attr(self, state_uid):
326 | return self.manager_().transition_attr(self.scxml_id_, state_uid)
327 |
328 |
329 | def num_of_states(self):
330 | return len(self.states_map_)
331 |
332 |
333 | def get_all_states(self):
334 | return self.manager_().get_all_states(self.scxml_id_)
335 |
336 | def manager(self):
337 | return self.manager_()
338 |
339 | def onPrepareActionCondSlots(self):
340 | pass
341 |
342 | def onConnectCondSlots(self):
343 | pass
344 |
345 | def onConnectActionSlots(self):
346 | pass
347 |
348 | def onLoadScxmlFailed(self):
349 | pass
350 |
351 | def register_state_slot(self, state, onentry, onexit):
352 | self.setActionSlot('onentry_'+state, onentry)
353 | self.setActionSlot('onexit_'+state, onexit)
354 |
355 | def register_action_slot(self, action, method):
356 | self.setActionSlot(action, method)
357 |
358 | def register_frame_move_slot(self, state, method):
359 | self.setFrameMoveSlot(state, method)
360 |
361 | def register_cond_slot(self, cond, method):
362 | self.setCondSlot(cond, method)
363 |
364 | def register_handler(self, handler):
365 | states = self.get_all_states()
366 | for state in states:
367 | s = state.replace('.', '_')
368 | try:
369 | self.setActionSlot('onentry_'+state, eval('handler.onentry_' + s))
370 | except:
371 | pass
372 | try:
373 | self.setActionSlot('onexit_'+state, eval('handler.onexit_' + s))
374 | except:
375 | pass
376 |
377 |
378 |
379 |
380 |
381 |
--------------------------------------------------------------------------------
/scm/StateMachineManager.py:
--------------------------------------------------------------------------------
1 | from .StateMachine import StateMachine
2 | from .State import State, TransitionAttr
3 | from .Parallel import Parallel
4 | import xml.etree.ElementTree as etree
5 | import json
6 |
7 | class ParseStruct:
8 | def __init__(self):
9 | self.current_state_ = 0
10 | self.machine_ = 0
11 | self.scxml_id_ = ''
12 |
13 |
14 | class StateMachineManager:
15 | def __init__(self):
16 | self.mach_map_ = dict()
17 | self.scxml_map_ = dict()
18 | self.onentry_action_map_ = dict()
19 | self.onexit_action_map_ = dict()
20 | self.frame_move_action_map_ = dict()
21 | self.initial_state_map_ = dict()
22 | self.history_type_map_ = dict()
23 | self.history_id_reside_state_ = dict()
24 | self.transition_attr_map_ = dict()
25 | self.state_uids_ = dict()
26 | self.non_unique_ids_ = dict()
27 | self.active_machs_ = []
28 |
29 | def getMach(self, scxml_id):
30 | if scxml_id in self.mach_map_:
31 | return self.mach_map_[scxml_id].clone()
32 | else:
33 | mach = StateMachine(self)
34 | self.mach_map_[scxml_id] = mach
35 | mach.scxml_id_ = scxml_id
36 | if scxml_id in self.scxml_map_:
37 | if self.scxml_map_[scxml_id].startswith('file:'):
38 | mach.scxml_loaded_ = self.loadMachFromFile(mach, self.scxml_map_[scxml_id][5:])
39 | else:
40 | mach.scxml_loaded_ = self.loadMachFromString(mach, self.scxml_map_[scxml_id])
41 | return mach.clone()
42 |
43 | def addToActiveMach(self, mach):
44 | if not mach:
45 | raise Exception('mach None?')
46 | self.active_machs_.append(mach)
47 |
48 | def pumpMachEvents(self):
49 | while self.active_machs_:
50 | machs = self.active_machs_
51 | self.active_machs_ = []
52 | for mach in machs:
53 | mach.pumpQueuedEvents()
54 |
55 | def set_scxml(self, scxml_id, scxml_str):
56 | self.scxml_map_[scxml_id] = scxml_str
57 |
58 | def set_scxml_file(self, scxml_id, scxml_filepath):
59 | self.scxml_map_[scxml_id] = 'file:' + scxml_filepath
60 |
61 | def prepare_machs(self):
62 | for scxml_id in self.scxml_map_:
63 | mach = None
64 | if scxml_id in self.mach_map_:
65 | mach = self.mach_map_[scxml_id]
66 | else:
67 | mach = StateMachine(self)
68 | mach.scxml_id_ = scxml_id
69 | self.mach_map_[scxml_id] = mach
70 | if not mach.scxml_loaded_:
71 | if self.scxml_map_[scxml_id].startswith('file:'):
72 | mach.scxml_loaded_ = self.loadMachFromFile(mach, self.scxml_map_[scxml_id][5:])
73 | else:
74 | mach.scxml_loaded_ = self.loadMachFromString(mach, self.scxml_map_[scxml_id])
75 |
76 | def loadMachFromFile(self, mach, scxml_file):
77 | with open(scxml_file, 'r') as f:
78 | self.loadMachFromString(mach, f.read())
79 |
80 | def loadMachFromString(self, mach, scxml_str):
81 | parset_data = ParseStruct()
82 | parset_data.scxml_id_ = scxml_id = mach.scxml_id_
83 | parset_data.machine_ = mach
84 | parset_data.current_state_ = mach
85 | self.transition_attr_map_[scxml_id] = dict()
86 | self.state_uids_[scxml_id] = []
87 | self.non_unique_ids_[scxml_id] = []
88 | self.onentry_action_map_[scxml_id] = dict()
89 | self.onexit_action_map_[scxml_id] = dict()
90 | self.frame_move_action_map_[scxml_id] = dict()
91 | self.initial_state_map_[scxml_id] = dict()
92 | self.history_type_map_[scxml_id] = dict()
93 | self.history_id_reside_state_[scxml_id] = dict()
94 |
95 | return self.parse_scm_tree(parset_data, scxml_str)
96 |
97 | def clearMachMap(self):
98 | self.mach_map_.clear()
99 | self.onentry_action_map_.clear()
100 | self.onexit_action_map_.clear()
101 | self.frame_move_action_map_.clear()
102 | self.initial_state_map_.clear()
103 | self.history_type_map_.clear()
104 | self.history_id_reside_state_.clear()
105 | self.transition_attr_map_.clear()
106 |
107 | def history_id_resided_state(self, scxml_id, history_id):
108 | try:
109 | return self.history_id_reside_state_[scxml_id][history_id]
110 | except KeyError:
111 | return ''
112 |
113 | def history_type(self, scxml_id, state_uid):
114 | try:
115 | return self.history_type_map_[scxml_id][state_uid]
116 | except KeyError:
117 | return ''
118 |
119 | def initial_state_of_state(self, scxml_id, state_uid):
120 | try:
121 | return self.initial_state_map_[scxml_id][state_uid]
122 | except KeyError:
123 | return ''
124 |
125 | def onentry_action(self, scxml_id, state_uid):
126 | try:
127 | return self.onentry_action_map_[scxml_id][state_uid]
128 | except KeyError:
129 | return ''
130 |
131 | def onexit_action(self, scxml_id, state_uid):
132 | try:
133 | return self.onexit_action_map_[scxml_id][state_uid]
134 | except KeyError:
135 | return ''
136 |
137 | def frame_move_action(self, scxml_id, state_uid):
138 | try:
139 | return self.frame_move_action_map_[scxml_id][state_uid]
140 | except KeyError:
141 | return ''
142 |
143 | def transition_attr(self, scxml_id, state_uid):
144 | try:
145 | return self.transition_attr_map_[scxml_id][state_uid]
146 | except KeyError:
147 | return []
148 |
149 | def num_of_states(self, scxml_id):
150 | return len(self.state_uids_[scxml_id])
151 |
152 | def is_unique_id(self, scxml_id, state_uid):
153 | return state_uid not in self.non_unique_ids_[scxml_id]
154 |
155 | def get_all_states(self, scxml_id):
156 | return self.state_uids_[scxml_id]
157 |
158 | def parse_scm_tree (self, data, scm_str):
159 | scm_str = scm_str.strip()
160 | try:
161 | if scm_str[0] == '<':
162 | self.parse_from_xml(scm_str, data)
163 | else:
164 | scm_str = scm_str.replace("'", '"')
165 | self.parse_from_json(scm_str, data)
166 | except:
167 | print('parse scxml failed!')
168 | raise
169 | #return False
170 |
171 | return True
172 |
173 | def parse_from_json(self, scm_str, data):
174 | 'todo: add json support'
175 | pass
176 |
177 | def parse_from_xml(self, scm_str, data):
178 | root = etree.fromstring(scm_str)
179 | self.parse_element_xml(data, root, 0)
180 |
181 | def validate_state_id(self, stateid):
182 | if stateid and stateid.startswith('_'):
183 | raise Exception("state id can't start with '_', which is reserved for internal use.")
184 |
185 | def parse_element_xml(self, data, element, level):
186 | manager = data.machine_.manager()
187 | scxml_id = data.scxml_id_
188 |
189 | if element.tag == 'scxml':
190 | manager.state_uids_[scxml_id].append(scxml_id)
191 | if 'non-unique' in element.attrib:
192 | non_unique_ids = element.attrib['non-unique'].split(',')
193 | manager.non_unique_ids_[scxml_id] += non_unique_ids
194 |
195 | elif element.tag == 'state':
196 | stateid = element.attrib['id'] if 'id' in element.attrib else ''
197 | self.validate_state_id(stateid)
198 | state = State(stateid, data.current_state_, data.machine_)
199 | data.current_state_.substates_.append(state)
200 | data.current_state_ = state
201 | manager.state_uids_[scxml_id].append(state.state_uid())
202 | self.handle_state_item(data, element.attrib)
203 | elif element.tag == 'parallel':
204 | stateid = element.attrib['id'] if 'id' in element.attrib else ''
205 | self.validate_state_id(stateid)
206 | state = Parallel(stateid, data.current_state_, data.machine_)
207 | data.current_state_.substates_.append(state)
208 | data.current_state_ = state
209 | manager.state_uids_[scxml_id].append(state.state_uid())
210 | self.handle_state_item(data, element.attrib)
211 | elif element.tag == 'final':
212 | stateid = element.attrib['id'] if 'id' in element.attrib else ''
213 | self.validate_state_id(stateid)
214 | state = State(stateid, data.current_state_, data.machine_)
215 | data.current_state_.substates_.append(state)
216 | data.current_state_ = state
217 | manager.state_uids_[scxml_id].append(state.state_uid())
218 | self.handle_final_item(data, element.attrib)
219 | elif element.tag == 'history':
220 | self.handle_history_item(data, element.attrib)
221 | elif element.tag == 'transition':
222 | self.handle_transition_item(data, element.attrib)
223 |
224 | for child in element:
225 | self.parse_element_xml(data, child, level+1)
226 |
227 | if element.tag == 'scxml':
228 | self.finish_scxml(data, element.attrib)
229 | #data.current_state_ = data.current_state_.parent_()
230 | elif element.tag == 'state':
231 | data.current_state_ = data.current_state_.parent_()
232 | elif element.tag == 'parallel':
233 | data.current_state_ = data.current_state_.parent_()
234 | elif element.tag == 'final':
235 | data.current_state_ = data.current_state_.parent_()
236 |
237 |
238 | @classmethod
239 | def finish_scxml(self, data, attrib):
240 | manager = data.machine_.manager()
241 | scxml_id = data.scxml_id_
242 |
243 | if 'initial' in attrib:
244 | manager.initial_state_map_[scxml_id][data.current_state_.state_uid()] = attrib['initial']
245 |
246 | # process multiple targets transition.
247 | # replace non-unique target to unique id
248 | transition_map = manager.transition_attr_map_[scxml_id]
249 | for state_uid,transitions in transition_map.items():
250 | st = data.machine_.getState(state_uid)
251 | for transition in transitions:
252 | target_str = transition.transition_target_
253 | if ',' not in target_str and data.machine_.is_unique_id(target_str):
254 | continue
255 | tstates = []
256 | targets = target_str.split(',')
257 | for i in range(len(targets)):
258 | if not data.machine_.is_unique_id(targets[i]):
259 | s = st.findState(targets[i])
260 | assert(s and "can't find transition target, not state id?")
261 | targets[i] = s.state_uid()
262 | tstates.append(data.machine_.getState(targets[i]))
263 | transition.transition_target_ = ','.join(targets)
264 |
265 | # transition to multiple targets only apply to parallel states.
266 | for i in range(len(tstates)-1):
267 | lca = tstates[0].findLCA(tstates[i+1])
268 | assert(isinstance(lca, Parallel) and "multiple targets but can't find common ancestor.")
269 |
270 | @classmethod
271 | def handle_state_item(self, data, attributes):
272 | manager = data.machine_.manager()
273 | scxml_id = data.scxml_id_
274 |
275 | onentry = ''
276 | onexit = ''
277 | framemove = ''
278 | history_type = ''
279 | leaving_delay = 0
280 |
281 | for key,value in attributes.items():
282 | if key == 'id':
283 | pass
284 | elif key == 'initial':
285 | manager.initial_state_map_[scxml_id][data.current_state_.state_uid()] = value
286 | elif key == 'history':
287 | history_type = value
288 | elif key == 'onentry':
289 | onentry = value
290 | elif key == 'onexit':
291 | onexit = value
292 | elif key == 'frame_move':
293 | framemove = value
294 | elif key == 'leaving_delay':
295 | leaving_delay = float(value)
296 | else:
297 | print(f'attribute "{key}" not supported')
298 | assert (0 and f'attribute "{key}" not supported')
299 |
300 | state_uid = data.current_state_.state_uid()
301 | if leaving_delay: data.current_state_.setLeavingDelay(leaving_delay)
302 | if not onentry: onentry = "onentry_" + state_uid
303 | if not onexit: onexit = "onexit_" + state_uid
304 | if not framemove: framemove = state_uid
305 |
306 | if data.current_state_.parent_().state_uid() in manager.history_type_map_[scxml_id] and manager.history_type_map_[scxml_id][data.current_state_.parent_().state_uid()] == 'deep':
307 | manager.history_type_map_[scxml_id][state_uid] = "deep"
308 | else:
309 | manager.history_type_map_[scxml_id][state_uid] = history_type
310 |
311 | if not manager.history_type_map_[scxml_id][state_uid]:
312 | data.machine_.with_history_ = True
313 |
314 | manager.onentry_action_map_[scxml_id][state_uid] = onentry
315 | manager.onexit_action_map_[scxml_id][state_uid] = onexit
316 | manager.frame_move_action_map_[scxml_id][state_uid] = framemove
317 |
318 |
319 | @classmethod
320 | def handle_final_item(self, data, attributes):
321 | manager = data.machine_.manager()
322 | scxml_id = data.scxml_id_
323 | state_uid = data.current_state_.state_uid()
324 | onentry = ''
325 | onexit = ''
326 | framemove = ''
327 |
328 | for key,value in attributes.items():
329 | if key == 'id':
330 | pass
331 | elif key == 'onentry':
332 | onentry = value
333 | elif key == 'frame_move':
334 | framemove = value
335 | else:
336 | assert (0 and f'attribute "key" not supported in "final" state')
337 |
338 | if not onentry: onentry = "onentry_" + state_uid
339 | if not framemove: framemove = state_uid
340 |
341 | manager.onentry_action_map_[scxml_id][state_uid] = onentry
342 | manager.frame_move_action_map_[scxml_id][state_uid] = framemove
343 |
344 | data.current_state_.is_a_final_ = True
345 |
346 | @classmethod
347 | def handle_transition_item(self, data, attributes):
348 | manager = data.machine_.manager()
349 | scxml_id = data.scxml_id_
350 | state_uid = data.current_state_.state_uid()
351 |
352 | tran = TransitionAttr('','')
353 |
354 | for key, value in attributes.items():
355 | if key == 'event':
356 | tran.event_ = value
357 | elif key == 'cond':
358 | tran.cond_ = value
359 | elif key == 'ontransit':
360 | tran.ontransit_ = value
361 | elif key == 'target':
362 | tran.transition_target_ = value
363 | elif key == 'random_target':
364 | tran.random_target_ = value.split(',')
365 |
366 | if tran.cond_ and tran.cond_.startswith('!'):
367 | tran.cond_ = tran.cond_[1:]
368 | tran.not_ = True
369 |
370 | if state_uid not in manager.transition_attr_map_[scxml_id]:
371 | manager.transition_attr_map_[scxml_id][state_uid] = []
372 | manager.transition_attr_map_[scxml_id][state_uid].append(tran)
373 |
374 |
375 | @classmethod
376 | def handle_history_item(self, data, attributes):
377 | manager = data.machine_.manager()
378 | scxml_id = data.scxml_id_
379 | state_uid = data.current_state_.state_uid()
380 |
381 | for key, value in attributes.items():
382 | if key == 'type':
383 | manager.history_type_map_[scxml_id][state_uid] = value
384 | elif key == 'id':
385 | manager.history_id_reside_state_[scxml_id][value] = state_uid
386 |
387 | instance_ = None
388 |
389 | @classmethod
390 | def instance(self):
391 | if not self.instance_:
392 | self.instance_ = StateMachineManager()
393 | return self.instance_
394 |
395 | @classmethod
396 | def release_instance(self):
397 | self.instance_ = None
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
--------------------------------------------------------------------------------
/scm/__init__.py:
--------------------------------------------------------------------------------
1 | from .StateMachineManager import StateMachineManager
2 | from .StateMachine import StateMachine
3 | from .FrameMover import FrameMover
4 |
5 |
--------------------------------------------------------------------------------
/scm_tutorial.py:
--------------------------------------------------------------------------------
1 | from scm import StateMachineManager
2 |
3 |
4 | client_scxml = """\
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | """
23 |
24 | class Life:
25 | def __init__(self):
26 | self.mach_ = StateMachineManager.instance().getMach('the life')
27 | self.mach_.set_do_exit_state_on_destroy(True)
28 | # Instead of register every entry/exit slot that follow this 'onentry_xxx', 'onexit_xxx' naming convetion,
29 | # self.mach_.register_state_slot("appear", self.onentry_appear, self.onexit_appear)
30 | # self.mach_.register_state_slot("live", self.onentry_live, self.onexit_live)
31 | # self.mach_.register_state_slot("eat", self.onentry_eat, self.onexit_eat)
32 | # self.mach_.register_state_slot("move", self.onentry_move, self.onexit_move)
33 | # self.mach_.register_state_slot("dead", self.onentry_dead, self.onexit_dead)
34 | # You can simply use StateMachine.register_handler() for brevity, this is python which is dynamic.
35 | self.mach_.register_handler(self)
36 | # self.mach_.register_state_slot("move.on", self.onentry_move_on, self.onexit_move_on)
37 | # For other cases, use register_action_slot as usual.
38 | self.mach_.register_action_slot('say_hello', self.say_hello)
39 | self.mach_.StartEngine()
40 |
41 | def test(self):
42 | self.mach_.enqueEvent("born")
43 | #self.mach_.frame_move(0) # state change to 'live'
44 | StateMachineManager.instance().pumpMachEvents()
45 | self.mach_.enqueEvent("hp_zero")
46 | #self.mach_.frame_move(0) # state change to 'dead'
47 | StateMachineManager.instance().pumpMachEvents()
48 |
49 | def onentry_appear(self):
50 | print("come to exist")
51 |
52 | def onexit_appear(self):
53 | print("we are going to...")
54 |
55 | def onentry_live(self):
56 | print("start living")
57 |
58 | def onexit_live(self):
59 | print("no longer live")
60 |
61 | def onentry_eat(self):
62 | print("start eating")
63 |
64 | def onexit_eat(self):
65 | print("stop eating")
66 |
67 | def onentry_move(self):
68 | print("start moving")
69 |
70 | def onexit_move(self):
71 | print("stop moving")
72 |
73 | def onentry_dead(self):
74 | print("end")
75 |
76 | def onexit_dead(self):
77 | assert (0 and "should not exit final state");
78 | print("no, this won't get called.")
79 |
80 | def say_hello(self):
81 | print("\n*** Hello, World! ***\n")
82 |
83 | if __name__ == '__main__':
84 | StateMachineManager.instance().set_scxml("the life", client_scxml)
85 | life = Life()
86 | life.test()
87 | StateMachineManager.instance().pumpMachEvents()
88 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import sys
3 |
4 | from setuptools import find_packages, setup
5 |
6 |
7 | # Get version without importing, which avoids dependency issues
8 |
9 | def readme():
10 | with open('README.rst') as f:
11 | return f.read()
12 |
13 |
14 | setup(name='py-scm',
15 | version='1.0.2',
16 | description='A python state machine framework based on statecharts (scxml)',
17 | long_description=readme(),
18 | author='Zen Chien',
19 | author_email='jixing.jian@gmail.com',
20 | url='https://github.com/zen747/pyscm',
21 | license="Apache License 2.0",
22 | keywords=['statecharts', 'state-machine', 'scxml', 'david-harel'],
23 | classifiers=["Development Status :: 5 - Production/Stable",
24 | "Intended Audience :: Developers",
25 | "License :: OSI Approved :: Apache Software License",
26 | "Operating System :: OS Independent",
27 | "Programming Language :: Python",
28 | 'Programming Language :: Python :: 3',
29 | 'Programming Language :: Python :: 3.6',
30 | "Topic :: Software Development :: Libraries :: Python Modules",
31 | "Topic :: Text Processing :: Linguistic"],
32 | packages=find_packages(),
33 | python_requires=">=3.6")
34 |
--------------------------------------------------------------------------------
/test-CandyMachine.py:
--------------------------------------------------------------------------------
1 | from scm import StateMachineManager
2 |
3 |
4 |
5 | cm_scxml = """
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | """
25 |
26 |
27 | class TheCandyMachine:
28 | def __init__(self):
29 | self.credit_ = 0
30 | self.num_of_candy_stored_ = 0
31 | mach = self.mach_ = StateMachineManager.instance().getMach("cm_scxml")
32 | mach.register_state_slot('idle', self.onentry_idle, self.onexit_idle)
33 | mach.register_state_slot('active', self.onentry_active, self.onexit_active)
34 | mach.register_state_slot('releasing', self.onentry_releasing, self.onexit_releasing)
35 | mach.register_state_slot('disabled', self.onentry_disabled, self.onexit_disabled)
36 |
37 | mach.register_cond_slot('condNoCandy', self.condNoCandy)
38 | mach.register_cond_slot('condNoCredit', self.condNoCredit)
39 |
40 | mach.register_action_slot('releaseCandy', self.releaseCandy)
41 | mach.register_action_slot('withdrawCoins', self.withdrawCoins)
42 |
43 | mach.StartEngine()
44 |
45 | def __del__(self):
46 | self.mach_.ShutDownEngine(True)
47 |
48 |
49 | def store_candy(self, num):
50 | self.num_of_candy_stored_ += num
51 | self.mach_.enqueEvent("add-candy")
52 | print(f"store {num} gumballs, now machine has {self.num_of_candy_stored_:.0f} gumballs.")
53 |
54 | def insertQuater(self):
55 | self.insert_coin(25)
56 | print(f"you insert a quarter, now credit = {self.credit_:.0f}")
57 |
58 | def ejectQuater(self):
59 | self.mach_.enqueEvent("withdraw-coin")
60 | print("you pulled the eject crank")
61 |
62 | def turnCrank(self):
63 | self.mach_.enqueEvent("release-candy")
64 | print("you turned release crank")
65 |
66 |
67 | def insert_coin(self, credit):
68 | self.credit_ += credit
69 | self.mach_.enqueEvent("coin")
70 |
71 | def onentry_idle(self):
72 | print("onentry_idle")
73 | print("Machine is waiting for quarter")
74 | if self.num_of_candy_stored_ == 0:
75 | self.mach_.enqueEvent ("empty")
76 |
77 | def onexit_idle(self):
78 | print("onexit_idle")
79 |
80 | def onentry_active(self):
81 | print("onentry_active")
82 |
83 | def onexit_active(self):
84 | print("onexit_active")
85 |
86 | def onentry_releasing(self):
87 | print("onentry_releasing")
88 | #PunctualFrameMover::registerTimedAction(1.0, boost::bind(&TheCandyMachine::candy_released, this))
89 | self.candy_released()
90 |
91 | def onexit_releasing(self):
92 | print("onexit_releasing")
93 |
94 | def onentry_disabled(self):
95 | print("onentry_disabled")
96 |
97 | def onexit_disabled(self):
98 | print("onexit_disabled")
99 |
100 | def condNoCandy(self):
101 | return self.num_of_candy_stored_ == 0
102 |
103 | def condNoCredit(self):
104 | return self.credit_ == 0
105 |
106 | def releaseCandy(self):
107 | num_to_release = self.credit_ / 25
108 | if num_to_release > self.num_of_candy_stored_:
109 | num_to_release = self.num_of_candy_stored_
110 |
111 | print(f"release {num_to_release:.0f} gumballs")
112 | self.num_of_candy_stored_ -= num_to_release
113 | self.credit_ -= num_to_release * 25
114 |
115 | def withdrawCoins(self):
116 | print(f"there you go, the money, {self.credit_:.0f}")
117 | self.credit_ = 0
118 | print("Quarter returned")
119 |
120 | def candy_released(self):
121 | self.mach_.enqueEvent("candy-released")
122 |
123 |
124 | def report(self):
125 | print("\nA Candy Selling Machine")
126 | print(f"Inventory: {self.num_of_candy_stored_:.0f} gumballs")
127 | print(f"Credit: {self.credit_:.0f}\n")
128 |
129 | def init(self):
130 | self.mach_.frame_move(0)
131 | assert (self.mach_.inState("disabled"))
132 | self.store_candy(5)
133 | self.mach_.frame_move(0)
134 | assert (self.mach_.inState("idle"))
135 | self.report()
136 |
137 | def frame_move(self):
138 | self.mach_.frame_move(0)
139 |
140 | def test(self):
141 | self.insertQuater()
142 | self.frame_move()
143 | self.turnCrank()
144 | self.frame_move()
145 | self.report ()
146 |
147 | self.insertQuater()
148 | self.frame_move()
149 | self.ejectQuater()
150 | self.frame_move()
151 | self.report ()
152 | self.turnCrank()
153 | self.frame_move()
154 | self.report ()
155 |
156 | self.insertQuater()
157 | self.frame_move()
158 | self.turnCrank()
159 | self.frame_move()
160 | self.insertQuater()
161 | self.frame_move()
162 | self.turnCrank()
163 | self.frame_move()
164 | self.ejectQuater()
165 | self.frame_move()
166 | self.report()
167 |
168 | self.insertQuater()
169 | self.frame_move()
170 | self.insertQuater()
171 | self.frame_move()
172 | self.turnCrank()
173 | self.frame_move()
174 | self.insertQuater()
175 | self.frame_move()
176 | self.turnCrank()
177 | self.frame_move()
178 | self.insertQuater()
179 | self.frame_move()
180 | self.ejectQuater()
181 | self.frame_move()
182 | self.report()
183 |
184 | self.store_candy(5)
185 | self.frame_move()
186 | self.turnCrank()
187 | self.frame_move()
188 | self.report()
189 |
190 |
191 | def test():
192 | mach = TheCandyMachine()
193 | mach.init ()
194 | mach.test ()
195 |
196 | if __name__ == '__main__':
197 |
198 | StateMachineManager.instance().set_scxml("cm_scxml", cm_scxml)
199 | # StateMachineManager::instance().set_scxml_file("cm_scxml", "cm.scxml") // optionally through a file
200 | # StateMachineManager::instance().prepare_machs() // optionally load all scxml at once or getMach() on the fly
201 |
202 | test()
203 |
204 | StateMachineManager.instance().pumpMachEvents()
205 | #StateMachineManager.instance().release_instance()
206 |
--------------------------------------------------------------------------------
/test-HistoryMachine.py:
--------------------------------------------------------------------------------
1 | from scm import StateMachineManager
2 |
3 | watch_scxml = """\
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | """
83 |
84 | class TheMachine:
85 | def __init__(self):
86 | self.mach_ = StateMachineManager.instance().getMach("watch")
87 | self.hr_ = 0
88 | self.min_ = 0
89 | self.sec_ = 0
90 |
91 | states = self.mach_.get_all_states()
92 | print('we have states:')
93 |
94 | for state in states:
95 | st = self.mach_.getState(state)
96 | if not st: # <- state machine itself
97 | continue
98 | print(' ' * st.depth(), state)
99 | self.mach_.setActionSlot('onentry_'+state, lambda : self.onentry_report_state(False) )
100 | self.mach_.setActionSlot('onexit_'+state, lambda s=state: self.onexit_report_state(s) )
101 | print('')
102 | self.mach_.setActionSlot('onentry_sec', self.onentry_sec)
103 | self.mach_.setActionSlot('onentry_1min', self.onentry_1min)
104 | self.mach_.setActionSlot('onentry_10min', self.onentry_10min)
105 | self.mach_.setActionSlot('onentry_hr', self.onentry_hr)
106 |
107 | self.mach_.StartEngine()
108 |
109 |
110 | def onentry_sec(self):
111 | if self.mach_.re_enter_state():
112 | self.sec_ = 0
113 | self.onentry_report_state(True)
114 |
115 | def onentry_1min(self):
116 | if self.mach_.re_enter_state():
117 | self.min_ += 1
118 | self.onentry_report_state(True)
119 |
120 | def onentry_10min(self):
121 | if self.mach_.re_enter_state():
122 | self.min_ += 10
123 | self.onentry_report_state(True)
124 |
125 | def onentry_hr(self):
126 | if self.mach_.re_enter_state():
127 | self.hr_ += 1
128 | self.onentry_report_state(True)
129 |
130 | def onentry_report_state(self, with_time):
131 | print("enter state ", self.mach_.getEnterState().state_uid(), end='')
132 | #if with_time:
133 | print(". time ", self.hr_, ":", self.min_, ":", self.sec_, end='')
134 |
135 | print('')
136 | self.sec_ += 1
137 |
138 | def onexit_report_state(self, st):
139 | print("exit state ", st)
140 |
141 | def test (self):
142 | # time
143 | self.mach_.enqueEvent("c_down") # -> wait
144 | self.mach_.enqueEvent("2_sec") # -> update, you will use registerTimedEvent to generate event after 2 seconds
145 | self.mach_.enqueEvent("d") # reset, 1 second
146 | self.mach_.enqueEvent("d") # reset, 1 second
147 | self.mach_.enqueEvent("d") # reset, 1 second
148 | self.mach_.enqueEvent("c") # -> 1min state
149 | self.mach_.enqueEvent("d") # 1 min
150 | self.mach_.enqueEvent("d") # 2 min
151 | self.mach_.enqueEvent("c") # -> 10min state
152 | self.mach_.enqueEvent("d") # 12 min
153 | self.mach_.enqueEvent("c") # -> hr state
154 | self.mach_.enqueEvent("d") # 1 hr
155 | self.mach_.enqueEvent("d") # 2 hr
156 | self.mach_.enqueEvent("c") # -> time
157 |
158 | self.mach_.enqueEvent("a") # -> alarm1
159 | self.mach_.enqueEvent("d")
160 | self.mach_.enqueEvent("a") # -> alarm2
161 | self.mach_.enqueEvent("d")
162 | self.mach_.enqueEvent("a") # -> chime
163 | self.mach_.enqueEvent("d")
164 | self.mach_.enqueEvent("a") # -> stopwatch.zero
165 | self.mach_.enqueEvent("b") # -> run.on
166 | self.mach_.enqueEvent("d") # -> display.lap
167 | self.mach_.enqueEvent("a") # -> time
168 | self.mach_.enqueEvent("d") # no effect
169 | self.mach_.enqueEvent("a") # -> alarm1
170 | self.mach_.enqueEvent("d")
171 | self.mach_.enqueEvent("a") # -> alarm2
172 | self.mach_.enqueEvent("d")
173 | self.mach_.enqueEvent("a") # -> chime
174 | self.mach_.enqueEvent("d")
175 | self.mach_.enqueEvent("a") # -> stopwatch, what's run and display in?
176 |
177 | self.mach_.frame_move(0)
178 |
179 | if __name__ == '__main__':
180 | StateMachineManager.instance().set_scxml("watch", watch_scxml)
181 | mach = TheMachine()
182 | mach.test()
183 | StateMachineManager.instance().pumpMachEvents()
184 |
185 |
--------------------------------------------------------------------------------
/test-StateMachine.py:
--------------------------------------------------------------------------------
1 | from scm import StateMachineManager
2 |
3 | client_scxml = """
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | '
38 |
39 |
40 | """
41 |
42 |
43 | class TheMachine:
44 | def __init__(self):
45 | self.mach_ = StateMachineManager.instance().getMach("test-machine")
46 | self.mach_.register_state_slot('new', self.onentry_new, self.onexit_new)
47 | self.mach_.register_state_slot('live', self.onentry_live, self.onexit_live)
48 | self.mach_.register_state_slot('punch', self.onentry_punch, self.onexit_punch)
49 | self.mach_.register_state_slot('punching', self.onentry_punching, self.onexit_punching)
50 | self.mach_.register_state_slot('linked', self.onentry_linked, self.onexit_linked)
51 | self.mach_.register_state_slot('punch_success', self.onentry_punch_success, self.onexit_punch_success)
52 | self.mach_.register_state_slot('punch_fail', self.onentry_punch_fail, self.onexit_punch_fail)
53 | self.mach_.register_state_slot('mode', self.onentry_mode, self.onexit_mode)
54 | self.mach_.register_state_slot('relay', self.onentry_relay, self.onexit_relay)
55 | self.mach_.register_state_slot('relay_init', self.onentry_relay_init, self.onexit_relay_init)
56 | self.mach_.register_state_slot('relay_work', self.onentry_relay_work, self.onexit_relay_work)
57 | self.mach_.register_state_slot('established', self.onentry_established, self.onexit_established)
58 | self.mach_.register_state_slot('closed', self.onentry_closed, self.onexit_closed)
59 |
60 | self.mach_.StartEngine()
61 |
62 |
63 | def onentry_new(self):
64 | print("onentry_new")
65 |
66 | def onexit_new(self):
67 | print("onexit_new")
68 |
69 | def onentry_live(self):
70 | print("onentry_live")
71 |
72 | def onexit_live(self):
73 | print("onexit_live")
74 |
75 | def onentry_punch(self):
76 | print("onentry_punch")
77 |
78 | def onexit_punch(self):
79 | print("onexit_punch")
80 |
81 | def onentry_punching(self):
82 | print("onentry_punching")
83 |
84 | def onexit_punching(self):
85 | print("onexit_punching")
86 |
87 | def onentry_linked(self):
88 | print("onentry_linked")
89 |
90 | def onexit_linked(self):
91 | print("onexit_linked")
92 |
93 | def onentry_punch_success(self):
94 | print("onentry_punch_success")
95 |
96 | def onexit_punch_success(self):
97 | print("onexit_punch_success")
98 |
99 | def onentry_punch_fail(self):
100 | print("onentry_punch_fail")
101 |
102 | def onexit_punch_fail(self):
103 | print("onexit_punch_fail")
104 |
105 | def onentry_mode(self):
106 | print("onentry_mode")
107 |
108 | def onexit_mode(self):
109 | print("onexit_mode")
110 |
111 | def onentry_relay(self):
112 | print("onentry_relay")
113 |
114 | def onexit_relay(self):
115 | print("onexit_relay")
116 |
117 | def onentry_relay_init(self):
118 | print("onentry_relay_init")
119 |
120 | def onexit_relay_init(self):
121 | print("onexit_relay_init")
122 |
123 | def onentry_relay_work(self):
124 | print("onentry_relay_work")
125 |
126 | def onexit_relay_work(self):
127 | print("onexit_relay_work")
128 |
129 | def onentry_established(self):
130 | print("onentry_established")
131 |
132 | def onexit_established(self):
133 | print("onexit_established")
134 |
135 | def onentry_closed(self):
136 | print("onentry_closed")
137 |
138 | def onexit_closed(self):
139 | assert(0 and "exit final")
140 | print("onexit_closed")
141 |
142 | def test(self):
143 | self.mach_.enqueEvent("punch")
144 | self.mach_.enqueEvent("linked")
145 | self.mach_.frame_move(0)
146 |
147 |
148 | if __name__ == '__main__':
149 | StateMachineManager.instance().set_scxml("test-machine", client_scxml)
150 | mach = TheMachine()
151 | mach.test ()
152 | StateMachineManager.instance().pumpMachEvents()
153 | StateMachineManager.instance().release_instance()
154 |
--------------------------------------------------------------------------------