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

Лабораторна робота 8

Лабораторна робота №08. Python-модуль грипера та сервоприводів

Мета: навчитися задавати апаратно незалежний GripperAdapter; реалізувати mock відкриття, закриття, позиціювання, обмеження нормованого зусилля, підтвердження стану й виявлення невдалого захоплення; перевірити формування та розбір навчальних команд без UART, сервоприводів і фізичного руху.

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

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

  • відокремлювати прикладний сценарій від конкретного драйвера грипера;
  • визначати контракт open, close, move_to, stop, status;
  • не змішувати нормовані навчальні величини з фізичною силою, струмом або моментом;
  • валідовувати положення й зусилля до виконання команди;
  • розрізняти «команду прийнято», «позицію досягнуто» і «об'єкт захоплено»;
  • виявляти порожнє захоплення, невалідний кадр та розбіжність станів;
  • зупиняти послідовність і вимагати ручного підтвердження після невизначеності.

Передумови: Python (Protocol, dataclass, JSON, винятки), автомати станів, ЛР-03–07 та правила безпеки MERC-I5. Апаратний допуск у цій редакції не потрібний.

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

СередовищеСтатусПримітка
Google ColabдозволеноПовний mock працює лише зі стандартною бібліотекою
Локальний ПКосновнеДля наведеного mock потрібен Python 3.10+; це не твердження про ПЗ стенда
Raspberry PiдозволеноТільки mock; не відкривати UART/USB і не імпортувати pyserial
Фізичний комплекс MERC-I5не потрібенГрипер і сервоприводи не рухати; фізична апробація не дозволена

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

Результат роботи — власний GripperAdapter із mock-backend. Апаратний backend цього адаптера в майбутньому обгортатиме BusGripper із gripper_control_by_mg400, але в межах ЛР-08 бібліотека не імпортується й порт не відкривається.

Штатний грипер комплексу

ПараметрЗначенняДжерело
Типадаптивний захват, розкриття 0–82 ммспецифікація постачальника
Привідшинний сервопривід HX-06L, 6–8,4 Вспецифікація постачальника
Максимальне зусилля стиснення1000 г-с ≈ 9,8 Нспецифікація постачальника
Підключеннягрипер → Bus Servo Linker → USB → Raspberry PiADR-0008
ПротоколLewanSoul/Hiwonder, кадр 55 55 …gripper_control_by_mg400.py
Швидкість115200 бодспецифікація сервопривода
Діапазон керування0…1000 (≈0°–240° повороту сервопривода)специфікація сервопривода

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

Ризики

НебезпекаНаслідокЗахист у цій роботі
Змикання пальців гриперазащемлення або пошкодження об'єктатільки mock, без UART/USB/GPIO
Надмірне зусилля або невірна межапошкодження грипера/деталіу mock лише нормовані одиниці; фізичні межі не припускаються
Втрата живлення з вантажемпадіння деталіне оголошувати «відкрити» універсально безпечним станом; потрібна оцінка конкретного сценарію
Хибне підтвердженняробот переміщує незахоплену детальholding=True лише після окремої перевірки mock
Автокалібруваннянепередбачений рух до упоракалібрування фізичного грипера заборонене

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

  • запускати codes/gripper_control_by_mg400.py або codes/myCobot Adaptive Gripper.py: секція __main__ кожного з них виконує фізичний рух губок;
  • імпортувати gripper_control_by_mg400 у власному коді ЛР-08 — BusGripper.__init__ відкриває послідовний порт одразу;
  • відкривати /dev/ttyUSB*, /dev/ttyAMA*, /dev/serial*, керувати Bus Servo Linker або подавати живлення;
  • вважати відповідність open_pos = 400 і close_limit = 570 фізичному розкриттю в міліметрах підтвердженою;
  • подавати нормовані навчальні 0..100 як відсоток реального розкриття або як ньютони;
  • стискати об'єкт, який тримає людина, обходити механічні межі або повторювати команду після втрати відповіді.

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

У майбутньому фізичному тесті: людина в зоні, невідомий стан/положення, втрата зв'язку, розбіжність команди й відповіді, падіння вантажу, незвичний звук/нагрів, перевищення струму, пошкодження кабелю. У mock: невідомий opcode, зайве поле, нечислове значення, відсутність підтвердження або необроблений виняток.

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

Безпечний стан mock — STOPPED або FAULT, заборона нових команд і зафіксована причина. Для реального грипера не можна наперед обрати «відкрити» чи «закрити»: відкриття може впустити вантаж, а закриття — створити защемлення. Фактичний стан визначають оцінкою ризику для інструмента, вантажу й робочої пози.

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

  • Файл містить лише mock та внутрішній навчальний codec.
  • Немає serial, socket, GPIO, subprocess і адрес пристроїв.
  • Усі величини позначені як нормовані симуляційні одиниці.
  • Невдале захоплення блокує подальшу послідовність.
  • Після FAULT потрібне явне підтвердження.
  • Фізичний грипер не під'єднаний до процесу.

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

  1. Визначити GripperAdapter і семантику підтверджень.
  2. Реалізувати самодостатній mock та навчальний codec.
  3. Перевірити відкриття, позиціювання й успішне захоплення.
  4. Перевірити порожнє захоплення та пошкоджені команди.
  5. Перевірити межі, зупинку й ручне відновлення.
  6. Оформити паспорт, метрики та звіт.

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

1. Adapter як межа відповідальності

Прикладний сценарій не повинен знати baud rate, байти кадру або бібліотеку привода. Він працює з контрактом. Апаратний backend у майбутньому реалізує той самий контракт, але лише після перевірки протоколу.

РівеньЗнаєНе повинен знати
сценарійopen/close/status, очікуваний результатUART, pinout, байти
GripperAdapterсемантику операцій і помилокконкретний transport
mockдетерміновану модель стануфізичні сили й геометрію
майбутній backendперевірений протокол і тайм-аутибізнес-логіку всієї комірки

2. Підтвердження захоплення

Досягнута позиція не доводить наявності деталі. Реальна система може використовувати позицію, струм, силу, окремий сенсор або комбінацію ознак — але конкретний спосіб MERC-I5 не підтверджено. У mock об'єкт задається явно, щоб перевірити логіку без підміни фізики.

3. Потік команди

Текстовий опис рисунка: сценарій звертається тільки до GripperAdapter; adapter перевіряє межі й передає валідну дію mock-backend, після чого окремо оцінюються досягнення позиції та наявність об'єкта; будь-яка помилка веде у FAULT/STOPPED, а не до наступного руху.

Mermaid
flowchart LR
    A["Сценарій"] --> B["GripperAdapter"]
    B --> C{"Валідація меж"}
    C -->|"OK"| D["Mock backend"]
    D --> E["Позиція підтверджена"]
    E --> F{"Об'єкт підтверджено?"}
    F -->|"так"| G["HOLDING"]
    F -->|"ні"| H["FAULT / STOPPED"]
    C -->|"помилка"| H

Рис. 1. Ілюстративне розділення adapter, валідації та mock-грипера

4. Навчальний codec

JSON-кадр у цій роботі є внутрішнім форматом mock, створеним для тестування часткової відповідальності «сформувати — розібрати — відхилити зайве». Він не є UART-протоколом Elephant Robotics або MERC-I5.

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

Несекретний файл gripper.mock.json для runtime:

JSON
{
  "backend": "mock",
  "min_position": 0.0,
  "max_position": 100.0,
  "max_effort": 60.0,
  "tolerance": 0.01
}

Це нормовані навчальні одиниці, не параметри реального грипера. Inline MockConfig() у safe-smoke є лише test fixture; runtime повинен прочитати JSON через pathlib/json, перевірити точний набір ключів і типи, а фізичний профіль — зберігати окремо й створювати лише після протокольної та електричної валідації.

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

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

Крок 1. Специфікувати контракт

Зафіксуйте операції й розділіть три результати: прийняття команди, досягнення позиції та підтвердження утримання.

Text
open() -> позиція відкрита, holding=False
move_to(position, effort) -> позиція підтверджена або GripperFault
close(expect_object=True) -> holding=True або GripperFault
stop(reason) -> STOPPED, нові команди заборонені
status() -> незмінний знімок стану

Очікуваний результат: контракт не містить UART, фізичних одиниць або конкретного виробника.

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

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

Крок 2. Реалізувати й запустити mock

Збережіть код як lab08_gripper_mock.py.

Python
# safe-smoke
from __future__ import annotations

import json
from dataclasses import dataclass
from enum import Enum
from typing import Protocol


class GripperState(str, Enum):
    OPEN = "OPEN"
    POSITIONED = "POSITIONED"
    MOVING = "MOVING"
    HOLDING = "HOLDING"
    STOPPED = "STOPPED"
    FAULT = "FAULT"


class GripperFault(RuntimeError):
    pass


@dataclass(frozen=True)
class GripperStatus:
    state: GripperState
    position: float
    holding: bool
    last_error: str | None


class GripperAdapter(Protocol):
    def open(self) -> GripperStatus: ...
    def close(self, *, expect_object: bool = True) -> GripperStatus: ...
    def move_to(self, position: float, effort: float) -> GripperStatus: ...
    def stop(self, reason: str) -> GripperStatus: ...
    def status(self) -> GripperStatus: ...


@dataclass(frozen=True)
class MockConfig:
    min_position: float = 0.0
    max_position: float = 100.0
    max_effort: float = 60.0
    tolerance: float = 0.01

    def __post_init__(self) -> None:
        if self.min_position >= self.max_position:
            raise ValueError("межі положення задані неправильно")
        if self.max_effort <= 0 or self.tolerance < 0:
            raise ValueError("межі effort/tolerance мають бути додатними")


class MockGripper:
    """Нормовані одиниці mock не є відсотками або фізичною силою."""

    def __init__(self, config: MockConfig | None = None) -> None:
        self.config = config or MockConfig()
        self.position = self.config.max_position
        self.state = GripperState.OPEN
        self.object_width: float | None = None
        self.holding = False
        self.last_error: str | None = None

    def status(self) -> GripperStatus:
        return GripperStatus(self.state, self.position, self.holding, self.last_error)

    def _ensure_operable(self) -> None:
        if self.state in {GripperState.FAULT, GripperState.STOPPED}:
            raise GripperFault("грипер заблокований; потрібна ручна перевірка")

    def _fail(self, message: str) -> None:
        self.state = GripperState.FAULT
        self.holding = False
        self.last_error = message
        raise GripperFault(message)

    def load_mock_object(self, width: float) -> None:
        if not self.config.min_position <= width <= self.config.max_position:
            raise ValueError("ширина mock-об'єкта поза межами")
        self.object_width = width

    def move_to(self, position: float, effort: float) -> GripperStatus:
        self._ensure_operable()
        if not self.config.min_position <= position <= self.config.max_position:
            raise ValueError("position поза межами mock")
        if not 0.0 < effort <= self.config.max_effort:
            raise ValueError("effort поза межами mock")
        self.state = GripperState.MOVING
        self.position = float(position)
        self.holding = self.object_width is not None and position <= self.object_width
        self.state = GripperState.HOLDING if self.holding else (
            GripperState.OPEN if position == self.config.max_position else GripperState.POSITIONED
        )
        return self.status()

    def open(self) -> GripperStatus:
        self._ensure_operable()
        self.object_width = None
        self.holding = False
        self.position = self.config.max_position
        self.state = GripperState.OPEN
        return self.status()

    def close(self, *, expect_object: bool = True) -> GripperStatus:
        self._ensure_operable()
        if expect_object and self.object_width is None:
            self._fail("об'єкт не підтверджено")
        target = self.object_width if self.object_width is not None else self.config.min_position
        result = self.move_to(target, effort=self.config.max_effort / 2)
        if expect_object and not result.holding:
            self._fail("захоплення не підтверджено")
        return result

    def stop(self, reason: str) -> GripperStatus:
        self.state = GripperState.STOPPED
        self.holding = False
        self.last_error = reason
        return self.status()

    def acknowledge(self, *, operator_ack: bool) -> None:
        if self.state not in {GripperState.FAULT, GripperState.STOPPED}:
            raise GripperFault("немає стану для скидання")
        if not operator_ack:
            raise GripperFault("немає явного підтвердження")
        self.object_width = None
        self.holding = False
        self.last_error = None
        self.position = self.config.max_position
        self.state = GripperState.OPEN


class MockCommandCodec:
    """Внутрішній JSON mock; це не кадр фізичного грипера."""

    allowed = {"op", "position", "effort"}

    @classmethod
    def encode_move(cls, position: float, effort: float) -> bytes:
        return json.dumps(
            {"op": "MOVE", "position": position, "effort": effort},
            sort_keys=True,
            separators=(",", ":"),
        ).encode("utf-8")

    @classmethod
    def decode(cls, frame: bytes) -> dict[str, object]:
        value = json.loads(frame.decode("utf-8"))
        if not isinstance(value, dict) or set(value) != cls.allowed:
            raise ValueError("неправильний набір полів mock-кадру")
        if value["op"] != "MOVE":
            raise ValueError("невідома операція mock")
        if not all(isinstance(value[key], (int, float)) for key in ("position", "effort")):
            raise ValueError("position/effort мають бути числами")
        return value


gripper: GripperAdapter = MockGripper()
assert gripper.open().state == GripperState.OPEN
mock = gripper
assert isinstance(mock, MockGripper)
mock.load_mock_object(42.0)
held = mock.close(expect_object=True)
assert held.holding and held.position == 42.0 and held.state == GripperState.HOLDING
assert mock.open().holding is False

positioned = MockGripper()
position_result = positioned.move_to(60.0, 10.0)
assert position_result.state == GripperState.POSITIONED and not position_result.holding

empty = MockGripper()
try:
    empty.close(expect_object=True)
except GripperFault as exc:
    assert "не підтверджено" in str(exc)
else:
    raise AssertionError("порожнє захоплення має завершитися помилкою")
assert empty.status().state == GripperState.FAULT
try:
    empty.acknowledge(operator_ack=False)
except GripperFault:
    pass
else:
    raise AssertionError("скидання без підтвердження заборонене")
empty.acknowledge(operator_ack=True)

frame = MockCommandCodec.encode_move(55.0, 20.0)
assert MockCommandCodec.decode(frame)["position"] == 55.0
try:
    MockCommandCodec.decode(b'{"op":"MOVE","position":1,"effort":2,"pin":7}')
except ValueError:
    pass
else:
    raise AssertionError("зайве поле має бути відхилене")

try:
    MockGripper().move_to(-0.01, 10.0)
except ValueError:
    pass
else:
    raise AssertionError("позиція поза межею має бути відхилена")

print(json.dumps({"checks": 8, "result": "PASS", "backend": "mock"}, ensure_ascii=False))
Bash
python -m py_compile lab08_gripper_mock.py
python lab08_gripper_mock.py

Очікуваний результат: JSON із "checks": 8, "result": "PASS", "backend": "mock".

Критерій правильності: процес повертає 0, не імпортує апаратні бібліотеки й не відкриває пристрої.

Якщо результат не отримано: не переходьте до UART; перевірте повноту копіювання, traceback та інваріанти mock.

Крок 3. Перевірити codec без обладнання

Порівняйте encode/decode для валідної команди та відхилення кадру з полем pin. Поясніть, чому whitelist полів важливий.

Text
Валідний mock: op, position, effort
Невалідний mock: зайве поле, невідомий op, нечислове значення, пошкоджений UTF-8/JSON

Очікуваний результат: валідний кадр відновлює ті самі дані; невідоме поле відхиляється.

Критерій правильності: parser не ігнорує пошкоджені або зайві дані мовчки.

Якщо результат не отримано: зупиніть обробку на першій помилці, не робіть «поблажливий» parser для апаратної команди.

Крок 4. Перевірити успішне й невдале захоплення

Простежте два сценарії: завантажений mock-об'єкт шириною 42.0 і відсутній об'єкт. Число 42.0 — лише тестова нормована координата.

СценарійПозиціяholdingСтан
mock-об'єкт присутній42.0TrueHOLDING
об'єкт відсутнійне використовується як доказFalseFAULT

Очікуваний результат: порожнє захоплення не оголошується успішним.

Критерій правильності: після помилки подальші команди заблоковані до acknowledge(operator_ack=True).

Якщо результат не отримано: викличте stop("невизначений результат захоплення") і виправте критерій підтвердження.

Крок 5. Перевірити межі та зупинку

Додайте локальні тести для position=0.0, position=100.0, position=100.01, effort=0.0 і effort=60.01. Ці межі є лише конфігурацією mock.

Bash
python lab08_gripper_mock.py

Очікуваний результат: дві крайні допустимі позиції прийняті; вихід за межі й нульове зусилля відхилені до зміни стану.

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

Якщо результат не отримано: перенесіть валідацію перед MOVING; не змінюйте межі без тесту.

Крок 6. Скласти паспорт модуля

ПолеПідтверджений результат
Прикладний інтерфейсGripperAdapter
РеалізаціяMockGripper
Навчальний transportвнутрішній JSON, не апаратний протокол
Безпечний стан mockSTOPPED/FAULT, команди заблоковані
Фізична модель і UARTне підтверджено
Сила/момент/позиціялише нормовані mock-одиниці
Апаратна апробаціяне виконувалася

Очікуваний результат: чітко видно межу між перевіреним API й неперевіреним backend.

Критерій правильності: немає заяви про фізичну силу, відсотки відкриття, baud rate чи pinout як факт MERC-I5.

Якщо результат не отримано: вилучіть непідтверджені величини та перелічіть потрібне джерело/вимірювання.

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

ТипВхідОчікуваний результат
позитивнийвідкрити mockOPEN, holding=False
позитивнийоб'єкт 42.0, закритиHOLDING, позиція 42.0
негативнийзакрити без об'єктаFAULT, нові команди заблоковані
негативнийзайве поле pinValueError, команда не виконується
негативнийскидання без підтвердженняGripperFault
граничниймінімальна/максимальна позиціяприйнято в mock
граничнийпозиція або effort поза межеювідхилено до рухового стану

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

ТестКомандаЦіль mockДосягнута позиціяholdingКінцевий станPASS/FAIL
1open
2close з об'єктом
3close без об'єкта
4пошкоджений frame
5межа позиції

Метрики: 100% обов'язкових тестів PASS; 0 необроблених винятків; 0 апаратних I/O; 0 випадків, коли відсутній об'єкт оголошено утримуваним; 0 автоматичних скидань.

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

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

  1. контракт GripperAdapter і рисунок потоку;
  2. повний mock-код та JSON smoke-результат;
  3. таблицю позитивних, негативних і граничних тестів;
  4. пояснення різниці між позицією та підтвердженим захопленням;
  5. паспорт із чітким позначенням нормованих одиниць;
  6. аналіз того, чому канонічний gripper_control_by_mg400 ще не можна викликати напряму зі сценарію: відкриття порту в конструкторі, відсутність тайм-аутів прикладного рівня та непідтверджена відповідність положень фізичному розкриттю;
  7. твердження, що фізична апробація не виконувалася.

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

СкладоваЧасткаКритерії
Підготовка10%джерела, межі, чекліст, відсутність апаратних припущень
Реалізація40%GripperAdapter, mock, codec, валідація, підтвердження й безпечний стан
Перевірка25%позитивні, негативні й граничні тести, код 0, відсутність I/O
Аналіз15%позиція проти захоплення, ризик падіння, межі канонічного драйвера
Звіт10%паспорт, таблиці, джерела, висновки, статус фізичної перевірки
Разом100%

Апаратний запуск або представлення mock-одиниць як фізичних параметрів є критичною помилкою безпеки.

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

  1. Навіщо потрібен GripperAdapter?
  2. Чому досягнення позиції не доводить захоплення деталі?
  3. Коли відкриття грипера може бути небезпечним станом?
  4. Чому parser повинен відхиляти зайві поля?
  5. Яка різниця між STOPPED і FAULT у цій моделі?
  6. Чому нормовані 0..100 не можна автоматично назвати відсотками реального відкриття?
  7. Які докази потрібні для підтвердження UART-протоколу?
  8. Чому апаратна калібровка не входить до цієї роботи?

Висновки

ЛР завершується перевіреним GripperAdapter, детермінованим mock і строгим навчальним codec. Вона демонструє підтвердження стану, невдале захоплення, межі та ручне відновлення без будь-якого фізичного I/O. Конкретний UART, розпіновка, сила, положення й безпечний стан комплектного грипера залишаються заблокованими до первинної документації та контрольованої апробації.

MERCI SPACE