Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# Copyright (c) 2020 ETH Zurich, SIS ID and HVL D-ITET 

2# 

3import logging 

4import time 

5from concurrent.futures._base import Future 

6from concurrent.futures.thread import ThreadPoolExecutor 

7from threading import Event 

8from typing import Callable, Optional 

9 

10from ..utils.typing import Number 

11 

12logger = logging.getLogger(__name__) 

13 

14 

15class Poller: 

16 """ 

17 Poller class wrapping `concurrent.futures.ThreadPoolExecutor` which enables passing 

18 of results and errors out of the polling thread. 

19 """ 

20 

21 def __init__( 

22 self, 

23 spoll_handler: Callable, 

24 polling_delay_sec: Number = 0, 

25 polling_interval_sec: Number = 1, 

26 polling_timeout_sec: Optional[Number] = None, 

27 ): 

28 """ 

29 Initialize the polling helper. 

30 

31 :param spoll_handler: Polling function. 

32 :param polling_delay_sec: Delay before starting the polling, in seconds. 

33 :param polling_interval_sec: Polling interval, in seconds. 

34 """ 

35 self.spoll_handler: Callable = spoll_handler 

36 self.polling_delay_sec: Number = polling_delay_sec 

37 self.polling_interval_sec: Number = polling_interval_sec 

38 self.polling_timeout_sec: Optional[Number] = polling_timeout_sec 

39 self._polling_future: Optional[Future] = None 

40 self._polling_stop_event: Optional[Event] = None 

41 

42 def is_polling(self) -> bool: 

43 """ 

44 Check if device status is being polled. 

45 

46 :return: `True` when polling thread is set and alive 

47 """ 

48 return self._polling_future is not None and self._polling_future.running() 

49 

50 def _if_poll_again( 

51 self, stop_event: Event, delay_sec: Number, stop_time: Optional[Number] 

52 ) -> bool: 

53 """ 

54 Check if to poll again. 

55 

56 :param stop_event: Polling stop event. 

57 :param delay_sec: Delay time (in seconds). 

58 :param stop_time: Absolute stop time. 

59 :return: `True` if another polling handler call is due, `False` otherwise. 

60 """ 

61 not_stopped = not stop_event.wait(delay_sec) 

62 not_timeout = stop_time is None or time.time() < stop_time 

63 return not_stopped and not_timeout 

64 

65 def _poll_until_stop_or_timeout(self, stop_event: Event) -> Optional[object]: 

66 """ 

67 Thread for polling until stopped or timed-out. 

68 

69 :param stop_event: Event used to stop the polling 

70 :return: Last result of the polling function call 

71 """ 

72 start_time = time.time() 

73 stop_time = ( 

74 (start_time + self.polling_timeout_sec) 

75 if self.polling_timeout_sec 

76 else None 

77 ) 

78 last_result = None 

79 

80 if self._if_poll_again(stop_event, self.polling_delay_sec, stop_time): 

81 last_result = self.spoll_handler() 

82 

83 while self._if_poll_again(stop_event, self.polling_interval_sec, stop_time): 

84 last_result = self.spoll_handler() 

85 

86 return last_result 

87 

88 def start_polling(self) -> bool: 

89 """ 

90 Start polling. 

91 

92 :return: `True` if was not polling before, `False` otherwise 

93 """ 

94 was_not_polling = not self.is_polling() 

95 

96 if was_not_polling: 

97 logger.info("Start polling") 

98 self._polling_stop_event = Event() 

99 pool = ThreadPoolExecutor(max_workers=1) 

100 self._polling_future = pool.submit( 

101 self._poll_until_stop_or_timeout, self._polling_stop_event 

102 ) 

103 

104 return was_not_polling 

105 

106 def stop_polling(self) -> bool: 

107 """ 

108 Stop polling. 

109 

110 Wait for until polling function returns a result as well as any exception that 

111 might have been raised within a thread. 

112 

113 :return: `True` if was polling before, `False` otherwise, and last result of 

114 the polling function call. 

115 :raises: polling function exceptions 

116 """ 

117 was_polling = self.is_polling() 

118 

119 if was_polling: 

120 logger.info("Stop polling") 

121 if self._polling_stop_event is None: 

122 raise RuntimeError("Was polling but stop event is missing.") 

123 self._polling_stop_event.set() 

124 if self._polling_future is None: 

125 raise RuntimeError("Was polling but polling future is missing.") 

126 

127 return was_polling 

128 

129 def wait_for_polling_result(self): 

130 """ 

131 Wait for until polling function returns a result as well as any exception that 

132 might have been raised within a thread. 

133 

134 :return: polling function result 

135 :raises: polling function errors 

136 """ 

137 

138 return self._polling_future.result()