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

Лабораторна робота 23

Лабораторна робота №23. Діагностика та відновлення після відмов

Мета: реалізувати read-only діагностичний автомат, який за журналами та симуляційними станами розпізнає відмови сенсора, механізму, камери, БД, робота й мережі, переходить у підтверджений безпечний стан, забороняє автоматичний restart і допускає відновлення лише після ручної звірки та усунення причини.

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

Після роботи студент уміє:

  1. починати діагностику з читання станів і журналів, а не рухового тесту;
  2. відокремлювати симптом, діагноз, доказ і recovery-умову;
  3. ін'єктувати software faults без створення фізичних несправностей;
  4. застосовувати timeout і heartbeat;
  5. гарантувати safe outputs у RECOVERY_REQUIRED;
  6. блокувати recovery без operator ack, cause removal, state reconciliation та clear area;
  7. формувати аудитований журнал переходів.

Передумови: ЛР-03, ЛР-05–09, ЛР-13, ЛР-20–22, базові журнали й finite-state machines.

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

СередовищеСтатусПримітка
Google ColabдозволеноЛише JSON і stdlib
Локальний ПКосновнеRead-only аналіз fixtures
Raspberry PiдозволеноЛише копії журналів/mock, без керування сервісами
СимуляціяосновнеІн'єкція відмов як даних
Фізичний комплекс MERC-I5не використовуєтьсяЖодних рухових або електричних тестів

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

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

Ризики

Невірний діагноз може приховати механічну або електричну причину; 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.

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

  1. Перевірити ручний recovery gate.
  2. Додати мережеві/ack/timeout fixtures.
  3. Реалізувати нормалізатор і діагностичний FSM.
  4. Прогнати repository та додаткові scenarios.
  5. Перевірити evidence/recovery matrix і метрики.

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

1. Симптом, причина, реакція

Один симптом може мати кілька причин. Наприклад, «сенсор не активувався» може означати відсутність деталі, jam, обрив або неправильний активний рівень. Діагностичний код повинен бути достатньо точним для наступної перевірки, але не оголошувати непідтверджену причину фактом.

2. Послідовність діагностики

  1. Зафіксувати час/цикл/стан і припинити нові команди.
  2. Зберегти журнали, heartbeat і останні підтверджені події.
  3. Нормалізувати симптом у fault-код.
  4. Перевести outputs у safe state.
  5. Визначити потрібні докази та процедуру reconciliation.
  6. Лише після усунення причини й ручного підтвердження перейти у READY; запуск нового циклу є окремою дією.

3. Timeout і unknown

Timeout має окреме джерело часу й прив'язаний до конкретного стану. Повторне підключення не скасовує його. Невідомий fault-код безпечніше класифікувати як UNKNOWN_FAULT і зупинити, ніж вгадувати.

Текстовий опис рисунка: моніторинг виявляє симптом, негайно блокує нові команди й переходить через safe stop до діагностики; будь-яка підтверджена або невідома відмова закінчується RECOVERY_REQUIRED, а READY можливий лише після чотирьох ручних перевірок.

Mermaid
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. Джерела

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

Крок 1. Safe-smoke recovery gate

Python
# 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.

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.

Python
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. Запуск і аналіз журналів

Bash
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 channelsnoneREADY
негативнийwire break/channel mismatchsensor faultRECOVERY_REQUIRED
негативнийcamera/DB/network lostservice-specificRECOVERY_REQUIRED
негативнийrobot no ack/link lostrobot-specificRECOVERY_REQUIRED
негативнийmechanism timeout/jammechanism-specificRECOVERY_REQUIRED
граничнийunknown codeUNKNOWN_FAULTfail-safe
граничнийrestart active cycleRESTART_ACTIVEreconciliation

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

ScenarioСимптомDiagnosisEvidence itemsПереходівSafe outputsRecovery без ack
sensor normal
wire/channel faultblocked
camera/databaseblocked
robot/networkblocked
mechanism timeoutblocked
unknown/restartblocked

Метрики: 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%

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

  1. Чому рух не є першим діагностичним тестом?
  2. Чим симптом відрізняється від підтвердженої причини?
  3. Чому reconnect не дозволяє automatic resume?
  4. Які докази потрібні після robot link loss?
  5. Як визначають timeout і чому null discrepancy time не можна вгадати?
  6. Чому unknown fault має бути fail-safe?

Висновки

Опишіть coverage діагностики, safe reactions, blocked recovery, найскладніші неоднозначні симптоми й матеріали, потрібні для затверджених фізичних recovery-процедур.

MERCI SPACE