isar 1.30.2__py3-none-any.whl → 1.30.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of isar might be problematic. Click here for more details.

@@ -0,0 +1,61 @@
1
+ import logging
2
+ import time
3
+ from enum import Enum
4
+ from typing import TYPE_CHECKING
5
+
6
+ from isar.models.communication.queues.queue_utils import check_shared_state
7
+ from robot_interface.models.mission.status import RobotStatus
8
+
9
+
10
+ class RobotUnavailableStates(str, Enum):
11
+ BlockedProtectiveStop = "blockedProtectiveStop"
12
+ Offline = "offline"
13
+
14
+
15
+ if TYPE_CHECKING:
16
+ from isar.state_machine.state_machine import StateMachine
17
+
18
+
19
+ class RobotUnavailable:
20
+ def __init__(
21
+ self,
22
+ state_machine: "StateMachine",
23
+ state: RobotUnavailableStates,
24
+ ) -> None:
25
+ self.state_machine: "StateMachine" = state_machine
26
+ self.logger = logging.getLogger("state_machine")
27
+ self.shared_state = self.state_machine.shared_state
28
+ self.signal_state_machine_to_stop = state_machine.signal_state_machine_to_stop
29
+ self.state: RobotUnavailableStates = state
30
+
31
+ def start(self) -> None:
32
+ self.state_machine.update_state()
33
+ self._run()
34
+
35
+ def stop(self) -> None:
36
+ return
37
+
38
+ def _run(self) -> None:
39
+ while True:
40
+ if self.signal_state_machine_to_stop.is_set():
41
+ self.logger.info(
42
+ "Stopping state machine from %s state", self.state.name
43
+ )
44
+ break
45
+
46
+ robot_status: RobotStatus = check_shared_state(
47
+ self.shared_state.robot_status
48
+ )
49
+
50
+ expected_status = (
51
+ RobotStatus.BlockedProtectiveStop
52
+ if self.state == RobotUnavailableStates.BlockedProtectiveStop
53
+ else RobotStatus.Offline
54
+ )
55
+ if robot_status != expected_status:
56
+ transition = self.state_machine.robot_status_changed # type: ignore
57
+ break
58
+
59
+ time.sleep(self.state_machine.sleep_time)
60
+
61
+ transition()
@@ -168,6 +168,9 @@ class StateMachine(object):
168
168
  self.signal_state_machine_to_stop.set()
169
169
 
170
170
  def iterate_current_task(self):
171
+ if self.current_task is None:
172
+ raise ValueError("No current task is set")
173
+
171
174
  if self.current_task.is_finished():
172
175
  try:
173
176
  self.current_task = self.task_selector.next_task()
@@ -313,6 +316,12 @@ class StateMachine(object):
313
316
  self.logger.info("Mission overview:\n%s", log_statement)
314
317
 
315
318
  def _make_control_mission_response(self) -> ControlMissionResponse:
319
+ if self.current_mission is None:
320
+ raise ValueError("No current mission is set")
321
+
322
+ if self.current_task is None:
323
+ raise ValueError("No current task is set")
324
+
316
325
  return ControlMissionResponse(
317
326
  mission_id=self.current_mission.id,
318
327
  mission_status=self.current_mission.status,
@@ -1,92 +1,20 @@
1
- import logging
2
- import time
3
- from queue import Queue
4
- from typing import TYPE_CHECKING, Optional
1
+ from typing import TYPE_CHECKING
5
2
 
6
3
  from transitions import State
7
4
 
8
- from isar.config.settings import settings
9
- from isar.models.communication.message import StartMissionMessage
10
- from isar.models.communication.queues.queue_io import QueueIO
11
- from isar.models.communication.queues.queue_utils import check_for_event
12
-
13
5
  if TYPE_CHECKING:
14
6
  from isar.state_machine.state_machine import StateMachine
15
7
 
8
+ from isar.state_machine.generic_states.idle import Idle, IdleStates
9
+
16
10
 
17
- class AwaitNextMission(State):
11
+ class AwaitNextMission(State, Idle):
18
12
  def __init__(self, state_machine: "StateMachine") -> None:
19
- super().__init__(
20
- name="await_next_mission", on_enter=self.start, on_exit=self.stop
13
+ State.__init__(
14
+ self, name="await_next_mission", on_enter=self.start, on_exit=self.stop
15
+ )
16
+ Idle.__init__(
17
+ self,
18
+ state_machine=state_machine,
19
+ state=IdleStates.AwaitNextMission,
21
20
  )
22
- self.state_machine: "StateMachine" = state_machine
23
- self.logger = logging.getLogger("state_machine")
24
- self.entered_time: float = time.time()
25
- self.return_home_delay: float = settings.RETURN_HOME_DELAY
26
- self.events = self.state_machine.events
27
- self.shared_state = self.state_machine.shared_state
28
- self.signal_state_machine_to_stop = state_machine.signal_state_machine_to_stop
29
-
30
- def start(self) -> None:
31
- self.state_machine.update_state()
32
- self.entered_time = time.time()
33
- self._run()
34
-
35
- def stop(self) -> None:
36
- pass
37
-
38
- def _check_and_handle_stop_mission_event(self, event: Queue) -> bool:
39
- if check_for_event(event):
40
- self.state_machine.stop() # type: ignore
41
- return True
42
- return False
43
-
44
- def _check_and_handle_start_mission_event(
45
- self, event: Queue[StartMissionMessage]
46
- ) -> bool:
47
- start_mission: Optional[StartMissionMessage] = check_for_event(event)
48
- if start_mission:
49
- self.state_machine.start_mission(mission=start_mission.mission)
50
- self.state_machine.request_mission_start() # type: ignore
51
- return True
52
- return False
53
-
54
- def _check_and_handle_return_home_event(self, event: QueueIO) -> bool:
55
- if check_for_event(event.input):
56
- event.output.put(True)
57
- self.state_machine.request_return_home() # type: ignore
58
- return True
59
- return False
60
-
61
- def _should_return_home(self) -> bool:
62
- time_since_entered = time.time() - self.entered_time
63
- return time_since_entered > self.return_home_delay
64
-
65
- def _run(self) -> None:
66
- while True:
67
- if self.signal_state_machine_to_stop.is_set():
68
- self.logger.info(
69
- "Stopping state machine from %s state", self.__class__.__name__
70
- )
71
- break
72
-
73
- if self._check_and_handle_stop_mission_event(
74
- self.events.api_requests.stop_mission.input
75
- ):
76
- break
77
-
78
- if self._check_and_handle_start_mission_event(
79
- self.events.api_requests.start_mission.input
80
- ):
81
- break
82
-
83
- if self._check_and_handle_return_home_event(
84
- self.events.api_requests.return_home
85
- ):
86
- break
87
-
88
- if self._should_return_home():
89
- self.state_machine.request_return_home() # type: ignore
90
- break
91
-
92
- time.sleep(self.state_machine.sleep_time)
@@ -1,50 +1,24 @@
1
- import logging
2
- import time
3
- from typing import TYPE_CHECKING, Optional
1
+ from typing import TYPE_CHECKING
4
2
 
5
3
  from transitions import State
6
4
 
7
- from isar.models.communication.queues.queue_utils import check_shared_state
8
- from isar.services.utilities.threaded_request import ThreadedRequest
9
- from robot_interface.models.mission.status import RobotStatus
5
+ from isar.state_machine.generic_states.robot_unavailable import (
6
+ RobotUnavailable,
7
+ RobotUnavailableStates,
8
+ )
10
9
 
11
10
  if TYPE_CHECKING:
12
11
  from isar.state_machine.state_machine import StateMachine
13
12
 
14
13
 
15
- class BlockedProtectiveStop(State):
14
+ class BlockedProtectiveStop(State, RobotUnavailable):
16
15
  def __init__(self, state_machine: "StateMachine") -> None:
17
- super().__init__(
18
- name="blocked_protective_stop", on_enter=self.start, on_exit=self.stop
16
+ State.__init__(
17
+ self, name="blocked_protective_stop", on_enter=self.start, on_exit=self.stop
19
18
  )
20
- self.state_machine: "StateMachine" = state_machine
21
- self.logger = logging.getLogger("state_machine")
22
- self.robot_status_thread: Optional[ThreadedRequest] = None
23
- self.shared_state = self.state_machine.shared_state
24
- self.signal_state_machine_to_stop = state_machine.signal_state_machine_to_stop
25
19
 
26
- def start(self) -> None:
27
- self.state_machine.update_state()
28
- self._run()
29
-
30
- def stop(self) -> None:
31
- return
32
-
33
- def _run(self) -> None:
34
- while True:
35
- if self.signal_state_machine_to_stop.is_set():
36
- self.logger.info(
37
- "Stopping state machine from %s state", self.__class__.__name__
38
- )
39
- break
40
-
41
- robot_status: RobotStatus = check_shared_state(
42
- self.shared_state.robot_status
43
- )
44
- if robot_status != RobotStatus.BlockedProtectiveStop:
45
- transition = self.state_machine.robot_status_changed # type: ignore
46
- break
47
-
48
- time.sleep(self.state_machine.sleep_time)
49
-
50
- transition()
20
+ RobotUnavailable.__init__(
21
+ self,
22
+ state_machine=state_machine,
23
+ state=RobotUnavailableStates.BlockedProtectiveStop,
24
+ )
@@ -1,87 +1,19 @@
1
- import logging
2
- import time
3
- from queue import Queue
4
- from typing import TYPE_CHECKING, Optional
1
+ from typing import TYPE_CHECKING
5
2
 
6
3
  from transitions import State
7
4
 
8
- from isar.models.communication.message import StartMissionMessage
9
- from isar.models.communication.queues.queue_io import QueueIO
10
- from isar.models.communication.queues.queue_utils import (
11
- check_for_event,
12
- check_shared_state,
13
- )
14
- from isar.models.communication.queues.status_queue import StatusQueue
15
- from robot_interface.models.mission.status import RobotStatus
16
-
17
5
  if TYPE_CHECKING:
18
6
  from isar.state_machine.state_machine import StateMachine
19
7
 
8
+ from isar.state_machine.generic_states.idle import Idle, IdleStates
20
9
 
21
- class Home(State):
22
- def __init__(self, state_machine: "StateMachine") -> None:
23
- super().__init__(name="home", on_enter=self.start, on_exit=self.stop)
24
- self.state_machine: "StateMachine" = state_machine
25
- self.logger = logging.getLogger("state_machine")
26
- self.events = self.state_machine.events
27
- self.shared_state = self.state_machine.shared_state
28
- self.signal_state_machine_to_stop = state_machine.signal_state_machine_to_stop
29
-
30
- def start(self) -> None:
31
- self.state_machine.update_state()
32
- self._run()
33
-
34
- def stop(self) -> None:
35
- return
36
-
37
- def _check_and_handle_start_mission_event(
38
- self, event: Queue[StartMissionMessage]
39
- ) -> bool:
40
- start_mission: Optional[StartMissionMessage] = check_for_event(event)
41
- if start_mission:
42
- self.state_machine.start_mission(mission=start_mission.mission)
43
- self.state_machine.request_mission_start() # type: ignore
44
- return True
45
- return False
46
-
47
- def _check_and_handle_return_home_event(self, event: QueueIO) -> bool:
48
- if check_for_event(event.input):
49
- event.output.put(True)
50
- self.state_machine.request_return_home() # type: ignore
51
- return True
52
- return False
53
10
 
54
- def _check_and_handle_robot_status_event(
55
- self, event: StatusQueue[RobotStatus]
56
- ) -> bool:
57
- robot_status: RobotStatus = check_shared_state(event)
58
- if robot_status != RobotStatus.Home:
59
- self.state_machine.robot_status_changed() # type: ignore
60
- return True
61
-
62
- return False
63
-
64
- def _run(self) -> None:
65
- while True:
66
- if self.signal_state_machine_to_stop.is_set():
67
- self.logger.info(
68
- "Stopping state machine from %s state", self.__class__.__name__
69
- )
70
- break
71
-
72
- if self._check_and_handle_start_mission_event(
73
- self.events.api_requests.start_mission.input
74
- ):
75
- break
76
-
77
- if self._check_and_handle_return_home_event(
78
- self.events.api_requests.return_home
79
- ):
80
- break
81
-
82
- if self._check_and_handle_robot_status_event(
83
- self.shared_state.robot_status
84
- ):
85
- break
11
+ class Home(State, Idle):
12
+ def __init__(self, state_machine: "StateMachine") -> None:
13
+ State.__init__(self, name="home", on_enter=self.start, on_exit=self.stop)
86
14
 
87
- time.sleep(self.state_machine.sleep_time)
15
+ Idle.__init__(
16
+ self,
17
+ state_machine=state_machine,
18
+ state=IdleStates.Home,
19
+ )
@@ -1,256 +1,24 @@
1
- import logging
2
- import time
3
- from copy import deepcopy
4
- from queue import Queue
5
- from threading import Event
6
- from typing import TYPE_CHECKING, Optional, Tuple
1
+ from typing import TYPE_CHECKING
7
2
 
8
3
  from dependency_injector.wiring import inject
9
4
  from transitions import State
10
5
 
11
- from isar.config.settings import settings
12
- from isar.models.communication.queues.queue_utils import check_for_event, trigger_event
13
- from isar.services.utilities.threaded_request import ThreadedRequest
14
- from robot_interface.models.exceptions.robot_exceptions import (
15
- ErrorMessage,
16
- RobotException,
17
- RobotRetrieveInspectionException,
6
+ from isar.state_machine.generic_states.ongoing_mission import (
7
+ OngoingMission,
8
+ OngoingMissionStates,
18
9
  )
19
- from robot_interface.models.inspection.inspection import Inspection
20
- from robot_interface.models.mission.mission import Mission
21
- from robot_interface.models.mission.status import TaskStatus
22
- from robot_interface.models.mission.task import InspectionTask, Task
23
10
 
24
11
  if TYPE_CHECKING:
25
12
  from isar.state_machine.state_machine import StateMachine
26
13
 
27
14
 
28
- class Monitor(State):
15
+ class Monitor(State, OngoingMission):
29
16
  @inject
30
17
  def __init__(self, state_machine: "StateMachine") -> None:
31
- super().__init__(name="monitor", on_enter=self.start, on_exit=self.stop)
32
- self.state_machine: "StateMachine" = state_machine
18
+ State.__init__(self, name="monitor", on_enter=self.start, on_exit=self.stop)
33
19
 
34
- self.logger = logging.getLogger("state_machine")
35
- self.events = self.state_machine.events
36
-
37
- self.awaiting_task_status: bool = False
38
-
39
- self.signal_state_machine_to_stop: Event = (
40
- state_machine.signal_state_machine_to_stop
41
- )
42
-
43
- def start(self) -> None:
44
- self.state_machine.update_state()
45
- self._run()
46
-
47
- def stop(self) -> None:
48
- self.state_machine.mission_ongoing = False
49
- return
50
-
51
- def _check_and_handle_stop_mission_event(self, event: Queue) -> bool:
52
- if check_for_event(event):
53
- self.state_machine.stop() # type: ignore
54
- return True
55
- return False
56
-
57
- def _check_and_handle_pause_mission_event(self, event: Queue) -> bool:
58
- if check_for_event(event):
59
- self.state_machine.pause() # type: ignore
60
- return True
61
- return False
62
-
63
- def _check_and_handle_mission_started_event(self, event: Queue) -> None:
64
- if check_for_event(event):
65
- self.state_machine.mission_ongoing = True
66
-
67
- def _check_and_handle_mission_failed_event(self, event: Queue) -> bool:
68
- mission_failed: Optional[ErrorMessage] = check_for_event(event)
69
- if mission_failed is not None:
70
- self.state_machine.logger.warning(
71
- f"Failed to initiate mission "
72
- f"{str(self.state_machine.current_mission.id)[:8]} because: "
73
- f"{mission_failed.error_description}"
74
- )
75
- self.state_machine.current_mission.error_message = ErrorMessage(
76
- error_reason=mission_failed.error_reason,
77
- error_description=mission_failed.error_description,
78
- )
79
-
80
- self.state_machine.mission_failed_to_start() # type: ignore
81
- return True
82
- return False
83
-
84
- def _check_and_handle_task_status_failed_event(self, event: Queue) -> bool:
85
- if not self.state_machine.mission_ongoing:
86
- return False
87
-
88
- task_failure: Optional[ErrorMessage] = check_for_event(event)
89
- if task_failure is not None:
90
- self.awaiting_task_status = False
91
- self.state_machine.current_task.error_message = task_failure
92
- self.logger.error(
93
- f"Monitoring task {self.state_machine.current_task.id[:8]} failed "
94
- f"because: {task_failure.error_description}"
95
- )
96
- return self._handle_new_task_status(TaskStatus.Failed)
97
- elif not self.awaiting_task_status:
98
- trigger_event(
99
- self.events.state_machine_events.task_status_request,
100
- self.state_machine.current_task.id,
101
- )
102
- self.awaiting_task_status = True
103
- return False
104
-
105
- def _check_and_handle_task_status_event(self, event: Queue) -> bool:
106
- if not self.state_machine.mission_ongoing:
107
- return False
108
-
109
- status: Optional[TaskStatus] = check_for_event(event)
110
- if status is not None:
111
- self.awaiting_task_status = False
112
- return self._handle_new_task_status(status)
113
- elif not self.awaiting_task_status:
114
- trigger_event(
115
- self.events.state_machine_events.task_status_request,
116
- self.state_machine.current_task.id,
117
- )
118
- self.awaiting_task_status = True
119
- return False
120
-
121
- def _handle_new_task_status(self, status: TaskStatus) -> bool:
122
- if self.state_machine.current_task is None:
123
- self.state_machine.iterate_current_task()
124
-
125
- self.state_machine.current_task.status = status
126
-
127
- if self._should_upload_inspections():
128
- get_inspection_thread = ThreadedRequest(self._queue_inspections_for_upload)
129
- get_inspection_thread.start_thread(
130
- deepcopy(self.state_machine.current_mission),
131
- deepcopy(self.state_machine.current_task),
132
- name="State Machine Get Inspections",
133
- )
134
-
135
- if self.state_machine.current_task.is_finished():
136
- self._report_task_status(self.state_machine.current_task)
137
- self.state_machine.publish_task_status(task=self.state_machine.current_task)
138
-
139
- self.state_machine.iterate_current_task()
140
- if self.state_machine.current_task is None:
141
- self.state_machine.mission_finished() # type: ignore
142
- return True
143
-
144
- # Report and update next task
145
- self.state_machine.current_task.update_task_status()
146
- self.state_machine.publish_task_status(task=self.state_machine.current_task)
147
- return False
148
-
149
- def _run(self) -> None:
150
- self.awaiting_task_status = False
151
- while True:
152
- if self.signal_state_machine_to_stop.is_set():
153
- self.logger.info(
154
- "Stopping state machine from %s state", self.__class__.__name__
155
- )
156
- break
157
-
158
- if self._check_and_handle_stop_mission_event(
159
- self.events.api_requests.stop_mission.input
160
- ):
161
- break
162
-
163
- if self._check_and_handle_pause_mission_event(
164
- self.events.api_requests.pause_mission.input
165
- ):
166
- break
167
-
168
- self._check_and_handle_mission_started_event(
169
- self.events.robot_service_events.mission_started
170
- )
171
-
172
- if self._check_and_handle_mission_failed_event(
173
- self.events.robot_service_events.mission_failed
174
- ):
175
- break
176
-
177
- if self._check_and_handle_task_status_failed_event(
178
- self.events.robot_service_events.task_status_failed
179
- ):
180
- break
181
-
182
- if self._check_and_handle_task_status_event(
183
- self.events.robot_service_events.task_status_updated
184
- ):
185
- break
186
-
187
- time.sleep(settings.FSM_SLEEP_TIME)
188
-
189
- def _queue_inspections_for_upload(
190
- self, mission: Mission, current_task: InspectionTask
191
- ) -> None:
192
- try:
193
- inspection: Inspection = self.state_machine.robot.get_inspection(
194
- task=current_task
195
- )
196
- if current_task.inspection_id != inspection.id:
197
- self.logger.warning(
198
- f"The inspection_id of task ({current_task.inspection_id}) "
199
- f"and result ({inspection.id}) is not matching. "
200
- f"This may lead to confusions when accessing the inspection later"
201
- )
202
-
203
- except (RobotRetrieveInspectionException, RobotException) as e:
204
- self._set_error_message(e)
205
- self.logger.error(
206
- f"Failed to retrieve inspections because: {e.error_description}"
207
- )
208
- return
209
-
210
- except Exception as e:
211
- self.logger.error(
212
- f"Failed to retrieve inspections because of unexpected error: {e}"
213
- )
214
- return
215
-
216
- if not inspection:
217
- self.logger.warning(
218
- f"No inspection result data retrieved for task {str(current_task.id)[:8]}"
219
- )
220
-
221
- inspection.metadata.tag_id = current_task.tag_id
222
-
223
- message: Tuple[Inspection, Mission] = (
224
- inspection,
225
- mission,
226
- )
227
- self.state_machine.events.upload_queue.put(message)
228
- self.logger.info(
229
- f"Inspection result: {str(inspection.id)[:8]} queued for upload"
230
- )
231
-
232
- def _report_task_status(self, task: Task) -> None:
233
- if task.status == TaskStatus.Failed:
234
- self.logger.warning(
235
- f"Task: {str(task.id)[:8]} was reported as failed by the robot"
236
- )
237
- elif task.status == TaskStatus.Successful:
238
- self.logger.info(
239
- f"{type(task).__name__} task: {str(task.id)[:8]} completed"
240
- )
241
-
242
- def _should_upload_inspections(self) -> bool:
243
- if settings.UPLOAD_INSPECTIONS_ASYNC:
244
- return False
245
-
246
- return (
247
- self.state_machine.current_task.is_finished()
248
- and self.state_machine.current_task.status == TaskStatus.Successful
249
- and isinstance(self.state_machine.current_task, InspectionTask)
250
- )
251
-
252
- def _set_error_message(self, e: RobotException) -> None:
253
- error_message: ErrorMessage = ErrorMessage(
254
- error_reason=e.error_reason, error_description=e.error_description
20
+ OngoingMission.__init__(
21
+ self,
22
+ state_machine=state_machine,
23
+ state=OngoingMissionStates.Monitor,
255
24
  )
256
- self.state_machine.current_task.error_message = error_message
@@ -1,47 +1,22 @@
1
- import logging
2
- import time
3
1
  from typing import TYPE_CHECKING
4
2
 
5
3
  from transitions import State
6
4
 
7
- from isar.models.communication.queues.queue_utils import check_shared_state
8
- from robot_interface.models.mission.status import RobotStatus
5
+ from isar.state_machine.generic_states.robot_unavailable import (
6
+ RobotUnavailable,
7
+ RobotUnavailableStates,
8
+ )
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from isar.state_machine.state_machine import StateMachine
12
12
 
13
13
 
14
- class Offline(State):
14
+ class Offline(State, RobotUnavailable):
15
15
  def __init__(self, state_machine: "StateMachine") -> None:
16
- super().__init__(name="offline", on_enter=self.start, on_exit=self.stop)
17
- self.state_machine: "StateMachine" = state_machine
18
- self.logger = logging.getLogger("state_machine")
19
- self.shared_state = self.state_machine.shared_state
20
- self.signal_state_machine_to_stop = state_machine.signal_state_machine_to_stop
16
+ State.__init__(self, name="offline", on_enter=self.start, on_exit=self.stop)
21
17
 
22
- def start(self) -> None:
23
- self.state_machine.update_state()
24
- self._run()
25
-
26
- def stop(self) -> None:
27
- return
28
-
29
- def _run(self) -> None:
30
- while True:
31
- if self.signal_state_machine_to_stop.is_set():
32
- self.logger.info(
33
- "Stopping state machine from %s state", self.__class__.__name__
34
- )
35
- break
36
-
37
- robot_status: RobotStatus = check_shared_state(
38
- self.shared_state.robot_status
39
- )
40
-
41
- if robot_status != RobotStatus.Offline:
42
- transition = self.state_machine.robot_status_changed # type: ignore
43
- break
44
-
45
- time.sleep(self.state_machine.sleep_time)
46
-
47
- transition()
18
+ RobotUnavailable.__init__(
19
+ self,
20
+ state_machine=state_machine,
21
+ state=RobotUnavailableStates.Offline,
22
+ )
@@ -31,7 +31,7 @@ class Paused(State):
31
31
  )
32
32
  break
33
33
 
34
- if check_for_event(self.events.api_requests.pause_mission.input):
34
+ if check_for_event(self.events.api_requests.stop_mission.input):
35
35
  transition = self.state_machine.stop # type: ignore
36
36
  break
37
37