Лабораторна робота 23
Лабораторна робота №23. Діагностика та відновлення після відмов
Мета: реалізувати read-only діагностичний автомат, який за журналами та симуляційними станами розпізнає відмови сенсора, механізму, камери, БД, робота й мережі, переходить у підтверджений безпечний стан, забороняє автоматичний restart і допускає відновлення лише після ручної звірки та усунення причини.
Результати навчання та передумови
Після роботи студент уміє:
- починати діагностику з читання станів і журналів, а не рухового тесту;
- відокремлювати симптом, діагноз, доказ і recovery-умову;
- ін'єктувати software faults без створення фізичних несправностей;
- застосовувати timeout і heartbeat;
- гарантувати safe outputs у
RECOVERY_REQUIRED; - блокувати recovery без operator ack, cause removal, state reconciliation та clear area;
- формувати аудитований журнал переходів.
Передумови: ЛР-03, ЛР-05–09, ЛР-13, ЛР-20–22, базові журнали й finite-state machines.
Середовище виконання
| Середовище | Статус | Примітка |
|---|---|---|
| Google Colab | дозволено | Лише JSON і stdlib |
| Локальний ПК | основне | Read-only аналіз fixtures |
| Raspberry Pi | дозволено | Лише копії журналів/mock, без керування сервісами |
| Симуляція | основне | Ін'єкція відмов як даних |
| Фізичний комплекс MERC-I5 | не використовується | Жодних рухових або електричних тестів |
Необхідні знання та матеріали
data/fault-scenarios.jsonзі scopesimulation_only;- Python зі стандартною бібліотекою;
- додаткові fixtures і recovery-конфігурація з цієї роботи;
- мінімальна діагностика та умови негайної зупинки.
Ризики та правила безпеки
Ризики
Невірний діагноз може приховати механічну або електричну причину; reconnect не означає, що поза/черга відомі; автоматичний reset після E-Stop або втрати мережі може спричинити несподіваний рух. Симуляційний сценарій short/wire break — це дані, не інструкція змінити проводку.
Заборонені дії
- не створювати обриви, замикання, jam або втрату живлення на MERC-I5;
- не виконувати рух як початковий діагностичний тест;
- не викликати
ClearError,ResetRobot,Continue, Enable або I/O; - не відновлювати цикл лише тому, що зв'язок повернувся;
- не змінювати E-Stop, safety circuit, wiring, ACL чи мережеві маршрути.
Умови негайної зупинки
Невідома поза, пропущений heartbeat/ack, contradictory sensor, механізм timeout, camera/DB unavailable, network lost, пошкоджений журнал або невідомий код. Реакція — safe_stop, latched reason, RECOVERY_REQUIRED.
Безпечний стан
Усі звичайні керівні виходи STOP/NO_NEW_GOAL; автомат не відновлюється; джерельні журнали не змінюються; оператор отримує діагноз і точний список доказів для reconciliation.
Передпусковий чекліст
- Вхідний файл має
scope=simulation_only. - Аналізатор не імпортує socket/GPIO/serial/ROS clients.
- Журнали читаються з копії.
- Усі fault cases закінчуються
RECOVERY_REQUIRED. - Recovery потребує чотирьох gates.
- E-Stop не тестується програмним fault injector.
Хід виконання роботи
- Перевірити ручний recovery gate.
- Додати мережеві/ack/timeout fixtures.
- Реалізувати нормалізатор і діагностичний FSM.
- Прогнати repository та додаткові scenarios.
- Перевірити evidence/recovery matrix і метрики.
Методичні вказівки й теоретичні відомості
1. Симптом, причина, реакція
Один симптом може мати кілька причин. Наприклад, «сенсор не активувався» може означати відсутність деталі, jam, обрив або неправильний активний рівень. Діагностичний код повинен бути достатньо точним для наступної перевірки, але не оголошувати непідтверджену причину фактом.
2. Послідовність діагностики
- Зафіксувати час/цикл/стан і припинити нові команди.
- Зберегти журнали, heartbeat і останні підтверджені події.
- Нормалізувати симптом у fault-код.
- Перевести outputs у safe state.
- Визначити потрібні докази та процедуру reconciliation.
- Лише після усунення причини й ручного підтвердження перейти у
READY; запуск нового циклу є окремою дією.
3. Timeout і unknown
Timeout має окреме джерело часу й прив'язаний до конкретного стану. Повторне підключення не скасовує його. Невідомий fault-код безпечніше класифікувати як UNKNOWN_FAULT і зупинити, ніж вгадувати.
Текстовий опис рисунка: моніторинг виявляє симптом, негайно блокує нові команди й переходить через safe stop до діагностики; будь-яка підтверджена або невідома відмова закінчується RECOVERY_REQUIRED, а READY можливий лише після чотирьох ручних перевірок.
stateDiagram-v2
[*] --> MONITORING
MONITORING --> READY: no fault
MONITORING --> FAULT_DETECTED: symptom / timeout / lost heartbeat
FAULT_DETECTED --> SAFE_STOPPED: stop all ordinary outputs
SAFE_STOPPED --> DIAGNOSING: snapshot preserved
DIAGNOSING --> RECOVERY_REQUIRED: diagnosis + evidence list
RECOVERY_REQUIRED --> RECOVERY_REQUIRED: incomplete gates
RECOVERY_REQUIRED --> READY: operator ack + cause removed + reconciled + area clear
Рис. 1. Read-only діагностика та ручне відновлення
4. Джерела
- симуляційні відмови MERC-I5;
- експлуатація й безпека;
- ручне відновлення MG400 після E-Stop;
- програма ЛР-23.
Виконання лабораторної роботи
Крок 1. Safe-smoke recovery gate
# safe-smoke
def may_recover(*, operator_ack: bool, cause_removed: bool, reconciled: bool, area_clear: bool) -> bool:
return operator_ack and cause_removed and reconciled and area_clear
assert not may_recover(operator_ack=False, cause_removed=True, reconciled=True, area_clear=True)
assert not may_recover(operator_ack=True, cause_removed=True, reconciled=False, area_clear=True)
assert may_recover(operator_ack=True, cause_removed=True, reconciled=True, area_clear=True)
print("safe-smoke: four recovery gates PASS")
Очікуваний результат: safe-smoke: four recovery gates PASS.
Критерій правильності: пропуск будь-якого gate блокує recovery.
Якщо результат не отримано: не запускати аналізатор; виправити gate.
Крок 2. Додаткові fixtures
Збережіть як lab23-extra-cases.json.
[
{"id":"network_lost","service":"network","available":false,"expected":"STOP_AND_ERROR"},
{"id":"robot_no_ack","service":"robot","available":true,"command_ack":false,"elapsed_ticks":4,"timeout_ticks":3,"expected":"STOP_AND_ERROR"},
{"id":"mechanism_timeout","service":"mechanism","completed":false,"elapsed_ticks":5,"timeout_ticks":4,"expected":"STOP_AND_ERROR"},
{"id":"unknown_fault","service":"unknown","code":"UNMAPPED","expected":"STOP_AND_ERROR"},
{"id":"restart_while_active","service":"orchestrator","restart":true,"cycle_active":true,"expected":"RECOVERY_REQUIRED"}
]
Очікуваний результат: п'ять scenarios, що не описують спосіб створення фізичної відмови.
Критерій правильності: значення лише логічні; timeout має межу elapsed > timeout.
Якщо результат не отримано: виправити JSON; не моделювати відмову на обладнанні.
Крок 3. Повна read-only реалізація
Збережіть як lab23_diagnostics.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):
MONITORING="MONITORING"; FAULT_DETECTED="FAULT_DETECTED"; SAFE_STOPPED="SAFE_STOPPED"
DIAGNOSING="DIAGNOSING"; RECOVERY_REQUIRED="RECOVERY_REQUIRED"; READY="READY"
EVIDENCE = {
"SENSOR_WIRE_OR_CHANNEL_FAULT": ["raw channels", "active-level config", "wiring inspection by operator"],
"CAMERA_LOST": ["camera service health", "last frame timestamp", "service log"],
"DATABASE_LOST": ["DB health", "transaction status", "storage log"],
"ROBOT_LINK_LOST": ["last feedback", "actual pose by approved read-only check", "queue status"],
"CONVEYOR_JAM": ["command/feedback history", "sensor history", "operator visual inspection"],
"FEEDER_JAM": ["command/feedback history", "part sensor history", "operator visual inspection"],
"NETWORK_LOST": ["local link status", "service heartbeat", "network log"],
"ROBOT_NO_ACK": ["goal id", "ack timeout", "actual robot state"],
"MECHANISM_TIMEOUT": ["command id", "completion signal", "operator inspection"],
"RESTART_ACTIVE": ["last committed state", "physical item location", "cycle event log"],
"UNKNOWN_FAULT": ["full snapshot", "raw log", "maintainer review"],
}
@dataclass
class Diagnosis:
scenario_id: str
state: str
code: str | None
safe_outputs: bool
evidence: list[str]
transitions: list[str] = field(default_factory=list)
def classify(case: dict) -> str | None:
sid = str(case["id"])
if sid in ("sensor_normal_low", "sensor_normal_high", "inverse_logic_nc"):
return None
if sid in ("wire_break", "short_to_0v", "short_to_24v"):
return "SENSOR_WIRE_OR_CHANNEL_FAULT"
direct = {
"camera_lost":"CAMERA_LOST", "database_lost":"DATABASE_LOST",
"robot_connection_lost":"ROBOT_LINK_LOST", "conveyor_jam":"CONVEYOR_JAM",
"feeder_jam":"FEEDER_JAM", "network_lost":"NETWORK_LOST",
"robot_no_ack":"ROBOT_NO_ACK", "mechanism_timeout":"MECHANISM_TIMEOUT",
"restart_while_active":"RESTART_ACTIVE", "unknown_fault":"UNKNOWN_FAULT",
}
return direct.get(sid, "UNKNOWN_FAULT")
class DiagnosticFSM:
def __init__(self) -> None:
self.state = State.MONITORING
self.outputs = {"feeder":"STOP", "conveyor":"STOP", "robot":"NO_NEW_GOAL", "gripper":"STOP"}
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 inspect(self, case: dict) -> Diagnosis:
code = classify(case)
if code is None:
self.move(State.READY, "no-fault")
return Diagnosis(str(case["id"]), self.state.value, None, True, [], list(self.log))
self.move(State.FAULT_DETECTED, code)
self.move(State.SAFE_STOPPED, "ordinary-outputs-stopped")
self.move(State.DIAGNOSING, "snapshot-preserved")
self.move(State.RECOVERY_REQUIRED, code)
safe = all(value in ("STOP", "NO_NEW_GOAL") for value in self.outputs.values())
return Diagnosis(str(case["id"]), self.state.value, code, safe, EVIDENCE[code], list(self.log))
def recover(self, operator_ack: bool, cause_removed: bool, reconciled: bool, area_clear: bool) -> bool:
if self.state is not State.RECOVERY_REQUIRED:
return False
if not (operator_ack and cause_removed and reconciled and area_clear):
return False
self.move(State.READY, "manual-recovery-complete")
return True
def main() -> int:
p = argparse.ArgumentParser()
p.add_argument("--base", type=Path, required=True); p.add_argument("--extra", type=Path, required=True)
p.add_argument("--out", type=Path, required=True); args = p.parse_args()
base = json.loads(args.base.read_text(encoding="utf-8"))
if base.get("scope") != "simulation_only":
raise ValueError("base scenarios are not marked simulation_only")
cases = list(base["scenarios"]) + json.loads(args.extra.read_text(encoding="utf-8"))
results: list[Diagnosis] = []
for case in cases:
fsm = DiagnosticFSM(); result = fsm.inspect(case); results.append(result)
if result.state == "RECOVERY_REQUIRED":
assert result.safe_outputs and result.evidence
assert not fsm.recover(False, True, True, True)
assert fsm.recover(True, True, True, True)
for result in results:
expected_ready = result.scenario_id in ("sensor_normal_low", "sensor_normal_high", "inverse_logic_nc")
assert (result.state == "READY") == expected_ready
args.out.write_text(json.dumps([asdict(r) for r in results], ensure_ascii=False, indent=2), encoding="utf-8")
print(f"PASS: ready={sum(r.state == 'READY' for r in results)} recovery={sum(r.state == 'RECOVERY_REQUIRED' for r in results)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
Очікуваний результат: три normal cases у READY; усі faults у RECOVERY_REQUIRED, з evidence list.
Критерій правильності: scope перевірено; unknown code fail-safe; усі fault outputs safe; recovery без ack заблоковано.
Якщо результат не отримано: зберегти перший невдалий diagnosis; не виконувати руховий тест для «уточнення».
Крок 4. Запуск і аналіз журналів
python lab23_diagnostics.py --base data/fault-scenarios.json --extra lab23-extra-cases.json --out lab23-results.json
Очікуваний результат: JSON із code, evidence і transitions для кожного scenario.
Критерій правильності: robot_connection_lost, robot_no_ack і restart не переходять автоматично у READY.
Якщо результат не отримано: перевірити schema fixtures і mapping; невідомий case має лишитися UNKNOWN_FAULT.
Сценарії перевірки
| Тип | Сценарій | Diagnosis | Результат |
|---|---|---|---|
| позитивний | normal channels | none | READY |
| негативний | wire break/channel mismatch | sensor fault | RECOVERY_REQUIRED |
| негативний | camera/DB/network lost | service-specific | RECOVERY_REQUIRED |
| негативний | robot no ack/link lost | robot-specific | RECOVERY_REQUIRED |
| негативний | mechanism timeout/jam | mechanism-specific | RECOVERY_REQUIRED |
| граничний | unknown code | UNKNOWN_FAULT | fail-safe |
| граничний | restart active cycle | RESTART_ACTIVE | reconciliation |
Таблиці вимірювань і метрики
| Scenario | Симптом | Diagnosis | Evidence items | Переходів | Safe outputs | Recovery без ack |
|---|---|---|---|---|---|---|
| sensor normal | — | |||||
| wire/channel fault | blocked | |||||
| camera/database | blocked | |||||
| robot/network | blocked | |||||
| mechanism timeout | blocked | |||||
| unknown/restart | blocked |
Метрики: detection coverage, false-safe count (ціль 0), unknown-fault count, evidence completeness, 100% safe outputs і 100% blocked automatic recovery.
Вимоги до звіту
Див. загальні вимоги. Додайте fault taxonomy, код, fixtures, журнали переходів, evidence matrix, recovery gate tests, метрики та фразу «аналіз виконано на симуляційних даних; фізичні відмови не створювалися».
Критерії оцінювання
| Складова | Частка | Що оцінюється |
|---|---|---|
| Підготовка | 10% | scope, джерела, safety-чекліст |
| Реалізація | 40% | normalizer, FSM, evidence, safe state |
| Перевірка | 25% | repository/extra faults, unknown, restart, recovery |
| Аналіз | 15% | симптом/причина, timeout, метрики й обмеження |
| Звіт | 10% | аудитовані журнали й практичні recovery-вимоги |
| Разом | 100% |
Контрольні питання
- Чому рух не є першим діагностичним тестом?
- Чим симптом відрізняється від підтвердженої причини?
- Чому reconnect не дозволяє automatic resume?
- Які докази потрібні після robot link loss?
- Як визначають timeout і чому
nulldiscrepancy time не можна вгадати? - Чому unknown fault має бути fail-safe?
Висновки
Опишіть coverage діагностики, safe reactions, blocked recovery, найскладніші неоднозначні симптоми й матеріали, потрібні для затверджених фізичних recovery-процедур.