dexcontrol 0.2.10__py3-none-any.whl → 0.3.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 dexcontrol might be problematic. Click here for more details.
- dexcontrol/__init__.py +2 -0
- dexcontrol/config/core/arm.py +5 -1
- dexcontrol/config/core/chassis.py +9 -4
- dexcontrol/config/core/hand.py +2 -1
- dexcontrol/config/core/head.py +7 -8
- dexcontrol/config/core/misc.py +14 -1
- dexcontrol/config/core/torso.py +8 -4
- dexcontrol/config/sensors/cameras/__init__.py +2 -1
- dexcontrol/config/sensors/cameras/luxonis_camera.py +51 -0
- dexcontrol/config/sensors/cameras/rgb_camera.py +1 -1
- dexcontrol/config/sensors/cameras/zed_camera.py +2 -2
- dexcontrol/config/sensors/vega_sensors.py +9 -1
- dexcontrol/config/vega.py +34 -3
- dexcontrol/core/arm.py +103 -58
- dexcontrol/core/chassis.py +146 -115
- dexcontrol/core/component.py +83 -20
- dexcontrol/core/hand.py +74 -39
- dexcontrol/core/head.py +41 -28
- dexcontrol/core/misc.py +256 -25
- dexcontrol/core/robot_query_interface.py +440 -0
- dexcontrol/core/torso.py +28 -10
- dexcontrol/proto/dexcontrol_msg_pb2.py +27 -37
- dexcontrol/proto/dexcontrol_msg_pb2.pyi +111 -126
- dexcontrol/proto/dexcontrol_query_pb2.py +39 -35
- dexcontrol/proto/dexcontrol_query_pb2.pyi +41 -4
- dexcontrol/robot.py +266 -409
- dexcontrol/sensors/__init__.py +2 -1
- dexcontrol/sensors/camera/__init__.py +2 -0
- dexcontrol/sensors/camera/luxonis_camera.py +169 -0
- dexcontrol/sensors/camera/zed_camera.py +17 -8
- dexcontrol/sensors/imu/chassis_imu.py +5 -1
- dexcontrol/sensors/imu/zed_imu.py +3 -2
- dexcontrol/sensors/lidar/rplidar.py +1 -0
- dexcontrol/sensors/manager.py +3 -0
- dexcontrol/utils/constants.py +3 -0
- dexcontrol/utils/error_code.py +236 -0
- dexcontrol/utils/os_utils.py +183 -1
- dexcontrol/utils/pb_utils.py +0 -22
- dexcontrol/utils/subscribers/lidar.py +1 -0
- dexcontrol/utils/trajectory_utils.py +17 -5
- dexcontrol/utils/viz_utils.py +86 -11
- dexcontrol/utils/zenoh_utils.py +288 -2
- {dexcontrol-0.2.10.dist-info → dexcontrol-0.3.0.dist-info}/METADATA +15 -2
- dexcontrol-0.3.0.dist-info/RECORD +76 -0
- dexcontrol-0.2.10.dist-info/RECORD +0 -72
- {dexcontrol-0.2.10.dist-info → dexcontrol-0.3.0.dist-info}/WHEEL +0 -0
- {dexcontrol-0.2.10.dist-info → dexcontrol-0.3.0.dist-info}/licenses/LICENSE +0 -0
dexcontrol/utils/os_utils.py
CHANGED
|
@@ -12,8 +12,11 @@
|
|
|
12
12
|
|
|
13
13
|
import os
|
|
14
14
|
import re
|
|
15
|
-
from typing import Final
|
|
15
|
+
from typing import Any, Final
|
|
16
16
|
|
|
17
|
+
from loguru import logger
|
|
18
|
+
|
|
19
|
+
import dexcontrol
|
|
17
20
|
from dexcontrol.utils.constants import ROBOT_NAME_ENV_VAR
|
|
18
21
|
|
|
19
22
|
|
|
@@ -56,3 +59,182 @@ def get_robot_model() -> str:
|
|
|
56
59
|
raise ValueError(f"Unknown robot model: {robot_model_abb}")
|
|
57
60
|
model = robot_model_abb_mapping[robot_model_abb] + "-" + robot_name.split("-")[-1]
|
|
58
61
|
return model
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def check_version_compatibility(
|
|
65
|
+
version_info: dict[str, Any], show_warnings: bool = True
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Check version compatibility between client and server.
|
|
68
|
+
|
|
69
|
+
This function uses the new JSON-based version interface to:
|
|
70
|
+
1. Compare client library version with server's minimum required version
|
|
71
|
+
2. Check server component versions for compatibility
|
|
72
|
+
3. Provide clear guidance for version mismatches
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
version_info: Dictionary containing version information from get_version_info()
|
|
76
|
+
show_warnings: Whether to show warning messages (default: True)
|
|
77
|
+
"""
|
|
78
|
+
validate_client_version(version_info)
|
|
79
|
+
validate_server_version(version_info)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def validate_client_version(version_info: dict[str, Any]) -> None:
|
|
83
|
+
"""Validate client library version against server requirements.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
version_info: Dictionary containing server and client version information.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
client_info = version_info.get("client", {})
|
|
90
|
+
min_required_version = client_info.get("minimal_version")
|
|
91
|
+
|
|
92
|
+
if not min_required_version:
|
|
93
|
+
logger.debug("No minimum version requirement from server")
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
# Get current library version
|
|
97
|
+
current_version = getattr(dexcontrol, "__version__", "unknown")
|
|
98
|
+
|
|
99
|
+
if current_version == "unknown":
|
|
100
|
+
logger.warning("Could not determine current library version")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
# Compare versions
|
|
104
|
+
comparison = compare_versions(current_version, min_required_version)
|
|
105
|
+
|
|
106
|
+
if comparison < 0:
|
|
107
|
+
show_version_upgrade_warning(current_version, min_required_version)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def show_version_upgrade_warning(current: str, required: str) -> None:
|
|
111
|
+
"""Display version upgrade warning to user.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
current: Current library version
|
|
115
|
+
required: Required minimum version
|
|
116
|
+
"""
|
|
117
|
+
logger.error(
|
|
118
|
+
f"🚨 CLIENT VERSION TOO OLD! 🚨\n"
|
|
119
|
+
f"Current library version: {current}\n"
|
|
120
|
+
f"Minimum required version: {required}\n"
|
|
121
|
+
f"\n"
|
|
122
|
+
f"⚠️ Your dexcontrol library is outdated and may not work correctly!\n"
|
|
123
|
+
f"📦 Please update your library using: pip install --upgrade dexcontrol\n"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def validate_server_version(version_info: dict[str, Any]) -> None:
|
|
128
|
+
"""Validate server software versions against minimum requirements.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
version_info: Dictionary containing server and client version information.
|
|
132
|
+
"""
|
|
133
|
+
server_info = version_info.get("server", {})
|
|
134
|
+
|
|
135
|
+
if not server_info:
|
|
136
|
+
logger.debug("No server version information available")
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
# Check each component's software version
|
|
140
|
+
components_below_min = []
|
|
141
|
+
for component_name, component_info in server_info.items():
|
|
142
|
+
if isinstance(component_info, dict):
|
|
143
|
+
software_version = component_info.get("software_version")
|
|
144
|
+
if software_version is not None:
|
|
145
|
+
try:
|
|
146
|
+
# Convert to int if it's a string
|
|
147
|
+
software_version_int = int(software_version)
|
|
148
|
+
if software_version_int < dexcontrol.MIN_SOC_SOFTWARE_VERSION:
|
|
149
|
+
components_below_min.append(
|
|
150
|
+
(component_name, software_version_int)
|
|
151
|
+
)
|
|
152
|
+
except (ValueError, TypeError) as e:
|
|
153
|
+
logger.debug(
|
|
154
|
+
f"Could not parse software version for {component_name}: {e}"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# If any components are below minimum version, show warning
|
|
158
|
+
if components_below_min:
|
|
159
|
+
show_server_version_warning(
|
|
160
|
+
components_below_min, dexcontrol.MIN_SOC_SOFTWARE_VERSION
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def show_server_version_warning(
|
|
165
|
+
components: list[tuple[str, int]], min_version: int
|
|
166
|
+
) -> None:
|
|
167
|
+
"""Display server version warning to user.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
components: List of (component_name, version) tuples for components below minimum.
|
|
171
|
+
min_version: Minimum required server software version.
|
|
172
|
+
"""
|
|
173
|
+
components_str = "\n".join(
|
|
174
|
+
f" - {name}: version {version}" for name, version in components
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
logger.error(
|
|
178
|
+
f"🚨 SERVER VERSION TOO OLD! 🚨\n"
|
|
179
|
+
f"The following server components are below minimum version {min_version}:\n"
|
|
180
|
+
f"{components_str}\n"
|
|
181
|
+
f"\n"
|
|
182
|
+
f"⚠️ Your robot's firmware may be outdated and some features may not work correctly!\n"
|
|
183
|
+
f"📦 Please contact your robot admin or check https://github.com/dexmate-ai/vega-firmware.\n"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def compare_versions(version1: str, version2: str) -> int:
|
|
188
|
+
"""Compare two semantic version strings.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
version1: First version string (e.g., "1.2.3")
|
|
192
|
+
version2: Second version string (e.g., "1.1.0")
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
-1 if version1 < version2
|
|
196
|
+
0 if version1 == version2
|
|
197
|
+
1 if version1 > version2
|
|
198
|
+
"""
|
|
199
|
+
try:
|
|
200
|
+
# Clean version strings (remove 'v' prefix, handle pre-release suffixes)
|
|
201
|
+
def clean_version(v: str) -> list[int]:
|
|
202
|
+
v = v.strip().lower()
|
|
203
|
+
if v.startswith("v"):
|
|
204
|
+
v = v[1:]
|
|
205
|
+
# Split by dots and take only numeric parts
|
|
206
|
+
parts = v.split(".")
|
|
207
|
+
numeric_parts = []
|
|
208
|
+
for part in parts:
|
|
209
|
+
# Remove any non-numeric suffixes (like -alpha, -rc1, etc.)
|
|
210
|
+
numeric_part = ""
|
|
211
|
+
for char in part:
|
|
212
|
+
if char.isdigit():
|
|
213
|
+
numeric_part += char
|
|
214
|
+
else:
|
|
215
|
+
break
|
|
216
|
+
if numeric_part:
|
|
217
|
+
numeric_parts.append(int(numeric_part))
|
|
218
|
+
return numeric_parts
|
|
219
|
+
|
|
220
|
+
parts1 = clean_version(version1)
|
|
221
|
+
parts2 = clean_version(version2)
|
|
222
|
+
|
|
223
|
+
# Pad shorter version with zeros
|
|
224
|
+
max_len = max(len(parts1), len(parts2))
|
|
225
|
+
parts1.extend([0] * (max_len - len(parts1)))
|
|
226
|
+
parts2.extend([0] * (max_len - len(parts2)))
|
|
227
|
+
|
|
228
|
+
# Compare part by part
|
|
229
|
+
for p1, p2 in zip(parts1, parts2):
|
|
230
|
+
if p1 < p2:
|
|
231
|
+
return -1
|
|
232
|
+
elif p1 > p2:
|
|
233
|
+
return 1
|
|
234
|
+
|
|
235
|
+
return 0
|
|
236
|
+
|
|
237
|
+
except Exception as e:
|
|
238
|
+
logger.debug(f"Version comparison error: {e}")
|
|
239
|
+
# Fallback to string comparison
|
|
240
|
+
return -1 if version1 < version2 else (1 if version1 > version2 else 0)
|
dexcontrol/utils/pb_utils.py
CHANGED
|
@@ -20,28 +20,6 @@ TYPE_SOFTWARE_VERSION = dict[
|
|
|
20
20
|
]
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def software_version_to_dict(
|
|
24
|
-
version_msg: dexcontrol_query_pb2.SoftwareVersion,
|
|
25
|
-
) -> dict[str, TYPE_SOFTWARE_VERSION]:
|
|
26
|
-
"""Convert a SoftwareVersion protobuf message to a dictionary.
|
|
27
|
-
|
|
28
|
-
Args:
|
|
29
|
-
version_msg: SoftwareVersion protobuf message.
|
|
30
|
-
|
|
31
|
-
Returns:
|
|
32
|
-
Dictionary containing version information with component names as keys.
|
|
33
|
-
"""
|
|
34
|
-
return {
|
|
35
|
-
key: {
|
|
36
|
-
"hardware_version": value.hardware_version,
|
|
37
|
-
"software_version": value.software_version,
|
|
38
|
-
"main_hash": value.main_hash,
|
|
39
|
-
"compile_time": value.compile_time,
|
|
40
|
-
}
|
|
41
|
-
for key, value in version_msg.firmware_version.items()
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
23
|
class ComponentStatus(Enum):
|
|
46
24
|
"""Enum representing the status of a component."""
|
|
47
25
|
|
|
@@ -92,6 +92,7 @@ class LidarSubscriber(BaseZenohSubscriber):
|
|
|
92
92
|
- ranges: Array of range measurements in meters
|
|
93
93
|
- angles: Array of corresponding angles in radians
|
|
94
94
|
- qualities: Array of quality values (0-255) if available, None otherwise
|
|
95
|
+
- timestamp: Timestamp in nanoseconds (int)
|
|
95
96
|
"""
|
|
96
97
|
with self._data_lock:
|
|
97
98
|
if self._latest_raw_data is None:
|
|
@@ -16,7 +16,7 @@ import numpy as np
|
|
|
16
16
|
def generate_linear_trajectory(
|
|
17
17
|
current_pos: np.ndarray,
|
|
18
18
|
target_pos: np.ndarray,
|
|
19
|
-
max_vel: float = 0.5,
|
|
19
|
+
max_vel: float | np.ndarray = 0.5,
|
|
20
20
|
control_hz: float = 100,
|
|
21
21
|
) -> tuple[np.ndarray, int]:
|
|
22
22
|
"""Generate a linear trajectory between current and target positions.
|
|
@@ -24,7 +24,9 @@ def generate_linear_trajectory(
|
|
|
24
24
|
Args:
|
|
25
25
|
current_pos: Current position array.
|
|
26
26
|
target_pos: Target position array.
|
|
27
|
-
max_vel: Maximum velocity in units per second.
|
|
27
|
+
max_vel: Maximum velocity in units per second. Can be:
|
|
28
|
+
- float: Same velocity limit for all dimensions
|
|
29
|
+
- numpy array: Per-dimension velocity limits (same length as current_pos)
|
|
28
30
|
control_hz: Control frequency in Hz.
|
|
29
31
|
|
|
30
32
|
Returns:
|
|
@@ -32,9 +34,19 @@ def generate_linear_trajectory(
|
|
|
32
34
|
- trajectory: Array of waypoints from current to target position.
|
|
33
35
|
- num_steps: Number of steps in the trajectory.
|
|
34
36
|
"""
|
|
35
|
-
# Calculate
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
# Calculate time needed for each dimension
|
|
38
|
+
pos_diff = np.abs(target_pos - current_pos)
|
|
39
|
+
|
|
40
|
+
if isinstance(max_vel, np.ndarray):
|
|
41
|
+
# Per-dimension velocity limits - find the dimension that takes longest
|
|
42
|
+
time_needed = pos_diff / max_vel
|
|
43
|
+
max_time = np.max(time_needed)
|
|
44
|
+
else:
|
|
45
|
+
# Single velocity limit for all dimensions
|
|
46
|
+
max_diff = np.max(pos_diff)
|
|
47
|
+
max_time = max_diff / max_vel
|
|
48
|
+
|
|
49
|
+
num_steps = int(max_time * control_hz)
|
|
38
50
|
|
|
39
51
|
# Ensure at least one step
|
|
40
52
|
num_steps = max(1, num_steps)
|
dexcontrol/utils/viz_utils.py
CHANGED
|
@@ -10,13 +10,14 @@
|
|
|
10
10
|
|
|
11
11
|
"""Utility functions for displaying information in a Rich table format."""
|
|
12
12
|
|
|
13
|
+
from loguru import logger
|
|
13
14
|
from rich.console import Console
|
|
14
15
|
from rich.table import Table
|
|
15
16
|
|
|
16
17
|
from dexcontrol.utils.pb_utils import TYPE_SOFTWARE_VERSION
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
def show_software_version(version_info: dict[str, TYPE_SOFTWARE_VERSION])
|
|
20
|
+
def show_software_version(version_info: dict[str, TYPE_SOFTWARE_VERSION]):
|
|
20
21
|
"""Create a Rich table for displaying firmware version information.
|
|
21
22
|
|
|
22
23
|
Args:
|
|
@@ -42,25 +43,26 @@ def show_software_version(version_info: dict[str, TYPE_SOFTWARE_VERSION]) -> Non
|
|
|
42
43
|
console.print(table)
|
|
43
44
|
|
|
44
45
|
|
|
45
|
-
def show_component_status(status_info: dict[str, dict])
|
|
46
|
+
def show_component_status(status_info: dict[str, dict]):
|
|
46
47
|
"""Create a Rich table for displaying component status information.
|
|
47
48
|
|
|
48
49
|
Args:
|
|
49
50
|
status_info: Dictionary containing status info for each component.
|
|
50
51
|
"""
|
|
52
|
+
from dexcontrol.utils.error_code import get_error_description
|
|
51
53
|
from dexcontrol.utils.pb_utils import ComponentStatus
|
|
52
54
|
|
|
53
55
|
table = Table(title="Component Status")
|
|
54
56
|
table.add_column("Component", style="cyan")
|
|
55
57
|
table.add_column("Connected", justify="center")
|
|
56
58
|
table.add_column("Enabled", justify="center")
|
|
57
|
-
table.add_column("Error", justify="
|
|
59
|
+
table.add_column("Error", justify="left")
|
|
58
60
|
|
|
59
61
|
status_icons = {
|
|
60
|
-
True: ":white_check_mark:",
|
|
61
|
-
False: ":x:",
|
|
62
|
-
ComponentStatus.NORMAL: ":white_check_mark:",
|
|
63
|
-
ComponentStatus.NA: "N/A",
|
|
62
|
+
True: "[green]:white_check_mark:[/green]",
|
|
63
|
+
False: "[red]:x:[/red]",
|
|
64
|
+
ComponentStatus.NORMAL: "[green]:white_check_mark:[/green]",
|
|
65
|
+
ComponentStatus.NA: "[dim]N/A[/dim]",
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
# Sort components by name to ensure consistent order
|
|
@@ -70,15 +72,51 @@ def show_component_status(status_info: dict[str, dict]) -> None:
|
|
|
70
72
|
connected = status_icons[status["connected"]]
|
|
71
73
|
|
|
72
74
|
# Format enabled status
|
|
73
|
-
enabled = status_icons.get(status["enabled"], ":x:")
|
|
75
|
+
enabled = status_icons.get(status["enabled"], "[red]:x:[/red]")
|
|
74
76
|
|
|
75
77
|
# Format error status
|
|
76
78
|
if status["error_state"] == ComponentStatus.NORMAL:
|
|
77
|
-
error = ":white_check_mark:"
|
|
79
|
+
error = "[green]:white_check_mark:[/green]"
|
|
78
80
|
elif status["error_state"] == ComponentStatus.NA:
|
|
79
|
-
error = "N/A"
|
|
81
|
+
error = "[dim]N/A[/dim]"
|
|
80
82
|
else:
|
|
81
|
-
error
|
|
83
|
+
# Convert error code to human-readable text
|
|
84
|
+
error_code = status["error_code"]
|
|
85
|
+
if isinstance(error_code, int):
|
|
86
|
+
error_desc = get_error_description(component, error_code)
|
|
87
|
+
# Show both raw code and description with formatting
|
|
88
|
+
if error_code == 0:
|
|
89
|
+
error = "[green]:white_check_mark:[/green]"
|
|
90
|
+
else:
|
|
91
|
+
# Check if this is an unknown error
|
|
92
|
+
if "Unknown" in error_desc:
|
|
93
|
+
# For unknown errors, show the hex code prominently
|
|
94
|
+
error = f"[bold red]:warning: 0x{error_code:08X}[/bold red]\n[dim italic]Unknown error code[/dim italic]"
|
|
95
|
+
else:
|
|
96
|
+
# For known errors, show both code and description
|
|
97
|
+
error = f"[bold red]:warning: 0x{error_code:08X}[/bold red]\n[yellow]{error_desc}[/yellow]"
|
|
98
|
+
else:
|
|
99
|
+
# Handle hex string format if provided
|
|
100
|
+
try:
|
|
101
|
+
error_code_int = (
|
|
102
|
+
int(error_code, 16)
|
|
103
|
+
if isinstance(error_code, str)
|
|
104
|
+
else error_code
|
|
105
|
+
)
|
|
106
|
+
error_desc = get_error_description(component, error_code_int)
|
|
107
|
+
# Show both raw code and description with formatting
|
|
108
|
+
if error_code_int == 0:
|
|
109
|
+
error = "[green]:white_check_mark:[/green]"
|
|
110
|
+
else:
|
|
111
|
+
# Check if this is an unknown error
|
|
112
|
+
if "Unknown" in error_desc:
|
|
113
|
+
# For unknown errors, show the hex code prominently
|
|
114
|
+
error = f"[bold red]:warning: 0x{error_code_int:08X}[/bold red]\n[dim italic]Unknown error code[/dim italic]"
|
|
115
|
+
else:
|
|
116
|
+
# For known errors, show both code and description
|
|
117
|
+
error = f"[bold red]:warning: 0x{error_code_int:08X}[/bold red]\n[yellow]{error_desc}[/yellow]"
|
|
118
|
+
except (ValueError, TypeError):
|
|
119
|
+
error = f"[red]{str(error_code)}[/red]"
|
|
82
120
|
|
|
83
121
|
table.add_row(
|
|
84
122
|
component,
|
|
@@ -89,3 +127,40 @@ def show_component_status(status_info: dict[str, dict]) -> None:
|
|
|
89
127
|
|
|
90
128
|
console = Console()
|
|
91
129
|
console.print(table)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def show_ntp_stats(stats: dict[str, float]):
|
|
133
|
+
"""Display NTP statistics in a Rich table format.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
stats: Dictionary containing NTP statistics (e.g., mean_offset, mean_rtt, etc.).
|
|
137
|
+
"""
|
|
138
|
+
table = Table()
|
|
139
|
+
table.add_column("Time Statistic", style="cyan")
|
|
140
|
+
table.add_column("Value (Unit: second)", justify="right")
|
|
141
|
+
|
|
142
|
+
for key, value in stats.items():
|
|
143
|
+
# Format floats to 6 decimal places, lists as comma-separated, others as str
|
|
144
|
+
if isinstance(value, float):
|
|
145
|
+
value_str = f"{value:.6f}"
|
|
146
|
+
elif isinstance(value, list):
|
|
147
|
+
value_str = ", ".join(
|
|
148
|
+
f"{v:.6f}" if isinstance(v, float) else str(v) for v in value
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
value_str = str(value)
|
|
152
|
+
table.add_row(key, value_str)
|
|
153
|
+
|
|
154
|
+
console = Console()
|
|
155
|
+
console.print(table)
|
|
156
|
+
|
|
157
|
+
if "offset (mean)" in stats:
|
|
158
|
+
offset = stats["offset (mean)"]
|
|
159
|
+
if offset > 0:
|
|
160
|
+
logger.info(
|
|
161
|
+
f"To synchronize: server_time ≈ local_time + {offset:.3f} second"
|
|
162
|
+
)
|
|
163
|
+
else:
|
|
164
|
+
logger.info(
|
|
165
|
+
f"To synchronize: server_time ≈ local_time - {abs(offset):.3f} second"
|
|
166
|
+
)
|