4 @todo Look into switching from POLL_TIME = min(F * 2^n, MAX) to POLL_TIME = min(CONST + F * 2^n, MAX)
5 @todo Look into supporting more states that have a different F and MAX
12 import util.go_utils as gobject_utils
13 import util.coroutines as coroutines
17 _moduleLogger = logging.getLogger("gvoice.state_machine")
20 def _to_milliseconds(**kwd):
21 if "milliseconds" in kwd:
22 return kwd["milliseconds"]
23 elif "seconds" in kwd:
24 return kwd["seconds"] * 1000
25 elif "minutes" in kwd:
26 return kwd["minutes"] * 1000 * 60
27 raise KeyError("Unknown arg: %r" % kwd)
30 class StateMachine(object):
32 STATE_ACTIVE = 0, "active"
33 STATE_IDLE = 1, "idle"
36 _ACTION_UPDATE = "update"
37 _ACTION_RESET = "reset"
40 _INITIAL_ACTIVE_PERIOD = int(_to_milliseconds(seconds=10))
41 _FINAL_ACTIVE_PERIOD = int(_to_milliseconds(minutes=10))
42 _IDLE_PERIOD = int(_to_milliseconds(minutes=30))
47 def __init__(self, initItems, updateItems):
48 self._initItems = initItems
49 self._updateItems = updateItems
51 self._state = self.STATE_ACTIVE
53 self._timeoutId = None
54 self._currentPeriod = self._INITIAL_ACTIVE_PERIOD
55 self._set_initial_period()
57 self._callback = coroutines.func_sink(
58 coroutines.expand_positional(
59 self._request_reset_timers
67 assert self._startId is None
68 assert self._timeoutId is None
69 self._startId = gobject.idle_add(self._start)
72 if self._startId is not None:
73 _moduleLogger.info("Stopping state machine before it even had a chance to start")
74 gobject.source_remove(self._startId)
78 def set_state(self, newState):
79 oldState = self._state
80 _moduleLogger.info("Transitioning from %s to %s" % (oldState, newState))
82 self._state = newState
88 def reset_timers(self):
92 def request_reset_timers(self):
96 @gtk_toolbox.log_exception(_moduleLogger)
97 def _request_reset_timers(self, *args):
100 def _set_initial_period(self):
101 self._currentPeriod = self._INITIAL_ACTIVE_PERIOD / 2 # We will double it later
103 def _schedule_update(self):
104 assert self._timeoutId is None
105 nextTimeout = self._calculate_step(self._state, self._currentPeriod)
106 nextTimeout = int(nextTimeout)
107 if nextTimeout != self._INFINITE_PERIOD:
108 self._timeoutId = gobject.timeout_add(nextTimeout, self._on_timeout)
109 _moduleLogger.info("Next update in %s ms" % (nextTimeout, ))
110 self._currentPeriod = nextTimeout
113 _moduleLogger.info("Starting State Machine")
114 for item in self._initItems:
118 _moduleLogger.exception("Initial update failed for %r" % item)
119 self._schedule_update()
121 return False # do not continue
123 def _stop_update(self):
124 if self._timeoutId is None:
125 _moduleLogger.info("Stopping an already stopped state machine")
127 gobject.source_remove(self._timeoutId)
128 self._timeoutId = None
130 def _reset_timers(self):
131 if self._timeoutId is None:
132 return # not started yet
134 self._set_initial_period()
135 self._schedule_update()
137 def _on_timeout(self):
138 _moduleLogger.info("Update")
139 for item in self._updateItems:
141 item.update(force=True)
143 _moduleLogger.exception("Update failed for %r" % item)
144 self._timeoutId = None
145 self._schedule_update()
146 return False # do not continue
149 def _calculate_step(cls, state, period):
150 if state == cls.STATE_ACTIVE:
151 return min(period * 2, cls._FINAL_ACTIVE_PERIOD)
152 elif state == cls.STATE_IDLE:
153 return cls._IDLE_PERIOD
154 elif state == cls.STATE_DND:
155 return cls._INFINITE_PERIOD
157 raise RuntimeError("Unknown state: %r" % (state, ))