MERCI SPACE
Навчальні матеріали

Лабораторна робота 12

Лабораторна робота №12. Палетизація в DobotStudio Pro

Мета: навчитися параметрично описувати палету для майбутньої програми DobotStudio Pro; генерувати рядки, стовпці, крок і послідовність комірок; будувати офлайн-план «підхід — укладання — відхід», контролювати заповнення та безпечно блокувати цикл при помилці без підключення до MG400.

Результати навчання та передумови

Після виконання студент уміє:

  • представляти палету базовою позою, кількістю рядків/стовпців і кроками;
  • виводити координати комірок формулою, а не вручну дублювати точки;
  • генерувати порядок row-major або serpentine та пояснювати його;
  • перевіряти унікальність, кількість і входження всіх поз у synthetic envelope;
  • додавати окремі точки approach/place/retract;
  • вести стан EMPTY → PLANNED → PLACED для кожної комірки;
  • припиняти offline-цикл при пропуску/повторі комірки та не відновлювати його автоматично;
  • готувати worksheet для DobotStudio Pro без перенесення синтетичних координат у робот.

Передумови: успішна ЛР-11, Python, декартові координати, цикли/списки та правила безпеки MERC-I5. Фізичний рух і підключення не входять до цієї редакції.

Середовище виконання

СередовищеСтатусПримітка
Google ColabдозволеноIn-memory генератор і тести зі стандартною бібліотекою
Локальний ПКосновнеPython; DobotStudio Pro — лише offline після звірки версії, без connect
Raspberry PiдозволеноТільки mock, без мережі й hardware I/O
Фізичний комплекс MERC-I5не потрібенПалета, грипер і MG400 не активуються

Необхідні знання та матеріали

Ризики та правила безпеки

Ризики

НебезпекаНаслідокЗахист у роботі
Невірний знак/вісь крокуточки виходять за палету або перешкодусинтетичний envelope, таблиця й унікальність усіх комірок
Ручне дублювання точокпропуск або повтор коміркипараметричний генератор і перевірка count
Низька точка approachзіткнення з деталямиокремий clearance у synthetic profile
Переповнена палетаукладання поверх зайнятої коміркистан комірки та блокування повтору
Автовідновлення після stopневідоме положення деталіBLOCKED; тільки нова звірка, без auto-resume

Заборонені дії

  • підключати DobotStudio Pro до MG400, активувати робот або запускати програму;
  • вводити synthetic base/pitch/clearance у фізичний контролер;
  • надсилати рухові, gripper або I/O команди;
  • навчати точки вручну, торкатися робочої зони чи переміщувати палету;
  • змінювати User/Tool/TCP, скидати помилки або автоматично продовжувати після stop;
  • вважати просту геометричну перевірку доказом відсутності зіткнень.

Умови негайної зупинки

Точка поза envelope, дубльована комірка, неправильна кількість, недостатній clearance, неузгоджені одиниці/frame, повторне укладання, пропущений індекс, невідома версія профілю. Для майбутнього стенда додатково діють усі умови з docs/06: людина/предмет у зоні, невідома поза, втрата зв'язку, падіння деталі, незвичний звук/нагрів.

Безпечний стан

BLOCKED, connected=false, жодної robot command, усі невиконані комірки лишаються EMPTY/PLANNED, останній підтверджений індекс збережено лише в журналі. Фізична деталь не вважається розміщеною на підставі offline-моделі.

Передпусковий чекліст

  • Конфіг має SYNTHETIC_PALLET_OFFLINE_ONLY.
  • Немає IP, реальних User/Tool/TCP і апаратних команд.
  • rows × columns збігається з кількістю унікальних комірок.
  • Для кожної комірки перевірено approach/place/retract.
  • Негативні та граничні тести проходять.
  • Добот і грипер фізично не залучені.

Хід виконання роботи

  1. Задати параметричну модель і зовнішній synthetic profile.
  2. Згенерувати комірки та offline-траєкторії.
  3. Перевірити порядок, унікальність і межі.
  4. Перевірити заповнення, stop і негативні сценарії.
  5. Створити in-memory worksheet для майбутньої адаптації.
  6. Заповнити метрики, звіт і перелік блокерів.

Методичні вказівки й теоретичні відомості

1. Параметрична палета

Для базової пози P₀=(x₀,y₀,z₀,r₀), рядка i і стовпця j проста прямокутна модель має вигляд:

Text
P(i,j) = (x₀ + j·pitch_x, y₀ + i·pitch_y, z₀, r₀)

Формула діє лише у визначеній User frame та для ортогональної рівної палети. Поворот, нахил або деформація потребують каліброваного перетворення; їх не можна приховати зміною одного pitch.

2. Порядок і стани

row_major проходить кожен рядок в одному напрямку. serpentine змінює напрямок через рядок і може скорочувати offline довжину переходів, але не гарантує безпечність фізичної траєкторії.

Стан коміркиЗначення
EMPTYне запланована/не підтверджена як зайнята
PLANNEDнаступна в offline-послідовності
PLACEDлише mock підтвердив крок; не факт фізичного стенда
BLOCKEDцикл припинено через розбіжність

3. Потік палетизації

Текстовий опис рисунка: генератор читає зовнішню геометрію, створює унікальні комірки й для кожної формує approach/place/retract; після символічного підтвердження комірка стає PLACED, а будь-який повтор, пропуск, вихід за envelope чи stop переводить весь offline-цикл у BLOCKED без автоматичного продовження.

Mermaid
flowchart LR
    A["Synthetic pallet config"] --> B["Generate unique cells"]
    B --> C["Validate envelope + clearance"]
    C --> D["Approach"]
    D --> E["Symbolic place"]
    E --> F["Retract"]
    F --> G{"Cells remain?"}
    G -->|"yes"| D
    G -->|"no"| H["OFFLINE_COMPLETE"]
    B -->|"duplicate/invalid"| X["BLOCKED"]
    C -->|"out of bounds"| X

Рис. 1. Ілюстративний offline-автомат параметричної палетизації

4. Конфігурація поза кодом

Файл pallet-studio.synthetic.json:

JSON
{
  "profile": "SYNTHETIC_PALLET_OFFLINE_ONLY",
  "units": "mm",
  "frame": "SYNTHETIC_PALLET",
  "rows": 2,
  "columns": 3,
  "pitch_x_mm": 20.0,
  "pitch_y_mm": 25.0,
  "clearance_mm": 30.0,
  "order": "serpentine",
  "base_pose": [-30.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]
  }
}

Числа — лише synthetic fixture. Runtime читає зовнішній файл; safe-smoke перевіряє його JSON-представлення в пам'яті без файлового запису.

5. Першоджерела

Виконання лабораторної роботи

Крок 1. Перевірити synthetic profile

Збережіть JSON у своєму дозволеному workspace для навчального runtime або використайте його текстове представлення в safe-smoke. Не замінюйте prefix SYNTHETIC.

Text
Expected cells = rows × columns = 2 × 3 = 6
Physical import = forbidden
Connection = none

Очікуваний результат: schema містить тільки геометрію mock і не містить IP/реальних frame.

Критерій правильності: усі ключі відомі, типи/межі валідні, profile позначений offline.

Якщо результат не отримано: не виправляйте невідомий ключ мовчки; відхиліть конфіг і звірте schema.

Крок 2. Запустити повний генератор і mock-цикл

Збережіть як lab12_pallet_offline.py.

Python
# safe-smoke
from __future__ import annotations

import csv
import io
import json
import math
from dataclasses import dataclass
from pathlib import Path


class PalletError(ValueError):
    pass


@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 PalletConfig:
    profile: str
    rows: int
    columns: int
    pitch_x: float
    pitch_y: float
    clearance: float
    order: str
    base: Pose
    envelope: dict[str, tuple[float, float]]


def _pose(value: object) -> Pose:
    if not isinstance(value, list) or len(value) != 4:
        raise PalletError("base_pose має містити 4 числа")
    if not all(isinstance(item, (int, float)) and math.isfinite(item) for item in value):
        raise PalletError("pose має бути скінченною")
    return Pose(*(float(item) for item in value))


def parse_config_text(text: str) -> PalletConfig:
    raw = json.loads(text)
    expected = {"profile", "units", "frame", "rows", "columns", "pitch_x_mm", "pitch_y_mm", "clearance_mm", "order", "base_pose", "envelope_mm"}
    if not isinstance(raw, dict) or set(raw) != expected:
        raise PalletError("неправильний набір ключів")
    if raw["profile"] != "SYNTHETIC_PALLET_OFFLINE_ONLY" or raw["units"] != "mm" or raw["frame"] != "SYNTHETIC_PALLET":
        raise PalletError("дозволено лише synthetic offline profile")
    if not isinstance(raw["rows"], int) or not isinstance(raw["columns"], int):
        raise PalletError("rows/columns мають бути цілими")
    if not 1 <= raw["rows"] <= 20 or not 1 <= raw["columns"] <= 20:
        raise PalletError("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 PalletError(f"{key} має бути додатним")
    if raw["order"] not in {"row_major", "serpentine"}:
        raise PalletError("невідомий порядок")
    axes = {"x", "y", "z", "r_deg"}
    if not isinstance(raw["envelope_mm"], dict) or set(raw["envelope_mm"]) != axes:
        raise PalletError("неповний 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 PalletError(f"невалідна межа {axis}")
        envelope[axis] = (float(limits[0]), float(limits[1]))
    return PalletConfig(raw["profile"], raw["rows"], raw["columns"], float(raw["pitch_x_mm"]), float(raw["pitch_y_mm"]), float(raw["clearance_mm"]), raw["order"], _pose(raw["base_pose"]), envelope)


def load_config(path: Path) -> PalletConfig:
    return parse_config_text(path.read_text(encoding="utf-8"))


def check_pose(pose: Pose, config: PalletConfig) -> None:
    values = {"x": pose.x, "y": pose.y, "z": pose.z, "r_deg": pose.r}
    for axis, value in values.items():
        low, high = config.envelope[axis]
        if not low <= value <= high:
            raise PalletError(f"{axis} поза synthetic envelope")


def generate_cells(config: PalletConfig) -> list[Cell]:
    cells: list[Cell] = []
    index = 0
    for row in range(config.rows):
        columns = list(range(config.columns))
        if config.order == "serpentine" and row % 2 == 1:
            columns.reverse()
        for column in columns:
            place = Pose(config.base.x + column * config.pitch_x, config.base.y + row * config.pitch_y, config.base.z, config.base.r)
            approach = Pose(place.x, place.y, place.z + config.clearance, place.r)
            check_pose(place, config)
            check_pose(approach, config)
            cells.append(Cell(index, row, column, place, approach))
            index += 1
    keys = {(cell.row, cell.column) for cell in cells}
    poses = {(cell.place.x, cell.place.y, cell.place.z, cell.place.r) for cell in cells}
    if len(cells) != config.rows * config.columns or len(keys) != len(cells) or len(poses) != len(cells):
        raise PalletError("кількість або унікальність комірок порушена")
    return cells


class OfflineCycle:
    def __init__(self, cells: list[Cell]) -> None:
        self.cells = cells
        self.next_index = 0
        self.placed: set[int] = set()
        self.blocked_reason: str | None = None

    def next_cell(self) -> Cell | None:
        if self.blocked_reason:
            raise PalletError("цикл BLOCKED")
        return self.cells[self.next_index] if self.next_index < len(self.cells) else None

    def confirm_mock_place(self, index: int) -> None:
        if self.blocked_reason:
            raise PalletError("цикл BLOCKED")
        if index != self.next_index or index in self.placed:
            self.blocked_reason = "пропуск або повтор комірки"
            raise PalletError(self.blocked_reason)
        self.placed.add(index)
        self.next_index += 1

    def stop(self, reason: str) -> None:
        self.blocked_reason = reason


def worksheet_csv(cells: list[Cell]) -> str:
    stream = io.StringIO(newline="")
    writer = csv.writer(stream)
    writer.writerow(["index", "row", "column", "x_synth", "y_synth", "z_synth", "approach_z_synth"])
    for cell in cells:
        writer.writerow([cell.index, cell.row, cell.column, cell.place.x, cell.place.y, cell.place.z, cell.approach.z])
    return stream.getvalue()


fixture = {
    "profile": "SYNTHETIC_PALLET_OFFLINE_ONLY",
    "units": "mm",
    "frame": "SYNTHETIC_PALLET",
    "rows": 2,
    "columns": 3,
    "pitch_x_mm": 20.0,
    "pitch_y_mm": 25.0,
    "clearance_mm": 30.0,
    "order": "serpentine",
    "base_pose": [-30.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))
cells = generate_cells(config)
assert [(c.row, c.column) for c in cells] == [(0, 0), (0, 1), (0, 2), (1, 2), (1, 1), (1, 0)]
cycle = OfflineCycle(cells)
while (cell := cycle.next_cell()) is not None:
    cycle.confirm_mock_place(cell.index)
assert len(cycle.placed) == 6 and cycle.next_cell() is None

bad_cycle = OfflineCycle(cells)
try:
    bad_cycle.confirm_mock_place(1)
except PalletError as exc:
    assert "пропуск" in str(exc)
else:
    raise AssertionError("пропуск індексу має блокувати цикл")

outside = dict(fixture)
outside["base_pose"] = [100.0, -20.0, 10.0, 0.0]
try:
    generate_cells(parse_config_text(json.dumps(outside)))
except PalletError as exc:
    assert "envelope" in str(exc)
else:
    raise AssertionError("поза envelope має бути відхилена")

csv_text = worksheet_csv(cells)
assert len(csv_text.splitlines()) == 7 and "x_synth" in csv_text
print(json.dumps({"result": "PASS", "cells": 6, "order": "serpentine", "connected": False}, ensure_ascii=False))
Bash
python -m py_compile lab12_pallet_offline.py
python lab12_pallet_offline.py

Очікуваний результат: result=PASS, cells=6, order=serpentine, connected=false.

Критерій правильності: код повертає 0, не пише файлів у smoke, не використовує мережу/робот і блокує пропуск індексу.

Якщо результат не отримано: не відкривайте connection; перевірте JSON schema, індексацію й envelope.

Крок 3. Перевірити порядок і геометрію

Побудуйте таблицю шести (row,column) і переконайтеся, що другий рядок serpentine іде 2,1,0. Обчисліть координати вручну для двох комірок.

Text
row 0: (0,0) -> (0,1) -> (0,2)
row 1: (1,2) -> (1,1) -> (1,0)

Очікуваний результат: кожна пара зустрічається рівно один раз; place та approach відрізняються лише z.

Критерій правильності: 6 унікальних індексів, 6 унікальних пар і 6 унікальних place poses.

Якщо результат не отримано: зупиніть цикл; не «виправляйте» дубль зміною координати вручну — виправте формулу/config.

Крок 4. Перевірити заповнення й safe stop

Проаналізуйте OfflineCycle: підтвердження дозволено лише для next_index. Додайте тест stop("mock camera unavailable"), після якого next_cell() має завершитися помилкою.

Bash
python lab12_pallet_offline.py

Очікуваний результат: штатний цикл завершує 6 комірок; пропуск або stop створює BLOCKED.

Критерій правильності: після block немає автоматичного переходу до наступної комірки.

Якщо результат не отримано: зробіть blocked_reason незворотним у поточному екземплярі й вимагайте нової звірки/нового циклу.

Крок 5. Перевірити негативні й граничні сценарії

Додайте тести: rows=0, columns=21, pitch=0, approach за z_max, base на межі, невідомий order, зайвий ключ, NaN.

Text
Гранична точка == envelope limit: дозволено
Точка > limit на будь-яку величину: BLOCKED
Автоматичний clipping: заборонено

Очікуваний результат: невалідні profile-и відхиляються до генерації worksheet.

Критерій правильності: усі негативні тести завершуються PalletError, а valid boundary проходить.

Якщо результат не отримано: посиліть loader/check_pose; не розширюйте envelope, щоб приховати помилку.

Крок 6. Підготувати worksheet і журнал

Перегляньте csv_text у пам'яті. Для студентського звіту можна зберегти його лише у власному дозволеному workspace; automatic smoke файл не створює.

JSON
{
  "mode": "offline",
  "profile": "SYNTHETIC_PALLET_OFFLINE_ONLY",
  "cells_generated": 6,
  "cells_physically_placed": 0,
  "connected": false
}

Очікуваний результат: worksheet має header + 6 рядків і synthetic labels.

Критерій правильності: у журналі фізично розміщених деталей 0, відсутні IP/секрети.

Якщо результат не отримано: не публікуйте двозначну таблицю; додайте suffix _synth і явний status offline.

Сценарії перевірки

ТипСценарійОчікування
позитивний2×3 serpentine6 унікальних cells, complete
позитивнийrow-majorпорядок без реверсу
негативнийпропуск index 0BLOCKED
негативнийповтор cellBLOCKED
негативнийapproach поза z envelopePalletError
граничний1×1одна валідна cell
граничнийточка на envelope limitприйнято; за межею — ні

Таблиці вимірювань і метрики

IndexRowColumnX synthY synthZ place synthZ approach synthУнікальнаУ межахСтатус mock
0
5

Метрики: generated == rows×columns; 100% унікальність; 100% poses/approaches у synthetic envelope; 0 пропусків/повторів; 0 robot commands; 0 connection; 100% негативних тестів відхилено.

[!WARNING] [ПОТРЕБУЄ ДОПРАЦЮВАННЯ: ФІЗИЧНА АПРОБАЦІЯ ПАЛЕТИЗАЦІЇ В DOBOTSTUDIO PRO] Не підтверджено: фактичне навчання бази палети, точність усіх комірок, траєкторії, collision clearance, захоплення/укладання, контроль заповнення, stop і відновлення на MERC-I5; фізична апробація не дозволена. Що потрібно додати: затверджений фактичний profile, вимірювальний протокол, безпечні швидкості/прискорення, дозволений тестовий вантаж, оцінку ризику, локального оператора, перевірений E-Stop і журнал першого запуску. Як завершити: пройти допуск, виконати read-only review і візуалізацію всіх точок, потім покроково без вантажу та з безпечним test piece на мінімальній затвердженій швидкості; перевірити кожну комірку, stop без auto-resume та журнал; вилучити block лише після повного протоколу.

Вимоги до звіту

Див. загальні вимоги. Додатково подайте:

  1. формулу палети й пояснення order;
  2. зовнішній synthetic JSON та повний код;
  3. таблицю всіх комірок/approaches і in-memory CSV;
  4. результати позитивних, негативних і граничних тестів;
  5. журнал із cells_physically_placed=0 і connected=false;
  6. перелік даних для фактичного DobotStudio profile;
  7. висновок про межі геометричної offline-перевірки.

Критерії оцінювання

СкладоваЧасткаКритерії
Підготовка10%джерела, safety, external synthetic config, відсутність фактичних припущень
Реалізація40%параметричний генератор, order, approach/retract, state/progress і worksheet
Перевірка25%count/unique/envelope, позитивні/негативні/граничні тести, code 0
Аналіз15%геометрія, frame, collision limits, stop/recovery і метрики
Звіт10%повні таблиці, журнал, джерела й чесний статус фізичної частини
Разом100%

Підключення, рух або перенесення synthetic координат у контролер є критичною помилкою.

Контрольні питання

  1. Які параметри однозначно задають прямокутну палету?
  2. Чим serpentine відрізняється від row-major?
  3. Чому однаковий z не доводить площинність реальної палети?
  4. Навіщо перевіряти approach окремо від place?
  5. Як захиститися від повторного укладання?
  6. Чому BLOCKED не можна автоматично скинути після перезапуску?
  7. Які дані потрібні для User/TCP profile?
  8. Чому offline-візуалізація не доводить collision safety?

Висновки

ЛР-12 створює відтворювану параметричну модель палети, строгий генератор, контроль заповнення, in-memory worksheet і негативні тести. Уся геометрія явно synthetic, зовнішня й не переноситься до MG400. Фактичний DobotStudio workflow, координати та рух залишаються заблокованими до інвентаризації, калібрування й операторської апробації.

MERCI SPACE