Лабораторна робота 21
Лабораторна робота №21. Автоматичне сортування
Мета: спроєктувати й перевірити інтеграційний автомат подавача, конвеєра, сенсора, камери, mock-MG400 і GripperAdapter, який координує готовність, класифікує деталь, формує символічний маршрут, вимірює цикл і переходить у безпечний стан при невпевненості, тайм-ауті або втраті підсистеми.
Результати навчання та передумови
Після роботи студент уміє:
- визначити контракти готовності та завершення шести підсистем;
- реалізувати послідовний автомат із локальними тайм-аутами;
- відокремити
REJECTчерез невизначеність відFAULTчерез несправність; - не використовувати застарілу
gripper_control_by_mg400, а працювати черезGripperAdapter/mock; - зупинити всі mock-виходи й вимагати ручної звірки після відмови або перезапуску;
- обчислити throughput, success/reject/fault rate та розподіл причин.
Передумови: ЛР-05–10, ЛР-13, ЛР-16–18 і ЛР-20.
Середовище виконання
| Середовище | Статус | Примітка |
|---|---|---|
| Google Colab | дозволено | Повний mock зі стандартною бібліотекою |
| Локальний ПК | основне | JSON-сценарії, assertions, журнали |
| Raspberry Pi | дозволено | Тільки mock; без GPIO, UART, USB і API MG400 |
| Симуляція | основне | Символічні команди та логічний час |
| Фізичний комплекс MERC-I5 | не використовується | Активація, рух, I/O та грипер заборонені |
Необхідні знання та матеріали
- Python зі стандартною бібліотекою;
- зовнішня конфігурація й fixtures із цієї роботи;
- цільова структура драйвера MG400;
- довідник грипера, операційна безпека.
Ризики та правила безпеки
Ризики
Різні підсистеми можуть мати несумісні або застарілі стани; деталь може бути втрачена між сенсором і захопленням; камера — повернути невпевненість; грипер — не підтвердити захоплення; robot action — зависнути. Повтор повідомлення може дублювати цикл.
Заборонені дії
- не імпортувати або відновлювати
gripper_control_by_mg400; - не викликати мережеві команди MG400, UART грипера, GPIO чи реле;
- не продовжувати pick при
UNKNOWN, низькій confidence або відсутній трансформації; - не виконувати automatic retry рухової команди;
- не скидати
FAULTпісля restart без фактичної звірки.
Умови негайної зупинки
Будь-яка неготова підсистема, timeout, camera lost, invalid class, robot/gripper no-ack, дублікат cycle ID, перезапуск посеред циклу або невідомий стан. Mock-виходи переходять у STOP, стан — RECOVERY_REQUIRED.
Безпечний стан
Подавач і конвеєр STOP; mock-robot не приймає нові goals; mock-gripper HOLD_OR_STOP (конкретна фізична реакція ще не підтверджена); причина latched; автоматичний restart відсутній.
Передпусковий чекліст
-
mode=simulation-only. - Усі маршрути символічні, без координат стенда.
- Адаптери лише записують журнал.
- Є окремі timeout для feed, transport, robot і gripper.
-
UNKNOWNне створює pick-plan. - Recovery вимагає ручного ack та reconciliation.
Хід виконання роботи
- Перевірити route/reject gate.
- Задати конфігурацію й сценарії.
- Реалізувати інтеграційний FSM і safe stop.
- Запустити нормальний, reject і fault cases.
- Перевірити restart та ручне відновлення.
- Розрахувати метрики.
Методичні вказівки й теоретичні відомості
1. Оркестрація, а не пряме керування
Оркестратор працює з контрактами ready, start, status, stop, ack, а конкретні драйвери приховані за адаптерами. Це дозволяє замінити mock апаратною реалізацією лише після окремої валідації. Команда не є доказом завершення: кожен етап має незалежне підтвердження.
2. REJECT, FAULT, RECOVERY_REQUIRED
REJECT — коректно оброблена невизначеність даних: виріб не класифіковано, новий рух не планується. FAULT — порушення контракту/тайм-аут. Після safe stop автомат переходить у RECOVERY_REQUIRED, бо положення деталі не можна вгадувати. COMPLETE у цій роботі означає завершення mock-циклу, а не фізичне сортування.
Текстовий опис рисунка: після перевірки готовності автомат координує подавання, транспортування, інспекцію, mock-захоплення й розміщення; невідомий клас веде у REJECT, а несправності — через FAULT у RECOVERY_REQUIRED до ручної звірки.
stateDiagram-v2
[*] --> IDLE
IDLE --> CHECK_READY: start
CHECK_READY --> FEEDING: all ready
FEEDING --> TRANSPORTING: one part confirmed
TRANSPORTING --> INSPECTING: sensor confirmed + stop ack
INSPECTING --> PICKING: class + route + transform valid
INSPECTING --> REJECT: UNKNOWN / low confidence
PICKING --> PLACING: robot + grip ack
PLACING --> COMPLETE: placement ack
CHECK_READY --> FAULT: subsystem unavailable
FEEDING --> FAULT: timeout / double feed
TRANSPORTING --> FAULT: timeout
INSPECTING --> FAULT: camera lost
PICKING --> FAULT: robot/gripper timeout
FAULT --> RECOVERY_REQUIRED: safe stop
RECOVERY_REQUIRED --> IDLE: manual reconciliation
Рис. 1. Інтеграційний автомат автоматичного сортування
3. Джерела
- програма ЛР-21;
- аналіз безпечного драйвера MG400;
- адаптивний грипер і статус старої залежності;
- правила допуску.
Виконання лабораторної роботи
Крок 1. Safe-smoke рішення про маршрут
# safe-smoke
def decision(label: str, confidence: float, routes: dict[str, str], threshold: float) -> tuple[str, str | None]:
if label not in routes or confidence < threshold:
return "REJECT", None
return "ACCEPT", routes[label]
routes = {"red-cylinder": "BOX_A", "blue-cube": "BOX_B"}
assert decision("red-cylinder", 0.90, routes, 0.80) == ("ACCEPT", "BOX_A")
assert decision("red-cylinder", 0.79, routes, 0.80) == ("REJECT", None)
assert decision("UNKNOWN", 1.00, routes, 0.80) == ("REJECT", None)
print("safe-smoke: accept/reject gate PASS")
Очікуваний результат: safe-smoke: accept/reject gate PASS.
Критерій правильності: UNKNOWN не має маршруту навіть із confidence 1.
Якщо результат не отримано: не переходити до інтеграційного FSM; виправити gate.
Крок 2. Конфігурація та fixtures
Збережіть як lab21-config.json.
{
"mode":"simulation-only",
"confidence_min":0.80,
"timeouts":{"feed":3,"transport":4,"robot":3,"gripper":2},
"routes":{"red-cylinder":"BOX_A","blue-cube":"BOX_B"},
"reject_zone":"MANUAL_REVIEW"
}
Збережіть як lab21-cases.json.
[
{"id":"normal","ready":true,"feed":2,"transport":3,"camera":true,"label":"red-cylinder","confidence":0.92,"robot":2,"gripper":1,"grip_ok":true,"restart":false},
{"id":"unknown","ready":true,"feed":1,"transport":1,"camera":true,"label":"UNKNOWN","confidence":0.35,"robot":0,"gripper":0,"grip_ok":false,"restart":false},
{"id":"camera-lost","ready":true,"feed":1,"transport":1,"camera":false,"label":"red-cylinder","confidence":0.95,"robot":0,"gripper":0,"grip_ok":false,"restart":false},
{"id":"robot-timeout","ready":true,"feed":1,"transport":1,"camera":true,"label":"blue-cube","confidence":0.90,"robot":4,"gripper":1,"grip_ok":true,"restart":false},
{"id":"grip-fail","ready":true,"feed":1,"transport":1,"camera":true,"label":"blue-cube","confidence":0.90,"robot":1,"gripper":1,"grip_ok":false,"restart":false},
{"id":"sensor-timeout","ready":true,"feed":1,"transport":5,"camera":true,"label":"red-cylinder","confidence":0.90,"robot":1,"gripper":1,"grip_ok":true,"restart":false},
{"id":"restart-mid-cycle","ready":true,"feed":1,"transport":1,"camera":true,"label":"red-cylinder","confidence":0.90,"robot":1,"gripper":1,"grip_ok":true,"restart":true}
]
Очікуваний результат: один complete, один reject і п'ять recovery cases.
Критерій правильності: конфігурація не містить координат/IP; кожна відмова має унікальний id.
Якщо результат не отримано: перевірити JSON; не видаляти restart case.
Крок 3. Повна mock-реалізація
Збережіть як lab21_sorting.py.
from __future__ import annotations
import argparse
import json
from dataclasses import asdict, dataclass, field
from enum import Enum
from pathlib import Path
class State(str, Enum):
IDLE="IDLE"; CHECK_READY="CHECK_READY"; FEEDING="FEEDING"
TRANSPORTING="TRANSPORTING"; INSPECTING="INSPECTING"
PICKING="PICKING"; PLACING="PLACING"; COMPLETE="COMPLETE"
REJECT="REJECT"; FAULT="FAULT"; RECOVERY_REQUIRED="RECOVERY_REQUIRED"
@dataclass
class Outcome:
case_id: str
state: str
reason: str
route: str | None
outputs: dict[str, str]
transitions: list[str] = field(default_factory=list)
class MockCell:
def __init__(self, cfg: dict) -> None:
if cfg.get("mode") != "simulation-only":
raise ValueError("only simulation-only mode")
self.cfg = cfg
self.state = State.IDLE
self.outputs = {"feeder":"STOP", "conveyor":"STOP", "robot":"NO_GOAL", "gripper":"STOP"}
self.log: list[str] = []
self.reason = ""
def move(self, target: State, reason: str) -> None:
self.log.append(f"{self.state.value}->{target.value}:{reason}")
self.state = target
def safe_stop(self) -> None:
self.outputs.update(feeder="STOP", conveyor="STOP", robot="NO_NEW_GOAL", gripper="HOLD_OR_STOP")
def fail(self, reason: str) -> None:
self.reason = reason
self.safe_stop()
self.move(State.FAULT, reason)
self.move(State.RECOVERY_REQUIRED, "manual-reconciliation-required")
def run(self, case: dict) -> Outcome:
self.move(State.CHECK_READY, "start")
if not bool(case["ready"]):
self.fail("subsystem-not-ready")
else:
self.move(State.FEEDING, "all-ready")
self.outputs["feeder"] = "RUN"
if self.state is State.FEEDING:
if int(case["feed"]) > int(self.cfg["timeouts"]["feed"]):
self.fail("feed-timeout")
else:
self.outputs.update(feeder="STOP", conveyor="RUN")
self.move(State.TRANSPORTING, "one-part-confirmed")
if self.state is State.TRANSPORTING:
if int(case["transport"]) > int(self.cfg["timeouts"]["transport"]):
self.fail("sensor-timeout")
else:
self.outputs["conveyor"] = "STOP"
self.move(State.INSPECTING, "part-stopped")
if self.state is State.INSPECTING:
if bool(case["restart"]):
self.fail("restart-mid-cycle")
elif not bool(case["camera"]):
self.fail("camera-lost")
elif str(case["label"]) not in self.cfg["routes"] or float(case["confidence"]) < float(self.cfg["confidence_min"]):
self.reason = "classification-rejected"
self.move(State.REJECT, self.reason)
else:
self.move(State.PICKING, "route-and-transform-valid")
self.outputs["robot"] = "MOCK_GOAL"
if self.state is State.PICKING:
if int(case["robot"]) > int(self.cfg["timeouts"]["robot"]):
self.fail("robot-timeout")
elif int(case["gripper"]) > int(self.cfg["timeouts"]["gripper"]) or not bool(case["grip_ok"]):
self.fail("grip-not-confirmed")
else:
self.outputs.update(robot="MOCK_GOAL_DONE", gripper="MOCK_GRIPPED")
self.move(State.PLACING, "pick-confirmed")
route = None
if self.state is State.PLACING:
route = str(self.cfg["routes"][str(case["label"])])
self.outputs.update(robot="NO_GOAL", gripper="STOP")
self.move(State.COMPLETE, f"placed-symbolically:{route}")
elif self.state is State.REJECT:
route = str(self.cfg["reject_zone"])
self.safe_stop()
assert self.outputs["feeder"] == self.outputs["conveyor"] == "STOP"
return Outcome(str(case["id"]), self.state.value, self.reason or "ok", route, dict(self.outputs), list(self.log))
def recover(self, operator_ack: bool, reconciled: bool, area_clear: bool) -> bool:
if self.state is not State.RECOVERY_REQUIRED or not (operator_ack and reconciled and area_clear):
return False
self.safe_stop()
self.move(State.IDLE, "manual-recovery")
return True
def main() -> int:
p = argparse.ArgumentParser()
p.add_argument("--config", type=Path, required=True); p.add_argument("--cases", type=Path, required=True)
p.add_argument("--out", type=Path, required=True); args = p.parse_args()
cfg = json.loads(args.config.read_text(encoding="utf-8"))
cases = json.loads(args.cases.read_text(encoding="utf-8"))
results: list[Outcome] = []
for case in cases:
cell = MockCell(cfg); result = cell.run(case); results.append(result)
if result.state == "RECOVERY_REQUIRED":
assert not cell.recover(False, True, True)
assert cell.recover(True, True, True)
states = {r.case_id:r.state for r in results}
assert states["normal"] == "COMPLETE" and states["unknown"] == "REJECT"
for key in ("camera-lost","robot-timeout","grip-fail","sensor-timeout","restart-mid-cycle"):
assert states[key] == "RECOVERY_REQUIRED"
args.out.write_text(json.dumps([asdict(r) for r in results], ensure_ascii=False, indent=2), encoding="utf-8")
print("PASS: COMPLETE=1 REJECT=1 RECOVERY_REQUIRED=5")
return 0
if __name__ == "__main__":
raise SystemExit(main())
Очікуваний результат: PASS: COMPLETE=1 REJECT=1 RECOVERY_REQUIRED=5.
Критерій правильності: у кожному terminal state feeder/conveyor STOP; fault не відновлюється без трьох ручних gates.
Якщо результат не отримано: аналізувати перший неправильний перехід; не підключати апаратні адаптери.
Крок 4. Запуск і метрики
python lab21_sorting.py --config lab21-config.json --cases lab21-cases.json --out lab21-results.json
Побудуйте таблицю причин і розрахуйте: success_rate, reject_rate, fault_rate, середню кількість переходів, умовний throughput як completed / sum(logical durations).
Очікуваний результат: усі сім cases у JSON, причини не порожні.
Критерій правильності: restart не завершується COMPLETE; UNKNOWN не містить mock-goal.
Якщо результат не отримано: перевірити журнал і вихідні fixtures; не змінювати expected states.
Перенесення на MERC-I5: адресні прогалини
Сценарії перевірки
| Тип | Сценарій | Стан | Реакція |
|---|---|---|---|
| позитивний | усі ack, відомий клас | COMPLETE | символічний BOX_A |
| негативний | UNKNOWN | REJECT | без robot goal |
| негативний | camera lost | RECOVERY_REQUIRED | safe stop |
| негативний | robot timeout | RECOVERY_REQUIRED | no new goal |
| негативний | grip fail | RECOVERY_REQUIRED | manual reconciliation |
| граничний | transport > timeout | RECOVERY_REQUIRED | stop conveyor |
| граничний | restart mid-cycle | RECOVERY_REQUIRED | no automatic resume |
Таблиці вимірювань і метрики
| Case | Клас/confidence | Маршрут | Стан | Причина | Переходів | Feeder/Conveyor safe | Recovery без ack |
|---|---|---|---|---|---|---|---|
| normal | blocked | ||||||
| unknown | — | ||||||
| camera-lost | — | blocked | |||||
| robot-timeout | — | blocked | |||||
| grip-fail | — | blocked | |||||
| sensor-timeout | — | blocked | |||||
| restart-mid-cycle | — | blocked |
Вимоги до звіту
Див. загальні вимоги. Додайте контракти адаптерів, FSM, конфігурацію, fixtures, код, журнали, метрики, recovery evidence та явну межу «mock-only; жоден фізичний модуль не запускався».
Критерії оцінювання
| Складова | Частка | Що оцінюється |
|---|---|---|
| Підготовка | 10% | контракти, конфігурація, safety |
| Реалізація | 40% | інтеграційний FSM, адаптери, gates, safe stop |
| Перевірка | 25% | 7 cases, тайм-аути, restart і recovery |
| Аналіз | 15% | причини, throughput, reject/fault розмежування |
| Звіт | 10% | журнали, метрики, відтворюваність |
| Разом | 100% |
Контрольні питання
- Чому оркестратор не повинен знати UART/TCP деталі драйвера?
- Чим
REJECTвідрізняється відFAULT? - Чому grip command потребує незалежного підтвердження?
- Як уникнути повторного виконання cycle ID?
- Чому restart вимагає reconciliation?
- Які фізичні метрики не можна замінити mock-результатами?
Висновки
Опишіть перевірену координацію підсистем, причини reject/fault, реакцію на restart, метрики mock-циклів і мінімальний набір доказів для фізичної апробації.