isar 1.10.14__py3-none-any.whl → 1.12.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 (47) hide show
  1. isar/apis/api.py +54 -4
  2. isar/apis/models/start_mission_definition.py +44 -35
  3. isar/apis/security/authentication.py +4 -5
  4. isar/config/keyvault/keyvault_service.py +38 -21
  5. isar/config/log.py +42 -13
  6. isar/config/predefined_mission_definition/__init__.py +0 -0
  7. isar/config/predefined_mission_definition/default_mission.json +98 -0
  8. isar/config/predefined_mission_definition/default_turtlebot.json +136 -0
  9. isar/config/predefined_missions/default.json +84 -84
  10. isar/config/settings.env +5 -0
  11. isar/config/settings.py +51 -10
  12. isar/mission_planner/echo_planner.py +1 -1
  13. isar/models/communication/queues/queues.py +1 -1
  14. isar/models/mission/status.py +5 -5
  15. isar/models/mission_metadata/mission_metadata.py +2 -0
  16. isar/modules.py +3 -2
  17. isar/services/service_connections/mqtt/mqtt_client.py +0 -18
  18. isar/services/service_connections/mqtt/robot_info_publisher.py +33 -0
  19. isar/services/service_connections/mqtt/robot_status_publisher.py +66 -0
  20. isar/services/service_connections/request_handler.py +1 -1
  21. isar/services/utilities/scheduling_utilities.py +5 -5
  22. isar/services/utilities/threaded_request.py +3 -3
  23. isar/state_machine/state_machine.py +13 -5
  24. isar/state_machine/states/idle.py +6 -6
  25. isar/state_machine/states/initialize.py +9 -8
  26. isar/state_machine/states/initiate_step.py +16 -16
  27. isar/state_machine/states/monitor.py +17 -11
  28. isar/state_machine/states/paused.py +6 -6
  29. isar/state_machine/states/stop_step.py +10 -10
  30. isar/state_machine/states_enum.py +0 -1
  31. isar/storage/local_storage.py +2 -2
  32. isar/storage/slimm_storage.py +107 -41
  33. isar/storage/uploader.py +4 -5
  34. isar/storage/utilities.py +1 -23
  35. {isar-1.10.14.dist-info → isar-1.12.0.dist-info}/LICENSE +0 -0
  36. {isar-1.10.14.dist-info → isar-1.12.0.dist-info}/METADATA +4 -1
  37. {isar-1.10.14.dist-info → isar-1.12.0.dist-info}/RECORD +47 -40
  38. {isar-1.10.14.dist-info → isar-1.12.0.dist-info}/WHEEL +0 -0
  39. {isar-1.10.14.dist-info → isar-1.12.0.dist-info}/top_level.txt +0 -0
  40. robot_interface/models/inspection/inspection.py +3 -22
  41. robot_interface/models/mission/status.py +6 -1
  42. robot_interface/models/mission/step.py +5 -32
  43. robot_interface/models/robots/__init__.py +0 -0
  44. robot_interface/models/robots/robot_model.py +13 -0
  45. robot_interface/robot_interface.py +17 -0
  46. robot_interface/telemetry/payloads.py +34 -0
  47. robot_interface/utilities/json_service.py +3 -0
isar/apis/api.py CHANGED
@@ -1,21 +1,34 @@
1
1
  import logging
2
2
  from http import HTTPStatus
3
3
  from logging import Logger
4
+ import os
4
5
  from typing import List, Union
5
6
 
6
7
  import click
7
8
  import uvicorn
8
- from fastapi import FastAPI, Security
9
+ from fastapi import FastAPI, Security, Request
9
10
  from fastapi.middleware.cors import CORSMiddleware
10
11
  from fastapi.routing import APIRouter
11
12
  from injector import inject
12
13
  from pydantic import AnyHttpUrl
13
14
 
15
+ from opencensus.ext.azure.trace_exporter import AzureExporter
16
+ from opencensus.trace.samplers import ProbabilitySampler
17
+ from opencensus.trace.tracer import Tracer
18
+ from opencensus.trace.span import SpanKind
19
+ from opencensus.trace.attributes_helper import COMMON_ATTRIBUTES
20
+
14
21
  from isar.apis.models.models import ControlMissionResponse, StartMissionResponse
15
22
  from isar.apis.schedule.scheduling_controller import SchedulingController
16
23
  from isar.apis.security.authentication import Authenticator
24
+ from isar.config.configuration_error import ConfigurationError
25
+ from isar.config.keyvault.keyvault_error import KeyvaultError
26
+ from isar.config.keyvault.keyvault_service import Keyvault
17
27
  from isar.config.settings import settings
18
28
 
29
+ HTTP_URL = COMMON_ATTRIBUTES["HTTP_URL"]
30
+ HTTP_STATUS_CODE = COMMON_ATTRIBUTES["HTTP_STATUS_CODE"]
31
+
19
32
 
20
33
  class API:
21
34
  @inject
@@ -23,13 +36,16 @@ class API:
23
36
  self,
24
37
  authenticator: Authenticator,
25
38
  scheduling_controller: SchedulingController,
39
+ keyvault_client: Keyvault,
26
40
  port: int = settings.API_PORT,
41
+ azure_ai_logging_enabled: bool = settings.LOG_HANDLER_APPLICATION_INSIGHTS_ENABLED,
27
42
  ) -> None:
28
-
29
43
  self.authenticator: Authenticator = authenticator
30
44
  self.scheduling_controller: SchedulingController = scheduling_controller
45
+ self.keyvault_client: Keyvault = keyvault_client
31
46
  self.host: str = "0.0.0.0" # Locking uvicorn to use 0.0.0.0
32
47
  self.port: int = port
48
+ self.azure_ai_logging_enabled: bool = azure_ai_logging_enabled
33
49
 
34
50
  self.logger: Logger = logging.getLogger("api")
35
51
 
@@ -77,6 +93,9 @@ class API:
77
93
  allow_headers=["*"],
78
94
  )
79
95
 
96
+ if self.azure_ai_logging_enabled:
97
+ self._add_request_logging_middleware(app)
98
+
80
99
  app.include_router(router=self._create_scheduler_router())
81
100
 
82
101
  app.include_router(router=self._create_info_router())
@@ -84,7 +103,6 @@ class API:
84
103
  return app
85
104
 
86
105
  def _create_scheduler_router(self) -> APIRouter:
87
-
88
106
  router: APIRouter = APIRouter(tags=["Scheduler"])
89
107
 
90
108
  authentication_dependency: Security = Security(self.authenticator.get_scheme())
@@ -213,7 +231,6 @@ class API:
213
231
  return router
214
232
 
215
233
  def _create_info_router(self) -> APIRouter:
216
-
217
234
  router: APIRouter = APIRouter(tags=["Info"])
218
235
 
219
236
  authentication_dependency: Security = Security(self.authenticator.get_scheme())
@@ -244,3 +261,36 @@ class API:
244
261
  self.port,
245
262
  extra={"color_message": color_message},
246
263
  )
264
+
265
+ def _add_request_logging_middleware(self, app: FastAPI) -> None:
266
+ connection_string: str
267
+ try:
268
+ connection_string = self.keyvault_client.get_secret(
269
+ "application-insights-connection-string"
270
+ ).value
271
+ except KeyvaultError:
272
+ message: str = (
273
+ "Missing connection string for Application Insights in key vault. "
274
+ )
275
+ self.logger.critical(message)
276
+ raise ConfigurationError(message)
277
+
278
+ @app.middleware("http")
279
+ async def middlewareOpencensus(request: Request, call_next):
280
+ tracer = Tracer(
281
+ exporter=AzureExporter(connection_string=connection_string),
282
+ sampler=ProbabilitySampler(1.0),
283
+ )
284
+ with tracer.span("main") as span:
285
+ span.span_kind = SpanKind.SERVER
286
+
287
+ response = await call_next(request)
288
+
289
+ tracer.add_attribute_to_current_span(
290
+ attribute_key=HTTP_STATUS_CODE, attribute_value=response.status_code
291
+ )
292
+ tracer.add_attribute_to_current_span(
293
+ attribute_key=HTTP_URL, attribute_value=str(request.url)
294
+ )
295
+
296
+ return response
@@ -1,37 +1,41 @@
1
- from typing import List, Optional, Type
1
+ from enum import Enum
2
+ from typing import List, Optional
2
3
 
3
4
  from alitra import Position
4
5
  from pydantic import BaseModel, Field
6
+
7
+ from isar.apis.models.models import InputPose, InputPosition
8
+ from isar.mission_planner.mission_planner_interface import MissionPlannerError
9
+ from isar.models.mission.mission import Mission, Task
5
10
  from robot_interface.models.mission.step import (
6
11
  STEPS,
7
12
  DriveToPose,
8
- InspectionStep,
9
13
  TakeImage,
10
14
  TakeThermalImage,
11
15
  TakeThermalVideo,
12
16
  TakeVideo,
13
17
  )
14
18
 
15
- from isar.apis.models.models import InputPose, InputPosition
16
- from isar.mission_planner.mission_planner_interface import MissionPlannerError
17
- from isar.models.mission.mission import Mission, Task
18
19
 
19
- inspection_step_types: List[Type[InspectionStep]] = [
20
- TakeImage,
21
- TakeThermalImage,
22
- TakeVideo,
23
- TakeThermalVideo,
24
- ]
20
+ class InspectionTypes(str, Enum):
21
+ image = "Image"
22
+ thermal_image = "ThermalImage"
23
+ video = "Video"
24
+ thermal_video = "ThermalVideo"
25
+
26
+
27
+ class StartMissionInspectionDefinition(BaseModel):
28
+ type: InspectionTypes = Field(default=InspectionTypes.image)
29
+ inspection_target: InputPosition
30
+ analysis_types: Optional[List]
31
+ duration: Optional[float]
32
+ metadata: Optional[dict]
25
33
 
26
34
 
27
35
  class StartMissionTaskDefinition(BaseModel):
28
36
  pose: InputPose
29
37
  tag: Optional[str]
30
- inspection_target: InputPosition
31
- inspection_types: List[str] = Field(
32
- default=[step.get_inspection_type().__name__ for step in inspection_step_types]
33
- )
34
- video_duration: Optional[float] = Field(default=10)
38
+ inspections: List[StartMissionInspectionDefinition]
35
39
 
36
40
 
37
41
  class StartMissionDefinition(BaseModel):
@@ -47,14 +51,16 @@ def to_isar_mission(mission_definition: StartMissionDefinition) -> Mission:
47
51
  drive_step: DriveToPose = DriveToPose(pose=task.pose.to_alitra_pose())
48
52
  inspection_steps: List[STEPS] = [
49
53
  create_inspection_step(
50
- inspection_type=inspection_type,
51
- duration=task.video_duration,
52
- target=task.inspection_target.to_alitra_position(),
54
+ inspection_type=inspection.type,
55
+ duration=inspection.duration,
56
+ target=inspection.inspection_target.to_alitra_position(),
53
57
  tag_id=tag_id,
58
+ analysis=inspection.analysis_types,
59
+ metadata=inspection.metadata,
54
60
  )
55
- for inspection_type in task.inspection_types
61
+ for inspection in task.inspections
56
62
  ]
57
- except (ValueError) as e:
63
+ except ValueError as e:
58
64
  raise MissionPlannerError(f"Failed to create task: {str(e)}")
59
65
  isar_task: Task = Task(steps=[drive_step, *inspection_steps], tag_id=tag_id)
60
66
  isar_tasks.append(isar_task)
@@ -68,27 +74,30 @@ def to_isar_mission(mission_definition: StartMissionDefinition) -> Mission:
68
74
 
69
75
 
70
76
  def create_inspection_step(
71
- inspection_type: str, duration: float, target: Position, tag_id: Optional[str]
77
+ inspection_type: InspectionTypes,
78
+ duration: float,
79
+ target: Position,
80
+ analysis: Optional[List],
81
+ tag_id: Optional[str],
82
+ metadata: Optional[dict],
72
83
  ) -> STEPS:
73
84
  inspection_step: STEPS
74
-
75
- if inspection_type == TakeImage.get_inspection_type().__name__:
85
+ if inspection_type == InspectionTypes.image.value:
76
86
  inspection_step = TakeImage(target=target)
77
- if tag_id:
78
- inspection_step.tag_id = tag_id
79
- elif inspection_type == TakeVideo.get_inspection_type().__name__:
87
+ elif inspection_type == InspectionTypes.video.value:
80
88
  inspection_step = TakeVideo(target=target, duration=duration)
81
- if tag_id:
82
- inspection_step.tag_id = tag_id
83
- elif inspection_type == TakeThermalImage.get_inspection_type().__name__:
89
+ elif inspection_type == InspectionTypes.thermal_image.value:
84
90
  inspection_step = TakeThermalImage(target=target)
85
- if tag_id:
86
- inspection_step.tag_id = tag_id
87
- elif inspection_type == TakeThermalVideo.get_inspection_type().__name__:
91
+ elif inspection_type == InspectionTypes.thermal_video.value:
88
92
  inspection_step = TakeThermalVideo(target=target, duration=duration)
89
- if tag_id:
90
- inspection_step.tag_id = tag_id
91
93
  else:
92
94
  raise ValueError(f"Inspection type '{inspection_type}' not supported")
93
95
 
96
+ if tag_id:
97
+ inspection_step.tag_id = tag_id
98
+ if analysis:
99
+ inspection_step.analysis = analysis
100
+ if metadata:
101
+ inspection_step.metadata = metadata
102
+
94
103
  return inspection_step
@@ -1,11 +1,10 @@
1
1
  import logging
2
2
 
3
-
4
3
  from fastapi import Depends
5
- from fastapi_azure_auth.exceptions import InvalidAuth
6
- from fastapi_azure_auth.user import User
7
4
  from fastapi.security.base import SecurityBase
8
5
  from fastapi_azure_auth import SingleTenantAzureAuthorizationCodeBearer
6
+ from fastapi_azure_auth.exceptions import InvalidAuth
7
+ from fastapi_azure_auth.user import User
9
8
  from pydantic import BaseModel
10
9
 
11
10
  from isar.config.settings import settings
@@ -32,8 +31,8 @@ azure_scheme = SingleTenantAzureAuthorizationCodeBearer(
32
31
 
33
32
  async def validate_has_role(user: User = Depends(azure_scheme)) -> None:
34
33
  """
35
- Validate that a user has the `role` role in order to access the API.
36
- Raises a 401 authentication error if not.
34
+ Validate if the user has the required role in order to access the API.
35
+ Raises a 401 authorization error if not.
37
36
  """
38
37
  if settings.REQUIRED_ROLE not in user.roles:
39
38
  raise InvalidAuth(
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  import traceback
3
+ from typing import Union
3
4
 
4
5
  from azure.core.exceptions import (
5
6
  ClientAuthenticationError,
@@ -20,25 +21,33 @@ class Keyvault:
20
21
  client_secret: str = None,
21
22
  tenant_id: str = None,
22
23
  ):
24
+ self.name = keyvault_name
23
25
  self.url = "https://" + keyvault_name + ".vault.azure.net"
24
26
  self.client_id = client_id
25
27
  self.client_secret = client_secret
26
28
  self.tenant_id = tenant_id
27
29
  self.logger = logging.getLogger("API")
30
+ self.client: SecretClient = None
28
31
 
29
32
  def get_secret(self, secret_name: str) -> KeyVaultSecret:
30
33
  secret_client: SecretClient = self.get_secret_client()
31
34
  try:
32
35
  secret: KeyVaultSecret = secret_client.get_secret(name=secret_name)
33
36
  except ResourceNotFoundError:
34
- self.logger.error("Secret was not found in keyvault.")
35
- traceback.print_exc()
37
+ self.logger.error(
38
+ "Secret '%s' was not found in keyvault '%s'.",
39
+ secret_name,
40
+ self.name,
41
+ exc_info=True,
42
+ )
36
43
  raise KeyvaultError # type: ignore
37
44
  except HttpResponseError:
38
45
  self.logger.error(
39
- "An error occurred while retrieving a secret from keyvault."
46
+ "An error occurred while retrieving the secret '%s' from keyvault '%s'.",
47
+ secret_name,
48
+ self.name,
49
+ exc_info=True,
40
50
  )
41
- traceback.print_exc()
42
51
  raise KeyvaultError # type: ignore
43
52
 
44
53
  return secret
@@ -48,24 +57,32 @@ class Keyvault:
48
57
  try:
49
58
  secret_client.set_secret(name=secret_name, value=secret_value)
50
59
  except HttpResponseError:
51
- self.logger.error("An error occurred while setting a secret in keyvault.")
52
- traceback.print_exc()
60
+ self.logger.error(
61
+ "An error occurred while setting secret '%s' in keyvault '%s'.",
62
+ secret_name,
63
+ self.name,
64
+ exc_info=True,
65
+ )
53
66
  raise KeyvaultError # type: ignore
54
67
 
55
- def get_secret_client(self):
56
- try:
57
- if self.client_id and self.client_secret and self.tenant_id:
58
- credential: ClientSecretCredential = ClientSecretCredential(
59
- tenant_id=self.tenant_id,
60
- client_id=self.client_id,
61
- client_secret=self.client_secret,
68
+ def get_secret_client(self) -> SecretClient:
69
+ if self.client == None:
70
+ try:
71
+ credential: Union[ClientSecretCredential, DefaultAzureCredential]
72
+ if self.client_id and self.client_secret and self.tenant_id:
73
+ credential = ClientSecretCredential(
74
+ tenant_id=self.tenant_id,
75
+ client_id=self.client_id,
76
+ client_secret=self.client_secret,
77
+ )
78
+ else:
79
+ credential = DefaultAzureCredential()
80
+ except ClientAuthenticationError:
81
+ self.logger.error(
82
+ "Failed to authenticate to Azure while connecting to KeyVault",
83
+ exc_info=True,
62
84
  )
63
- else:
64
- credential: DefaultAzureCredential = DefaultAzureCredential()
65
- except ClientAuthenticationError:
66
- self.logger.error("Failed to authenticate to Azure.")
67
- traceback.print_exc()
68
- raise KeyvaultError
85
+ raise KeyvaultError
69
86
 
70
- secret_client = SecretClient(vault_url=self.url, credential=credential)
71
- return secret_client
87
+ self.client = SecretClient(vault_url=self.url, credential=credential)
88
+ return self.client
isar/config/log.py CHANGED
@@ -1,32 +1,61 @@
1
1
  import importlib.resources as pkg_resources
2
2
  import logging
3
3
  import logging.config
4
-
5
4
  import yaml
6
5
  from uvicorn.logging import ColourizedFormatter
7
-
6
+ from opencensus.ext.azure.log_exporter import AzureLogHandler
7
+ from isar.config.configuration_error import ConfigurationError
8
+ from isar.config.keyvault.keyvault_error import KeyvaultError
9
+ from isar.config.keyvault.keyvault_service import Keyvault
8
10
  from isar.config.settings import settings
9
11
 
10
12
 
11
- def setup_logger():
13
+ def setup_loggers(keyvault: Keyvault) -> None:
12
14
  log_levels: dict = settings.LOG_LEVELS
13
15
  with pkg_resources.path("isar.config", "logging.conf") as path:
14
16
  log_config = yaml.safe_load(open(path))
15
17
 
16
- log_handler = logging.StreamHandler()
18
+ logging.config.dictConfig(log_config)
19
+
20
+ handlers = []
21
+ if settings.LOG_HANDLER_LOCAL_ENABLED:
22
+ handlers.append(configure_console_handler(log_config=log_config))
23
+ if settings.LOG_HANDLER_APPLICATION_INSIGHTS_ENABLED:
24
+ handlers.append(
25
+ configure_azure_handler(log_config=log_config, keyvault=keyvault)
26
+ )
27
+
28
+ for log_handler in handlers:
29
+ for loggers in log_config["loggers"].keys():
30
+ logging.getLogger(loggers).addHandler(log_handler)
31
+ logging.getLogger(loggers).setLevel(log_levels[loggers])
32
+ logging.getLogger().addHandler(log_handler)
33
+
17
34
 
18
- log_handler.setLevel(log_config["root"]["level"])
19
- log_handler.setFormatter(
35
+ def configure_console_handler(log_config: dict) -> logging.Handler:
36
+ handler = logging.StreamHandler()
37
+ handler.setLevel(log_config["root"]["level"])
38
+ handler.setFormatter(
20
39
  ColourizedFormatter(
21
40
  log_config["formatters"]["colourized"]["format"],
22
41
  style="{",
23
42
  use_colors=True,
24
43
  )
25
44
  )
26
-
27
- logging.config.dictConfig(log_config)
28
-
29
- for loggers in log_config["loggers"].keys():
30
- logging.getLogger(loggers).addHandler(log_handler)
31
- logging.getLogger(loggers).setLevel(log_levels[loggers])
32
- logging.getLogger().addHandler(log_handler)
45
+ return handler
46
+
47
+
48
+ def configure_azure_handler(log_config: dict, keyvault: Keyvault) -> logging.Handler:
49
+ connection_string: str
50
+ try:
51
+ connection_string = keyvault.get_secret(
52
+ "application-insights-connection-string"
53
+ ).value
54
+ except KeyvaultError:
55
+ message: str = f"CRITICAL ERROR: Missing connection string for Application Insights in key vault '{keyvault.name}'."
56
+ print(f"\n{message} \n")
57
+ raise ConfigurationError(message)
58
+
59
+ handler = AzureLogHandler(connection_string=connection_string)
60
+ handler.setLevel(log_config["root"]["level"])
61
+ return handler
File without changes
@@ -0,0 +1,98 @@
1
+ {
2
+ "mission_definition": {
3
+ "tasks": [
4
+ {
5
+ "pose": {
6
+ "position": {
7
+ "x": 0,
8
+ "y": 0,
9
+ "z": 0,
10
+ "frame_name": "robot"
11
+ },
12
+ "orientation": {
13
+ "x": 0,
14
+ "y": 0,
15
+ "z": 0,
16
+ "w": 1,
17
+ "frame_name": "robot"
18
+ },
19
+ "frame_name": "robot"
20
+ },
21
+ "tag": "1-A",
22
+ "inspections": [
23
+ {
24
+ "type": "Image",
25
+ "inspection_target": {
26
+ "x": 0,
27
+ "y": 0,
28
+ "z": 0,
29
+ "frame_name": "robot"
30
+ },
31
+ "analysis_types": [
32
+ "CarSeal",
33
+ "Rust"
34
+ ],
35
+ "metadata": {
36
+ "zoom": "2x"
37
+ }
38
+ },
39
+ {
40
+ "type": "ThermalVideo",
41
+ "inspection_target": {
42
+ "x": 0,
43
+ "y": 0,
44
+ "z": 0,
45
+ "frame_name": "robot"
46
+ },
47
+ "analysis_types": [
48
+ "GasDetection"
49
+ ],
50
+ "duration": 10
51
+ }
52
+ ]
53
+ },
54
+ {
55
+ "pose": {
56
+ "position": {
57
+ "x": 1,
58
+ "y": 1,
59
+ "z": 1,
60
+ "frame_name": "robot"
61
+ },
62
+ "orientation": {
63
+ "x": 0,
64
+ "y": 0,
65
+ "z": 0,
66
+ "w": 1,
67
+ "frame_name": "robot"
68
+ },
69
+ "frame_name": "robot"
70
+ },
71
+ "inspections": [
72
+ {
73
+ "type": "ThermalImage",
74
+ "inspection_target": {
75
+ "x": 0,
76
+ "y": 0,
77
+ "z": 0,
78
+ "frame_name": "robot"
79
+ },
80
+ "analysis_types": [
81
+ "ColdSpot",
82
+ "HotSpot"
83
+ ]
84
+ },
85
+ {
86
+ "type": "Video",
87
+ "inspection_target": {
88
+ "x": 0,
89
+ "y": 0,
90
+ "z": 0,
91
+ "frame_name": "robot"
92
+ }
93
+ }
94
+ ]
95
+ }
96
+ ]
97
+ }
98
+ }
@@ -0,0 +1,136 @@
1
+ {
2
+ "mission_definition": {
3
+ "tasks": [
4
+ {
5
+ "pose": {
6
+ "position": {
7
+ "x": -3.6,
8
+ "y": 4,
9
+ "z": 0,
10
+ "frame": "asset"
11
+ },
12
+ "orientation": {
13
+ "x": 0,
14
+ "y": 0,
15
+ "z": -0.7286672256879113,
16
+ "w": -0.6848660759820616,
17
+ "frame": "asset"
18
+ },
19
+ "frame_name": "asset"
20
+ },
21
+ "tag": "1-A",
22
+ "inspections": [
23
+ {
24
+ "type": "Image",
25
+ "inspection_target": {
26
+ "x": -4.7,
27
+ "y": 4.9,
28
+ "z": 0,
29
+ "frame": "robot"
30
+ },
31
+ "analysis_types": [
32
+ "CarSeal",
33
+ "Rust"
34
+ ],
35
+ "metadata": {
36
+ "zoom": "2x"
37
+ }
38
+ },
39
+ {
40
+ "type": "ThermalImage",
41
+ "inspection_target": {
42
+ "x": -4.7,
43
+ "y": 4.9,
44
+ "z": 0,
45
+ "frame": "robot"
46
+ },
47
+ "analysis_types": [
48
+ "GasDetection"
49
+ ]
50
+ }
51
+ ]
52
+ },
53
+ {
54
+ "pose": {
55
+ "position": {
56
+ "x": 4.7,
57
+ "y": 3,
58
+ "z": 0,
59
+ "frame": "asset"
60
+ },
61
+ "orientation": {
62
+ "x": 0,
63
+ "y": 0,
64
+ "z": 0.5769585,
65
+ "w": 0.8167734,
66
+ "frame": "asset"
67
+ },
68
+ "frame_name": "asset"
69
+ },
70
+ "tag": "2-B",
71
+ "inspections": [
72
+ {
73
+ "type": "Image",
74
+ "inspection_target": {
75
+ "x": 5.6,
76
+ "y": 5.2,
77
+ "z": 0,
78
+ "frame": "robot"
79
+ },
80
+ "analysis_types": [
81
+ "ColdSpot",
82
+ "HotSpot"
83
+ ]
84
+ }
85
+ ]
86
+ },
87
+ {
88
+ "pose": {
89
+ "position": {
90
+ "x": 0.95,
91
+ "y": 2.6,
92
+ "z": 0,
93
+ "frame": "asset"
94
+ },
95
+ "orientation": {
96
+ "x": 0,
97
+ "y": 0,
98
+ "z": -0.6992469,
99
+ "w": 0.7148802,
100
+ "frame": "asset"
101
+ },
102
+ "frame_name": "asset"
103
+ },
104
+ "tag": "3-C",
105
+ "inspections": [
106
+ {
107
+ "type": "Image",
108
+ "inspection_target": {
109
+ "x": 5.6,
110
+ "y": 5.2,
111
+ "z": 0,
112
+ "frame": "robot"
113
+ },
114
+ "analysis_types": [
115
+ "ColdSpot",
116
+ "HotSpot"
117
+ ]
118
+ },
119
+ {
120
+ "type": "ThermalImage",
121
+ "inspection_target": {
122
+ "x": 1.9,
123
+ "y": 1.9,
124
+ "z": 0,
125
+ "frame": "robot"
126
+ },
127
+ "analysis_types": [
128
+ "ColdSpot",
129
+ "HotSpot"
130
+ ]
131
+ }
132
+ ]
133
+ }
134
+ ]
135
+ }
136
+ }