greenstream-config 3.31.0__tar.gz → 4.0.1__tar.gz
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.
- {greenstream_config-3.31.0 → greenstream_config-4.0.1}/PKG-INFO +1 -2
- greenstream_config-4.0.1/greenstream_config/__init__.py +31 -0
- greenstream_config-4.0.1/greenstream_config/namespace_helpers.py +36 -0
- greenstream_config-4.0.1/greenstream_config/test/test_namespace_helpers.py +46 -0
- greenstream_config-4.0.1/greenstream_config/test/test_types.py +179 -0
- greenstream_config-4.0.1/greenstream_config/types.py +137 -0
- {greenstream_config-3.31.0 → greenstream_config-4.0.1}/greenstream_config.egg-info/PKG-INFO +1 -2
- {greenstream_config-3.31.0 → greenstream_config-4.0.1}/greenstream_config.egg-info/SOURCES.txt +2 -2
- {greenstream_config-3.31.0 → greenstream_config-4.0.1}/greenstream_config.egg-info/requires.txt +0 -1
- {greenstream_config-3.31.0 → greenstream_config-4.0.1}/setup.cfg +1 -2
- greenstream_config-3.31.0/greenstream_config/__init__.py +0 -33
- greenstream_config-3.31.0/greenstream_config/namespace_helpers.py +0 -54
- greenstream_config-3.31.0/greenstream_config/test/test_namespace_helpers.py +0 -73
- greenstream_config-3.31.0/greenstream_config/types.py +0 -67
- greenstream_config-3.31.0/greenstream_config/urdf.py +0 -197
- {greenstream_config-3.31.0 → greenstream_config-4.0.1}/README.md +0 -0
- {greenstream_config-3.31.0 → greenstream_config-4.0.1}/greenstream_config.egg-info/dependency_links.txt +0 -0
- {greenstream_config-3.31.0 → greenstream_config-4.0.1}/greenstream_config.egg-info/top_level.txt +0 -0
- {greenstream_config-3.31.0 → greenstream_config-4.0.1}/greenstream_config.egg-info/zip-safe +0 -0
- {greenstream_config-3.31.0 → greenstream_config-4.0.1}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: greenstream_config
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.1
|
|
4
4
|
Summary: A library for reading / writing Greenstream config files
|
|
5
5
|
Home-page: https://github.com/Greenroom-Robotics/greenstream
|
|
6
6
|
Author: Greenroom Robotics
|
|
@@ -14,7 +14,6 @@ Classifier: Intended Audience :: Developers
|
|
|
14
14
|
Classifier: Programming Language :: Python
|
|
15
15
|
Classifier: Topic :: Software Development :: Build Tools
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
|
-
Requires-Dist: gr-urchin
|
|
18
17
|
Requires-Dist: setuptools
|
|
19
18
|
|
|
20
19
|
# Greenstream Config
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from greenstream_config.namespace_helpers import (
|
|
2
|
+
get_camera_namespace,
|
|
3
|
+
get_camera_topic_base,
|
|
4
|
+
get_sensor_frame_compressed_topic,
|
|
5
|
+
get_sensor_frame_id,
|
|
6
|
+
get_sensor_frame_topic,
|
|
7
|
+
get_sensor_node_name,
|
|
8
|
+
get_sensor_topic,
|
|
9
|
+
)
|
|
10
|
+
from greenstream_config.types import (
|
|
11
|
+
Camera,
|
|
12
|
+
Cameras,
|
|
13
|
+
CameraSensor,
|
|
14
|
+
CameraSensorType,
|
|
15
|
+
GreenstreamConfig,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"GreenstreamConfig",
|
|
20
|
+
"Camera",
|
|
21
|
+
"Cameras",
|
|
22
|
+
"CameraSensor",
|
|
23
|
+
"CameraSensorType",
|
|
24
|
+
"get_camera_namespace",
|
|
25
|
+
"get_camera_topic_base",
|
|
26
|
+
"get_sensor_frame_compressed_topic",
|
|
27
|
+
"get_sensor_frame_id",
|
|
28
|
+
"get_sensor_frame_topic",
|
|
29
|
+
"get_sensor_node_name",
|
|
30
|
+
"get_sensor_topic",
|
|
31
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
def get_sensor_topic(namespace_vessel: str, sensor_name: str) -> str:
|
|
2
|
+
"""Generate sensor camera topic path for a sensor."""
|
|
3
|
+
return f"/{namespace_vessel}/sensors/cameras/{sensor_name}"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_sensor_frame_topic(namespace_vessel: str, sensor_name: str) -> str:
|
|
7
|
+
"""Generate frame topic path for camera images."""
|
|
8
|
+
if namespace_vessel == "":
|
|
9
|
+
return f"perception/frames/{sensor_name}"
|
|
10
|
+
else:
|
|
11
|
+
return f"/{namespace_vessel}/perception/frames/{sensor_name}"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_sensor_frame_compressed_topic(namespace_vessel: str, sensor_name: str) -> str:
|
|
15
|
+
"""Generate compressed frame topic path."""
|
|
16
|
+
return f"{get_sensor_frame_topic(namespace_vessel, sensor_name)}/compressed"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_sensor_frame_id(namespace_vessel: str, sensor_name: str) -> str:
|
|
20
|
+
"""Generate optical frame ID for a camera sensor."""
|
|
21
|
+
return f"{namespace_vessel}_{sensor_name}_optical_frame"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_sensor_node_name(node_type: str, sensor_name: str) -> str:
|
|
25
|
+
"""Generate ROS node name for a sensor-specific node."""
|
|
26
|
+
return f"{node_type}_{sensor_name}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_camera_namespace(namespace_full: str, camera_name: str) -> str:
|
|
30
|
+
"""Generate ROS namespace for PTZ driver."""
|
|
31
|
+
return f"{namespace_full}/cameras/{camera_name}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_camera_topic_base(namespace_vessel: str, camera_name: str) -> str:
|
|
35
|
+
"""Generate base topic path for PTZ control."""
|
|
36
|
+
return f"/{namespace_vessel}/sensors/cameras/{camera_name}"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from greenstream_config.namespace_helpers import (
|
|
2
|
+
get_camera_namespace,
|
|
3
|
+
get_camera_topic_base,
|
|
4
|
+
get_sensor_frame_compressed_topic,
|
|
5
|
+
get_sensor_frame_id,
|
|
6
|
+
get_sensor_frame_topic,
|
|
7
|
+
get_sensor_node_name,
|
|
8
|
+
get_sensor_topic,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_get_sensor_topic():
|
|
13
|
+
assert get_sensor_topic("vessel_1", "bow_color") == "/vessel_1/sensors/cameras/bow_color"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_get_sensor_frame_id():
|
|
17
|
+
assert get_sensor_frame_id("vessel_1", "bow_color") == "vessel_1_bow_color_optical_frame"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_get_sensor_node_name():
|
|
21
|
+
assert get_sensor_node_name("node", "bow_color") == "node_bow_color"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_get_sensor_frame_topic():
|
|
25
|
+
assert (
|
|
26
|
+
get_sensor_frame_topic("vessel_1", "bow_color") == "/vessel_1/perception/frames/bow_color"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_get_sensor_frame_topic_empty_namespace():
|
|
31
|
+
assert get_sensor_frame_topic("", "bow_color") == "perception/frames/bow_color"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_get_sensor_frame_compressed_topic():
|
|
35
|
+
assert (
|
|
36
|
+
get_sensor_frame_compressed_topic("vessel_1", "bow_color")
|
|
37
|
+
== "/vessel_1/perception/frames/bow_color/compressed"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_get_camera_namespace():
|
|
42
|
+
assert get_camera_namespace("namespace", "bow") == "namespace/cameras/bow"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_get_camera_topic_base():
|
|
46
|
+
assert get_camera_topic_base("vessel_1", "bow") == "/vessel_1/sensors/cameras/bow"
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from greenstream_config.types import Camera, CameraSensor, CameraSensorType
|
|
3
|
+
from pydantic import ValidationError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestCameraSensorValidation:
|
|
7
|
+
"""Test ROS-friendly name validation for CameraSensor."""
|
|
8
|
+
|
|
9
|
+
def test_valid_lowercase_name(self):
|
|
10
|
+
"""Valid: lowercase name."""
|
|
11
|
+
sensor = CameraSensor(name="bow_color", type=CameraSensorType.COLOR)
|
|
12
|
+
assert sensor.name == "bow_color"
|
|
13
|
+
|
|
14
|
+
def test_valid_name_with_underscores(self):
|
|
15
|
+
"""Valid: lowercase with underscores."""
|
|
16
|
+
sensor = CameraSensor(name="stern_ir_sensor", type=CameraSensorType.IR)
|
|
17
|
+
assert sensor.name == "stern_ir_sensor"
|
|
18
|
+
|
|
19
|
+
def test_valid_name_with_numbers(self):
|
|
20
|
+
"""Valid: lowercase with numbers (not at start)."""
|
|
21
|
+
sensor = CameraSensor(name="camera_1_color", type=CameraSensorType.COLOR)
|
|
22
|
+
assert sensor.name == "camera_1_color"
|
|
23
|
+
|
|
24
|
+
def test_valid_name_starting_with_underscore(self):
|
|
25
|
+
"""Valid: name starting with underscore."""
|
|
26
|
+
sensor = CameraSensor(name="_private_sensor", type=CameraSensorType.COLOR)
|
|
27
|
+
assert sensor.name == "_private_sensor"
|
|
28
|
+
|
|
29
|
+
def test_invalid_uppercase_name(self):
|
|
30
|
+
"""Invalid: uppercase letters."""
|
|
31
|
+
with pytest.raises(ValidationError, match="not ROS-friendly"):
|
|
32
|
+
CameraSensor(name="BowColor", type=CameraSensorType.COLOR)
|
|
33
|
+
|
|
34
|
+
def test_invalid_camel_case_name(self):
|
|
35
|
+
"""Invalid: camelCase name."""
|
|
36
|
+
with pytest.raises(ValidationError, match="not ROS-friendly"):
|
|
37
|
+
CameraSensor(name="bowColor", type=CameraSensorType.COLOR)
|
|
38
|
+
|
|
39
|
+
def test_invalid_name_with_hyphen(self):
|
|
40
|
+
"""Invalid: name with hyphens."""
|
|
41
|
+
with pytest.raises(ValidationError, match="not ROS-friendly"):
|
|
42
|
+
CameraSensor(name="bow-color", type=CameraSensorType.COLOR)
|
|
43
|
+
|
|
44
|
+
def test_invalid_name_with_space(self):
|
|
45
|
+
"""Invalid: name with spaces."""
|
|
46
|
+
with pytest.raises(ValidationError, match="not ROS-friendly"):
|
|
47
|
+
CameraSensor(name="bow color", type=CameraSensorType.COLOR)
|
|
48
|
+
|
|
49
|
+
def test_invalid_name_starting_with_number(self):
|
|
50
|
+
"""Invalid: name starting with number."""
|
|
51
|
+
with pytest.raises(ValidationError, match="not ROS-friendly"):
|
|
52
|
+
CameraSensor(name="1_bow_color", type=CameraSensorType.COLOR)
|
|
53
|
+
|
|
54
|
+
def test_invalid_name_with_special_chars(self):
|
|
55
|
+
"""Invalid: name with special characters."""
|
|
56
|
+
with pytest.raises(ValidationError, match="not ROS-friendly"):
|
|
57
|
+
CameraSensor(name="bow@color", type=CameraSensorType.COLOR)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TestCameraValidation:
|
|
61
|
+
"""Test ROS-friendly name validation for Camera."""
|
|
62
|
+
|
|
63
|
+
def test_valid_lowercase_name(self):
|
|
64
|
+
"""Valid: lowercase name."""
|
|
65
|
+
camera = Camera(
|
|
66
|
+
name="bow",
|
|
67
|
+
with_ptz=False,
|
|
68
|
+
sensors=[CameraSensor(name="bow_color", type=CameraSensorType.COLOR)],
|
|
69
|
+
)
|
|
70
|
+
assert camera.name == "bow"
|
|
71
|
+
|
|
72
|
+
def test_valid_name_with_underscores(self):
|
|
73
|
+
"""Valid: lowercase with underscores."""
|
|
74
|
+
camera = Camera(
|
|
75
|
+
name="port_camera",
|
|
76
|
+
with_ptz=False,
|
|
77
|
+
sensors=[CameraSensor(name="port_camera_color", type=CameraSensorType.COLOR)],
|
|
78
|
+
)
|
|
79
|
+
assert camera.name == "port_camera"
|
|
80
|
+
|
|
81
|
+
def test_valid_name_with_numbers(self):
|
|
82
|
+
"""Valid: lowercase with numbers (not at start)."""
|
|
83
|
+
camera = Camera(
|
|
84
|
+
name="camera_1",
|
|
85
|
+
with_ptz=False,
|
|
86
|
+
sensors=[CameraSensor(name="camera_1_color", type=CameraSensorType.COLOR)],
|
|
87
|
+
)
|
|
88
|
+
assert camera.name == "camera_1"
|
|
89
|
+
|
|
90
|
+
def test_valid_name_starting_with_underscore(self):
|
|
91
|
+
"""Valid: name starting with underscore."""
|
|
92
|
+
camera = Camera(
|
|
93
|
+
name="_test_camera",
|
|
94
|
+
with_ptz=False,
|
|
95
|
+
sensors=[CameraSensor(name="_test_camera_color", type=CameraSensorType.COLOR)],
|
|
96
|
+
)
|
|
97
|
+
assert camera.name == "_test_camera"
|
|
98
|
+
|
|
99
|
+
def test_invalid_uppercase_name(self):
|
|
100
|
+
"""Invalid: uppercase letters."""
|
|
101
|
+
with pytest.raises(ValidationError, match="not ROS-friendly"):
|
|
102
|
+
Camera(
|
|
103
|
+
name="BowCamera",
|
|
104
|
+
with_ptz=False,
|
|
105
|
+
sensors=[CameraSensor(name="bow_color", type=CameraSensorType.COLOR)],
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def test_invalid_camel_case_name(self):
|
|
109
|
+
"""Invalid: camelCase name."""
|
|
110
|
+
with pytest.raises(ValidationError, match="not ROS-friendly"):
|
|
111
|
+
Camera(
|
|
112
|
+
name="bowCamera",
|
|
113
|
+
with_ptz=False,
|
|
114
|
+
sensors=[CameraSensor(name="bow_color", type=CameraSensorType.COLOR)],
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def test_invalid_name_with_hyphen(self):
|
|
118
|
+
"""Invalid: name with hyphens."""
|
|
119
|
+
with pytest.raises(ValidationError, match="not ROS-friendly"):
|
|
120
|
+
Camera(
|
|
121
|
+
name="bow-camera",
|
|
122
|
+
with_ptz=False,
|
|
123
|
+
sensors=[CameraSensor(name="bow_color", type=CameraSensorType.COLOR)],
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def test_invalid_name_with_space(self):
|
|
127
|
+
"""Invalid: name with spaces."""
|
|
128
|
+
with pytest.raises(ValidationError, match="not ROS-friendly"):
|
|
129
|
+
Camera(
|
|
130
|
+
name="bow camera",
|
|
131
|
+
with_ptz=False,
|
|
132
|
+
sensors=[CameraSensor(name="bow_color", type=CameraSensorType.COLOR)],
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def test_invalid_name_starting_with_number(self):
|
|
136
|
+
"""Invalid: name starting with number."""
|
|
137
|
+
with pytest.raises(ValidationError, match="not ROS-friendly"):
|
|
138
|
+
Camera(
|
|
139
|
+
name="1_bow",
|
|
140
|
+
with_ptz=False,
|
|
141
|
+
sensors=[CameraSensor(name="bow_color", type=CameraSensorType.COLOR)],
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def test_invalid_name_with_special_chars(self):
|
|
145
|
+
"""Invalid: name with special characters."""
|
|
146
|
+
with pytest.raises(ValidationError, match="not ROS-friendly"):
|
|
147
|
+
Camera(
|
|
148
|
+
name="bow@camera",
|
|
149
|
+
with_ptz=False,
|
|
150
|
+
sensors=[CameraSensor(name="bow_color", type=CameraSensorType.COLOR)],
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def test_default_sensor_creation(self):
|
|
154
|
+
"""Test that a camera without sensors gets a default sensor named {camera_name}_color."""
|
|
155
|
+
camera = Camera(name="bow", with_ptz=False)
|
|
156
|
+
assert len(camera.sensors) == 1
|
|
157
|
+
assert camera.sensors[0].name == "bow_color"
|
|
158
|
+
assert camera.sensors[0].type == CameraSensorType.COLOR
|
|
159
|
+
|
|
160
|
+
def test_default_sensor_with_ptz(self):
|
|
161
|
+
"""Test that a PTZ camera without sensors gets a default sensor named {camera_name}_color."""
|
|
162
|
+
camera = Camera(name="stern", with_ptz=True)
|
|
163
|
+
assert len(camera.sensors) == 1
|
|
164
|
+
assert camera.sensors[0].name == "stern_color"
|
|
165
|
+
assert camera.sensors[0].type == CameraSensorType.COLOR
|
|
166
|
+
|
|
167
|
+
def test_explicit_sensors_preserved(self):
|
|
168
|
+
"""Test that explicitly provided sensors are preserved and default is not created."""
|
|
169
|
+
camera = Camera(
|
|
170
|
+
name="bow",
|
|
171
|
+
with_ptz=False,
|
|
172
|
+
sensors=[
|
|
173
|
+
CameraSensor(name="bow_color", type=CameraSensorType.COLOR),
|
|
174
|
+
CameraSensor(name="bow_ir", type=CameraSensorType.IR),
|
|
175
|
+
],
|
|
176
|
+
)
|
|
177
|
+
assert len(camera.sensors) == 2
|
|
178
|
+
assert camera.sensors[0].name == "bow_color"
|
|
179
|
+
assert camera.sensors[1].name == "bow_ir"
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field, RootModel, field_validator, model_validator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CameraSensorType(str, Enum):
|
|
9
|
+
"""Supported camera sensor types for different sensor modalities."""
|
|
10
|
+
|
|
11
|
+
COLOR = "color"
|
|
12
|
+
IR = "ir"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CameraSensor(BaseModel):
|
|
16
|
+
"""Individual camera sensor within a physical camera housing.
|
|
17
|
+
|
|
18
|
+
Represents a logical camera stream (e.g., color, thermal) that shares
|
|
19
|
+
the same physical mounting point and PTZ control with other sensors.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
name: str = Field(
|
|
23
|
+
description="Sensor identifier used in ROS topics and frame IDs. This should be prefixed with the camera housing name"
|
|
24
|
+
)
|
|
25
|
+
type: CameraSensorType = Field(
|
|
26
|
+
default=CameraSensorType.COLOR, description="Type of camera sensor/modality"
|
|
27
|
+
)
|
|
28
|
+
with_camera_info: bool = Field(
|
|
29
|
+
default=True, description="Whether to launch camera_info publisher for this sensor"
|
|
30
|
+
)
|
|
31
|
+
with_pipeline: bool = Field(default=True, description="Whether to launch the pipeline")
|
|
32
|
+
with_image_compressor: bool = Field(
|
|
33
|
+
default=True, description="Whether to launch the image compressor"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
@field_validator("name")
|
|
37
|
+
@classmethod
|
|
38
|
+
def validate_ros_friendly_name(cls, v: str) -> str:
|
|
39
|
+
"""Ensure name follows ROS naming convention: lowercase with underscores, not starting with numbers."""
|
|
40
|
+
if not re.match(r"^[a-z_][a-z0-9_]*$", v):
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f"Name '{v}' is not ROS-friendly. Names must be lowercase with underscores, "
|
|
43
|
+
"not starting with numbers (e.g., 'bow_color', 'stern_ir')"
|
|
44
|
+
)
|
|
45
|
+
return v
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Camera(BaseModel):
|
|
49
|
+
"""Physical camera housing/mount that can host multiple camera sensors.
|
|
50
|
+
|
|
51
|
+
Represents the physical hardware unit including PTZ mechanisms.
|
|
52
|
+
Multiple sensors (color, thermal, etc.) can share the same PTZ control.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
name: str = Field(
|
|
56
|
+
description="Physical camera housing identifier (e.g., 'bow', 'stern', 'port')"
|
|
57
|
+
)
|
|
58
|
+
with_ptz: bool = Field(
|
|
59
|
+
default=False,
|
|
60
|
+
description="Whether we should launch a PTZ driver",
|
|
61
|
+
)
|
|
62
|
+
sensors: List[CameraSensor] = Field(
|
|
63
|
+
default_factory=list, description="List of camera sensors mounted in this housing"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
@field_validator("name")
|
|
67
|
+
@classmethod
|
|
68
|
+
def validate_ros_friendly_name(cls, v: str) -> str:
|
|
69
|
+
"""Ensure name follows ROS naming convention: lowercase with underscores, not starting with numbers."""
|
|
70
|
+
if not re.match(r"^[a-z_][a-z0-9_]*$", v):
|
|
71
|
+
raise ValueError(
|
|
72
|
+
f"Name '{v}' is not ROS-friendly. Names must be lowercase with underscores, "
|
|
73
|
+
"not starting with numbers (e.g., 'bow', 'stern', 'port_camera')"
|
|
74
|
+
)
|
|
75
|
+
return v
|
|
76
|
+
|
|
77
|
+
@model_validator(mode="after")
|
|
78
|
+
def validate_camera_sensors(self):
|
|
79
|
+
"""Ensure camera has valid sensor configuration.
|
|
80
|
+
|
|
81
|
+
If no sensors are specified, creates a default COLOR sensor named {camera_name}_{type}.
|
|
82
|
+
"""
|
|
83
|
+
if not self.sensors:
|
|
84
|
+
# Create default sensor with camera's name and type suffix
|
|
85
|
+
sensor_type = CameraSensorType.COLOR
|
|
86
|
+
self.sensors = [
|
|
87
|
+
CameraSensor(name=f"{self.name}_{sensor_type.value}", type=sensor_type)
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
return self
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Cameras(RootModel):
|
|
94
|
+
root: list[Camera] = Field(default_factory=list)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class GreenstreamConfig(BaseModel):
|
|
98
|
+
"""Complete configuration for the Greenstream video streaming system.
|
|
99
|
+
|
|
100
|
+
Defines the camera housings and their sensors, along with system-wide
|
|
101
|
+
settings for WebRTC streaming and ROS integration.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
cameras: List[Camera] = Field(
|
|
105
|
+
description="List of physical camera housings and their sensors to deploy"
|
|
106
|
+
)
|
|
107
|
+
signalling_server_port: int = Field(
|
|
108
|
+
default=8443, description="Port for the WebRTC signalling server"
|
|
109
|
+
)
|
|
110
|
+
namespace_vessel: str = Field(
|
|
111
|
+
default="vessel_1", description="Vessel identifier for multi-vessel deployments"
|
|
112
|
+
)
|
|
113
|
+
namespace_application: str = Field(default="greenstream", description="Application namespace")
|
|
114
|
+
ui_port: int = Field(default=8000, description="Port for the web UI server")
|
|
115
|
+
debug: bool = Field(default=False, description="Enable debug logging and tracing")
|
|
116
|
+
diagnostics_topic: str = Field(default="diagnostics", description="ROS diagnostics topic name")
|
|
117
|
+
cert_path: Optional[str] = Field(None, description="SSL certificate path for HTTPS signalling")
|
|
118
|
+
cert_password: Optional[str] = Field(None, description="SSL certificate password")
|
|
119
|
+
|
|
120
|
+
@model_validator(mode="after")
|
|
121
|
+
def validate_global_configuration(self):
|
|
122
|
+
"""Ensure system-wide configuration is valid."""
|
|
123
|
+
|
|
124
|
+
# Check for duplicate camera names
|
|
125
|
+
camera_names = [camera.name for camera in self.cameras]
|
|
126
|
+
if len(set(camera_names)) != len(camera_names):
|
|
127
|
+
raise ValueError("Duplicate camera names found")
|
|
128
|
+
|
|
129
|
+
# Check for duplicate sensor names across all cameras
|
|
130
|
+
all_sensor_names = []
|
|
131
|
+
for camera in self.cameras:
|
|
132
|
+
for sensor in camera.sensors:
|
|
133
|
+
all_sensor_names.append(sensor.name)
|
|
134
|
+
if len(set(all_sensor_names)) != len(all_sensor_names):
|
|
135
|
+
raise ValueError("Duplicate sensor names found across cameras")
|
|
136
|
+
|
|
137
|
+
return self
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: greenstream_config
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.1
|
|
4
4
|
Summary: A library for reading / writing Greenstream config files
|
|
5
5
|
Home-page: https://github.com/Greenroom-Robotics/greenstream
|
|
6
6
|
Author: Greenroom Robotics
|
|
@@ -14,7 +14,6 @@ Classifier: Intended Audience :: Developers
|
|
|
14
14
|
Classifier: Programming Language :: Python
|
|
15
15
|
Classifier: Topic :: Software Development :: Build Tools
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
|
-
Requires-Dist: gr-urchin
|
|
18
17
|
Requires-Dist: setuptools
|
|
19
18
|
|
|
20
19
|
# Greenstream Config
|
{greenstream_config-3.31.0 → greenstream_config-4.0.1}/greenstream_config.egg-info/SOURCES.txt
RENAMED
|
@@ -4,11 +4,11 @@ setup.py
|
|
|
4
4
|
greenstream_config/__init__.py
|
|
5
5
|
greenstream_config/namespace_helpers.py
|
|
6
6
|
greenstream_config/types.py
|
|
7
|
-
greenstream_config/urdf.py
|
|
8
7
|
greenstream_config.egg-info/PKG-INFO
|
|
9
8
|
greenstream_config.egg-info/SOURCES.txt
|
|
10
9
|
greenstream_config.egg-info/dependency_links.txt
|
|
11
10
|
greenstream_config.egg-info/requires.txt
|
|
12
11
|
greenstream_config.egg-info/top_level.txt
|
|
13
12
|
greenstream_config.egg-info/zip-safe
|
|
14
|
-
greenstream_config/test/test_namespace_helpers.py
|
|
13
|
+
greenstream_config/test/test_namespace_helpers.py
|
|
14
|
+
greenstream_config/test/test_types.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[metadata]
|
|
2
2
|
name = greenstream_config
|
|
3
|
-
version =
|
|
3
|
+
version = 4.0.1
|
|
4
4
|
url = https://github.com/Greenroom-Robotics/greenstream
|
|
5
5
|
author = Greenroom Robotics
|
|
6
6
|
author_email = team@greenroomrobotics.com
|
|
@@ -21,7 +21,6 @@ long_description_content_type = text/markdown
|
|
|
21
21
|
[options]
|
|
22
22
|
packages = find:
|
|
23
23
|
install_requires =
|
|
24
|
-
gr-urchin
|
|
25
24
|
setuptools
|
|
26
25
|
zip_safe = true
|
|
27
26
|
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
from greenstream_config.namespace_helpers import (
|
|
2
|
-
camera_frame_topic,
|
|
3
|
-
camera_frame_topic_from_camera,
|
|
4
|
-
camera_namespace,
|
|
5
|
-
camera_namespace_from_camera,
|
|
6
|
-
camera_node_name,
|
|
7
|
-
camera_node_name_from_camera,
|
|
8
|
-
camera_topic,
|
|
9
|
-
camera_topic_from_camera,
|
|
10
|
-
frame_id,
|
|
11
|
-
frame_id_from_camera,
|
|
12
|
-
)
|
|
13
|
-
from greenstream_config.types import Camera, GreenstreamConfig, Offsets, PTZOffsets
|
|
14
|
-
from greenstream_config.urdf import get_camera_urdf, get_cameras_urdf
|
|
15
|
-
|
|
16
|
-
__all__ = [
|
|
17
|
-
"GreenstreamConfig",
|
|
18
|
-
"Camera",
|
|
19
|
-
"Offsets",
|
|
20
|
-
"PTZOffsets",
|
|
21
|
-
"get_camera_urdf",
|
|
22
|
-
"get_cameras_urdf",
|
|
23
|
-
"camera_topic",
|
|
24
|
-
"frame_id",
|
|
25
|
-
"camera_namespace",
|
|
26
|
-
"camera_node_name",
|
|
27
|
-
"camera_topic_from_camera",
|
|
28
|
-
"frame_id_from_camera",
|
|
29
|
-
"camera_namespace_from_camera",
|
|
30
|
-
"camera_node_name_from_camera",
|
|
31
|
-
"camera_frame_topic",
|
|
32
|
-
"camera_frame_topic_from_camera",
|
|
33
|
-
]
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
from pathlib import PosixPath as Path
|
|
2
|
-
|
|
3
|
-
from greenstream_config.types import Camera
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def camera_topic(namespace_vessel: str, camera_name: str, camera_type: str) -> str:
|
|
7
|
-
return str(
|
|
8
|
-
Path("/") / namespace_vessel / "sensors" / "cameras" / f"{camera_name}_{camera_type}"
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def camera_topic_from_camera(namespace_vessel: str, camera: Camera) -> str:
|
|
13
|
-
return camera_topic(namespace_vessel, camera.name, camera.type)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def frame_id(namespace_vessel: str, camera_name: str, camera_type: str) -> str:
|
|
17
|
-
return f"{namespace_vessel}_{camera_name}_{camera_type}_optical_frame"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def frame_id_from_camera(namespace_vessel: str, camera: Camera) -> str:
|
|
21
|
-
return frame_id(namespace_vessel, camera.name, camera.type)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def camera_namespace(namespace_full: str, camera_name: str) -> str:
|
|
25
|
-
return str(Path(namespace_full) / "cameras" / camera_name)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def camera_namespace_from_camera(namespace_full: str, camera: Camera) -> str:
|
|
29
|
-
return camera_namespace(namespace_full, camera.name)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def camera_node_name(node_type: str, camera_name: str, camera_type: str) -> str:
|
|
33
|
-
return f"{node_type}_{camera_name}_{camera_type}"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def camera_node_name_from_camera(node_type: str, camera: Camera) -> str:
|
|
37
|
-
return camera_node_name(node_type, camera.name, camera.type)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def camera_frame_topic(namespace_vessel: str, camera_name: str, camera_type: str) -> str:
|
|
41
|
-
if namespace_vessel == "":
|
|
42
|
-
return str(Path("perception") / "frames" / f"{camera_name}_{camera_type}")
|
|
43
|
-
else:
|
|
44
|
-
return str(
|
|
45
|
-
Path("/") / namespace_vessel / "perception" / "frames" / f"{camera_name}_{camera_type}"
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def camera_frame_topic_from_camera(namespace_vessel: str, camera: Camera) -> str:
|
|
50
|
-
return camera_frame_topic(namespace_vessel, camera.name, camera.type)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def camera_frame_compressed_topic_from_camera(namespace_vessel: str, camera: Camera) -> str:
|
|
54
|
-
return str(Path(camera_frame_topic_from_camera(namespace_vessel, camera)) / "compressed")
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from greenstream_config.types import Camera
|
|
3
|
-
|
|
4
|
-
from libs.greenstream_config.greenstream_config.namespace_helpers import (
|
|
5
|
-
camera_frame_topic,
|
|
6
|
-
camera_frame_topic_from_camera,
|
|
7
|
-
camera_namespace,
|
|
8
|
-
camera_namespace_from_camera,
|
|
9
|
-
camera_node_name,
|
|
10
|
-
camera_node_name_from_camera,
|
|
11
|
-
camera_topic,
|
|
12
|
-
camera_topic_from_camera,
|
|
13
|
-
frame_id,
|
|
14
|
-
frame_id_from_camera,
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@pytest.fixture
|
|
19
|
-
def camera():
|
|
20
|
-
return Camera(name="bow", type="color", order=0)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def test_camera_topic():
|
|
24
|
-
assert camera_topic("vessel_1", "bow", "color") == "/vessel_1/sensors/cameras/bow_color"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def test_camera_topic_from_camera(camera):
|
|
28
|
-
assert camera_topic_from_camera("vessel_1", camera) == "/vessel_1/sensors/cameras/bow_color"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def test_frame_id():
|
|
32
|
-
assert frame_id("vessel_1", "bow", "color") == "vessel_1_bow_color_optical_frame"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def test_frame_id_from_camera(camera):
|
|
36
|
-
assert frame_id_from_camera("vessel_1", camera) == "vessel_1_bow_color_optical_frame"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def test_camera_namespace():
|
|
40
|
-
assert camera_namespace("namespace", "bow") == "namespace/cameras/bow"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def test_camera_namespace_from_camera(camera):
|
|
44
|
-
assert camera_namespace_from_camera("namespace", camera) == "namespace/cameras/bow"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def test_camera_node_name():
|
|
48
|
-
assert camera_node_name("node", "bow", "color") == "node_bow_color"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def test_camera_node_name_from_camera(camera):
|
|
52
|
-
assert camera_node_name_from_camera("node", camera) == "node_bow_color"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def test_camera_frame_topic():
|
|
56
|
-
assert (
|
|
57
|
-
camera_frame_topic("vessel_1", "bow", "color") == "/vessel_1/perception/frames/bow_color"
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def test_camera_frame_topic_empty_namespace():
|
|
62
|
-
assert camera_frame_topic("", "bow", "color") == "perception/frames/bow_color"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def test_camera_frame_topic_from_camera(camera):
|
|
66
|
-
assert (
|
|
67
|
-
camera_frame_topic_from_camera("vessel_1", camera)
|
|
68
|
-
== "/vessel_1/perception/frames/bow_color"
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def test_camera_frame_topic_from_camera_empty_namespace(camera):
|
|
73
|
-
assert camera_frame_topic_from_camera("", camera) == "perception/frames/bow_color"
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
from typing import List, Literal, Optional
|
|
2
|
-
|
|
3
|
-
from pydantic import BaseModel, Field, model_validator
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class Offsets(BaseModel):
|
|
7
|
-
"""Spatial offsets in the Front-Left-Up (FLU) coordinate frame."""
|
|
8
|
-
|
|
9
|
-
roll: Optional[float] = Field(None, description="Roll rotation in radians (FLU frame)")
|
|
10
|
-
pitch: Optional[float] = Field(None, description="Pitch rotation in radians (FLU frame)")
|
|
11
|
-
yaw: Optional[float] = Field(None, description="Yaw rotation in radians (FLU frame)")
|
|
12
|
-
forward: Optional[float] = Field(None, description="Forward translation in meters")
|
|
13
|
-
left: Optional[float] = Field(None, description="Left translation in meters")
|
|
14
|
-
up: Optional[float] = Field(None, description="Up translation in meters")
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class PTZOffsets(Offsets):
|
|
18
|
-
"""PTZ-specific offsets with joint type specification."""
|
|
19
|
-
|
|
20
|
-
type: Literal["pan", "tilt"] = Field(description="PTZ joint type (pan or tilt)")
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class Camera(BaseModel):
|
|
24
|
-
"""Camera configuration for video streaming and control."""
|
|
25
|
-
|
|
26
|
-
name: str = Field(
|
|
27
|
-
description="Camera identifier used in frame IDs, ROS topics, and WebRTC streams"
|
|
28
|
-
)
|
|
29
|
-
order: int = Field(description="Display order in the web UI")
|
|
30
|
-
type: str = Field(default="color", description="Camera type (e.g., color, ir, depth)")
|
|
31
|
-
publish_camera_info: bool = Field(
|
|
32
|
-
default=True, description="Whether to launch camera info publisher node"
|
|
33
|
-
)
|
|
34
|
-
ptz: bool = Field(
|
|
35
|
-
default=False, description="Whether to launch PTZ driver for pan-tilt-zoom control"
|
|
36
|
-
)
|
|
37
|
-
camera_offsets: Optional[Offsets] = Field(
|
|
38
|
-
None, description="Camera position offsets relative to base_link"
|
|
39
|
-
)
|
|
40
|
-
ptz_offsets: List[PTZOffsets] = Field(
|
|
41
|
-
default=[], description="PTZ joint offsets (required when ptz=True)"
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
@model_validator(mode="after")
|
|
45
|
-
def validate_ptz_with_offsets(self):
|
|
46
|
-
"""Ensure that ptz_offsets is set when ptz is True."""
|
|
47
|
-
if self.ptz and not self.ptz_offsets:
|
|
48
|
-
raise ValueError("ptz_offsets cannot be empty when ptz is set to True")
|
|
49
|
-
return self
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
class GreenstreamConfig(BaseModel):
|
|
53
|
-
"""Complete configuration for the Greenstream video streaming system."""
|
|
54
|
-
|
|
55
|
-
cameras: List[Camera] = Field(description="List of camera configurations to deploy")
|
|
56
|
-
signalling_server_port: int = Field(
|
|
57
|
-
default=8443, description="Port for the WebRTC signalling server"
|
|
58
|
-
)
|
|
59
|
-
namespace_vessel: str = Field(
|
|
60
|
-
default="vessel_1", description="Vessel identifier for multi-vessel deployments"
|
|
61
|
-
)
|
|
62
|
-
namespace_application: str = Field(default="greenstream", description="Application namespace")
|
|
63
|
-
ui_port: int = Field(default=8000, description="Port for the web UI server")
|
|
64
|
-
debug: bool = Field(default=False, description="Enable debug logging and tracing")
|
|
65
|
-
diagnostics_topic: str = Field(default="diagnostics", description="ROS diagnostics topic name")
|
|
66
|
-
cert_path: Optional[str] = Field(None, description="SSL certificate path for HTTPS signalling")
|
|
67
|
-
cert_password: Optional[str] = Field(None, description="SSL certificate password")
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
from typing import List, Tuple
|
|
2
|
-
|
|
3
|
-
from gr_urchin import Joint, Link, xyz_rpy_to_matrix
|
|
4
|
-
from greenstream_config.types import Camera
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def get_camera_urdf(
|
|
8
|
-
camera: Camera,
|
|
9
|
-
links: List[Link],
|
|
10
|
-
joints: List[Joint],
|
|
11
|
-
add_optical_frame: bool = True,
|
|
12
|
-
has_duplicate_camera_link: bool = False,
|
|
13
|
-
) -> Tuple[List[Link], List[Joint]]:
|
|
14
|
-
# This is the camera urdf from the gama/lookout greenstream.launch.py
|
|
15
|
-
# We need to generate this from the camera config
|
|
16
|
-
|
|
17
|
-
# Only generate camera link if it currently doesn't exist. This checks for multiple cameras within the same housing
|
|
18
|
-
# etc: bow camera has both visible and thermal cameras, it is assumed that they are connected via the same ptz system
|
|
19
|
-
if not has_duplicate_camera_link:
|
|
20
|
-
camera_xyz_rpy = (
|
|
21
|
-
[
|
|
22
|
-
camera.camera_offsets.forward or 0.0,
|
|
23
|
-
camera.camera_offsets.left or 0.0,
|
|
24
|
-
camera.camera_offsets.up or 0.0,
|
|
25
|
-
camera.camera_offsets.roll or 0.0,
|
|
26
|
-
camera.camera_offsets.pitch or 0.0,
|
|
27
|
-
camera.camera_offsets.yaw or 0.0,
|
|
28
|
-
]
|
|
29
|
-
if camera.camera_offsets
|
|
30
|
-
else [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
links.append(
|
|
34
|
-
Link(
|
|
35
|
-
name=f"{camera.name}_link",
|
|
36
|
-
inertial=None,
|
|
37
|
-
visuals=None,
|
|
38
|
-
collisions=None,
|
|
39
|
-
)
|
|
40
|
-
)
|
|
41
|
-
joints.append(
|
|
42
|
-
Joint(
|
|
43
|
-
name=f"{camera.name}_joint",
|
|
44
|
-
parent="base_link",
|
|
45
|
-
child=f"{camera.name}_link",
|
|
46
|
-
joint_type="fixed",
|
|
47
|
-
origin=xyz_rpy_to_matrix(camera_xyz_rpy),
|
|
48
|
-
)
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
if camera.ptz:
|
|
52
|
-
for ptz_component in camera.ptz_offsets:
|
|
53
|
-
parent_link = links[-1].name
|
|
54
|
-
if ptz_component.type == "pan":
|
|
55
|
-
links.append(
|
|
56
|
-
Link(
|
|
57
|
-
name=f"{camera.name}_pan_link",
|
|
58
|
-
inertial=None,
|
|
59
|
-
visuals=None,
|
|
60
|
-
collisions=None,
|
|
61
|
-
),
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
camera_pan_xyz_rpy = [
|
|
65
|
-
ptz_component.forward or 0.0,
|
|
66
|
-
ptz_component.left or 0.0,
|
|
67
|
-
ptz_component.up or 0.0,
|
|
68
|
-
ptz_component.roll or 0.0,
|
|
69
|
-
ptz_component.pitch or 0.0,
|
|
70
|
-
ptz_component.yaw or 0.0,
|
|
71
|
-
]
|
|
72
|
-
|
|
73
|
-
joints.append(
|
|
74
|
-
Joint(
|
|
75
|
-
name=f"{camera.name}_pan_joint",
|
|
76
|
-
parent=parent_link,
|
|
77
|
-
child=f"{camera.name}_pan_link",
|
|
78
|
-
joint_type="continuous",
|
|
79
|
-
origin=xyz_rpy_to_matrix(camera_pan_xyz_rpy),
|
|
80
|
-
axis=[0, 0, 1],
|
|
81
|
-
)
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
elif ptz_component.type == "tilt":
|
|
85
|
-
links.append(
|
|
86
|
-
Link(
|
|
87
|
-
name=f"{camera.name}_tilt_link",
|
|
88
|
-
inertial=None,
|
|
89
|
-
visuals=None,
|
|
90
|
-
collisions=None,
|
|
91
|
-
),
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
camera_tilt_xyz_rpy = [
|
|
95
|
-
ptz_component.forward or 0.0,
|
|
96
|
-
ptz_component.left or 0.0,
|
|
97
|
-
ptz_component.up or 0.0,
|
|
98
|
-
ptz_component.roll or 0.0,
|
|
99
|
-
ptz_component.pitch or 0.0,
|
|
100
|
-
ptz_component.yaw or 0.0,
|
|
101
|
-
]
|
|
102
|
-
|
|
103
|
-
joints.append(
|
|
104
|
-
Joint(
|
|
105
|
-
name=f"{camera.name}_tilt_joint",
|
|
106
|
-
parent=parent_link,
|
|
107
|
-
child=f"{camera.name}_tilt_link",
|
|
108
|
-
joint_type="continuous",
|
|
109
|
-
origin=xyz_rpy_to_matrix(camera_tilt_xyz_rpy),
|
|
110
|
-
axis=[0, 1, 0],
|
|
111
|
-
)
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
if add_optical_frame:
|
|
115
|
-
|
|
116
|
-
if has_duplicate_camera_link:
|
|
117
|
-
# search for the parent link of another camera frame bounded by the same camera link (i.e. color, thermal within the same housing)
|
|
118
|
-
for joint in reversed(joints):
|
|
119
|
-
child_link_name = joint.child
|
|
120
|
-
if (
|
|
121
|
-
child_link_name.startswith(camera.name)
|
|
122
|
-
and child_link_name.endswith("frame")
|
|
123
|
-
and "optical" not in child_link_name
|
|
124
|
-
):
|
|
125
|
-
parent_link = joint.parent
|
|
126
|
-
break
|
|
127
|
-
else:
|
|
128
|
-
parent_link = links[-1].name
|
|
129
|
-
|
|
130
|
-
links.append(
|
|
131
|
-
Link(
|
|
132
|
-
name=f"{camera.name}_{camera.type}_frame",
|
|
133
|
-
inertial=None,
|
|
134
|
-
visuals=None,
|
|
135
|
-
collisions=None,
|
|
136
|
-
)
|
|
137
|
-
)
|
|
138
|
-
links.append(
|
|
139
|
-
Link(
|
|
140
|
-
name=f"{camera.name}_{camera.type}_optical_frame",
|
|
141
|
-
inertial=None,
|
|
142
|
-
visuals=None,
|
|
143
|
-
collisions=None,
|
|
144
|
-
)
|
|
145
|
-
)
|
|
146
|
-
# fixed transforms between camera frame and optical frame FRD -> NED
|
|
147
|
-
joints.append(
|
|
148
|
-
Joint(
|
|
149
|
-
name=f"{parent_link}_to_{camera.type}_frame",
|
|
150
|
-
parent=f"{parent_link}",
|
|
151
|
-
child=f"{camera.name}_{camera.type}_frame",
|
|
152
|
-
joint_type="fixed",
|
|
153
|
-
origin=xyz_rpy_to_matrix([0, 0, 0, 0, 0, 0]),
|
|
154
|
-
)
|
|
155
|
-
)
|
|
156
|
-
joints.append(
|
|
157
|
-
Joint(
|
|
158
|
-
name=f"{camera.name}_{camera.type}_frame_to_optical_frame",
|
|
159
|
-
parent=f"{camera.name}_{camera.type}_frame",
|
|
160
|
-
child=f"{camera.name}_{camera.type}_optical_frame",
|
|
161
|
-
joint_type="fixed",
|
|
162
|
-
origin=xyz_rpy_to_matrix([0, 0, 0, -1.570796, 0, -1.570796]),
|
|
163
|
-
)
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
return (links, joints)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
def get_cameras_urdf(
|
|
170
|
-
cameras: List[Camera],
|
|
171
|
-
add_optical_frame: bool = True,
|
|
172
|
-
) -> Tuple[List[Link], List[Joint]]:
|
|
173
|
-
|
|
174
|
-
links: List[Link] = []
|
|
175
|
-
joints: List[Joint] = []
|
|
176
|
-
|
|
177
|
-
for camera in cameras:
|
|
178
|
-
|
|
179
|
-
# skip duplicate camera links, only add optical frame of camera of a different type (i.e. color, thermal)
|
|
180
|
-
if camera.name in [prev_camera.name for prev_camera in cameras[: cameras.index(camera)]]:
|
|
181
|
-
links, joints = get_camera_urdf(
|
|
182
|
-
camera,
|
|
183
|
-
links,
|
|
184
|
-
joints,
|
|
185
|
-
add_optical_frame,
|
|
186
|
-
has_duplicate_camera_link=True,
|
|
187
|
-
)
|
|
188
|
-
else:
|
|
189
|
-
links, joints = get_camera_urdf(
|
|
190
|
-
camera,
|
|
191
|
-
links,
|
|
192
|
-
joints,
|
|
193
|
-
add_optical_frame,
|
|
194
|
-
has_duplicate_camera_link=False,
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
return links, joints
|
|
File without changes
|
|
File without changes
|
{greenstream_config-3.31.0 → greenstream_config-4.0.1}/greenstream_config.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|