Files
dasha_f 0d32f32db0 Сортировочная ячейка Isaac Sim: CV-пайплайн и меши товаров
Замкнутый контур "поток -> CV -> механика": товары идут по конвейеру с шагом 700 мм,
класс определяется стереопайплайном во время движения, пушер и плуг реагируют физически.

Состав:
* control_test/ - ячейка и CV. run_sorting_cv.py + cv_worker.py (два процесса, потому что
  torch внутри Isaac роняет сцену), cell.py (физика лент, плуга, пушера), measure_plane.py
  (замер габаритов), README.md и .memory.md с замерами, проблемами и ловушками
* robozon_sorter/ - модули симуляции, scripts/ - утилиты, scene/ - сцены
* assets/ - меши товаров, плуг, объекты Objaverse

Бейзлайн CV: DEFOM-Stereo vitl, вход 480, iters 24, кроп зоны осмотра, без сегментации.
На потоке 700 мм - классы 8/9, габариты MAE 32.8 мм, 469 мс на товар при такте 700 мс.

Веса моделей (4.5 ГБ) и пропсы конвейера NVIDIA (274 МБ) не включены - источники и
команды скачивания в MODELS.md. Выход прогонов (captures/, runtime/) не включён:
воспроизводится.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 13:07:24 +00:00

136 lines
5.2 KiB
Python

"""Self-running item feeder: press Play and goods appear on the infeed belt one at a time,
spaced by a fixed pitch along the belt.
It hooks a PhysX step callback rather than living in an outer async loop, so the scene runs
on its own from the Play button - no driver script has to be babysitting it. The same
callback also drives the laser gate and the pusher when `route` is enabled.
Pitch is measured along the belt between consecutive items, so the release condition is
simply "the last one released has travelled PITCH from the spawn point".
"""
from __future__ import annotations
from .. import config as C
class AutoFeeder:
def __init__(self, cell, order=None, pitch=None, loop=False,
route=None, on_event=None):
"""
cell : mechanics.Cell
order : release order; defaults to every loaded item
pitch : metres between consecutive items along the belt
route : dict name -> class; when given, class D is diverted by the pusher
on_event : optional callback(kind, name, payload) for logging
"""
self.cell = cell
self.order = list(order or cell.items)
self.pitch = pitch if pitch is not None else C.RELEASE_GAP
self.loop = loop
self.route = route or {}
self.on_event = on_event
self._sub = None
self.reset()
def reset(self):
self.next_index = 0
self.active = []
self.released = []
self.diverted = set()
self.finished = {}
self._busy = False # a push cycle owns the blade until it completes
self._cycle = None
# ------------------------------------------------------------------ install
def install(self):
"""subscribe to the physics step; from here on the cell runs itself on Play"""
from omni.physx import get_physx_interface
if self._sub is None:
self._sub = get_physx_interface().subscribe_physics_step_events(self._on_step)
return self
def remove(self):
self._sub = None
def _emit(self, kind, name, payload=None):
if self.on_event:
self.on_event(kind, name, payload or {})
# ------------------------------------------------------------------ per step
def _on_step(self, dt):
try:
self._release_due()
self._service_gate(dt)
self._retire()
except Exception as exc: # never let a callback kill the sim
self._emit("error", "", {"exc": repr(exc)})
def _release_due(self):
if self._busy or self.next_index >= len(self.order):
if self.loop and self.next_index >= len(self.order) and not self.active:
self.next_index = 0
return
if self.active:
travelled = C.SPAWN_X - float(self.cell.pose(self.active[-1])[0])
if travelled < self.pitch:
return
name = self.order[self.next_index]
self.cell.release(name)
self.active.append(name)
self.released.append(name)
self.next_index += 1
self._emit("release", name, {"pitch": self.pitch})
def _service_gate(self, dt):
"""laser gate -> pusher, as a small state machine so it spans several steps"""
if self._cycle is not None:
self._step_cycle(dt)
return
for name in list(self.active):
if name in self.diverted or self.route.get(name) != "D":
continue
if self.cell.laser() == name:
self._cycle = dict(name=name, phase="extend", t=0.0,
y=C.BLADE_HOME_Y, held=0.0)
self._busy = True
self._emit("gate", name, {})
return
def _step_cycle(self, dt):
c = self._cycle
name = c["name"]
speed = C.PUSHER_SPEED
if c["phase"] == "extend":
c["y"] = min(C.BLADE_OUT_Y, c["y"] + speed * dt)
self.cell.blade_to(c["y"])
if c["y"] >= C.BLADE_OUT_Y - 1e-6:
c["phase"] = "clear"
elif c["phase"] == "clear":
c["t"] += dt
if float(self.cell.pose(name)[1]) > 0.50 or c["t"] > 1.5:
c["phase"] = "wait"
c["t"] = 0.0
elif c["phase"] == "wait":
# do not sweep the blade back through whatever has already arrived
busy = self.cell.blade_path_busy(name)
c["t"] += dt
if busy is None or c["t"] > 1.5:
c["held"] = c["t"]
c["phase"] = "retract"
elif c["phase"] == "retract":
c["y"] = max(C.BLADE_HOME_Y, c["y"] - speed * dt)
self.cell.blade_to(c["y"])
if c["y"] <= C.BLADE_HOME_Y + 1e-6:
self.diverted.add(name)
self._busy = False
self._cycle = None
self._emit("divert", name, {"held": round(c["held"], 3)})
def _retire(self):
for name in list(self.active):
place = self.cell.where(name)
if place in ("bin", "line-end"):
self.finished[name] = place
self.active.remove(name)
self._emit("done", name, {"where": place})