ros-dds-manager 0.1.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.
- ros_dds_manager/__init__.py +62 -0
- ros_dds_manager/commands/config_cmd.py +131 -0
- ros_dds_manager/commands/doctor_cmd.py +157 -0
- ros_dds_manager/commands/profile_cmd.py +508 -0
- ros_dds_manager/commands/run_cmd.py +47 -0
- ros_dds_manager/config_mgr.py +138 -0
- ros_dds_manager/errors.py +61 -0
- ros_dds_manager/generator.py +37 -0
- ros_dds_manager/main.py +113 -0
- ros_dds_manager/models.py +132 -0
- ros_dds_manager/network.py +108 -0
- ros_dds_manager/storage.py +181 -0
- ros_dds_manager/templates/cyclonedds.xml.j2 +43 -0
- ros_dds_manager/templates/fastdds.xml.j2 +85 -0
- ros_dds_manager-0.1.0.dist-info/METADATA +234 -0
- ros_dds_manager-0.1.0.dist-info/RECORD +19 -0
- ros_dds_manager-0.1.0.dist-info/WHEEL +4 -0
- ros_dds_manager-0.1.0.dist-info/entry_points.txt +3 -0
- ros_dds_manager-0.1.0.dist-info/licenses/LICENSE +122 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ros_dds_manager.errors import (
|
|
4
|
+
EXIT_ERROR,
|
|
5
|
+
EXIT_INTERRUPT,
|
|
6
|
+
EXIT_NOT_FOUND,
|
|
7
|
+
EXIT_SUCCESS,
|
|
8
|
+
EXIT_USAGE,
|
|
9
|
+
ConfigurationError,
|
|
10
|
+
NonInteractiveError,
|
|
11
|
+
ProfileAlreadyExistsError,
|
|
12
|
+
ProfileNotFoundError,
|
|
13
|
+
RosDdsManagerError,
|
|
14
|
+
XMLValidationError,
|
|
15
|
+
)
|
|
16
|
+
from ros_dds_manager.generator import render_profile_xml, validate_xml
|
|
17
|
+
from ros_dds_manager.models import DDSProfile, DDSVendor
|
|
18
|
+
from ros_dds_manager.storage import (
|
|
19
|
+
activate_profile,
|
|
20
|
+
deactivate_profile,
|
|
21
|
+
delete_profile,
|
|
22
|
+
get_active_profile_name,
|
|
23
|
+
get_base_dir,
|
|
24
|
+
get_profiles_dir,
|
|
25
|
+
list_profiles,
|
|
26
|
+
load_profile,
|
|
27
|
+
save_profile,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__version__ = "0.1.0"
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"__version__",
|
|
34
|
+
# Models
|
|
35
|
+
"DDSProfile",
|
|
36
|
+
"DDSVendor",
|
|
37
|
+
# Generator
|
|
38
|
+
"render_profile_xml",
|
|
39
|
+
"validate_xml",
|
|
40
|
+
# Storage
|
|
41
|
+
"activate_profile",
|
|
42
|
+
"deactivate_profile",
|
|
43
|
+
"delete_profile",
|
|
44
|
+
"get_active_profile_name",
|
|
45
|
+
"get_base_dir",
|
|
46
|
+
"get_profiles_dir",
|
|
47
|
+
"list_profiles",
|
|
48
|
+
"load_profile",
|
|
49
|
+
"save_profile",
|
|
50
|
+
# Errors & Exit Codes
|
|
51
|
+
"EXIT_SUCCESS",
|
|
52
|
+
"EXIT_ERROR",
|
|
53
|
+
"EXIT_USAGE",
|
|
54
|
+
"EXIT_NOT_FOUND",
|
|
55
|
+
"EXIT_INTERRUPT",
|
|
56
|
+
"RosDdsManagerError",
|
|
57
|
+
"ProfileNotFoundError",
|
|
58
|
+
"ProfileAlreadyExistsError",
|
|
59
|
+
"XMLValidationError",
|
|
60
|
+
"NonInteractiveError",
|
|
61
|
+
"ConfigurationError",
|
|
62
|
+
]
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from wpycli import Command, CommandContext
|
|
6
|
+
|
|
7
|
+
from ros_dds_manager.config_mgr import (
|
|
8
|
+
find_active_config_path,
|
|
9
|
+
format_toml,
|
|
10
|
+
get_config_path,
|
|
11
|
+
get_config_value,
|
|
12
|
+
init_config,
|
|
13
|
+
read_config,
|
|
14
|
+
set_config_value,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_config_command() -> Command:
|
|
19
|
+
config = Command(
|
|
20
|
+
use="config",
|
|
21
|
+
short="ros-dds-manager 설정을 관리한다.",
|
|
22
|
+
long="ros-dds-manager 의 전역 또는 로컬 설정(config.toml)을 초기화, 조회, 수정합니다.",
|
|
23
|
+
run=run_config_show,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
init_cmd = Command(
|
|
27
|
+
use="init",
|
|
28
|
+
short="기본 config.toml 설정 파일을 생성한다.",
|
|
29
|
+
run=run_config_init,
|
|
30
|
+
)
|
|
31
|
+
init_cmd.add_bool_flag("local", help="현재 디렉토리에 config.toml 을 생성한다.", shorthand="l")
|
|
32
|
+
init_cmd.add_bool_flag("overwrite", help="이미 파일이 존재할 경우 덮어쓴다.", shorthand="f")
|
|
33
|
+
|
|
34
|
+
show_cmd = Command(
|
|
35
|
+
use="show",
|
|
36
|
+
short="현재 적용된 설정을 출력한다.",
|
|
37
|
+
run=run_config_show,
|
|
38
|
+
)
|
|
39
|
+
show_cmd.add_bool_flag("json", help="JSON 포맷으로 출력한다.", shorthand="j")
|
|
40
|
+
|
|
41
|
+
path_cmd = Command(
|
|
42
|
+
use="path",
|
|
43
|
+
short="설정 파일의 경로를 출력한다.",
|
|
44
|
+
run=run_config_path,
|
|
45
|
+
)
|
|
46
|
+
path_cmd.add_bool_flag("local", help="로컬 설정 파일 경로를 확인한다.", shorthand="l")
|
|
47
|
+
|
|
48
|
+
set_cmd = Command(
|
|
49
|
+
use="set <key> <value>",
|
|
50
|
+
short="설정 항목의 값을 변경한다. (예: app.default_vendor fastdds)",
|
|
51
|
+
run=run_config_set,
|
|
52
|
+
)
|
|
53
|
+
set_cmd.add_bool_flag("local", help="로컬 config.toml 에 저장한다.", shorthand="l")
|
|
54
|
+
|
|
55
|
+
get_cmd = Command(
|
|
56
|
+
use="get <key>",
|
|
57
|
+
short="특정 설정 항목의 값을 조회한다. (예: app.default_vendor)",
|
|
58
|
+
run=run_config_get,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
config.add_command(init_cmd, show_cmd, path_cmd, set_cmd, get_cmd)
|
|
62
|
+
return config
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def run_config_init(ctx: CommandContext) -> int:
|
|
66
|
+
is_local = bool(ctx.flags.get("local"))
|
|
67
|
+
overwrite = bool(ctx.flags.get("overwrite"))
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
path = init_config(is_local=is_local, overwrite=overwrite)
|
|
71
|
+
target_type = "로컬" if is_local else "전역"
|
|
72
|
+
print(ctx.terminal.message("success", f"{target_type} 설정 초기화 완료", f"파일 경로: {path}"))
|
|
73
|
+
return 0
|
|
74
|
+
except FileExistsError as e:
|
|
75
|
+
print(ctx.terminal.message("error", "설정 초기화 실패", f"{e}\n덮어쓰려면 --overwrite (-f) 옵션을 사용하세요."))
|
|
76
|
+
return 1
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def run_config_show(ctx: CommandContext) -> int:
|
|
80
|
+
active_path = find_active_config_path()
|
|
81
|
+
cfg = read_config(active_path)
|
|
82
|
+
|
|
83
|
+
as_json = bool(ctx.flags.get("json"))
|
|
84
|
+
if as_json:
|
|
85
|
+
print(ctx.terminal.pretty_json(json.dumps(cfg, indent=2)))
|
|
86
|
+
else:
|
|
87
|
+
path_str = str(active_path) if active_path else "(기본 내장값 사용 중)"
|
|
88
|
+
toml_content = format_toml(cfg)
|
|
89
|
+
print(ctx.terminal.panel(f"Configuration ({path_str})", toml_content))
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def run_config_path(ctx: CommandContext) -> int:
|
|
94
|
+
is_local = bool(ctx.flags.get("local"))
|
|
95
|
+
path = get_config_path(is_local=is_local)
|
|
96
|
+
exists_str = "존재함" if path.is_file() else "없음 (기본값 사용 중)"
|
|
97
|
+
print(f"경로: {path} [{exists_str}]")
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def run_config_set(ctx: CommandContext) -> int:
|
|
102
|
+
if len(ctx.args) < 2:
|
|
103
|
+
print(ctx.terminal.message("error", "인수 부족", "사용법: ros-dds-manager config set <key> <value>"))
|
|
104
|
+
return 1
|
|
105
|
+
|
|
106
|
+
key = ctx.args[0]
|
|
107
|
+
value = ctx.args[1]
|
|
108
|
+
is_local = bool(ctx.flags.get("local"))
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
path, parsed = set_config_value(key, value, is_local=is_local)
|
|
112
|
+
print(ctx.terminal.message("success", "설정 변경 완료", f"[{path}]\n{key} = {parsed}"))
|
|
113
|
+
return 0
|
|
114
|
+
except Exception as e:
|
|
115
|
+
print(ctx.terminal.message("error", "설정 변경 실패", str(e)))
|
|
116
|
+
return 1
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def run_config_get(ctx: CommandContext) -> int:
|
|
120
|
+
if not ctx.args:
|
|
121
|
+
print(ctx.terminal.message("error", "인수 부족", "사용법: ros-dds-manager config get <key>"))
|
|
122
|
+
return 1
|
|
123
|
+
|
|
124
|
+
key = ctx.args[0]
|
|
125
|
+
try:
|
|
126
|
+
value = get_config_value(key)
|
|
127
|
+
print(value)
|
|
128
|
+
return 0
|
|
129
|
+
except KeyError as e:
|
|
130
|
+
print(ctx.terminal.message("error", "설정 조회 실패", str(e)))
|
|
131
|
+
return 1
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from wpycli import Command, CommandContext
|
|
8
|
+
|
|
9
|
+
from ros_dds_manager.errors import EXIT_SUCCESS
|
|
10
|
+
from ros_dds_manager.generator import validate_xml
|
|
11
|
+
from ros_dds_manager.network import get_network_interfaces
|
|
12
|
+
from ros_dds_manager.storage import (
|
|
13
|
+
get_active_profile_name,
|
|
14
|
+
get_base_dir,
|
|
15
|
+
get_profile_dir,
|
|
16
|
+
load_profile,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_doctor_command() -> Command:
|
|
21
|
+
cmd = Command(
|
|
22
|
+
use="doctor",
|
|
23
|
+
short="현재 ROS 2 환경 및 DDS 설정을 진단한다.",
|
|
24
|
+
long="현재 쉘의 DDS 환경변수, 활성 프로파일 파일 상태, 네트워크 카드 상태를 점검합니다.",
|
|
25
|
+
run=run_doctor,
|
|
26
|
+
)
|
|
27
|
+
cmd.add_bool_flag("json", help="기계 판독 가능한 JSON 포맷으로 진단 결과를 출력한다.", shorthand="j")
|
|
28
|
+
return cmd
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def run_doctor(ctx: CommandContext) -> int:
|
|
32
|
+
issues: list[str] = []
|
|
33
|
+
|
|
34
|
+
# 1. ROS 2 Distro
|
|
35
|
+
ros_distro = os.environ.get("ROS_DISTRO")
|
|
36
|
+
if not ros_distro:
|
|
37
|
+
issues.append("ROS_DISTRO 환경변수가 설정되지 않았습니다. (ROS 2 환경 미로드)")
|
|
38
|
+
|
|
39
|
+
# 2. Active Profile
|
|
40
|
+
active_name = get_active_profile_name()
|
|
41
|
+
active_info: dict | None = None
|
|
42
|
+
if active_name:
|
|
43
|
+
try:
|
|
44
|
+
profile = load_profile(active_name)
|
|
45
|
+
prof_dir = get_profile_dir(active_name)
|
|
46
|
+
xml_path = prof_dir / "config.xml"
|
|
47
|
+
xml_ok = xml_path.is_file()
|
|
48
|
+
xml_valid = False
|
|
49
|
+
if xml_ok:
|
|
50
|
+
xml_valid, err = validate_xml(xml_path.read_text(encoding="utf-8"))
|
|
51
|
+
if not xml_valid:
|
|
52
|
+
issues.append(f"프로파일 XML 구문 오류: {err}")
|
|
53
|
+
else:
|
|
54
|
+
issues.append("활성 프로파일의 config.xml 이 존재하지 않습니다.")
|
|
55
|
+
|
|
56
|
+
active_info = {
|
|
57
|
+
"name": profile.name,
|
|
58
|
+
"vendor": profile.vendor.value,
|
|
59
|
+
"dir": str(prof_dir),
|
|
60
|
+
"xml_path": str(xml_path) if xml_ok else None,
|
|
61
|
+
"xml_valid": xml_valid,
|
|
62
|
+
}
|
|
63
|
+
except Exception as e:
|
|
64
|
+
issues.append(f"활성 프로파일 로드 실패: {e}")
|
|
65
|
+
|
|
66
|
+
# 3. Shell Environment
|
|
67
|
+
current_rmw = os.environ.get("RMW_IMPLEMENTATION", "(default)")
|
|
68
|
+
current_cyclone_uri = os.environ.get("CYCLONEDDS_URI")
|
|
69
|
+
current_fastdds_file = os.environ.get("FASTRTPS_DEFAULT_PROFILES_FILE")
|
|
70
|
+
current_domain_id = os.environ.get("ROS_DOMAIN_ID", "0")
|
|
71
|
+
current_lh = os.environ.get("ROS_LOCALHOST_ONLY")
|
|
72
|
+
current_ds = os.environ.get("ROS_DISCOVERY_SERVER")
|
|
73
|
+
|
|
74
|
+
if current_cyclone_uri and current_cyclone_uri.startswith("file://"):
|
|
75
|
+
target_f = Path(current_cyclone_uri[7:])
|
|
76
|
+
if not target_f.is_file():
|
|
77
|
+
issues.append(f"CYCLONEDDS_URI 파일이 존재하지 않습니다: {target_f}")
|
|
78
|
+
|
|
79
|
+
if current_fastdds_file:
|
|
80
|
+
target_f = Path(current_fastdds_file)
|
|
81
|
+
if not target_f.is_file():
|
|
82
|
+
issues.append(f"FASTRTPS_DEFAULT_PROFILES_FILE 파일이 존재하지 않습니다: {target_f}")
|
|
83
|
+
|
|
84
|
+
env_info = {
|
|
85
|
+
"RMW_IMPLEMENTATION": current_rmw,
|
|
86
|
+
"CYCLONEDDS_URI": current_cyclone_uri,
|
|
87
|
+
"FASTRTPS_DEFAULT_PROFILES_FILE": current_fastdds_file,
|
|
88
|
+
"ROS_DOMAIN_ID": current_domain_id,
|
|
89
|
+
"ROS_LOCALHOST_ONLY": current_lh,
|
|
90
|
+
"ROS_DISCOVERY_SERVER": current_ds,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# 4. Network Interfaces
|
|
94
|
+
interfaces = get_network_interfaces()
|
|
95
|
+
ifaces_data = [
|
|
96
|
+
{
|
|
97
|
+
"name": iface.name,
|
|
98
|
+
"ipv4": iface.ipv4,
|
|
99
|
+
"state": iface.state,
|
|
100
|
+
"is_up": iface.is_up,
|
|
101
|
+
"is_loopback": iface.is_loopback,
|
|
102
|
+
}
|
|
103
|
+
for iface in interfaces
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
# Handle JSON output
|
|
107
|
+
if ctx.flags.get("json"):
|
|
108
|
+
report = {
|
|
109
|
+
"ros_distro": ros_distro,
|
|
110
|
+
"active_profile": active_info,
|
|
111
|
+
"environment": env_info,
|
|
112
|
+
"interfaces": ifaces_data,
|
|
113
|
+
"issues": issues,
|
|
114
|
+
"status": "healthy" if not issues else "warning",
|
|
115
|
+
}
|
|
116
|
+
print(json.dumps(report, indent=2))
|
|
117
|
+
return EXIT_SUCCESS
|
|
118
|
+
|
|
119
|
+
# Text rendering
|
|
120
|
+
term = ctx.terminal
|
|
121
|
+
print(term.panel("ROS 2 DDS Diagnostic Report (Doctor)", "현재 쉘 환경, 활성 프로파일, 네트워크 상태를 점검합니다."))
|
|
122
|
+
|
|
123
|
+
print("\n[1] ROS 2 환경:")
|
|
124
|
+
if ros_distro:
|
|
125
|
+
print(f" ✓ ROS_DISTRO: {ros_distro}")
|
|
126
|
+
else:
|
|
127
|
+
print(" ! ROS_DISTRO 환경변수가 설정되지 않았습니다. (ROS 2 언더레이 setup.bash 미로드)")
|
|
128
|
+
|
|
129
|
+
print("\n[2] 활성 프로파일 상태:")
|
|
130
|
+
if active_info:
|
|
131
|
+
print(f" ✓ 활성 프로파일: {active_info['name']} (벤더: {active_info['vendor']})")
|
|
132
|
+
print(f" ✓ 설정 디렉토리: {active_info['dir']}")
|
|
133
|
+
if active_info.get("xml_valid"):
|
|
134
|
+
print(" ✓ XML 유효성: 정상")
|
|
135
|
+
else:
|
|
136
|
+
print(" ✗ XML 구문 오류 또는 파일 부재")
|
|
137
|
+
else:
|
|
138
|
+
print(" - 현재 활성화된 프로파일이 없습니다. ('ros-dds-manager switch <name>' 으로 활성화 가능)")
|
|
139
|
+
|
|
140
|
+
print("\n[3] 현재 쉘 환경변수 (Environment Variables):")
|
|
141
|
+
for k, v in env_info.items():
|
|
142
|
+
val_str = v if v is not None else "(미설정)"
|
|
143
|
+
print(f" - {k:<30} {val_str}")
|
|
144
|
+
|
|
145
|
+
print("\n[4] 감지된 네트워크 인터페이스 (Network Interfaces):")
|
|
146
|
+
for iface in interfaces:
|
|
147
|
+
status_symbol = "✓" if iface.is_up else "✗"
|
|
148
|
+
print(f" {status_symbol} {iface.summary()}")
|
|
149
|
+
|
|
150
|
+
print("\n" + "=" * 50)
|
|
151
|
+
if issues:
|
|
152
|
+
print(term.message("warning", "진단 주의사항 발견", "\n".join(f"- {i}" for i in issues)))
|
|
153
|
+
return EXIT_SUCCESS
|
|
154
|
+
else:
|
|
155
|
+
current_sh = get_base_dir() / "current.sh"
|
|
156
|
+
print(term.message("success", "진단 결과 양호", f"모든 필수 항목이 정상입니다.\n(활성 프로파일 스크립트: {current_sh})"))
|
|
157
|
+
return EXIT_SUCCESS
|