|
|
@@ -1,10 +1,10 @@
|
|
|
from dataclasses import dataclass
|
|
|
from enum import Enum
|
|
|
from typing import List, Tuple
|
|
|
-from utils.config import ConfigParser, SlotConfig
|
|
|
+from utils.config import ConfigParser, SlotConfig, DeviceConfig
|
|
|
from robot.movement import RobotMovement
|
|
|
from utils.logging import LoggerSingleton
|
|
|
-from api.mqtt_handler import MQTTHandler, Device
|
|
|
+from api.mqtt_handler import MQTTHandler, MeasurementResult
|
|
|
from api.grbl_handler import GRBLHandler
|
|
|
|
|
|
logger = LoggerSingleton.get_logger()
|
|
|
@@ -17,7 +17,7 @@ class CellStatus(Enum):
|
|
|
|
|
|
@dataclass
|
|
|
class Cell:
|
|
|
- id: str
|
|
|
+ id: int
|
|
|
status: CellStatus = CellStatus.WAITING
|
|
|
measurement_slot: int = None
|
|
|
capacity: float = None
|
|
|
@@ -30,6 +30,7 @@ class DropoffGrade:
|
|
|
class RobotController:
|
|
|
def __init__(self, config: ConfigParser):
|
|
|
self.config = config
|
|
|
+ self.cells:dict[Cell] = {}
|
|
|
self.devices = self.config.get_devices()
|
|
|
self.feeder = self.config.get_feeder()
|
|
|
self.dropoff_grades = self.config.get_dropoff_grades()
|
|
|
@@ -45,7 +46,7 @@ class RobotController:
|
|
|
|
|
|
# Initialize with configured values
|
|
|
self.total_slots = sum(len(device.slots) for device in self.devices)
|
|
|
- self.work_queue: List[SlotConfig] = []
|
|
|
+ self.work_queue: List[MeasurementResult] = []
|
|
|
|
|
|
self.gripper_occupied = False
|
|
|
|
|
|
@@ -61,7 +62,9 @@ class RobotController:
|
|
|
# Register all devices with MQTT handler
|
|
|
for device in self.devices:
|
|
|
self.mqtt_handler.register_device(
|
|
|
- Device(device_id=device.id, num_slots=len(device.slots))
|
|
|
+ device_id=device.id,
|
|
|
+ num_slots=len(device.slots),
|
|
|
+ callback=lambda measurement_result: self.work_queue.append(measurement_result)
|
|
|
)
|
|
|
|
|
|
async def connect(self):
|
|
|
@@ -73,13 +76,30 @@ class RobotController:
|
|
|
# await self.movement.cleanup() TODO[SG]: Implement cleanup method in movement.py
|
|
|
self.mqtt_handler.cleanup()
|
|
|
|
|
|
- def get_device_by_id(self, device_id: str):
|
|
|
+ def add_cell(self, cell_id: str):
|
|
|
+ self.cells[cell_id] = Cell(cell_id)
|
|
|
+ return self.cells[cell_id]
|
|
|
+
|
|
|
+ def get_cell_by_id(self, cell_id: str) -> Cell:
|
|
|
+ return self.cells.get(cell_id, None)
|
|
|
+
|
|
|
+ def get_device_by_id(self, device_id: str) -> DeviceConfig:
|
|
|
for device in self.devices:
|
|
|
if device.id == device_id:
|
|
|
return device
|
|
|
+ logger.error(f"Device {device_id} not found")
|
|
|
return None
|
|
|
+
|
|
|
+ def get_slot_by_id(self, device_id: str, slot_id: int) -> Cell:
|
|
|
+ try:
|
|
|
+ return self.get_device_by_id(device_id).slots[slot_id]
|
|
|
+ except AttributeError as e:
|
|
|
+ return None
|
|
|
+ except IndexError as e:
|
|
|
+ logger.error(f"Slot {slot_id} not found in device {device_id}")
|
|
|
+ return None
|
|
|
|
|
|
- def get_next_free_slot(self):
|
|
|
+ def get_next_free_slot(self) -> SlotConfig:
|
|
|
for device in self.devices:
|
|
|
for slot in device.slots:
|
|
|
if not slot.occupied:
|
|
|
@@ -89,14 +109,34 @@ class RobotController:
|
|
|
logger.warning("No free slots available")
|
|
|
return None
|
|
|
|
|
|
- def process_finished_measurement(self):
|
|
|
+ async def process_finished_measurement(self):
|
|
|
if not self.work_queue:
|
|
|
logger.warning("No finished measurements in queue")
|
|
|
return
|
|
|
- completed_slot = self.work_queue.pop(0)
|
|
|
- cell_id = self.collect_cell_from_slot(completed_slot)
|
|
|
- cell = Cell(cell_id)
|
|
|
- self.sort_cell(cell)
|
|
|
+
|
|
|
+ measurement_result = self.work_queue.pop(0)
|
|
|
+ cell = self.get_cell_by_id(measurement_result.cell_id)
|
|
|
+ try:
|
|
|
+ cell_status = CellStatus(measurement_result.status)
|
|
|
+ except ValueError as e:
|
|
|
+ logger.error(f"Measurement ({measurement_result}) could not be processed. Invalid cell status '{measurement_result.status}'")
|
|
|
+ return
|
|
|
+
|
|
|
+ waiting_slot = self.get_slot_by_id(measurement_result.device_id, measurement_result.slot_id)
|
|
|
+ if not waiting_slot:
|
|
|
+ logger.error(f"Measurement ({measurement_result}) could not be processed.")
|
|
|
+ return
|
|
|
+ await self.collect_cell_from_slot(waiting_slot)
|
|
|
+
|
|
|
+
|
|
|
+ if not cell: # Cell not found, create new
|
|
|
+ cell = Cell(measurement_result.cell_id, cell_status, capacity=measurement_result.capacity)
|
|
|
+ self.cells[measurement_result.cell_id] = cell
|
|
|
+ else:
|
|
|
+ cell.capacity = measurement_result.capacity
|
|
|
+ cell.status = cell_status
|
|
|
+
|
|
|
+ await self.sort_cell(cell)
|
|
|
|
|
|
async def pick_cell_from_feeder(self):
|
|
|
if self.gripper_occupied:
|
|
|
@@ -165,8 +205,7 @@ class RobotController:
|
|
|
self.mqtt_handler.start_measurement(
|
|
|
device_id=slot.device_id,
|
|
|
slot=slot.slot_id,
|
|
|
- cell_id=cell.id,
|
|
|
- callback=lambda device_id, slot, cell_id, capacity: self.work_queue.append((cell_id, slot, device_id, capacity))
|
|
|
+ cell_id=cell.id
|
|
|
)
|
|
|
|
|
|
# Move back to safe height
|
|
|
@@ -212,14 +251,17 @@ class RobotController:
|
|
|
if not self.gripper_occupied:
|
|
|
logger.error("Gripper not occupied")
|
|
|
return False
|
|
|
- if cell.status == CellStatus.FAILED:
|
|
|
+
|
|
|
+ if cell.id in self.cells:
|
|
|
+ del self.cells[cell.id] # Remove cell from our database TODO [SG]: Should we keep it for history?
|
|
|
+
|
|
|
+ if cell.status is CellStatus.FAILED:
|
|
|
await self.dropoff_cell(self.rejected_grade)
|
|
|
logger.info(f"Cell {cell.id} sorted to rejected grade")
|
|
|
return True
|
|
|
for name, grade in self.dropoff_grades.items():
|
|
|
if cell.capacity >= grade.capacity_threshold:
|
|
|
await self.dropoff_cell(grade)
|
|
|
- self.gripper_occupied = False
|
|
|
logger.info(f"Cell {cell.id} sorted to grade {name}")
|
|
|
return True
|
|
|
logger.error(f"No suitable grade found for cell {cell.id} with capacity {cell.capacity}")
|