palletizer-full-stack 0.2.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.
Files changed (35) hide show
  1. palletizer_full/__init__.py +37 -0
  2. palletizer_full/config.py +122 -0
  3. palletizer_full/control/__init__.py +1 -0
  4. palletizer_full/control/gripper_controller.py +75 -0
  5. palletizer_full/control/joint_synchronization.py +77 -0
  6. palletizer_full/control/motion_controller.py +87 -0
  7. palletizer_full/core/__init__.py +1 -0
  8. palletizer_full/core/communication.py +70 -0
  9. palletizer_full/core/concurrency_management.py +39 -0
  10. palletizer_full/core/consistency_verification.py +53 -0
  11. palletizer_full/core/environment_adapter.py +53 -0
  12. palletizer_full/core/execution_stack.py +70 -0
  13. palletizer_full/core/fault_detection.py +38 -0
  14. palletizer_full/core/hardware_synchronization.py +45 -0
  15. palletizer_full/core/hazard_manager.py +129 -0
  16. palletizer_full/core/memory_management.py +65 -0
  17. palletizer_full/optimizer.py +377 -0
  18. palletizer_full/orchestrator.py +153 -0
  19. palletizer_full/perception/__init__.py +1 -0
  20. palletizer_full/perception/sensor_io.py +32 -0
  21. palletizer_full/perception/sensor_processing.py +45 -0
  22. palletizer_full/planning/__init__.py +1 -0
  23. palletizer_full/planning/mission_planner.py +74 -0
  24. palletizer_full/planning/pattern_manager.py +56 -0
  25. palletizer_full/power/__init__.py +1 -0
  26. palletizer_full/power/battery_management.py +65 -0
  27. palletizer_full/power/thermal_management.py +69 -0
  28. palletizer_full/robot_config.py +125 -0
  29. palletizer_full/run.py +69 -0
  30. palletizer_full_stack-0.2.0.dist-info/METADATA +396 -0
  31. palletizer_full_stack-0.2.0.dist-info/RECORD +35 -0
  32. palletizer_full_stack-0.2.0.dist-info/WHEEL +5 -0
  33. palletizer_full_stack-0.2.0.dist-info/entry_points.txt +3 -0
  34. palletizer_full_stack-0.2.0.dist-info/licenses/LICENSE +189 -0
  35. palletizer_full_stack-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,153 @@
1
+ """
2
+ Orchestrator for the Palletiser
3
+ ===============================
4
+
5
+ The :class:`PalletiserOrchestrator` instantiates and wires together
6
+ all subsystems: sensor IO, processing, planning, motion control,
7
+ gripper control, power and thermal management, hazard detection and
8
+ communication. It runs these subsystems in a deterministic loop
9
+ via the :class:`ExecutionStack`.
10
+
11
+ Usage::
12
+
13
+ from palletizer_full.orchestrator import PalletiserOrchestrator
14
+
15
+ config = Config()
16
+ robot = MyRobotSDKInterface() # implements RobotInterface
17
+ orchestrator = PalletiserOrchestrator(config, robot)
18
+ orchestrator.run()
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import logging
24
+ import time
25
+ from typing import Any
26
+
27
+ from .config import Config
28
+ from .control.gripper_controller import GripperController
29
+ from .control.motion_controller import MotionController, RobotInterface
30
+ from .core.communication import CommunicationInterface
31
+ from .core.concurrency_management import ReentrantLock
32
+ from .core.consistency_verification import ConsistencyVerifier
33
+ from .core.execution_stack import ExecutionStack
34
+ from .core.fault_detection import FaultDetector
35
+ from .core.hazard_manager import HazardManager
36
+ from .core.memory_management import MemoryManager
37
+ from .perception.sensor_io import SensorIO
38
+ from .perception.sensor_processing import SensorProcessor
39
+ from .planning.mission_planner import MissionPlanner
40
+ from .planning.pattern_manager import PatternManager
41
+ from .power.battery_management import BatteryManager
42
+ from .power.thermal_management import ThermalManager
43
+
44
+
45
+ class PalletiserOrchestrator:
46
+ """High-level orchestrator for the palletising cell."""
47
+
48
+ def __init__(self, config: Config, robot: RobotInterface) -> None:
49
+ self.config = config
50
+
51
+ # Subsystems
52
+ self.memory = MemoryManager(config.total_memory_bytes)
53
+ self.hazard_manager = HazardManager(config)
54
+ self.fault_detector = FaultDetector()
55
+ self.consistency = ConsistencyVerifier()
56
+ self.communication = CommunicationInterface(config)
57
+ self.battery = BatteryManager(
58
+ config.battery_capacity_wh,
59
+ config.low_battery_threshold,
60
+ )
61
+ self.thermal = ThermalManager(
62
+ config.max_temperature_c,
63
+ config.cooling_hysteresis_c,
64
+ )
65
+ self.sensor_io = SensorIO()
66
+ self.sensor_processor = SensorProcessor()
67
+ self.pattern_manager = PatternManager()
68
+ self.planner = MissionPlanner(self.pattern_manager)
69
+
70
+ # Robot motion and gripper
71
+ default_velocities = tuple(1.0 for _ in robot.get_joint_positions())
72
+ self.motion = MotionController(robot, default_velocities)
73
+ self.gripper = GripperController(
74
+ close_fn=lambda: None,
75
+ open_fn=lambda: None,
76
+ read_pressure_fn=None,
77
+ )
78
+
79
+ # Concurrency primitive protecting shared state
80
+ self._lock = ReentrantLock()
81
+ self._logger = logging.getLogger(self.__class__.__name__)
82
+
83
+ # Execution stack
84
+ self._stack = ExecutionStack(
85
+ config=self.config,
86
+ on_cycle=self._on_cycle,
87
+ on_health_check=self._collect_metrics,
88
+ )
89
+
90
+ # ------------------------------------------------------------------
91
+ # Public API
92
+ # ------------------------------------------------------------------
93
+
94
+ def run(self, cycles: int | None = None) -> None:
95
+ """Start the main control loop."""
96
+ self.communication.connect()
97
+ self._stack.run(cycles)
98
+
99
+ def add_order(self, sku: str, quantity: int) -> None:
100
+ """Add a new palletising order to the planner."""
101
+ with self._lock:
102
+ self.planner.add_order(sku, quantity)
103
+
104
+ # ------------------------------------------------------------------
105
+ # Internal callbacks invoked by ExecutionStack
106
+ # ------------------------------------------------------------------
107
+
108
+ def _on_cycle(self, dt: float) -> None:
109
+ """Perform one control cycle."""
110
+ with self._lock:
111
+ # 1. Poll sensors
112
+ raw = self.sensor_io.read_all()
113
+ processed = self.sensor_processor.process(raw)
114
+
115
+ # 2. Update power and thermal subsystems
116
+ self.battery.update(current_draw_w=100.0)
117
+ self.thermal.update()
118
+
119
+ # 3. Evaluate hazards
120
+ hazard_inputs: dict[str, Any] = {
121
+ "proximity": processed.get("proximity"),
122
+ "faults": self.fault_detector.get_active_faults(),
123
+ "high_voltage": None,
124
+ "gas": None,
125
+ "radiation": None,
126
+ }
127
+ self.hazard_manager.update(hazard_inputs)
128
+
129
+ # 4. If safe and there is a pending order, perform a task
130
+ if self.hazard_manager.is_safe() and self.planner.has_next_task():
131
+ task = self.planner.next_task()
132
+ if task:
133
+ picked = self.gripper.pick()
134
+ if not picked:
135
+ self.fault_detector.report_fault("gripper_pick_failure")
136
+ return
137
+ time.sleep(0.01)
138
+ self.gripper.release()
139
+
140
+ # 5. Fault handling: if SOC is low, trigger a fault
141
+ if self.battery.is_low():
142
+ self.fault_detector.report_fault("low_battery")
143
+
144
+ def _collect_metrics(self) -> dict[str, Any]:
145
+ """Collect telemetry and health metrics."""
146
+ metrics: dict[str, Any] = {}
147
+ metrics.update(self.battery.telemetry())
148
+ metrics.update(self.thermal.telemetry())
149
+ metrics["memory_ok"] = self.memory.check_health()
150
+ metrics["faults"] = list(self.fault_detector.get_active_faults())
151
+ metrics["hazards"] = self.hazard_manager.current_hazards()
152
+ self.communication.send_telemetry(metrics)
153
+ return metrics
@@ -0,0 +1 @@
1
+ # Sensor IO and basic perception pipeline.
@@ -0,0 +1,32 @@
1
+ """
2
+ Sensor IO Module
3
+ ================
4
+
5
+ The :class:`SensorIO` class is the entry point for reading raw data
6
+ from cameras, lidars, proximity sensors and other devices. In this
7
+ reference implementation it returns placeholder data; replace the
8
+ ``read_all`` method with calls to your sensor SDKs.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from typing import Any
15
+
16
+
17
+ class SensorIO:
18
+ """Read raw sensor data from hardware devices."""
19
+
20
+ def __init__(self) -> None:
21
+ self._logger = logging.getLogger(self.__class__.__name__)
22
+
23
+ def read_all(self) -> dict[str, Any]:
24
+ """Poll all sensors and return a dictionary of raw readings.
25
+
26
+ Override this method to integrate real sensor hardware.
27
+ """
28
+ return {
29
+ "proximity": 1.0,
30
+ "box_present": False,
31
+ "weight_kg": 0.0,
32
+ }
@@ -0,0 +1,45 @@
1
+ """
2
+ Sensor Processing Module
3
+ ========================
4
+
5
+ The :class:`SensorProcessor` fuses raw sensor inputs to extract
6
+ higher-level information such as box presence, alignment and
7
+ orientation. For a palletiser the perception stack is intentionally
8
+ simple; you may add machine-learning-based detection later.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from typing import Any
15
+
16
+
17
+ class SensorProcessor:
18
+ """Process and fuse raw sensor readings."""
19
+
20
+ def __init__(self) -> None:
21
+ self._logger = logging.getLogger(self.__class__.__name__)
22
+
23
+ def process(self, raw: dict[str, Any]) -> dict[str, Any]:
24
+ """Process raw sensor data and return fused results.
25
+
26
+ Parameters
27
+ ----------
28
+ raw : dict
29
+ Raw sensor readings from :class:`SensorIO`.
30
+
31
+ Returns
32
+ -------
33
+ dict
34
+ Processed data including ``proximity``, ``box_detected``
35
+ and ``weight_kg``.
36
+ """
37
+ proximity = raw.get("proximity")
38
+ box_present = raw.get("box_present", False)
39
+ weight = raw.get("weight_kg", 0.0)
40
+
41
+ return {
42
+ "proximity": proximity,
43
+ "box_detected": bool(box_present) or (weight is not None and weight > 0.1),
44
+ "weight_kg": weight,
45
+ }
@@ -0,0 +1 @@
1
+ # Task and trajectory planning.
@@ -0,0 +1,74 @@
1
+ """
2
+ Mission Planner Module
3
+ ======================
4
+
5
+ The :class:`MissionPlanner` breaks down orders into pick/stack
6
+ sequences. It tracks progress, handles errors and recovers from
7
+ faults by re-planning or pausing the cell.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from collections import deque
14
+ from typing import Any
15
+
16
+ from .pattern_manager import PatternManager
17
+
18
+
19
+ class MissionPlanner:
20
+ """Plan and sequence palletising tasks.
21
+
22
+ Parameters
23
+ ----------
24
+ pattern_manager : PatternManager
25
+ Manager providing stacking patterns for each SKU.
26
+ """
27
+
28
+ def __init__(self, pattern_manager: PatternManager) -> None:
29
+ self._pattern_manager = pattern_manager
30
+ self._task_queue: deque[dict[str, Any]] = deque()
31
+ self._completed: int = 0
32
+ self._logger = logging.getLogger(self.__class__.__name__)
33
+
34
+ def add_order(self, sku: str, quantity: int) -> None:
35
+ """Add a palletising order for *quantity* cases of *sku*.
36
+
37
+ The planner looks up the stacking pattern for the SKU and
38
+ generates one task per case.
39
+ """
40
+ try:
41
+ pattern = self._pattern_manager.get_pattern(sku)
42
+ except KeyError:
43
+ self._logger.error("No pattern for SKU '%s'", sku)
44
+ return
45
+ for i in range(quantity):
46
+ pose = pattern[i % len(pattern)]
47
+ self._task_queue.append({
48
+ "sku": sku,
49
+ "index": i,
50
+ "pose": pose,
51
+ })
52
+ self._logger.info("Queued %d tasks for SKU '%s'", quantity, sku)
53
+
54
+ def has_next_task(self) -> bool:
55
+ """Return ``True`` if there are pending tasks."""
56
+ return len(self._task_queue) > 0
57
+
58
+ def next_task(self) -> dict[str, Any] | None:
59
+ """Pop and return the next task, or ``None`` if empty."""
60
+ if not self._task_queue:
61
+ return None
62
+ task = self._task_queue.popleft()
63
+ self._completed += 1
64
+ return task
65
+
66
+ @property
67
+ def completed_count(self) -> int:
68
+ """Number of tasks completed so far."""
69
+ return self._completed
70
+
71
+ @property
72
+ def pending_count(self) -> int:
73
+ """Number of tasks remaining in the queue."""
74
+ return len(self._task_queue)
@@ -0,0 +1,56 @@
1
+ """
2
+ Pattern Manager Module
3
+ ======================
4
+
5
+ The :class:`PatternManager` generates and stores pallet patterns.
6
+ Patterns are lists of ``(x, y, rotation)`` tuples describing where
7
+ each case should be placed on a layer. Patterns can be saved as
8
+ JSON and loaded into the planner.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import logging
15
+ from pathlib import Path
16
+
17
+
18
+ class PatternManager:
19
+ """Manage pallet stacking patterns."""
20
+
21
+ def __init__(self) -> None:
22
+ self._patterns: dict[str, list[tuple[float, float, float]]] = {}
23
+ self._logger = logging.getLogger(self.__class__.__name__)
24
+
25
+ def load_pattern(
26
+ self,
27
+ name: str,
28
+ poses: list[tuple[float, float, float]],
29
+ ) -> None:
30
+ """Register a pattern under *name*."""
31
+ self._patterns[name] = list(poses)
32
+ self._logger.info("Loaded pattern '%s' with %d poses", name, len(poses))
33
+
34
+ def get_pattern(self, name: str) -> list[tuple[float, float, float]]:
35
+ """Return the poses for pattern *name*."""
36
+ if name not in self._patterns:
37
+ msg = f"Unknown pattern: {name}"
38
+ raise KeyError(msg)
39
+ return list(self._patterns[name])
40
+
41
+ def list_patterns(self) -> list[str]:
42
+ """Return names of all registered patterns."""
43
+ return list(self._patterns.keys())
44
+
45
+ def save_to_json(self, path: str | Path) -> None:
46
+ """Persist all patterns to a JSON file."""
47
+ with open(path, "w") as fh:
48
+ json.dump(self._patterns, fh, indent=2)
49
+
50
+ def load_from_json(self, path: str | Path) -> None:
51
+ """Load patterns from a JSON file."""
52
+ with open(path) as fh:
53
+ data = json.load(fh)
54
+ for name, poses in data.items():
55
+ self._patterns[name] = [tuple(p) for p in poses]
56
+ self._logger.info("Loaded %d patterns from %s", len(data), path)
@@ -0,0 +1 @@
1
+ # Energy and thermal management.
@@ -0,0 +1,65 @@
1
+ """
2
+ Battery Management Module
3
+ =========================
4
+
5
+ The :class:`BatteryManager` monitors state-of-charge and available
6
+ energy. It uses LiFePO4 battery models by default, as these cells
7
+ offer high energy density, long cycle life and excellent safety
8
+ characteristics.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from typing import Any
15
+
16
+
17
+ class BatteryManager:
18
+ """Monitor battery state-of-charge and energy.
19
+
20
+ Parameters
21
+ ----------
22
+ capacity_wh : float
23
+ Total battery capacity in watt-hours.
24
+ low_threshold : float
25
+ Fractional charge level (0-1) below which the battery is
26
+ considered low.
27
+ """
28
+
29
+ def __init__(self, capacity_wh: float, low_threshold: float = 0.2) -> None:
30
+ self._capacity_wh = capacity_wh
31
+ self._remaining_wh = capacity_wh
32
+ self._low_threshold = low_threshold
33
+ self._logger = logging.getLogger(self.__class__.__name__)
34
+
35
+ def update(self, current_draw_w: float, dt_s: float = 0.02) -> None:
36
+ """Update the remaining energy based on current draw.
37
+
38
+ Parameters
39
+ ----------
40
+ current_draw_w : float
41
+ Instantaneous power draw in watts.
42
+ dt_s : float
43
+ Time step in seconds (defaults to 50 Hz cycle).
44
+ """
45
+ consumed = current_draw_w * (dt_s / 3600.0)
46
+ self._remaining_wh = max(0.0, self._remaining_wh - consumed)
47
+
48
+ @property
49
+ def soc(self) -> float:
50
+ """State of charge as a fraction (0-1)."""
51
+ if self._capacity_wh <= 0:
52
+ return 0.0
53
+ return self._remaining_wh / self._capacity_wh
54
+
55
+ def is_low(self) -> bool:
56
+ """Return ``True`` if the battery is below the low threshold."""
57
+ return self.soc < self._low_threshold
58
+
59
+ def telemetry(self) -> dict[str, Any]:
60
+ """Return battery telemetry data."""
61
+ return {
62
+ "battery_soc": round(self.soc, 4),
63
+ "battery_remaining_wh": round(self._remaining_wh, 2),
64
+ "battery_low": self.is_low(),
65
+ }
@@ -0,0 +1,69 @@
1
+ """
2
+ Thermal Management Module
3
+ =========================
4
+
5
+ The :class:`ThermalManager` monitors actuator temperatures and
6
+ triggers cooling when thresholds are exceeded. It applies a
7
+ hysteresis strategy to avoid rapid cycling of fans or pumps.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from typing import Any
14
+
15
+
16
+ class ThermalManager:
17
+ """Monitor temperatures and control cooling.
18
+
19
+ Parameters
20
+ ----------
21
+ max_temp_c : float
22
+ Maximum safe temperature in degrees Celsius.
23
+ hysteresis_c : float
24
+ Temperature difference between activating and deactivating
25
+ cooling.
26
+ """
27
+
28
+ def __init__(self, max_temp_c: float = 70.0, hysteresis_c: float = 10.0) -> None:
29
+ self._max_temp = max_temp_c
30
+ self._hysteresis = hysteresis_c
31
+ self._current_temp = 25.0 # ambient default
32
+ self._cooling_active = False
33
+ self._logger = logging.getLogger(self.__class__.__name__)
34
+
35
+ def update(self, temperature_c: float | None = None) -> None:
36
+ """Update the current temperature reading.
37
+
38
+ If *temperature_c* is ``None``, the manager simulates a
39
+ small temperature increase for demonstration purposes.
40
+ """
41
+ if temperature_c is not None:
42
+ self._current_temp = temperature_c
43
+ else:
44
+ self._current_temp += 0.01 # placeholder drift
45
+
46
+ if self._current_temp >= self._max_temp:
47
+ if not self._cooling_active:
48
+ self._logger.warning(
49
+ "Temperature %.1f C exceeds max %.1f C — activating cooling",
50
+ self._current_temp,
51
+ self._max_temp,
52
+ )
53
+ self._cooling_active = True
54
+ elif self._current_temp <= self._max_temp - self._hysteresis:
55
+ if self._cooling_active:
56
+ self._logger.info("Temperature normalised — deactivating cooling")
57
+ self._cooling_active = False
58
+
59
+ @property
60
+ def is_cooling(self) -> bool:
61
+ """Return ``True`` if cooling is currently active."""
62
+ return self._cooling_active
63
+
64
+ def telemetry(self) -> dict[str, Any]:
65
+ """Return thermal telemetry data."""
66
+ return {
67
+ "temperature_c": round(self._current_temp, 2),
68
+ "cooling_active": self._cooling_active,
69
+ }
@@ -0,0 +1,125 @@
1
+ """
2
+ Robot Configuration Parser
3
+ ==========================
4
+
5
+ Reads configuration from environment variables and exposes a
6
+ strongly-typed, frozen :class:`RobotConfig` dataclass. This allows
7
+ the system to adapt to different factory environments, hardware
8
+ configurations and operating modes without changing code.
9
+
10
+ Boolean values accept ``1``, ``true``, ``t``, ``yes``, ``y`` or
11
+ ``on`` (case-insensitive) as true; everything else is false.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ from dataclasses import dataclass
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Helpers
21
+ # ---------------------------------------------------------------------------
22
+
23
+ def _env(name: str, default: str) -> str:
24
+ return os.getenv(name, default)
25
+
26
+
27
+ def _env_bool(name: str, default: bool) -> bool:
28
+ val = os.getenv(name)
29
+ if val is None:
30
+ return default
31
+ return str(val).strip().lower() in {"1", "true", "t", "yes", "y", "on"}
32
+
33
+
34
+ def _env_floats(name: str, default: tuple[float, ...]) -> tuple[float, ...]:
35
+ raw = os.getenv(name)
36
+ if not raw:
37
+ return default
38
+ out: list[float] = []
39
+ for part in raw.split(","):
40
+ part = part.strip()
41
+ if not part:
42
+ continue
43
+ try:
44
+ out.append(float(part))
45
+ except ValueError:
46
+ msg = f"Invalid float in {name}: {part}"
47
+ raise ValueError(msg) from None
48
+ return tuple(out)
49
+
50
+
51
+ def _env_csv(name: str, default: tuple[str, ...]) -> tuple[str, ...]:
52
+ raw = os.getenv(name)
53
+ if not raw:
54
+ return default
55
+ return tuple(s.strip() for s in raw.split(",") if s.strip())
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # RobotConfig
60
+ # ---------------------------------------------------------------------------
61
+
62
+ @dataclass(frozen=True)
63
+ class RobotConfig:
64
+ """Configuration for the palletiser control stack.
65
+
66
+ Fields correspond to environment variables prefixed with
67
+ ``PALLETIZER_``.
68
+ """
69
+
70
+ robot_environment: str
71
+ operation_mode: str
72
+ simulator_enabled: bool
73
+ joint_ids: tuple[str, ...]
74
+ actuator_types: tuple[str, ...]
75
+ max_velocity_per_joint: tuple[float, ...]
76
+ max_torque_per_joint: tuple[float, ...]
77
+ min_safety_margin: float
78
+ safety_thresholds: tuple[float, ...]
79
+ control_frequency_hz: float
80
+ communication_endpoint: str | None
81
+
82
+ def __init__(self) -> None:
83
+ object.__setattr__(self, "robot_environment", _env("PALLETIZER_ENV", "FACTORY"))
84
+ object.__setattr__(self, "operation_mode", _env("PALLETIZER_MODE", "PRODUCTION"))
85
+ object.__setattr__(self, "simulator_enabled", _env_bool("PALLETIZER_SIM", False))
86
+ object.__setattr__(
87
+ self,
88
+ "joint_ids",
89
+ _env_csv("PALLETIZER_JOINT_IDS", ("j0", "j1", "j2", "j3", "j4", "j5")),
90
+ )
91
+ object.__setattr__(
92
+ self,
93
+ "actuator_types",
94
+ _env_csv("PALLETIZER_ACTUATORS", ("REVOLUTE",) * 6),
95
+ )
96
+ object.__setattr__(
97
+ self,
98
+ "max_velocity_per_joint",
99
+ _env_floats("PALLETIZER_MAX_VEL", (1.0,) * 6),
100
+ )
101
+ object.__setattr__(
102
+ self,
103
+ "max_torque_per_joint",
104
+ _env_floats("PALLETIZER_MAX_TORQUE", (100.0,) * 6),
105
+ )
106
+ object.__setattr__(
107
+ self,
108
+ "min_safety_margin",
109
+ float(os.getenv("PALLETIZER_SAFETY_MARGIN", "0.4")),
110
+ )
111
+ object.__setattr__(
112
+ self,
113
+ "safety_thresholds",
114
+ _env_floats("PALLETIZER_SAFETY_THRESHOLDS", ()),
115
+ )
116
+ object.__setattr__(
117
+ self,
118
+ "control_frequency_hz",
119
+ float(os.getenv("PALLETIZER_FREQ", "50.0")),
120
+ )
121
+ object.__setattr__(
122
+ self,
123
+ "communication_endpoint",
124
+ os.getenv("PALLETIZER_COMM_ENDPOINT"),
125
+ )