├── 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 | --------------------------------------------------------------------------------