Лабораторна робота 20
Лабораторна робота №20. Подавач, конвеєр і сенсор як автомат станів
Мета: реалізувати детермінований автомат зі станами IDLE, FEEDING, TRANSPORTING, DETECTED, COMPLETE, FAULT, тайм-аутами, захистом від подвійної подачі, контролем втрати/заклинювання деталі, журналом переходів і лише ручним відновленням; перевірити його повністю на mock-компонентах.
Результати навчання та передумови
Після роботи студент уміє:
- задавати дозволені переходи та інваріанти станів;
- відокремлювати команди від підтверджень стану;
- використовувати monotonic/logical time для тайм-аутів;
- ін'єктувати подвійне подавання, заклинювання, втрату деталі та stuck-sensor;
- гарантувати
feeder=STOPіconveyor=STOPуFAULT; - виконувати відновлення лише після усунення причини, огляду та ручного підтвердження.
Передумови: ЛР-05–07, Python, Enum, JSON і unit-test мислення.
Середовище виконання
| Середовище | Статус | Примітка |
|---|---|---|
| Google Colab | дозволено | Стандартна бібліотека, логічні ticks |
| Локальний ПК | основне | Mock-автомат і JSON-сценарії |
| Raspberry Pi | дозволено | Лише mock; без GPIO та апаратних сервісів |
| Симуляція | основне | Ін'єкція відмов без створення електричних несправностей |
| Фізичний комплекс MERC-I5 | не використовується | Подавач, конвеєр і сенсор не вмикаються |
Необхідні знання та матеріали
- Python зі стандартною бібліотекою;
data/fault-scenarios.jsonяк джерело назв симуляційних відмов;- зовнішня JSON-конфігурація й fixtures, наведені нижче;
- операційна безпека і правила допуску.
Ризики та правила безпеки
Ризики
Повторна подача може затиснути дві деталі; відсутність сенсорної події може означати втрату або заклинювання; stuck-active — заблоковану зону; автоматичний reset може запустити механізм біля людини. Симуляційний тайм-аут не є фізично підтвердженим.
Заборонені дії
- не підключати mock до GPIO, реле чи мережевого сервісу;
- не створювати обрив, замикання або заклинювання на штатному обладнанні;
- не переходити
FAULT → IDLEлише за часом або після перезапуску; - не видавати другу команду feed, доки перша деталь не завершила цикл;
- не трактувати два звичайні сенсори як safety-rated канал.
Умови негайної зупинки
Тайм-аут подавача/транспорту, count ≠ 1, суперечливий або stuck-сигнал, повторна команда, невідомий стан, втрата зв'язку чи виняток. Реакція: обидва mock-виходи STOP, зафіксований FAULT.
Безпечний стан
feeder_output=STOP, conveyor_output=STOP, помилка latched, автоматичний restart заборонений. Ручне відновлення потребує трьох незалежних булевих підтверджень: оператор, причина усунена, зона вільна.
Передпусковий чекліст
- Режим конфігурації —
simulation-only. - Тайм-аути додатні й позначені як навчальні ticks.
- Адаптери не імпортують GPIO/serial/socket.
- Кожний
FAULTвикликаєsafe_stop(). - Підготовлені позитивний, негативні й граничні fixtures.
- Відновлення не виконується автоматично.
Хід виконання роботи
- Перевірити мінімальний контракт станів.
- Створити зовнішню конфігурацію сценарію.
- Реалізувати FSM і журнал переходів.
- Запустити нормальний цикл і відмови.
- Перевірити ручне відновлення та метрики.
Методичні вказівки й теоретичні відомості
1. Стани та інваріанти
| Стан | Дозволені виходи | Обов'язкова умова виходу |
|---|---|---|
IDLE | обидва STOP | start і зона вільна |
FEEDING | feeder RUN, conveyor STOP | рівно одна деталь підтверджена до timeout |
TRANSPORTING | feeder STOP, conveyor RUN | сенсор підтвердив деталь до timeout |
DETECTED | обидва STOP | валідний pulse і count = 1 |
COMPLETE | обидва STOP | цикл завершено, новий start окремо |
FAULT | обидва STOP | лише ручна процедура recovery |
Команда означає намір; підтвердження має походити з незалежного стану/моделі. Тайм-аут вимірюється від входу в стан і перевіряється на кожному tick. Межа в цій ЛР: подія в tick, що дорівнює timeout, приймається; timeout + 1 — відмова.
Текстовий опис рисунка: IDLE переходить через подавання, транспортування та виявлення до COMPLETE; timeout, подвійна подача, втрата чи некоректний сенсор ведуть у latched FAULT, з якого можливе лише ручне відновлення.
stateDiagram-v2
[*] --> IDLE
IDLE --> FEEDING: start && zone_clear
FEEDING --> TRANSPORTING: one part fed
TRANSPORTING --> DETECTED: sensor active
DETECTED --> COMPLETE: pulse valid && count=1
FEEDING --> FAULT: timeout / double feed
TRANSPORTING --> FAULT: timeout / lost part
DETECTED --> FAULT: stuck / inconsistent
FAULT --> IDLE: manual ack + cause removed + area clear
COMPLETE --> IDLE: new cycle reset
Рис. 1. Автомат подавання й транспортування з latched FAULT
2. Джерела
- програма ЛР-20;
- симуляційні сценарії відмов;
- вимоги до безпечного стану MERC-I5;
- політика доступу студентів.
Виконання лабораторної роботи
Крок 1. Safe-smoke latched FAULT
# safe-smoke
from enum import Enum
class State(str, Enum):
IDLE = "IDLE"
FAULT = "FAULT"
def recover(state: State, *, operator_ack: bool, cause_removed: bool, area_clear: bool) -> State:
if state is not State.FAULT:
return state
return State.IDLE if operator_ack and cause_removed and area_clear else State.FAULT
assert recover(State.FAULT, operator_ack=False, cause_removed=True, area_clear=True) is State.FAULT
assert recover(State.FAULT, operator_ack=True, cause_removed=False, area_clear=True) is State.FAULT
assert recover(State.FAULT, operator_ack=True, cause_removed=True, area_clear=True) is State.IDLE
print("safe-smoke: manual recovery gate PASS")
Очікуваний результат: safe-smoke: manual recovery gate PASS.
Критерій правильності: жодного автоматичного переходу з FAULT.
Якщо результат не отримано: не продовжувати; виправити recovery gate.
Крок 2. Зовнішня конфігурація
Збережіть як lab20-config.json.
{
"mode": "simulation-only",
"feed_timeout_ticks": 3,
"transport_timeout_ticks": 4,
"sensor_min_on_ticks": 1,
"sensor_max_on_ticks": 2
}
Збережіть як lab20-cases.json.
[
{"id":"normal","feed_delay":2,"sensor_delay":3,"sensor_on_ticks":1,"part_count":1,"double_feed":false},
{"id":"feed-boundary","feed_delay":3,"sensor_delay":4,"sensor_on_ticks":2,"part_count":1,"double_feed":false},
{"id":"feeder-jam","feed_delay":4,"sensor_delay":1,"sensor_on_ticks":1,"part_count":1,"double_feed":false},
{"id":"conveyor-jam","feed_delay":1,"sensor_delay":5,"sensor_on_ticks":1,"part_count":1,"double_feed":false},
{"id":"double-feed","feed_delay":1,"sensor_delay":1,"sensor_on_ticks":1,"part_count":2,"double_feed":true},
{"id":"lost-part","feed_delay":1,"sensor_delay":2,"sensor_on_ticks":1,"part_count":0,"double_feed":false},
{"id":"sensor-stuck","feed_delay":1,"sensor_delay":2,"sensor_on_ticks":3,"part_count":1,"double_feed":false}
]
Очікуваний результат: два успішні й п'ять fault-сценаріїв.
Критерій правильності: рівність timeout присутня окремо; відмови не створюються фізично.
Якщо результат не отримано: перевірити JSON; не зменшувати набір сценаріїв.
Крок 3. Повна mock-реалізація
Збережіть як lab20_fsm.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"
FEEDING = "FEEDING"
TRANSPORTING = "TRANSPORTING"
DETECTED = "DETECTED"
COMPLETE = "COMPLETE"
FAULT = "FAULT"
@dataclass
class Result:
case_id: str
state: str
fault: str | None
feeder_output: str
conveyor_output: str
transitions: list[str] = field(default_factory=list)
class CellFSM:
def __init__(self, cfg: dict) -> None:
if cfg.get("mode") != "simulation-only":
raise ValueError("hardware mode is forbidden")
self.cfg = cfg
self.state = State.IDLE
self.feeder = "STOP"
self.conveyor = "STOP"
self.fault_code: str | None = None
self.log: list[str] = []
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.feeder = "STOP"
self.conveyor = "STOP"
def fail(self, code: str) -> None:
self.safe_stop()
self.fault_code = code
self.move(State.FAULT, code)
def run(self, case: dict) -> Result:
if self.state is not State.IDLE:
raise RuntimeError("new cycle requires IDLE")
self.move(State.FEEDING, "start")
self.feeder = "RUN"
feed_delay = int(case["feed_delay"])
if bool(case["double_feed"]) or int(case["part_count"]) > 1:
self.fail("double-feed")
elif feed_delay > int(self.cfg["feed_timeout_ticks"]):
self.fail("feeder-timeout")
else:
self.feeder = "STOP"
self.conveyor = "RUN"
self.move(State.TRANSPORTING, f"feed-confirmed@{feed_delay}")
if self.state is State.TRANSPORTING:
sensor_delay = int(case["sensor_delay"])
if sensor_delay > int(self.cfg["transport_timeout_ticks"]):
self.fail("transport-timeout")
elif int(case["part_count"]) != 1:
self.fail("part-count-invalid")
else:
self.conveyor = "STOP"
self.move(State.DETECTED, f"sensor-active@{sensor_delay}")
if self.state is State.DETECTED:
pulse = int(case["sensor_on_ticks"])
low = int(self.cfg["sensor_min_on_ticks"])
high = int(self.cfg["sensor_max_on_ticks"])
if not low <= pulse <= high:
self.fail("sensor-pulse-invalid")
else:
self.safe_stop()
self.move(State.COMPLETE, f"pulse-valid:{pulse}")
assert self.state not in (State.COMPLETE, State.FAULT) or (
self.feeder == "STOP" and self.conveyor == "STOP"
)
return Result(
str(case["id"]), self.state.value, self.fault_code,
self.feeder, self.conveyor, list(self.log),
)
def recover(self, operator_ack: bool, cause_removed: bool, area_clear: bool) -> bool:
if self.state is not State.FAULT:
return False
if not (operator_ack and cause_removed and area_clear):
return False
self.fault_code = None
self.safe_stop()
self.move(State.IDLE, "manual-recovery")
return True
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--config", type=Path, required=True)
parser.add_argument("--cases", type=Path, required=True)
parser.add_argument("--out", type=Path, required=True)
args = parser.parse_args()
cfg = json.loads(args.config.read_text(encoding="utf-8"))
cases = json.loads(args.cases.read_text(encoding="utf-8"))
results: list[Result] = []
for case in cases:
fsm = CellFSM(cfg)
result = fsm.run(case)
results.append(result)
if result.state == "FAULT":
assert not fsm.recover(False, True, True)
assert fsm.state is State.FAULT
assert fsm.recover(True, True, True)
assert fsm.state is State.IDLE
states = {result.case_id: result.state for result in results}
assert states["normal"] == states["feed-boundary"] == "COMPLETE"
for case_id in ("feeder-jam", "conveyor-jam", "double-feed", "lost-part", "sensor-stuck"):
assert states[case_id] == "FAULT"
args.out.write_text(
json.dumps([asdict(result) for result in results], ensure_ascii=False, indent=2),
encoding="utf-8",
)
print(f"PASS: complete={sum(r.state == 'COMPLETE' for r in results)} fault={sum(r.state == 'FAULT' for r in results)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
Очікуваний результат: PASS: complete=2 fault=5 і JSON-журнал.
Критерій правильності: усі кінцеві стани мають обидва виходи STOP; recovery без ack не проходить.
Якщо результат не отримано: перевірити перший неправильний перехід; не обходити assertion і не підключати обладнання.
Крок 4. Запуск і перевірка меж
python lab20_fsm.py --config lab20-config.json --cases lab20-cases.json --out lab20-results.json
Очікуваний результат: граничний case з delay = timeout завершується COMPLETE; timeout + 1 — FAULT.
Критерій правильності: журнал містить причину кожного переходу; fault-код не порожній.
Якщо результат не отримано: звірити семантику межі в документації й коді; зупинити сценарій.
Фізична реалізація: адресна прогалина
Еталон потребує підтверджених FeederAdapter, ConveyorAdapter, SensorAdapter, незалежних feedback-сигналів, watchdog, виміряних часів і безпечних апаратних виходів. GPIO 3,3 V не можна з'єднувати з промисловими 24 V без підтвердженої розв'язки.
Сценарії перевірки
| Тип | Сценарій | Очікування | Безпечна реакція |
|---|---|---|---|
| позитивний | нормальний цикл | COMPLETE | outputs STOP |
| позитивний граничний | delay = timeout | COMPLETE | журнал межі |
| негативний | подвійна подача | FAULT/double-feed | негайний STOP |
| негативний | подавач/конвеєр timeout | FAULT | обидва STOP |
| негативний | count = 0 | FAULT/part-count-invalid | не продовжувати |
| граничний | pulse > max | FAULT/sensor-pulse-invalid | latched fault |
Таблиці вимірювань і метрики
| Case | Feed delay | Transport delay | Pulse | Count | Кінцевий стан | Fault | Переходів | Recovery без ack |
|---|---|---|---|---|---|---|---|---|
| normal | має бути blocked | |||||||
| feed-boundary | blocked | |||||||
| feeder-jam | blocked | |||||||
| conveyor-jam | blocked | |||||||
| double-feed | blocked | |||||||
| lost-part | blocked | |||||||
| sensor-stuck | blocked |
Метрики: completion/fault rate, частка fault із правильним кодом, максимальна кількість переходів, 100% підтвердження safe outputs у terminal states.
Вимоги до звіту
Див. загальні вимоги. Додайте таблицю станів/інваріантів, конфігурацію, fixtures, код, JSON-журнал, тести recovery, метрики та позначення «тільки симуляція; фізичні модулі не запускалися».
Критерії оцінювання
| Складова | Частка | Що оцінюється |
|---|---|---|
| Підготовка | 10% | конфігурація, інваріанти, safety |
| Реалізація | 40% | шість станів, тайм-аути, safe stop, журнал |
| Перевірка | 25% | позитивні, негативні, граничні й recovery-тести |
| Аналіз | 15% | причини fault, межі timeout, метрики |
| Звіт | 10% | відтворюваність і чесна межа апробації |
| Разом | 100% |
Контрольні питання
- Чому команда
RUNне є підтвердженням руху? - Які інваріанти діють у
FAULT? - Як відрізняються jam, lost part і sensor stuck у моделі?
- Чому recovery потребує трьох підтверджень?
- Як обрати timeout за фізичними вимірюваннями?
- Чому simulated short/wire break не треба створювати на MERC-I5?
Висновки
Опишіть перевірені переходи, реакцію на п'ять відмов, поведінку на межі тайм-ауту, ручне відновлення та дані, яких бракує для фізичної реалізації.