isar 1.22.3__py3-none-any.whl → 1.23.0__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.

Files changed (35) hide show
  1. isar/apis/api.py +1 -0
  2. isar/apis/models/models.py +2 -7
  3. isar/apis/models/start_mission_definition.py +86 -97
  4. isar/apis/schedule/scheduling_controller.py +11 -20
  5. isar/config/predefined_missions/default.json +70 -88
  6. isar/config/predefined_missions/default_turtlebot.json +102 -106
  7. isar/config/settings.env +1 -1
  8. isar/config/settings.py +3 -3
  9. isar/mission_planner/local_planner.py +3 -0
  10. isar/mission_planner/sequential_task_selector.py +3 -3
  11. isar/mission_planner/task_selector_interface.py +4 -4
  12. isar/script.py +1 -1
  13. isar/services/service_connections/mqtt/mqtt_client.py +1 -1
  14. isar/services/utilities/scheduling_utilities.py +3 -4
  15. isar/state_machine/state_machine.py +22 -98
  16. isar/state_machine/states/idle.py +2 -0
  17. isar/state_machine/states/initialize.py +1 -1
  18. isar/state_machine/states/initiate.py +10 -10
  19. isar/state_machine/states/monitor.py +78 -98
  20. isar/state_machine/states/offline.py +1 -0
  21. isar/state_machine/states/paused.py +1 -1
  22. isar/state_machine/states/stop.py +1 -1
  23. {isar-1.22.3.dist-info → isar-1.23.0.dist-info}/METADATA +1 -1
  24. {isar-1.22.3.dist-info → isar-1.23.0.dist-info}/RECORD +34 -35
  25. robot_interface/models/exceptions/robot_exceptions.py +3 -3
  26. robot_interface/models/inspection/inspection.py +1 -1
  27. robot_interface/models/mission/mission.py +2 -2
  28. robot_interface/models/mission/status.py +0 -8
  29. robot_interface/models/mission/task.py +182 -115
  30. robot_interface/robot_interface.py +29 -54
  31. robot_interface/models/mission/step.py +0 -234
  32. {isar-1.22.3.dist-info → isar-1.23.0.dist-info}/LICENSE +0 -0
  33. {isar-1.22.3.dist-info → isar-1.23.0.dist-info}/WHEEL +0 -0
  34. {isar-1.22.3.dist-info → isar-1.23.0.dist-info}/entry_points.txt +0 -0
  35. {isar-1.22.3.dist-info → isar-1.23.0.dist-info}/top_level.txt +0 -0
isar/apis/api.py CHANGED
@@ -213,6 +213,7 @@ class API:
213
213
  methods=["POST"],
214
214
  dependencies=[authentication_dependency],
215
215
  summary="Drive to the provided pose",
216
+ deprecated=True,
216
217
  responses={
217
218
  HTTPStatus.OK.value: {
218
219
  "description": "Drive to succesfully started",
@@ -3,16 +3,13 @@ from typing import List, Optional
3
3
  from alitra import Frame, Orientation, Pose, Position
4
4
  from pydantic import BaseModel, Field
5
5
 
6
-
7
- class StepResponse(BaseModel):
8
- id: str
9
- type: str
6
+ from robot_interface.models.mission.task import TaskTypes
10
7
 
11
8
 
12
9
  class TaskResponse(BaseModel):
13
10
  id: str
14
11
  tag_id: Optional[str] = None
15
- steps: List[StepResponse]
12
+ type: TaskTypes
16
13
 
17
14
 
18
15
  class StartMissionResponse(BaseModel):
@@ -25,8 +22,6 @@ class ControlMissionResponse(BaseModel):
25
22
  mission_status: str
26
23
  task_id: str
27
24
  task_status: str
28
- step_id: str
29
- step_status: str
30
25
 
31
26
 
32
27
  class RobotInfoResponse(BaseModel):
@@ -1,6 +1,6 @@
1
1
  import time
2
2
  from enum import Enum
3
- from typing import Any, Dict, List, Optional, Union
3
+ from typing import Any, Dict, List, Optional
4
4
 
5
5
  from alitra import Frame, Orientation, Pose, Position
6
6
  from pydantic import BaseModel, Field
@@ -8,11 +8,11 @@ from pydantic import BaseModel, Field
8
8
  from isar.apis.models.models import InputPose, InputPosition
9
9
  from isar.config.settings import settings
10
10
  from isar.mission_planner.mission_planner_interface import MissionPlannerError
11
+ from robot_interface.models.inspection.inspection import Inspection, InspectionMetadata
11
12
  from robot_interface.models.mission.mission import Mission
12
- from robot_interface.models.mission.step import (
13
- STEPS,
13
+ from robot_interface.models.mission.task import (
14
+ TASKS,
14
15
  DockingProcedure,
15
- DriveToPose,
16
16
  Localize,
17
17
  RecordAudio,
18
18
  ReturnToHome,
@@ -34,7 +34,6 @@ class InspectionTypes(str, Enum):
34
34
 
35
35
  class TaskType(str, Enum):
36
36
  Inspection: str = "inspection"
37
- DriveTo: str = "drive_to"
38
37
  Localization: str = "localization"
39
38
  ReturnToHome: str = "return_to_home"
40
39
  Dock: str = "dock"
@@ -52,7 +51,7 @@ class StartMissionInspectionDefinition(BaseModel):
52
51
  class StartMissionTaskDefinition(BaseModel):
53
52
  type: TaskType = Field(default=TaskType.Inspection)
54
53
  pose: InputPose
55
- inspections: List[StartMissionInspectionDefinition]
54
+ inspection: StartMissionInspectionDefinition
56
55
  tag: Optional[str] = None
57
56
  id: Optional[str] = None
58
57
 
@@ -66,40 +65,35 @@ class StartMissionDefinition(BaseModel):
66
65
  undock: Optional[bool] = None
67
66
 
68
67
 
69
- def to_isar_mission(mission_definition: StartMissionDefinition) -> Mission:
70
- isar_tasks: List[Task] = []
71
- all_steps_in_mission: List[STEPS] = []
68
+ def to_isar_mission(start_mission_definition: StartMissionDefinition) -> Mission:
69
+ isar_tasks: List[TASKS] = []
72
70
 
73
- for task in mission_definition.tasks:
74
- steps: List[STEPS] = generate_steps(task)
75
- all_steps_in_mission.extend(steps)
76
-
77
- isar_task: Task = Task(steps=steps, tag_id=task.tag)
78
- if task.id:
79
- isar_task.id = task.id
80
- isar_tasks.append(isar_task)
71
+ for start_mission_task_definition in start_mission_definition.tasks:
72
+ task: TASKS = create_isar_task(start_mission_task_definition)
73
+ if start_mission_task_definition.id:
74
+ task.id = start_mission_task_definition.id
75
+ isar_tasks.append(task)
81
76
 
82
77
  if not isar_tasks:
83
78
  raise MissionPlannerError("Mission does not contain any valid tasks")
84
79
 
85
80
  check_for_duplicate_ids(isar_tasks)
86
- check_for_duplicate_ids(all_steps_in_mission)
87
81
 
88
82
  isar_mission: Mission = Mission(tasks=isar_tasks)
89
83
 
90
- isar_mission.dock = mission_definition.dock
91
- isar_mission.undock = mission_definition.undock
84
+ isar_mission.dock = start_mission_definition.dock
85
+ isar_mission.undock = start_mission_definition.undock
92
86
 
93
- if mission_definition.name:
94
- isar_mission.name = mission_definition.name
87
+ if start_mission_definition.name:
88
+ isar_mission.name = start_mission_definition.name
95
89
  else:
96
90
  isar_mission.name = _build_mission_name()
97
91
 
98
- if mission_definition.id:
99
- isar_mission.id = mission_definition.id
92
+ if start_mission_definition.id:
93
+ isar_mission.id = start_mission_definition.id
100
94
 
101
- if mission_definition.start_pose:
102
- input_pose: InputPose = mission_definition.start_pose
95
+ if start_mission_definition.start_pose:
96
+ input_pose: InputPose = start_mission_definition.start_pose
103
97
  input_frame: Frame = Frame(name=input_pose.frame_name)
104
98
  input_position: Position = Position(
105
99
  input_pose.position.x,
@@ -121,7 +115,7 @@ def to_isar_mission(mission_definition: StartMissionDefinition) -> Mission:
121
115
  return isar_mission
122
116
 
123
117
 
124
- def check_for_duplicate_ids(items: Union[List[Task], List[STEPS]]):
118
+ def check_for_duplicate_ids(items: List[TASKS]):
125
119
  duplicate_ids = get_duplicate_ids(items=items)
126
120
  if len(duplicate_ids) > 0:
127
121
  raise MissionPlannerError(
@@ -130,97 +124,92 @@ def check_for_duplicate_ids(items: Union[List[Task], List[STEPS]]):
130
124
  )
131
125
 
132
126
 
133
- def generate_steps(task) -> List[STEPS]:
134
- steps: List[STEPS] = []
127
+ def create_isar_task(start_mission_task_definition) -> TASKS:
135
128
 
136
- if task.type == TaskType.Inspection:
137
- steps.extend(generate_steps_for_inspection_task(task=task))
138
- elif task.type == TaskType.DriveTo:
139
- steps.append(generate_steps_for_drive_to_task(task=task))
140
- elif task.type == TaskType.Localization:
141
- steps.append(generate_steps_for_localization_task(task=task))
142
- elif task.type == TaskType.ReturnToHome:
143
- steps.append(generate_steps_for_return_to_home_task(task=task))
144
- elif task.type == TaskType.Dock:
145
- steps.append(generate_steps_for_dock_task())
129
+ if start_mission_task_definition.type == TaskType.Inspection:
130
+ return create_inspection_task(start_mission_task_definition)
131
+ elif start_mission_task_definition.type == TaskType.Localization:
132
+ return create_localization_task(start_mission_task_definition)
133
+ elif start_mission_task_definition.type == TaskType.ReturnToHome:
134
+ return create_return_to_home_task(start_mission_task_definition)
135
+ elif start_mission_task_definition.type == TaskType.Dock:
136
+ return create_dock_task()
146
137
  else:
147
138
  raise MissionPlannerError(
148
- f"Failed to create task: '{task.type}' is not a valid"
139
+ f"Failed to create task: '{start_mission_task_definition.type}' is not a valid"
149
140
  )
150
141
 
151
- return steps
152
-
153
142
 
154
- def generate_steps_for_inspection_task(task: StartMissionTaskDefinition) -> List[STEPS]:
155
- drive_step: DriveToPose = DriveToPose(pose=task.pose.to_alitra_pose())
143
+ def create_inspection_task(
144
+ start_mission_task_definition: StartMissionTaskDefinition,
145
+ ) -> TASKS:
156
146
 
157
- inspection_steps: List[STEPS] = [
158
- create_inspection_step(
159
- inspection_type=inspection.type,
160
- duration=inspection.duration,
161
- target=inspection.inspection_target.to_alitra_position(),
162
- tag_id=task.tag,
163
- metadata=inspection.metadata,
164
- id=inspection.id,
147
+ if start_mission_task_definition.inspection.type == InspectionTypes.image:
148
+ return TakeImage(
149
+ target=start_mission_task_definition.inspection.inspection_target.to_alitra_position(),
150
+ tag_id=start_mission_task_definition.tag,
151
+ robot_pose=start_mission_task_definition.pose.to_alitra_pose(),
152
+ metadata=start_mission_task_definition.inspection.metadata,
153
+ )
154
+ elif start_mission_task_definition.inspection.type == InspectionTypes.video:
155
+ return TakeVideo(
156
+ target=start_mission_task_definition.inspection.inspection_target.to_alitra_position(),
157
+ duration=start_mission_task_definition.inspection.duration,
158
+ tag_id=start_mission_task_definition.tag,
159
+ robot_pose=start_mission_task_definition.pose.to_alitra_pose(),
160
+ metadata=start_mission_task_definition.inspection.metadata,
165
161
  )
166
- for inspection in task.inspections
167
- ]
168
162
 
169
- return [drive_step, *inspection_steps]
163
+ elif start_mission_task_definition.inspection.type == InspectionTypes.thermal_image:
164
+ return TakeThermalImage(
165
+ target=start_mission_task_definition.inspection.inspection_target.to_alitra_position(),
166
+ tag_id=start_mission_task_definition.tag,
167
+ robot_pose=start_mission_task_definition.pose.to_alitra_pose(),
168
+ metadata=start_mission_task_definition.inspection.metadata,
169
+ )
170
170
 
171
+ elif start_mission_task_definition.inspection.type == InspectionTypes.thermal_video:
172
+ return TakeThermalVideo(
173
+ target=start_mission_task_definition.inspection.inspection_target.to_alitra_position(),
174
+ duration=start_mission_task_definition.inspection.duration,
175
+ tag_id=start_mission_task_definition.tag,
176
+ robot_pose=start_mission_task_definition.pose.to_alitra_pose(),
177
+ metadata=start_mission_task_definition.inspection.metadata,
178
+ )
171
179
 
172
- def generate_steps_for_drive_to_task(task: StartMissionTaskDefinition) -> DriveToPose:
173
- return DriveToPose(pose=task.pose.to_alitra_pose())
180
+ elif start_mission_task_definition.inspection.type == InspectionTypes.audio:
181
+ return RecordAudio(
182
+ target=start_mission_task_definition.inspection.inspection_target.to_alitra_position(),
183
+ duration=start_mission_task_definition.inspection.duration,
184
+ tag_id=start_mission_task_definition.tag,
185
+ robot_pose=start_mission_task_definition.pose.to_alitra_pose(),
186
+ metadata=start_mission_task_definition.inspection.metadata,
187
+ )
188
+ else:
189
+ raise ValueError(
190
+ f"Inspection type '{start_mission_task_definition.inspection.type}' not supported"
191
+ )
174
192
 
175
193
 
176
- def generate_steps_for_localization_task(task: StartMissionTaskDefinition) -> Localize:
177
- return Localize(localization_pose=task.pose.to_alitra_pose())
194
+ def create_localization_task(
195
+ start_mission_task_definition: StartMissionTaskDefinition,
196
+ ) -> Localize:
197
+ return Localize(
198
+ localization_pose=start_mission_task_definition.pose.to_alitra_pose()
199
+ )
178
200
 
179
201
 
180
- def generate_steps_for_return_to_home_task(
181
- task: StartMissionTaskDefinition,
202
+ def create_return_to_home_task(
203
+ start_mission_task_definition: StartMissionTaskDefinition,
182
204
  ) -> ReturnToHome:
183
- return ReturnToHome(pose=task.pose.to_alitra_pose())
205
+ return ReturnToHome(pose=start_mission_task_definition.pose.to_alitra_pose())
184
206
 
185
207
 
186
- def generate_steps_for_dock_task() -> DockingProcedure:
208
+ def create_dock_task() -> DockingProcedure:
187
209
  return DockingProcedure(behavior="dock")
188
210
 
189
211
 
190
- def create_inspection_step(
191
- inspection_type: InspectionTypes,
192
- duration: float,
193
- target: Position,
194
- tag_id: Optional[str],
195
- metadata: Optional[dict],
196
- id: Optional[str],
197
- ) -> STEPS:
198
- inspection_step_dict: Dict[str, Any] = {
199
- InspectionTypes.image.value: TakeImage(target=target),
200
- InspectionTypes.video.value: TakeVideo(target=target, duration=duration),
201
- InspectionTypes.thermal_image.value: TakeThermalImage(target=target),
202
- InspectionTypes.thermal_video.value: TakeThermalVideo(
203
- target=target, duration=duration
204
- ),
205
- InspectionTypes.audio.value: RecordAudio(target=target, duration=duration),
206
- }
207
-
208
- if inspection_type not in inspection_step_dict:
209
- raise ValueError(f"Inspection type '{inspection_type}' not supported")
210
- else:
211
- inspection_step = inspection_step_dict[inspection_type]
212
-
213
- if tag_id:
214
- inspection_step.tag_id = tag_id
215
- if metadata:
216
- inspection_step.metadata = metadata
217
- if id:
218
- inspection_step.id = id
219
-
220
- return inspection_step
221
-
222
-
223
- def get_duplicate_ids(items: Union[List[Task], List[STEPS]]) -> List[str]:
212
+ def get_duplicate_ids(items: List[TASKS]) -> List[str]:
224
213
  unique_ids: List[str] = []
225
214
  duplicate_ids: List[str] = []
226
215
  for item in items:
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
  from http import HTTPStatus
3
- from typing import List, Optional
3
+ from typing import Optional
4
4
 
5
5
  from alitra import Pose
6
6
  from fastapi import Body, HTTPException, Path
@@ -21,8 +21,8 @@ from isar.mission_planner.mission_planner_interface import MissionPlannerError
21
21
  from isar.services.utilities.scheduling_utilities import SchedulingUtilities
22
22
  from isar.state_machine.states_enum import States
23
23
  from robot_interface.models.mission.mission import Mission
24
- from robot_interface.models.mission.step import (
25
- DriveToPose,
24
+ from robot_interface.models.mission.task import (
25
+ TASKS,
26
26
  Localize,
27
27
  MoveArm,
28
28
  ReturnToHome,
@@ -67,8 +67,7 @@ class SchedulingController:
67
67
  mission: Mission = self.scheduling_utilities.get_mission(mission_id)
68
68
  if return_pose:
69
69
  pose: Pose = return_pose.to_alitra_pose()
70
- step: DriveToPose = DriveToPose(pose=pose)
71
- mission.tasks.append(Task(steps=[step]))
70
+ mission.tasks.append(ReturnToHome(pose=pose))
72
71
 
73
72
  self.scheduling_utilities.verify_robot_capable_of_mission(
74
73
  mission=mission, robot_capabilities=robot_settings.CAPABILITIES
@@ -136,8 +135,7 @@ class SchedulingController:
136
135
  )
137
136
  if return_pose:
138
137
  pose: Pose = return_pose.to_alitra_pose()
139
- step: DriveToPose = DriveToPose(pose=pose)
140
- mission.tasks.append(Task(steps=[step]))
138
+ mission.tasks.append(ReturnToHome(pose=pose))
141
139
 
142
140
  initial_pose_alitra: Optional[Pose] = (
143
141
  initial_pose.to_alitra_pose() if initial_pose else None
@@ -211,7 +209,7 @@ class SchedulingController:
211
209
  target_pose: InputPose = Body(
212
210
  default=None,
213
211
  title="Target Pose",
214
- description="The target pose for the drive_to step",
212
+ description="The target pose for the drive_to task",
215
213
  ),
216
214
  ) -> StartMissionResponse:
217
215
  self.logger.info("Received request to start new drive-to mission")
@@ -221,8 +219,7 @@ class SchedulingController:
221
219
  self.scheduling_utilities.verify_state_machine_ready_to_receive_mission(state)
222
220
 
223
221
  pose: Pose = target_pose.to_alitra_pose()
224
- step: DriveToPose = DriveToPose(pose=pose)
225
- mission: Mission = Mission(tasks=[Task(steps=[step])])
222
+ mission: Mission = Mission(tasks=[ReturnToHome(pose=pose)])
226
223
 
227
224
  self.logger.info(
228
225
  f"Starting drive to mission with ISAR Mission ID: '{mission.id}'"
@@ -246,8 +243,7 @@ class SchedulingController:
246
243
  self.scheduling_utilities.verify_state_machine_ready_to_receive_mission(state)
247
244
 
248
245
  pose: Pose = localization_pose.to_alitra_pose()
249
- step: Localize = Localize(localization_pose=pose)
250
- mission: Mission = Mission(tasks=[Task(steps=[step])])
246
+ mission: Mission = Mission(tasks=[Localize(localization_pose=pose)])
251
247
 
252
248
  self.logger.info(
253
249
  f"Starting localization mission with ISAR Mission ID: '{mission.id}'"
@@ -294,8 +290,7 @@ class SchedulingController:
294
290
 
295
291
  self.scheduling_utilities.verify_state_machine_ready_to_receive_mission(state)
296
292
 
297
- step: MoveArm = MoveArm(arm_pose=arm_pose_literal)
298
- mission: Mission = Mission(tasks=[Task(steps=[step])])
293
+ mission: Mission = Mission(tasks=[MoveArm(arm_pose=arm_pose_literal)])
299
294
 
300
295
  self.logger.info(
301
296
  f"Starting move arm mission with ISAR Mission ID: '{mission.id}'"
@@ -322,9 +317,5 @@ class SchedulingController:
322
317
  tasks=[self._task_api_response(task) for task in mission.tasks],
323
318
  )
324
319
 
325
- def _task_api_response(self, task: Task) -> TaskResponse:
326
- steps: List[dict] = []
327
- for step in task.steps:
328
- steps.append({"id": step.id, "type": step.__class__.__name__})
329
-
330
- return TaskResponse(id=task.id, tag_id=task.tag_id, steps=steps)
320
+ def _task_api_response(self, task: TASKS) -> TaskResponse:
321
+ return TaskResponse(id=task.id, tag_id=task.tag_id, type=task.type)
@@ -1,92 +1,74 @@
1
1
  {
2
- "id": "1",
3
- "tasks": [
4
- {
5
- "steps": [
6
- {
7
- "type": "drive_to_pose",
8
- "pose": {
9
- "position": {
10
- "x": -2,
11
- "y": -2,
12
- "z": 0,
13
- "frame": "asset"
14
- },
15
- "orientation": {
16
- "x": 0,
17
- "y": 0,
18
- "z": 0.4794255,
19
- "w": 0.8775826,
20
- "frame": "asset"
21
- },
22
- "frame": "asset"
23
- }
24
- },
25
- {
26
- "type": "take_image",
27
- "target": {
28
- "x": 2,
29
- "y": 2,
30
- "z": 0,
31
- "frame": "robot"
32
- }
33
- }
34
- ]
2
+ "id": "1",
3
+ "tasks": [
4
+ {
5
+ "type": "take_image",
6
+ "robot_pose": {
7
+ "position": {
8
+ "x": -2,
9
+ "y": -2,
10
+ "z": 0,
11
+ "frame": "asset"
35
12
  },
36
- {
37
- "steps": [
38
- {
39
- "type": "drive_to_pose",
40
- "pose": {
41
- "position": {
42
- "x": -2,
43
- "y": 2,
44
- "z": 0,
45
- "frame": "asset"
46
- },
47
- "orientation": {
48
- "x": 0,
49
- "y": 0,
50
- "z": 0.4794255,
51
- "w": 0.8775826,
52
- "frame": "asset"
53
- },
54
- "frame": "asset"
55
- }
56
- },
57
- {
58
- "type": "take_thermal_image",
59
- "target": {
60
- "x": 2,
61
- "y": 2,
62
- "z": 0,
63
- "frame": "robot"
64
- }
65
- }
66
- ]
13
+ "orientation": {
14
+ "x": 0,
15
+ "y": 0,
16
+ "z": 0.4794255,
17
+ "w": 0.8775826,
18
+ "frame": "asset"
67
19
  },
68
- {
69
- "steps": [
70
- {
71
- "type": "drive_to_pose",
72
- "pose": {
73
- "position": {
74
- "x": 2,
75
- "y": 2,
76
- "z": 0,
77
- "frame": "asset"
78
- },
79
- "orientation": {
80
- "x": 0,
81
- "y": 0,
82
- "z": 0.4794255,
83
- "w": 0.8775826,
84
- "frame": "asset"
85
- },
86
- "frame": "asset"
87
- }
88
- }
89
- ]
90
- }
91
- ]
20
+ "frame": "asset"
21
+ },
22
+ "target": {
23
+ "x": 2,
24
+ "y": 2,
25
+ "z": 0,
26
+ "frame": "robot"
27
+ }
28
+ },
29
+ {
30
+ "type": "take_thermal_image",
31
+ "robot_pose": {
32
+ "position": {
33
+ "x": -2,
34
+ "y": 2,
35
+ "z": 0,
36
+ "frame": "asset"
37
+ },
38
+ "orientation": {
39
+ "x": 0,
40
+ "y": 0,
41
+ "z": 0.4794255,
42
+ "w": 0.8775826,
43
+ "frame": "asset"
44
+ },
45
+ "frame": "asset"
46
+ },
47
+ "target": {
48
+ "x": 2,
49
+ "y": 2,
50
+ "z": 0,
51
+ "frame": "robot"
52
+ }
53
+ },
54
+ {
55
+ "type": "return_to_home",
56
+ "pose": {
57
+ "position": {
58
+ "x": 2,
59
+ "y": 2,
60
+ "z": 0,
61
+ "frame": "asset"
62
+ },
63
+ "orientation": {
64
+ "x": 0,
65
+ "y": 0,
66
+ "z": 0.4794255,
67
+ "w": 0.8775826,
68
+ "frame": "asset"
69
+ },
70
+ "frame": "asset"
71
+ }
72
+ }
73
+ ]
92
74
  }