Handling interruption

Especially when dealing with the physical world, such as in robotics, it becomes important to deal with unexpected events and interruptions. In BeTFSM, these things are dealt with using events. This tutorial gives a basic introduction to one of the more basic type of event, a user pressing Ctrl-C. It is probably not a good idea to let the robot continue its current task when the BeTFSM coordinating these robots tasks is interrupted by the default Ctrl-C handler of Python.

In BeTFSM, this is handled by EventReceiver, typically singleton classes, that receive events and expose them to BeTFSM. Conditions that check if one of the listed events occurred and return the corresponding event string. And Event checkers that coordinate everything with the behavior tree and handle the event concurrently, sequentially or by generating a specific outcome.

Explanation of the code: - From line 13-26, we define a custom node, that at each tick counts down until the counter reaches zero and then returns SUCCEED.

  • Line 30-37, we define our nominal "application", in this case just a sequence that uses our Countdown node.

  • Line 40-52 defines the cleanup procedure in case of interruption. Instead of the TimedWait node, here we use EventOutcome to achieve equivalent behavior. Its second argument is a Timeout_Condition that will fire the event string "DONE" 5 seconds after it is started. In the event_map we specify what should happen when "DONE" arrives, in this case give out SUCCEED outcome.

  • Line 66-69 define the handling of Ctrl-C events. Here EventSequential is used. Its event_map maps event strings to not to outcome strings but whole BeTFSM subtrees. EventSequential does interrupt the nominal subtree (specified with NO_EVENT), but the evaluation is switched over to another subtree when an event string comes in. (and in the process pauses the nominal statemachine).

    • When any of the subtrees return something different from SUCCEED or TICKING, EventSequential returns with that outcome.
    • It also returns whenall subtrees returned SUCCEED. In other words, if the nominal subtree finishes with SUCCEED, EventSequential will still wait until the cleanup subtree is finished.
    • The cleanup subtree can also decide to will continue running the paused nominal subtree by returning SUCCEED.

Running of the code:

  • When you run the code, you can click on the link that is printed in the console, this gives you a graphical visualization of what going on.
  • See what happens when you press Ctrl-C
  • Try to press Ctrl-C within 5 seconds of the end, can you predict what happens?
  • Change EventSequential to EventConcurrent, and press Ctrl-C, can you predict what happens?
 1
 2
 3
 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
#!/usr/bin/env python3

import time
from betfsm import (
    TICKING,SUCCEED,CANCEL,NO_EVENT,
    Runner, Sequence,  
    EventOutcome, EventSequential, EventConcurrent,
    Ctrl_C_Condition, Timeout_Condition,
    Message, Generator, get_logger, AlwaysOutcome
)

# A user defined TickingState:
class CountDown(Generator):
    """
    A simple generator state that counts down from a given number.
    """
    def __init__(self, name, count):
        super().__init__(name, [SUCCEED])
        self.count = count

    def co_execute(self, blackboard):
        for i in range(self.count, 0, -1):
            get_logger().info(f"{self.name}: {i}")
            yield TICKING
        get_logger().info(f"{self.name}: Finished counting down!")
        yield SUCCEED

# The sequence defined as a class:

class MySequence(Sequence):
    def __init__(self, count):
        super().__init__("MySequence") # do not forget to initialize the super class
        self.count = count
        self.add_state( Message(msg="--- Starting Sequence Phase ---") )
        self.add_state( CountDown("seq_counter_1", self.count) )
        self.add_state( Message(msg="--- Sequence Phase Finished --- (message 1 of 2)"))
        self.add_state( Message(msg="--- Sequence Phase Finished --- (message 2 of 2)"))


def CleanupSeq():
    seq = Sequence("cleanup")
    seq.add_state(Message(msg="--Now cleaning up, takes 5 seconds --"))
    # as an alternative to TimedWait, we can use the event mechanism 
    # with a timer event, this blocks until DONE is received:
    waiting = EventOutcome("Waiting",Timeout_Condition("DONE",5.0),
                           event_map={"DONE":SUCCEED})
    seq.add_state(waiting)
    seq.add_state(Message(msg="---Done cleaning up ---"))
    # make sure that the sequence will generate outcome CANCEL, such that 
    # the nominal_sm is interrupted and stopped.
    seq.add_state(AlwaysOutcome(CANCEL))
    return seq

def main():
    # Create a blackboard
    bb = {}

    # 1. Example Sequence, defined as a custom class:
    nominal_sm = MySequence(100)
    cleanup_sm = CleanupSeq()


    # 2. nominal_sm will be paused when handling CTRL_C
    # This ends when nominal_sm returns with outcome != TICKING or
    # cleanup_sm returns with outcome !=TICKING or != SUCCEED
    sm = EventSequential("ctrl-c-check",Ctrl_C_Condition("CTRL_C"),{
        NO_EVENT:  nominal_sm,
        "CTRL_C" : cleanup_sm
    })

    # 2. Run it using BeTFSMRunner at 2 Hz
    runner = Runner(sm, bb, frequency=2) # Hz
    get_logger().info("Running State Machine... (explicitly logged in main)")
    outcome = runner.run()
    get_logger().info(f"State Machine Finished with outcome: {outcome} (explicitly logged in main)")


if __name__ == "__main__":
    main()