Лабораторна робота 14
Лабораторна робота №14. Палетизація через Raspberry Pi та Python
Мета: навчитися будувати параметричну Python-оркестрацію палетизації для Raspberry Pi; повторно використовувати adapter-и робота й грипера, генерувати координати без ручного дублювання, виконувати повний dry-run, зберігати прогрес у mock-сховищі, вимагати звірку перед відновленням і журналювати помилки без мережі та фізичного руху.
Результати навчання та передумови
Після виконання студент уміє:
- читати несекретний параметричний profile поза кодом;
- генерувати комірки палети й точки approach/place/retract;
- працювати через
RobotAdapterіGripperAdapter, не знаючи transport; - виконувати dry-run повного циклу Pick-and-Place;
- зберігати progress після кожної підтвердженої mock-комірки;
- після перезапуску переходити у
RECONCILIATION_REQUIRED, а не продовжувати автоматично; - відхиляти невірну фактичну/очікувану карту комірок;
- переводити обидва mock-модулі у stop при невдалому захопленні або плані;
- формувати JSONL-журнал у пам'яті й метрики циклу.
Передумови: ЛР-08, ЛР-11–13, Python, JSON, adapter pattern, автомати станів і правила безпеки. Фізична апробація не входить до цієї редакції.
Середовище виконання
| Середовище | Статус | Примітка |
|---|---|---|
| Google Colab | дозволено | Повний in-memory dry-run зі стандартною бібліотекою |
| Локальний ПК | основне | Python; без socket, serial, GPIO і файлових записів у smoke |
| Raspberry Pi | дозволено | Лише dry-run з mock-adapter-ами; код потребує Python 3.10+, а паспорт ОС охоплено marker-ами фізичної інтеграції |
| Фізичний комплекс MERC-I5 | не потрібен | MG400 і грипер не підключаються та не рухаються |
Необхідні знання та матеріали
- ЛР-08, ЛР-13 і програма ЛР-14;
- аналіз цільової архітектури драйвера, базова лінія API та адаптивний грипер;
- перевірка координат без руху і експлуатаційна безпека;
- Python зі стандартними
json,math,dataclasses,enum,typing,pathlib; - зовнішній synthetic JSON із цієї роботи.
Ризики та правила безпеки
Ризики
| Небезпека | Наслідок | Захист у цій редакції |
|---|---|---|
| Жорстко записані точки | траєкторія не відповідає стенду | external synthetic profile + strict loader |
| Невдале захоплення | рух без деталі або падіння | mock-gripper error зупиняє robot/gripper й progress |
| Перезапуск посеред циклу | дубль або пропуск комірки | обов'язковий RECONCILIATION_REQUIRED |
| Запис progress неатомарний | суперечливий стан | у smoke in-memory round-trip; фактичне сховище потребує окремого рішення |
| Втрата робота/feedback | невідоме виконання | no retry, ERROR_RECONCILIATION_REQUIRED |
| Віддалений запуск | немає локального контролю | жодної мережі; фізичний запуск заборонено |
Заборонені дії
- імпортувати/викликати socket, SDK MG400, pyserial, GPIO або застарілий gripper module;
- додавати IP, pin, пароль, токен, реальні координати чи speed у source code;
- надсилати
EnableRobot, рух,Continue,ClearError, gripper або I/O commands; - автоматично відновлювати progress після process restart;
- вважати
mock placedдоказом фізично зайнятої комірки; - запускати Raspberry Pi сценарій на активній лінії без окремого дозволу й локального оператора.
Умови негайної зупинки
Неузгоджені progress і observed cells, невдале захоплення, pose поза envelope, timeout, duplicate index, помилка store/parser, невідомий state, втрата adapter-а. Для майбутнього обладнання — будь-яка умова з docs/06, включно з людиною/предметом у зоні, невідомою позою, падінням деталі, незвичним звуком/нагрівом.
Безпечний стан
Обидва mock-adapter-и STOPPED, прогрес PAUSED_RECONCILIATION_REQUIRED або ERROR_RECONCILIATION_REQUIRED, новий цикл заблоковано, last_error і journal збережено, автоматичних повторів немає. Фізичний безпечний стан конкретних робота/грипера потребує окремого підтвердження.
Передпусковий чекліст
- Profile має
SYNTHETIC_PALLETIZER_DRY_RUNі strict schema. -
dry_run=true; external profile не містить IP/pin/speed/реальних frame. - Robot/Gripper adapter-и — тільки mock.
- Progress store — in-memory у smoke.
- Reconciliation тестується до resume.
- Фізичний комплекс не підключений.
Хід виконання роботи
- Спроєктувати composition root, adapters, progress store і profile.
- Реалізувати parameterized pallet та dry-run.
- Перевірити повну послідовність і журнал.
- Перевірити controlled pause, restart і reconciliation.
- Перевірити gripper/robot faults, межі та відсутність auto-resume.
- Оформити метрики, паспорт і звіт.
Методичні вказівки й теоретичні відомості
1. Composition root
Єдине місце програми обирає concrete adapter-и. Бізнес-логіка палетизації отримує RobotAdapter, GripperAdapter і ProgressStore через конструктор. Таким чином dry-run не містить умов if real_hardware у кожному кроці.
| Інтерфейс | Mock цієї ЛР | Майбутня реалізація |
|---|---|---|
RobotAdapter | запис plan poses у пам'ять | лише після ЛР-13/API validation |
GripperAdapter | лічильник synthetic objects | лише після ЛР-08/protocol validation |
ProgressStore | JSON round-trip у пам'яті | атомарне локальне сховище після рішення |
| journal | JSONL string у пам'яті | локальний журнал із retention policy |
2. Progress і відновлення
Checkpoint після mock-комірки містить next_index, перелік placed_indices, status і error. Після нового process session навіть валідний checkpoint не доводить фактичне положення деталі. Тому конструктор переводить незавершений progress у RECONCILIATION_REQUIRED; resume дозволяється лише коли observed map точно збігається й оператор явно підтвердив.
3. Потік dry-run
Текстовий опис рисунка: зовнішня synthetic-конфігурація створює палету; для кожної комірки mock-robot планує підхід до Pick, mock-gripper підтверджує захоплення, robot планує відхід і підхід/укладання/відхід над палетою, після чого progress атомарно оновлюється в mock; pause, restart або будь-яка помилка ведуть до reconciliation gate чи error-stop, а не автоматичного продовження.
flowchart LR
A["External synthetic config"] --> B["Generate cells"]
B --> C["Mock Pick approach/pick/retract"]
C --> D{"Mock grasp confirmed?"}
D -->|"yes"| E["Mock pallet approach/place/retract"]
E --> F["Save in-memory progress + journal"]
F --> G{"More cells?"}
G -->|"yes"| C
G -->|"no"| H["DRY_RUN_COMPLETE"]
D -->|"no"| X["ERROR_RECONCILIATION_REQUIRED"]
F -->|"pause/restart"| Y["RECONCILIATION_REQUIRED"]
Рис. 1. Ілюстративна оркестрація Python-палетизації у dry-run
4. Конфігурація поза кодом
Файл palletizer.synthetic.json:
{
"profile": "SYNTHETIC_PALLETIZER_DRY_RUN",
"dry_run": true,
"units": "mm",
"frame": "SYNTHETIC_USER",
"rows": 2,
"columns": 2,
"pitch_x_mm": 20.0,
"pitch_y_mm": 25.0,
"clearance_mm": 30.0,
"pick_pose": [0.0, -40.0, 10.0, 0.0],
"pallet_base_pose": [-20.0, 20.0, 10.0, 0.0],
"envelope_mm": {
"x": [-100.0, 100.0],
"y": [-100.0, 100.0],
"z": [0.0, 120.0],
"r_deg": [-180.0, 180.0]
}
}
Усі числа — лише test fixture. Runtime loader читає зовнішній файл; safe-smoke виконує JSON round-trip у пам'яті.
5. Першоджерела
- Офіційний DOBOT TCP-IP-4Axis-Python SDK — майбутня transport baseline, не використовується у коді ЛР.
- Цільова архітектура безпечного драйвера і командні прогалини.
- Грипер і статус протоколу та процедура перевірки координат.
- Python:
typing.Protocol, Python:json— adapter contracts і progress codec.
Виконання лабораторної роботи
Крок 1. Перевірити composition і profile
Зафіксуйте, що hardware adapter-и відсутні. External JSON має dry_run=true, synthetic frame і не має transport address.
RobotAdapter = MockRobot
GripperAdapter = MockGripper
ProgressStore = MemoryProgressStore
Physical movement = false
Очікуваний результат: composition root однозначно безапаратний.
Критерій правильності: невідомий ключ або dry_run=false відхиляється loader-ом.
Якщо результат не отримано: припиніть запуск; не підставляйте hardware fallback за замовчуванням.
Крок 2. Запустити повний dry-run
Збережіть як lab14_palletizer_dry_run.py.
# safe-smoke
from __future__ import annotations
import json
import math
from dataclasses import asdict, dataclass
from enum import Enum
from pathlib import Path
from typing import Protocol
class PalletizerError(RuntimeError):
pass
class RunStatus(str, Enum):
READY = "READY"
PAUSED_RECONCILIATION_REQUIRED = "PAUSED_RECONCILIATION_REQUIRED"
RECONCILIATION_REQUIRED = "RECONCILIATION_REQUIRED"
ERROR_RECONCILIATION_REQUIRED = "ERROR_RECONCILIATION_REQUIRED"
COMPLETE = "COMPLETE"
@dataclass(frozen=True)
class Pose:
x: float
y: float
z: float
r: float
@dataclass(frozen=True)
class Cell:
index: int
row: int
column: int
place: Pose
approach: Pose
@dataclass(frozen=True)
class Config:
profile: str
rows: int
columns: int
pitch_x: float
pitch_y: float
clearance: float
pick: Pose
base: Pose
envelope: dict[str, tuple[float, float]]
@dataclass
class Progress:
next_index: int
placed_indices: list[int]
status: str
last_error: str | None = None
class RobotAdapter(Protocol):
def plan(self, kind: str, pose: Pose) -> None: ...
def stop(self, reason: str) -> None: ...
class GripperAdapter(Protocol):
def close(self) -> None: ...
def open(self) -> None: ...
def stop(self, reason: str) -> None: ...
class ProgressStore(Protocol):
def load(self) -> Progress | None: ...
def save(self, progress: Progress) -> None: ...
def _pose(value: object) -> Pose:
if not isinstance(value, list) or len(value) != 4:
raise PalletizerError("pose має містити 4 числа")
if not all(isinstance(item, (int, float)) and math.isfinite(item) for item in value):
raise PalletizerError("pose має бути скінченною")
return Pose(*(float(item) for item in value))
def parse_config_text(text: str) -> Config:
raw = json.loads(text)
expected = {"profile", "dry_run", "units", "frame", "rows", "columns", "pitch_x_mm", "pitch_y_mm", "clearance_mm", "pick_pose", "pallet_base_pose", "envelope_mm"}
if not isinstance(raw, dict) or set(raw) != expected:
raise PalletizerError("неправильна schema config")
if raw["profile"] != "SYNTHETIC_PALLETIZER_DRY_RUN" or raw["dry_run"] is not True:
raise PalletizerError("дозволено лише dry-run profile")
if raw["units"] != "mm" or raw["frame"] != "SYNTHETIC_USER":
raise PalletizerError("невідомі synthetic units/frame")
if not isinstance(raw["rows"], int) or not isinstance(raw["columns"], int) or not 1 <= raw["rows"] <= 20 or not 1 <= raw["columns"] <= 20:
raise PalletizerError("rows/columns поза межею")
for key in ("pitch_x_mm", "pitch_y_mm", "clearance_mm"):
if not isinstance(raw[key], (int, float)) or not math.isfinite(raw[key]) or raw[key] <= 0:
raise PalletizerError(f"{key} має бути додатним")
axes = {"x", "y", "z", "r_deg"}
if not isinstance(raw["envelope_mm"], dict) or set(raw["envelope_mm"]) != axes:
raise PalletizerError("неповний envelope")
envelope: dict[str, tuple[float, float]] = {}
for axis, limits in raw["envelope_mm"].items():
if not isinstance(limits, list) or len(limits) != 2 or limits[0] >= limits[1]:
raise PalletizerError(f"невалідна межа {axis}")
envelope[axis] = (float(limits[0]), float(limits[1]))
return Config(raw["profile"], raw["rows"], raw["columns"], float(raw["pitch_x_mm"]), float(raw["pitch_y_mm"]), float(raw["clearance_mm"]), _pose(raw["pick_pose"]), _pose(raw["pallet_base_pose"]), envelope)
def load_config(path: Path) -> Config:
return parse_config_text(path.read_text(encoding="utf-8"))
def check_pose(pose: Pose, config: Config) -> None:
for axis, value in {"x": pose.x, "y": pose.y, "z": pose.z, "r_deg": pose.r}.items():
low, high = config.envelope[axis]
if not low <= value <= high:
raise PalletizerError(f"{axis} поза synthetic envelope")
def above(pose: Pose, clearance: float) -> Pose:
return Pose(pose.x, pose.y, pose.z + clearance, pose.r)
def generate_cells(config: Config) -> list[Cell]:
cells: list[Cell] = []
for row in range(config.rows):
for column in range(config.columns):
place = Pose(config.base.x + column * config.pitch_x, config.base.y + row * config.pitch_y, config.base.z, config.base.r)
approach = above(place, config.clearance)
check_pose(place, config)
check_pose(approach, config)
cells.append(Cell(len(cells), row, column, place, approach))
if len({(cell.place.x, cell.place.y, cell.place.z, cell.place.r) for cell in cells}) != len(cells):
raise PalletizerError("duplicate cell pose")
return cells
class MockRobot:
def __init__(self, fail_after_plans: int | None = None) -> None:
self.plans: list[tuple[str, Pose]] = []
self.stopped = False
self.fail_after_plans = fail_after_plans
def plan(self, kind: str, pose: Pose) -> None:
if self.stopped:
raise PalletizerError("mock robot stopped")
if kind not in {"JOINT_PLAN", "LINEAR_PLAN"}:
raise PalletizerError("невідомий mock plan kind")
if self.fail_after_plans is not None and len(self.plans) >= self.fail_after_plans:
self.stop("injected robot planning fault")
raise PalletizerError("injected robot planning fault")
self.plans.append((kind, pose))
def stop(self, reason: str) -> None:
self.stopped = True
class MockGripper:
def __init__(self, available_objects: int) -> None:
self.available_objects = available_objects
self.holding = False
self.stopped = False
def close(self) -> None:
if self.stopped:
raise PalletizerError("mock gripper stopped")
if self.available_objects <= 0:
self.stop("object not confirmed")
raise PalletizerError("object not confirmed")
self.available_objects -= 1
self.holding = True
def open(self) -> None:
if self.stopped or not self.holding:
raise PalletizerError("release without confirmed object")
self.holding = False
def stop(self, reason: str) -> None:
self.stopped = True
self.holding = False
class MemoryProgressStore:
def __init__(self) -> None:
self.payload: str | None = None
def load(self) -> Progress | None:
if self.payload is None:
return None
raw = json.loads(self.payload)
return Progress(raw["next_index"], list(raw["placed_indices"]), raw["status"], raw["last_error"])
def save(self, progress: Progress) -> None:
self.payload = json.dumps(asdict(progress), sort_keys=True)
class Palletizer:
def __init__(self, config: Config, robot: RobotAdapter, gripper: GripperAdapter, store: ProgressStore) -> None:
self.config = config
self.cells = generate_cells(config)
self.robot = robot
self.gripper = gripper
self.store = store
self.journal: list[dict[str, object]] = []
saved = store.load()
if saved is None:
self.progress = Progress(0, [], RunStatus.READY.value)
elif saved.status == RunStatus.COMPLETE.value:
self.progress = saved
else:
saved.status = RunStatus.RECONCILIATION_REQUIRED.value
self.progress = saved
self.store.save(self.progress)
def reconcile(self, observed_placed: list[int], *, operator_ack: bool) -> None:
if self.progress.status != RunStatus.RECONCILIATION_REQUIRED.value:
raise PalletizerError("reconciliation зараз не очікується")
if not operator_ack or observed_placed != self.progress.placed_indices:
raise PalletizerError("observed map не збігається або немає ack")
self.progress.status = RunStatus.READY.value
self.progress.last_error = None
self.store.save(self.progress)
self.journal.append({"event": "RECONCILED", "next_index": self.progress.next_index})
def _run_cell(self, cell: Cell) -> None:
pick_approach = above(self.config.pick, self.config.clearance)
for pose in (pick_approach, self.config.pick, pick_approach, cell.approach, cell.place, cell.approach):
check_pose(pose, self.config)
self.robot.plan("JOINT_PLAN", pick_approach)
self.robot.plan("LINEAR_PLAN", self.config.pick)
self.gripper.close()
self.robot.plan("LINEAR_PLAN", pick_approach)
self.robot.plan("JOINT_PLAN", cell.approach)
self.robot.plan("LINEAR_PLAN", cell.place)
self.gripper.open()
self.robot.plan("LINEAR_PLAN", cell.approach)
def run(self, *, max_cells: int | None = None) -> Progress:
if self.progress.status != RunStatus.READY.value:
raise PalletizerError("run заблоковано до reconciliation")
completed_now = 0
while self.progress.next_index < len(self.cells):
cell = self.cells[self.progress.next_index]
try:
self._run_cell(cell)
except PalletizerError as exc:
self.robot.stop(str(exc))
self.gripper.stop(str(exc))
self.progress.status = RunStatus.ERROR_RECONCILIATION_REQUIRED.value
self.progress.last_error = str(exc)
self.store.save(self.progress)
self.journal.append({"event": "ERROR", "cell": cell.index, "reason": str(exc)})
raise
self.progress.placed_indices.append(cell.index)
self.progress.next_index += 1
completed_now += 1
self.journal.append({"event": "MOCK_PLACED", "cell": cell.index})
self.store.save(self.progress)
if max_cells is not None and completed_now >= max_cells and self.progress.next_index < len(self.cells):
self.progress.status = RunStatus.PAUSED_RECONCILIATION_REQUIRED.value
self.store.save(self.progress)
return self.progress
self.progress.status = RunStatus.COMPLETE.value
self.store.save(self.progress)
self.journal.append({"event": "DRY_RUN_COMPLETE", "cells": len(self.cells)})
return self.progress
def journal_jsonl(self) -> str:
return "\n".join(json.dumps(event, sort_keys=True) for event in self.journal)
fixture = {
"profile": "SYNTHETIC_PALLETIZER_DRY_RUN",
"dry_run": True,
"units": "mm",
"frame": "SYNTHETIC_USER",
"rows": 2,
"columns": 2,
"pitch_x_mm": 20.0,
"pitch_y_mm": 25.0,
"clearance_mm": 30.0,
"pick_pose": [0.0, -40.0, 10.0, 0.0],
"pallet_base_pose": [-20.0, 20.0, 10.0, 0.0],
"envelope_mm": {"x": [-100.0, 100.0], "y": [-100.0, 100.0], "z": [0.0, 120.0], "r_deg": [-180.0, 180.0]},
}
config = parse_config_text(json.dumps(fixture))
store = MemoryProgressStore()
robot_a = MockRobot()
first = Palletizer(config, robot_a, MockGripper(available_objects=2), store)
paused = first.run(max_cells=2)
assert paused.next_index == 2 and paused.status == RunStatus.PAUSED_RECONCILIATION_REQUIRED.value
robot_b = MockRobot()
second = Palletizer(config, robot_b, MockGripper(available_objects=2), store)
try:
second.run()
except PalletizerError as exc:
assert "reconciliation" in str(exc)
else:
raise AssertionError("auto-resume має бути заборонене")
try:
second.reconcile([0], operator_ack=True)
except PalletizerError:
pass
else:
raise AssertionError("невірна observed map має бути відхилена")
second.reconcile([0, 1], operator_ack=True)
complete = second.run()
assert complete.status == RunStatus.COMPLETE.value and complete.placed_indices == [0, 1, 2, 3]
assert len(robot_a.plans) + len(robot_b.plans) == 24
assert "DRY_RUN_COMPLETE" in second.journal_jsonl()
fault_store = MemoryProgressStore()
fault_robot = MockRobot()
faulty = Palletizer(config, fault_robot, MockGripper(available_objects=0), fault_store)
try:
faulty.run(max_cells=1)
except PalletizerError as exc:
assert "not confirmed" in str(exc)
else:
raise AssertionError("невдале захоплення має зупинити dry-run")
assert fault_robot.stopped and faulty.progress.status == RunStatus.ERROR_RECONCILIATION_REQUIRED.value
print(json.dumps({"result": "PASS", "cells": 4, "planned_poses": 24, "resumed_after_reconciliation": True, "hardware": False}, ensure_ascii=False))
python -m py_compile lab14_palletizer_dry_run.py
python lab14_palletizer_dry_run.py
Очікуваний результат: JSON result=PASS, cells=4, planned_poses=24, resumed_after_reconciliation=true, hardware=false.
Критерій правильності: code 0, no files/network, progress проходить JSON round-trip in-memory, а resume без reconciliation відхилено.
Якщо результат не отримано: не підключайте adapter-и; перевірте traceback, progress transition і кількість plan poses.
Крок 3. Перевірити параметричні cells і послідовність
Для 2×2 має бути 4 комірки. Кожна cell створює 6 plan poses: pick approach, pick, pick retract, pallet approach, place, pallet retract.
4 cells × 6 plan poses = 24 mock poses
Physical moves = 0
Очікуваний результат: 24 записи mock-plans і 4 MOCK_PLACED після повного сценарію.
Критерій правильності: усі poses у synthetic envelope, комірки унікальні, progress [0,1,2,3].
Якщо результат не отримано: не виправляйте count вручну; перевірте генератор і _run_cell.
Крок 4. Перевірити pause/restart/reconciliation
Перший session завершує 2 cells і зберігає paused status. Другий session має блокувати run(), відхилити observed [0] і дозволити resume лише після exact [0,1] + ack.
PAUSED_RECONCILIATION_REQUIRED
-> process restart
RECONCILIATION_REQUIRED
-> exact observed map + operator_ack
READY -> remaining dry-run -> COMPLETE
Очікуваний результат: автоматичного resume немає.
Критерій правильності: жоден plan не додається другим session до успішного reconcile.
Якщо результат не отримано: зробіть run() fail-closed для всіх status, крім READY; не прирівнюйте checkpoint до physical observation.
Крок 5. Перевірити відмови robot/gripper/store
Додайте injected MockRobot(fail_after_plans=3), corrupted progress JSON і duplicate placed index. Кожна невизначеність має зупиняти обидва adapter-и та вимагати reconciliation.
python lab14_palletizer_dry_run.py
Очікуваний результат: fault до mock place не збільшує next_index; last_error і ERROR event збережені.
Критерій правильності: 0 retries, 0 auto-clear, 0 пропущених/подвійних індексів.
Якщо результат не отримано: оновлюйте progress тільки після повного підтвердженого mock place; обгорніть межу cell у fail-closed error transition.
Крок 6. Оформити паспорт, журнал і метрики
| Поле | Результат |
|---|---|
| Robot adapter | MockRobot, plan-only |
| Gripper adapter | MockGripper, synthetic objects |
| Progress | JSON round-trip in memory |
| Resume | exact reconciliation + ack |
| Journal | JSONL string in memory |
| Config | external synthetic JSON / in-memory smoke representation |
| Hardware | не підключалося й не перевірялося |
Очікуваний результат: артефакти відтворюють dry-run і явно не є hardware log.
Критерій правильності: hardware=false, жодного IP/pin/секрету чи заяви про фізично укладені деталі.
Якщо результат не отримано: знеособте journal, додайте synthetic labels і вилучіть двозначні поля.
Сценарії перевірки
| Тип | Сценарій | Очікування |
|---|---|---|
| позитивний | повний 2×2 dry-run | 4 cells, 24 plans, COMPLETE |
| позитивний | pause 2 + exact reconcile | remaining cells complete |
| негативний | resume без reconcile | відхилено, 0 нових plans |
| негативний | observed map не збігається | reconciliation відхилено |
| негативний | gripper без object | обидва stop, ERROR status |
| негативний | robot injected fault | progress не просувається |
| граничний | 1×1 | 6 plans, одна cell |
| граничний | approach рівно на envelope | прийнято; вище — відхилено |
Таблиці вимірювань і метрики
| Session | Cell | Plans before | Grasp | Mock place | Progress next | Status | Error | Auto-retry |
|---|---|---|---|---|---|---|---|---|
| 1 | 0 | 0 | ||||||
| 1 | 1 | 0 | ||||||
| 2 | reconcile | — | — | — | 0 | |||
| 2 | 2 | 0 | ||||||
| 2 | 3 | 0 |
Метрики: 100% cells у synthetic envelope; 6 plans/cell; 0 hardware calls; 0 auto-resume; 0 retry після невідомості; progress оновлено рівно раз після кожного mock place; 100% faults fail-closed.
[!WARNING] [ПОТРЕБУЄ ДОПРАЦЮВАННЯ: СХОВИЩЕ ПРОГРЕСУ ТА ФІЗИЧНА АПРОБАЦІЯ ЛР-14] Не підтверджено: формат/шлях/права/атомарність/backup/retention фактичного progress store на Raspberry Pi, процедура звірки фізичних комірок, робота adapter-ів, траєкторії та stop/recovery на MERC-I5; фізична апробація не дозволена. Що потрібно додати: затверджене локальне сховище без секретів із atomic write/locking, schema migration і corruption test; операційну процедуру reconciliation, фактичний profile/adapters, оцінку ризику, E-Stop check, локального оператора й журнал першого запуску. Як завершити: спершу протестувати store з fault injection без обладнання, потім read-only звірку стенда; лише за окремим дозволом — покроковий dry-run gateway/перший рух на мінімальній затвердженій швидкості, контроль pause/restart без auto-resume; додати датований журнал і вилучити block.
Вимоги до звіту
Див. загальні вимоги. Додатково подайте:
- composition diagram і adapter contracts;
- external synthetic JSON та повний код;
- generated cells/poses і 24-plan audit;
- progress snapshots до/після pause/restart/reconcile;
- JSONL-журнал позитивного й fault scenarios;
- таблицю негативних і граничних тестів;
- явну фразу «MG400, грипер і Raspberry Pi hardware I/O не використовувалися».
Критерії оцінювання
| Складова | Частка | Критерії |
|---|---|---|
| Підготовка | 10% | джерела, safety scope, external config, composition root |
| Реалізація | 40% | adapters, generator, dry-run, progress, reconciliation, journal, stop |
| Перевірка | 25% | full/pause/resume/fault/bounds тести, code 0, no I/O |
| Аналіз | 15% | checkpoint vs physical state, exactly-once limits, recovery і ризики |
| Звіт | 10% | повні logs/tables/passport, джерела й hardware status |
| Разом | 100% |
Мережевий/фізичний запуск, auto-resume або підміна observed state checkpoint-ом є критичною помилкою.
Контрольні питання
- Навіщо composition root відокремлює concrete adapter-и?
- Чому checkpoint не доводить фізичне положення деталі?
- Що саме має перевіряти reconciliation?
- Коли progress дозволено збільшити?
- Чому timeout не дозволяє повторити Pick/Place автоматично?
- Як atomic store захищає від часткового запису?
- Чому dry-run із 100% PASS не доводить collision safety?
- Який найменший пакет даних потрібний для hardware composition root?
Висновки
ЛР-14 завершується parameterized Python dry-run із mock robot/gripper adapter-ами, external synthetic profile, progress store, reconciliation gate та in-memory JSONL journal. Повний цикл, pause/restart і fault cases автоматично перевіряються без файлів, мережі й обладнання. Фактичні профілі, adapter-и, store та physical behavior залишаються адресно заблокованими до окремої валідації й дозволу.