Лабораторна робота 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 | не потрібен | Грипер і сервоприводи не рухати; фізична апробація не дозволена |
Необхідні знання та матеріали
- довідник грипера MERC-I5, програма ЛР-08 і статус вихідних даних;
- аналіз інтеграції MG400 та політика допуску;
- Python зі стандартними модулями
typing,dataclasses,enum,json; - канонічний драйвер
codes/gripper_control_by_mg400.py— лише для читання й аналізу API, без імпорту та запуску; - файл
codes/myCobot Adaptive Gripper.py— приклад іншого UART-протоколу для порівняння; він не описує грипер 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 Pi | ADR-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потрібне явне підтвердження. - Фізичний грипер не під'єднаний до процесу.
Хід виконання роботи
- Визначити
GripperAdapterі семантику підтверджень. - Реалізувати самодостатній mock та навчальний codec.
- Перевірити відкриття, позиціювання й успішне захоплення.
- Перевірити порожнє захоплення та пошкоджені команди.
- Перевірити межі, зупинку й ручне відновлення.
- Оформити паспорт, метрики та звіт.
Методичні вказівки й теоретичні відомості
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, а не до наступного руху.
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:
{
"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. Першоджерела
codes/gripper_control_by_mg400.py— канонічний драйвер грипера MERC-I5: структура кадру, контрольна сума,open/close_and_learn_hold, механізм утримання.- Офіційний репозиторій
pymycobotElephant Robotics — бібліотека іншого виробника; використовується лише для порівняння протоколів і не описує грипер MERC-I5. - Python:
typing.Protocolі Python:json— офіційна документація інструментів mock. - Довідник грипера MERC-I5 і аналіз драйвера MG400 — обладнання, протокол, статус перевірки та вимога
GripperAdapter. - Правила експлуатації MERC-I5 — зупинка й відновлення.
Виконання лабораторної роботи
Крок 1. Специфікувати контракт
Зафіксуйте операції й розділіть три результати: прийняття команди, досягнення позиції та підтвердження утримання.
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.
# 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))
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 полів важливий.
Валідний mock: op, position, effort
Невалідний mock: зайве поле, невідомий op, нечислове значення, пошкоджений UTF-8/JSON
Очікуваний результат: валідний кадр відновлює ті самі дані; невідоме поле відхиляється.
Критерій правильності: parser не ігнорує пошкоджені або зайві дані мовчки.
Якщо результат не отримано: зупиніть обробку на першій помилці, не робіть «поблажливий» parser для апаратної команди.
Крок 4. Перевірити успішне й невдале захоплення
Простежте два сценарії: завантажений mock-об'єкт шириною 42.0 і відсутній об'єкт. Число 42.0 — лише тестова нормована координата.
| Сценарій | Позиція | holding | Стан |
|---|---|---|---|
| mock-об'єкт присутній | 42.0 | True | HOLDING |
| об'єкт відсутній | не використовується як доказ | False | FAULT |
Очікуваний результат: порожнє захоплення не оголошується успішним.
Критерій правильності: після помилки подальші команди заблоковані до acknowledge(operator_ack=True).
Якщо результат не отримано: викличте stop("невизначений результат захоплення") і виправте критерій підтвердження.
Крок 5. Перевірити межі та зупинку
Додайте локальні тести для position=0.0, position=100.0, position=100.01, effort=0.0 і effort=60.01. Ці межі є лише конфігурацією mock.
python lab08_gripper_mock.py
Очікуваний результат: дві крайні допустимі позиції прийняті; вихід за межі й нульове зусилля відхилені до зміни стану.
Критерій правильності: невалідна команда не створює HOLDING і не маскується як успіх.
Якщо результат не отримано: перенесіть валідацію перед MOVING; не змінюйте межі без тесту.
Крок 6. Скласти паспорт модуля
| Поле | Підтверджений результат |
|---|---|
| Прикладний інтерфейс | GripperAdapter |
| Реалізація | MockGripper |
| Навчальний transport | внутрішній JSON, не апаратний протокол |
| Безпечний стан mock | STOPPED/FAULT, команди заблоковані |
| Фізична модель і UART | не підтверджено |
| Сила/момент/позиція | лише нормовані mock-одиниці |
| Апаратна апробація | не виконувалася |
Очікуваний результат: чітко видно межу між перевіреним API й неперевіреним backend.
Критерій правильності: немає заяви про фізичну силу, відсотки відкриття, baud rate чи pinout як факт MERC-I5.
Якщо результат не отримано: вилучіть непідтверджені величини та перелічіть потрібне джерело/вимірювання.
Сценарії перевірки
| Тип | Вхід | Очікуваний результат |
|---|---|---|
| позитивний | відкрити mock | OPEN, holding=False |
| позитивний | об'єкт 42.0, закрити | HOLDING, позиція 42.0 |
| негативний | закрити без об'єкта | FAULT, нові команди заблоковані |
| негативний | зайве поле pin | ValueError, команда не виконується |
| негативний | скидання без підтвердження | GripperFault |
| граничний | мінімальна/максимальна позиція | прийнято в mock |
| граничний | позиція або effort поза межею | відхилено до рухового стану |
Таблиці вимірювань і метрики
| Тест | Команда | Ціль mock | Досягнута позиція | holding | Кінцевий стан | PASS/FAIL |
|---|---|---|---|---|---|---|
| 1 | open | |||||
| 2 | close з об'єктом | |||||
| 3 | close без об'єкта | — | — | |||
| 4 | пошкоджений frame | — | — | — | ||
| 5 | межа позиції |
Метрики: 100% обов'язкових тестів PASS; 0 необроблених винятків; 0 апаратних I/O; 0 випадків, коли відсутній об'єкт оголошено утримуваним; 0 автоматичних скидань.
Вимоги до звіту
Див. загальні вимоги. Додатково подайте:
- контракт
GripperAdapterі рисунок потоку; - повний mock-код та JSON smoke-результат;
- таблицю позитивних, негативних і граничних тестів;
- пояснення різниці між позицією та підтвердженим захопленням;
- паспорт із чітким позначенням нормованих одиниць;
- аналіз того, чому канонічний
gripper_control_by_mg400ще не можна викликати напряму зі сценарію: відкриття порту в конструкторі, відсутність тайм-аутів прикладного рівня та непідтверджена відповідність положень фізичному розкриттю; - твердження, що фізична апробація не виконувалася.
Критерії оцінювання
| Складова | Частка | Критерії |
|---|---|---|
| Підготовка | 10% | джерела, межі, чекліст, відсутність апаратних припущень |
| Реалізація | 40% | GripperAdapter, mock, codec, валідація, підтвердження й безпечний стан |
| Перевірка | 25% | позитивні, негативні й граничні тести, код 0, відсутність I/O |
| Аналіз | 15% | позиція проти захоплення, ризик падіння, межі канонічного драйвера |
| Звіт | 10% | паспорт, таблиці, джерела, висновки, статус фізичної перевірки |
| Разом | 100% |
Апаратний запуск або представлення mock-одиниць як фізичних параметрів є критичною помилкою безпеки.
Контрольні питання
- Навіщо потрібен
GripperAdapter? - Чому досягнення позиції не доводить захоплення деталі?
- Коли відкриття грипера може бути небезпечним станом?
- Чому parser повинен відхиляти зайві поля?
- Яка різниця між
STOPPEDіFAULTу цій моделі? - Чому нормовані
0..100не можна автоматично назвати відсотками реального відкриття? - Які докази потрібні для підтвердження UART-протоколу?
- Чому апаратна калібровка не входить до цієї роботи?
Висновки
ЛР завершується перевіреним GripperAdapter, детермінованим mock і строгим навчальним codec. Вона демонструє підтвердження стану, невдале захоплення, межі та ручне відновлення без будь-якого фізичного I/O. Конкретний UART, розпіновка, сила, положення й безпечний стан комплектного грипера залишаються заблокованими до первинної документації та контрольованої апробації.