Лабораторна робота 4
Лабораторна робота №04. Raspberry Pi, Linux, GPIO та структура програми
Результати навчання та передумови
Після роботи студент уміє:
- зібрати версії ОС, Python і процесу як спостереження, не оголошуючи їх затвердженою базовою лінією;
- пояснити призначення файлів, каталогів, процесів, кодів завершення, журналів і сигналів завершення Linux;
- співвіднести код, конфігурацію, змінні дані, журнали й студентські результати з рекомендованими каталогами MERC-I5;
- читати несекретну JSON-конфігурацію та валідувати її до запуску;
- використовувати залежність
DigitalIOчерез mock, не імпортуючи апаратну бібліотеку; - довести тестами, що
finallyпереводить усі mock-виходи у безпечний стан навіть після помилки; - пояснити, чому обліковий запис, контейнер або SSH-сеанс не повинні автоматично мати доступ до GPIO;
- скласти план переходу від 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-роз'єм, реле, сенсори й приводи не потрібні.
ПЗ і вхідні матеріали
- Python зі стандартною бібліотекою; точну підтримувану версію не затверджено;
- програмне середовище MERC-I5;
- електричні інтерфейси та карта GPIO;
- мережа та безпека;
- базова лінія ПЗ/API;
- політика допуску;
- офіційна документація Raspberry Pi.
Ризики та правила безпеки
Ризики
| Ризик | Джерело | Захист у цій ЛР |
|---|---|---|
| Пошкодження 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.
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() | звільнити ресурс | повторно безпечно | не залишає активних виходів |
Хід виконання роботи
- Провести read-only інвентаризацію середовища й файлової системи.
- Побудувати карту розміщення файлів MERC-I5.
- Створити несекретну mock-конфігурацію.
- Реалізувати адаптер і контрольоване завершення.
- Запустити позитивний, негативний і граничний тести.
- Оформити паспорт програмного модуля та план апаратного перенесення.
Виконання лабораторної роботи
Крок 1. Інвентаризувати середовище без змін
На Linux/Raspberry Pi виконайте лише read-only команди нижче. У звіті не публікуйте ім'я користувача, hostname, мережеві адреси або повний шлях домашнього каталогу.
pwd
uname -srm
python3 --version
ps -o pid,ppid,state,comm -p $$
На Windows достатньо python --version; інші команди замініть на опис середовища.
Очікуваний результат: фактично спостережувані ОС/архітектура, версія Python і один рядок процесу.
Критерій правильності: жодна команда не змінює систему й не потребує підвищення прав; версії названо «спостережуваними», не «затвердженими».
Якщо результат не отримано: не використовуйте sudo; зафіксуйте недоступну команду й продовжте на локальному ПК у mock.
Крок 2. Дослідити файлову систему та спроєктувати розміщення файлів
Не переходячи в чужі домашні каталоги й не використовуючи sudo, перевірте типи та права лише для виданого робочого каталогу і, якщо вони існують та доступні, коренів MERC-I5:
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 і не карта стенда.
{
"mode": "mock",
"poll_interval_s": 0.0,
"max_cycles": 3,
"inputs": {"part_present": false},
"outputs": {"actuator_request": false},
"log_level": "INFO"
}
python -m json.tool lab04-config.json
Очікуваний результат: JSON валідний; у ньому немає пінів, адрес, паролів і токенів.
Критерій правильності: mode дорівнює тільки mock, max_cycles — додатне ціле, вихід початково false.
Якщо результат не отримано: виправте JSON; не переносіть параметри конкретного стенда в код.
Крок 4. Реалізувати застосунок із mock-DigitalIO
Збережіть код як lab04_app.py. Без аргументів він використовує безпечну вбудовану mock-конфігурацію; --config читає зовнішній JSON.
# 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())
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.
Пропонований апаратний сценарій:
- Mock-тести поточної ЛР пройдено повністю.
- Оператор звіряє паспорт конкретного комплексу й обирає один гальванічно розв'язаний канал Power Hub, який не запускає рух: вхід читається без керування механізмом або вихід підключено до контрольного індикатора/тестового навантаження.
- Конфігурація зберігає символічні імена; номер BCM GPIO, напрямок, активний рівень і безпечний стан містяться лише в локальному профілі сервісу поза студентським репозиторієм.
- Студент спочатку читає стан, потім за окремим дозволом виконує один імпульс з обмеженою тривалістю; оператор незалежно підтверджує фізичний результат.
- Перевіряються нормальне завершення,
SIGINT, виняток, тайм-аут клієнта та перезапуск сервісу. У кожному випадку канал має перейти у підтверджений безпечний стан. - Результат заноситься до підписаного протоколу апаратної апробації; лише після цього у звіті дозволено
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=100 | 100 циклів | скінченне завершення |
| Граничний | 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 після помилки.
Контрольні питання
- Чому конфігурацію стенда відокремлюють від коду?
- Що доводить і чого не доводить код завершення
0? - Для чого потрібен
try/finally? - Чому
finallyне захищає від усіх відмов живлення/ОС? - Навіщо обмежувати кількість циклів і час очікування?
- Чому student Docker не повинен отримувати Docker socket або GPIO напряму?
- Які дані є секретами або чутливими конфігураційними значеннями?
- Чому physical backend відхиляється ще на етапі конфігурації?
- Які документи потрібні перед реалізацією GPIO-адаптера?
- Як відновлювати процес після невідомого стану?
Висновки
У висновку наведіть спостережуване середовище, результати smoke/негативних тестів, стан виходів після винятку й відмінність mock від фізичного gateway. Не стверджуйте сумісність із конкретною ОС, GPIO-картою чи обладнанням: ці дані та фізична апробація відсутні.