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

Лабораторна робота 4

Лабораторна робота №04. Raspberry Pi, Linux, GPIO та структура програми

Мета: навчитися виконувати безпечну read-only інвентаризацію Linux-середовища й файлової системи; розрізняти код, конфігурацію, змінні дані, журнали та студентський робочий каталог; відокремлювати конфігурацію стенда від коду; спроєктувати API доступу до цифрових входів/виходів через mock-адаптер; реалізувати журналювання, контрольоване завершення та гарантоване вимкнення логічних виходів; перевірити відмову фізичного режиму за замовчуванням і підготувати проєкт безпечного переходу до реального GPIO.

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

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

  1. зібрати версії ОС, Python і процесу як спостереження, не оголошуючи їх затвердженою базовою лінією;
  2. пояснити призначення файлів, каталогів, процесів, кодів завершення, журналів і сигналів завершення Linux;
  3. співвіднести код, конфігурацію, змінні дані, журнали й студентські результати з рекомендованими каталогами MERC-I5;
  4. читати несекретну JSON-конфігурацію та валідувати її до запуску;
  5. використовувати залежність DigitalIO через mock, не імпортуючи апаратну бібліотеку;
  6. довести тестами, що finally переводить усі mock-виходи у безпечний стан навіть після помилки;
  7. пояснити, чому обліковий запис, контейнер або SSH-сеанс не повинні автоматично мати доступ до GPIO;
  8. скласти план переходу від mock до реального GPIO без призначення неперевірених номерів пінів.

Передумови: базовий Python, командний рядок, ЛР-01 і ЛР-03 або еквівалентне розуміння безпечного стану й рівнів GPIO.

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

СередовищеСтатусПримітка
Google ColabдозволеноЛише Python mock; Linux-команди можуть відрізнятися від Raspberry Pi
Локальний ПКдозволеноОсновний для коду; використовуйте команду запуску свого Python
Raspberry Piосновне після інвентаризаціїЛише непривілейований mock/read-only; фізичний GPIO не відкривається
Фізичний комплекс MERC-I5не обов'язковоЖодних команд периферії, мережевого API MG400 чи руху

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

Знання

  • шлях до файла, поточний каталог, код завершення процесу;
  • JSON, класи, протоколи/інтерфейси, try/finally;
  • різниця між конфігурацією, секретом і вихідним кодом;
  • 3,3 V GPIO не сумісний напряму з 5/24 V.

Обладнання

  • локальний ПК або Raspberry Pi 5 без доступу до фізичних інтерфейсів;
  • MERC-I5, GPIO-роз'єм, реле, сенсори й приводи не потрібні.

ПЗ і вхідні матеріали

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

Ризики

РизикДжерелоЗахист у цій ЛР
Пошкодження GPIOневідома напруга/пін або прямі 5/24 Vнемає апаратного backend; тільки mock
Ненавмисний рухстудентський процес отримав периферійні правазаборона GPIO/UART/USB/MG400; mode=mock єдиний дозволений
Залишений активний вихідвиняток або переривання процесуtry/finally, safe_shutdown(), тест стану після помилки
Витік секретівконфігурація/журнал містить ключі чи адреси стендаприклад не містить секретів; реальні значення поза Git
Помилкове відновленняпроцес перезапущено без звірки станупісля запуску починати з безпечного mock-стану й read-only перевірки

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

  • запускати скрипт із sudo, додавати користувача до привілейованих груп або давати контейнеру --privileged;
  • відкривати /dev/gpiomem, GPIO character devices, UART, USB, Docker socket або мережевий API MG400;
  • записувати пароль, приватний IP стенда, SSH/Tailscale-ключ чи токен у JSON/журнал/Git;
  • підключати 5 V/24 V до GPIO або визначати піни експериментом;
  • надсилати команди руху чи I/O.

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

Завершити процес і повідомити оператора, якщо з'явився фізичний рух, апаратний backend, запит привілеїв, невідома конфігурація, втрата зв'язку, суперечливий стан, нагрів/запах або пошкодження кабелю.

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

Для цієї ЛР: mode=mock; усі логічні виходи False; нові команди не приймаються; процес має скінченний цикл; журнал не містить секретів; після будь-якого винятку виконується safe_shutdown; відновлення не запускає попередню дію автоматично.

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

  • Робоча копія не містить секретів і локальних адрес.
  • Команда не використовує sudo/привілейований контейнер.
  • Конфігурація має mode: mock.
  • Код не імпортує GPIO/SDK/мережевий клієнт робота.
  • Цикл обмежений max_cycles.
  • finally викликає безпечне завершення.
  • Позитивний, негативний і граничний тести виконані до будь-якого майбутнього апаратного етапу.

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

1. Файли, процеси й журнали

Файл конфігурації описує змінні параметри, а код — поведінку. Процес має PID, стан і код завершення. Нульовий код означає успішне завершення програми, але не доводить фізичну правильність. Журнал має містити час, рівень, подію і контекст без секретів. Для довготривалого сервісу окремо документують запуск, зупинку, health check, тайм-аут, safe-state і відновлення.

Сигнали SIGINT/SIGTERM у Linux дають процесу можливість контрольовано завершитися, але SIGKILL, втрата живлення або збій ОС не викликають finally. Тому реальна система не може покладати безпечний стан лише на Python; потрібна підтверджена апаратна поведінка й контрольований сервіс.

2. Файлова система MERC-I5

У Linux каталоги розділяють за призначенням, щоб код не змішувався з локальною конфігурацією, журналами та змінними даними. У описі програмного середовища запропоновано таку модель:

ШляхЩо має зберігатисяЧи змінює студент у цій ЛР
/opt/merc-i5/app/, /opt/merc-i5/labs/код системи та лабораторні матеріалині, лише read-only огляд за наявності
/opt/merc-i5/config/несекретні приклади конфігураціїні; робочу копію створює у власному каталозі
/etc/merc-i5/локальна конфігурація і посилання на секретині; вміст не копіює у звіт
/var/lib/merc-i5/змінні дані й калібруванняні, якщо оператор не надав окремий тестовий каталог
/var/log/merc-i5/журнали сервісівлише дозволене read-only читання без чутливих даних
/srv/merc-i5/students/<student-id>/workspace/персональна робоча копіятак, у межах виданих прав
/srv/merc-i5/students/<student-id>/data/дозволені результати роботитак, у межах виданих прав

Ця структура є рекомендованою, але ще не підтвердженою на конкретному Raspberry Pi. Відсутність каталогу під час інвентаризації не є помилкою студента й не дає дозволу створювати його через sudo. Відносний шлях, наприклад lab04-config.json, залежить від поточного каталогу; абсолютний шлях починається з /. У програмі шляхи слід формувати через pathlib.Path, а не склеювати рядки вручну.

3. Розділення відповідальностей

Текстовий опис рисунка: несекретний JSON спочатку проходить валідацію, далі керує логікою застосунку, яка працює через інтерфейс DigitalIO; у цій ЛР інтерфейс веде лише до mock, а фізичний шлюз відокремлений допуском; сигнал, помилка або ліміт циклів спрямовує виконання у finally та safe_shutdown.

Mermaid
flowchart TD
    C["Несекретний JSON-профіль"] --> V["Валідація конфігурації"]
    V --> A["Логіка застосунку"]
    A --> P["Інтерфейс DigitalIO"]
    P --> M["Mock backend — ця ЛР"]
    P -. "окремий допуск" .-> G["Контрольований hardware gateway"]
    A --> J["Структурований журнал"]
    X["SIGINT / помилка / ліміт циклів"] --> F["finally: safe_shutdown"]
    F --> M
    F -.-> G

Рис. 1. Ілюстративна структура програми з конфігурацією, адаптером і гарантованим завершенням

Інтерфейс дає змогу тестувати логіку без плати. Фізичний адаптер не додають, доки не підтверджено електричну карту й контроль доступу. Конфігурація конкретного стенда не повинна бути жорстко записана у вихідний код.

4. Мінімальний API

МетодПризначенняПоведінка mockВимога безпеки
read(name)прочитати нормалізований вхідповертає задане булеве значенняневідоме ім'я — помилка
write(name, value)змінити логічний вихідзберігає boolфізичний режим відсутній
safe_shutdown()вимкнути всі виходиусі Falseвикликається в finally
close()звільнити ресурсповторно безпечноне залишає активних виходів

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

  1. Провести read-only інвентаризацію середовища й файлової системи.
  2. Побудувати карту розміщення файлів MERC-I5.
  3. Створити несекретну mock-конфігурацію.
  4. Реалізувати адаптер і контрольоване завершення.
  5. Запустити позитивний, негативний і граничний тести.
  6. Оформити паспорт програмного модуля та план апаратного перенесення.

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

Крок 1. Інвентаризувати середовище без змін

На Linux/Raspberry Pi виконайте лише read-only команди нижче. У звіті не публікуйте ім'я користувача, hostname, мережеві адреси або повний шлях домашнього каталогу.

Bash
pwd
uname -srm
python3 --version
ps -o pid,ppid,state,comm -p $$

На Windows достатньо python --version; інші команди замініть на опис середовища.

Очікуваний результат: фактично спостережувані ОС/архітектура, версія Python і один рядок процесу.

Критерій правильності: жодна команда не змінює систему й не потребує підвищення прав; версії названо «спостережуваними», не «затвердженими».

Якщо результат не отримано: не використовуйте sudo; зафіксуйте недоступну команду й продовжте на локальному ПК у mock.

Крок 2. Дослідити файлову систему та спроєктувати розміщення файлів

Не переходячи в чужі домашні каталоги й не використовуючи sudo, перевірте типи та права лише для виданого робочого каталогу і, якщо вони існують та доступні, коренів MERC-I5:

Bash
printf 'working directory: '; pwd
ls -ld . /opt/merc-i5 /etc/merc-i5 /var/lib/merc-i5 /var/log/merc-i5 2>/dev/null
find . -maxdepth 2 -type f -printf '%P\n' | sort

Команда find виконується тільки у виданому студентському каталозі (.). У звіті складіть таблицю: файл або тип даних → рекомендований системний каталог → фактичне місце у вашій роботі. Для цієї ЛР lab04_app.py, lab04-config.json і результати тестів залишаються у студентському каталозі; нічого не копіюйте до /opt, /etc або /var.

Очікуваний результат: визначено поточний каталог, доступні корені MERC-I5 і межі прав; створено карту щонайменше для коду, конфігурації, журналу та результатів.

Критерій правильності: системні каталоги не створено й не змінено; запропоновану структуру не названо фактично розгорнутою без доказу; у виводі та звіті немає імен користувачів, секретів і приватних адрес.

Якщо результат не отримано: зафіксуйте «каталог відсутній або недоступний» і виконайте класифікацію за документацією; не змінюйте права та не використовуйте sudo.

Крок 3. Підготувати зовнішню конфігурацію

Збережіть як lab04-config.json. Це приклад для mock і не карта стенда.

JSON
{
  "mode": "mock",
  "poll_interval_s": 0.0,
  "max_cycles": 3,
  "inputs": {"part_present": false},
  "outputs": {"actuator_request": false},
  "log_level": "INFO"
}
Text
python -m json.tool lab04-config.json

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

Критерій правильності: mode дорівнює тільки mock, max_cycles — додатне ціле, вихід початково false.

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

Крок 4. Реалізувати застосунок із mock-DigitalIO

Збережіть код як lab04_app.py. Без аргументів він використовує безпечну вбудовану mock-конфігурацію; --config читає зовнішній JSON.

Python
# safe-smoke
from __future__ import annotations

import argparse
import json
import logging
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Mapping, Protocol


class DigitalIO(Protocol):
    def read(self, name: str) -> bool: ...
    def write(self, name: str, value: bool) -> None: ...
    def safe_shutdown(self) -> None: ...
    def close(self) -> None: ...


class MockDigitalIO:
    def __init__(self, inputs: Mapping[str, bool], outputs: Mapping[str, bool]) -> None:
        self.inputs = dict(inputs)
        self.outputs = dict(outputs)
        self.closed = False

    def read(self, name: str) -> bool:
        if self.closed or name not in self.inputs:
            raise RuntimeError(f"unavailable input: {name}")
        return self.inputs[name]

    def write(self, name: str, value: bool) -> None:
        if self.closed or name not in self.outputs:
            raise RuntimeError(f"unavailable output: {name}")
        self.outputs[name] = bool(value)

    def safe_shutdown(self) -> None:
        for name in self.outputs:
            self.outputs[name] = False

    def close(self) -> None:
        self.safe_shutdown()
        self.closed = True


@dataclass(frozen=True)
class Config:
    mode: str
    poll_interval_s: float
    max_cycles: int
    inputs: dict[str, bool]
    outputs: dict[str, bool]
    log_level: str


def parse_config(raw: Mapping[str, Any]) -> Config:
    if raw.get("mode") != "mock":
        raise ValueError("only mode=mock is permitted in this lab")
    cycles = raw.get("max_cycles")
    interval = raw.get("poll_interval_s")
    if not isinstance(cycles, int) or isinstance(cycles, bool) or not 1 <= cycles <= 100:
        raise ValueError("max_cycles must be an integer from 1 to 100")
    if not isinstance(interval, (int, float)) or isinstance(interval, bool) or not 0 <= float(interval) <= 1:
        raise ValueError("poll_interval_s must be from 0 to 1")
    inputs, outputs = raw.get("inputs"), raw.get("outputs")
    if not isinstance(inputs, dict) or not isinstance(outputs, dict):
        raise ValueError("inputs and outputs must be objects")
    if not all(isinstance(k, str) and isinstance(v, bool) for group in (inputs, outputs) for k, v in group.items()):
        raise ValueError("mock signal names must be strings and values booleans")
    if "part_present" not in inputs or "actuator_request" not in outputs:
        raise ValueError("required mock signals are missing")
    level = str(raw.get("log_level", "INFO")).upper()
    if level not in {"DEBUG", "INFO", "WARNING", "ERROR"}:
        raise ValueError("unsupported log_level")
    return Config("mock", float(interval), cycles, dict(inputs), dict(outputs), level)


def run(config: Config, io: MockDigitalIO, fail_at: int | None = None) -> dict[str, object]:
    completed = 0
    try:
        for cycle in range(config.max_cycles):
            if fail_at is not None and cycle == fail_at:
                raise RuntimeError("simulated process failure")
            detected = io.read("part_present")
            io.write("actuator_request", detected)
            logging.info("cycle=%d detected=%s", cycle, detected)
            completed += 1
        return {"completed_cycles": completed, "reason": "limit_reached"}
    finally:
        io.safe_shutdown()


def smoke_test() -> dict[str, object]:
    raw = {
        "mode": "mock", "poll_interval_s": 0.0, "max_cycles": 3,
        "inputs": {"part_present": True},
        "outputs": {"actuator_request": False}, "log_level": "ERROR",
    }
    config = parse_config(raw)
    io = MockDigitalIO(config.inputs, config.outputs)
    result = run(config, io)
    assert result["completed_cycles"] == 3
    assert io.outputs == {"actuator_request": False}

    failing_io = MockDigitalIO(config.inputs, config.outputs)
    try:
        run(config, failing_io, fail_at=1)
    except RuntimeError:
        pass
    else:
        raise AssertionError("simulated failure was not raised")
    assert failing_io.outputs == {"actuator_request": False}

    try:
        parse_config(dict(raw, mode="physical"))
    except ValueError:
        pass
    else:
        raise AssertionError("physical mode must be rejected")
    return {**result, "outputs_safe": True, "physical_mode_rejected": True, "hardware_tested": False}


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--config", type=Path)
    args = parser.parse_args()
    if args.config:
        config = parse_config(json.loads(args.config.read_text(encoding="utf-8")))
        logging.basicConfig(level=getattr(logging, config.log_level), format="%(levelname)s %(message)s")
        io = MockDigitalIO(config.inputs, config.outputs)
        result = run(config, io)
        result.update(outputs_safe=not any(io.outputs.values()), hardware_tested=False)
    else:
        result = smoke_test()
    print(json.dumps(result, ensure_ascii=False))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
Text
python lab04_app.py
python lab04_app.py --config lab04-config.json

Очікуваний результат: обидва запуски скінченні; JSON має outputs_safe: true, hardware_tested: false; безаргументний тест має physical_mode_rejected: true.

Критерій правильності: код завершення 0; після нормального та аварійного сценарію всі виходи False; mode=physical відхилено до створення backend.

Якщо результат не отримано: перевірте JSON і traceback; не запускайте з привілеями й не додавайте апаратний імпорт.

Крок 5. Перевірити негативні та граничні конфігурації

У копії JSON по черзі задайте physical, max_cycles=0, max_cycles=101, відсутній actuator_request, нечисловий інтервал та невідомий log_level.

Код/команда: python lab04_app.py --config lab04-config.json після кожної контрольованої зміни.

Очікуваний результат: кожна некоректна конфігурація завершується до циклу з точним ValueError; фізичні ресурси не відкриваються.

Критерій правильності: 6/6 відмов; жоден тест не створив апаратний backend; вихідний JSON відновлено.

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

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

Опишіть призначення, API, конфігурацію, залежності, розміщення коду/конфігурації/даних/журналів, запуск/зупинку, журнал, safe-state, health check, негативні тести та процедуру відновлення. Фізичний backend позначте заблокованим.

Код/команда: не потрібні.

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

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

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

Варіант переходу до реального GPIO

Рекомендований розвиток ЛР — не замінювати mock прямим доступом студентського процесу до /dev/gpiochip*, а додати окремий апаратний етап після програмного допуску. Той самий інтерфейс DigitalIO зберігається, але реалізація звертається до контрольованого сервісу MERC-I5. Сервіс запускає викладач або оператор; він володіє GPIO, читає затверджений локальний профіль стенда, дозволяє лише названий навчальний канал, контролює тайм-аут і при втраті клієнта переводить вихід у безпечний стан. Студентський контейнер залишається без --privileged, Docker socket і прямого доступу до GPIO.

Пропонований апаратний сценарій:

  1. Mock-тести поточної ЛР пройдено повністю.
  2. Оператор звіряє паспорт конкретного комплексу й обирає один гальванічно розв'язаний канал Power Hub, який не запускає рух: вхід читається без керування механізмом або вихід підключено до контрольного індикатора/тестового навантаження.
  3. Конфігурація зберігає символічні імена; номер BCM GPIO, напрямок, активний рівень і безпечний стан містяться лише в локальному профілі сервісу поза студентським репозиторієм.
  4. Студент спочатку читає стан, потім за окремим дозволом виконує один імпульс з обмеженою тривалістю; оператор незалежно підтверджує фізичний результат.
  5. Перевіряються нормальне завершення, SIGINT, виняток, тайм-аут клієнта та перезапуск сервісу. У кожному випадку канал має перейти у підтверджений безпечний стан.
  6. Результат заноситься до підписаного протоколу апаратної апробації; лише після цього у звіті дозволено hardware_tested: true.

Мінімальні додаткові поля локального профілю сервісу: chip, line, direction, active_level, bias, safe_value, max_on_time_s і символічне ім'я каналу. Конкретні значення в цій редакції навмисно не наведено.

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

ТипВхід/подіяОчікуванняКритерій
Позитивний3 mock-циклискінченне завершенняcompleted_cycles=3
Позитивнийpart_present=trueлогічний вихід змінюється в циклі, потім вимикаєтьсяфінально False
Негативнийпомилка на циклі 1виняток + finallyвихід False
Негативнийmode=physicalвідхиленняbackend не створено
Негативнийвідсутній сигналValueErrorназвано потрібне поле
Граничнийmax_cycles=1один циклкод 0
Граничнийmax_cycles=100100 циклівскінченне завершення
Граничнийmax_cycles=0/101відхилення2/2

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

Інвентаризація

ПараметрСпостережуване значенняКоманда/джерелоЗатверджено для курсу?
ОС/ядроні
Архітектурані
Pythonні

Перевірка програми

МетрикаРезультатКритерій
Smoke-тесткод 0
Нормальних циклів3
Негативних конфігурацій відхилено6/6
Виходів True після завершення0
Апаратних імпортів/операцій0рівно 0
Секретів у конфігурації/журналі0рівно 0

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

Дотримуйтеся загальних вимог. Додатково подайте знеособлену інвентаризацію, JSON, код, журнальний вивід, результати нормального й аварійного завершення, шість негативних і два граничні тести, паспорт модуля й явну фразу «GPIO та фізичне обладнання не відкривалися і не перевірялися».

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

СкладоваЧасткаОзнака повного виконання
Підготовка10%read-only інвентаризація й безпечний профіль без секретів
Реалізація40%конфігурація, інтерфейс, mock, журнал і finally реалізовані
Перевірка25%нормальний, аварійний, негативні й граничні тести відтворено
Аналіз15%пояснено процеси, сигнали, права, safe-state і межі finally
Звіт10%паспорт, таблиці, вивід і межі апаратної перевірки повні
Разом100%

Критична помилка: запуск із привілеями, фізичний GPIO/рух, секрет у файлі або відсутній safe-state після помилки.

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

  1. Чому конфігурацію стенда відокремлюють від коду?
  2. Що доводить і чого не доводить код завершення 0?
  3. Для чого потрібен try/finally?
  4. Чому finally не захищає від усіх відмов живлення/ОС?
  5. Навіщо обмежувати кількість циклів і час очікування?
  6. Чому student Docker не повинен отримувати Docker socket або GPIO напряму?
  7. Які дані є секретами або чутливими конфігураційними значеннями?
  8. Чому physical backend відхиляється ще на етапі конфігурації?
  9. Які документи потрібні перед реалізацією GPIO-адаптера?
  10. Як відновлювати процес після невідомого стану?

Висновки

У висновку наведіть спостережуване середовище, результати smoke/негативних тестів, стан виходів після винятку й відмінність mock від фізичного gateway. Не стверджуйте сумісність із конкретною ОС, GPIO-картою чи обладнанням: ці дані та фізична апробація відсутні.

MERCI SPACE