Модуль 12 · итоговый проект
Итоговый проект: анализатор миссии «Модуля»
4 блок(ов) теории · 11 заданий · 165 баллов
Итоговый проект курса — анализатор миссии: из строк бортового журнала квадро «Модуля» программа получает сводку полёта и печатает отчёт, который диспетчер читает командами бортового компьютера. Такую задачу не пишут одной функцией — её разбивают на этапы, и каждый этап становится функцией или классом с контрактом «аргументы → возврат», зафиксированным до кода: что принято на входе, что возвращено, что происходит на плохом входе. План проекта из восьми шагов: (1) parse_log_line — строка журнала в запись-словарь; (2) load_mission — журнал целиком в записи и счётчик битых строк; (3) mission_span — длительность миссии; (4) класс MissionAnalyzer — агрегаты высоты и частоты событий; (5) battery_report — сводка АКБ по банкам; (6) mission_distance — длина маршрута облёта; (7) generate_report — текст отчёта; (8) CLI бортового компьютера — команды report, altitude, battery. Порядок этапов не переставляют: данные текут в одну сторону, каждый этап потребляет вывод предыдущего — парсер не знает про отчёт, отчёт не знает про парсер. Если сборка падает, ищут первый этап, где нарушен контракт: traceback из модуля 8 называет файл и строку, а контракт говорит, какое значение было обязано прийти.
Структуры данных проекта — знакомые, но в новых ролях. Запись журнала — словарь {"ts": ..., "event": ..., "value": ...} из модуля 5: именованные поля читаются сами — record["event"] понятнее безымянного кортежа. Результат загрузки — кортеж (records, skipped): одна функция возвращает два результата одной поездки, вызывающий распаковывает их, как в модуле 3. Отказы — по контрактам модуля 8, и в проекте их три разных: парсер не разобрал строку — мягкий отказ None (вызывающий сам решает, что делать); battery_report получил пустой список замеров — жёсткий отказ raise ValueError("no samples") (анализировать нечего, молчаливый ноль соврал бы диспетчеру); битые строки журнала — третий путь, пропуск со счётчиком skipped: мусор учтён, но загрузке не мешает. Выбор отказа — часть контракта: пустой журнал в mission_span по контракту честный 0.0, а не ошибка — миссия длиной в ноль минут существует. Числа проекта — с настоящей платформы: квадро «Модуль» — 1150 г без АКБ, полезная нагрузка до 1200 г, LiPo 6S из шести банок по 3.7 В (6 × 3.7 = 22.2 В); порог возврата RTL — 21.8 В, как в диспетчере модуля 9. Журнал миссии этого проекта — 14 записей за 3.7 минуты, маршрут облёта четырёх точек — 96 м: эти числа вы посчитаете сами по ходу этапов, и финальный прогон платформы сойдётся с расчётом. Захочется повторить миссию на своём — «Модуль» в каталоге /drones, и расчёты этого проекта переносятся на него один в один.
Середина проекта — аналитика. Класс MissionAnalyzer собирает агрегаты из модулей 4 и 9 поверх записей миссии: конструктор хранит исходный список как атрибут records и тут же строит производный altitudes — значения записей с событием "ALT" (фильтр из модуля 4; напряжение BAT в высоты не попадает). Методы считают max, среднее (сумма, делённая на число замеров, с округлением) и частоты событий — Counter из модуля 9, перевёрнутый в обычный словарь dict(Counter(...)), чтобы тесты сравнивали его честно. battery_report принимает замеры банок — список списков, по одному на момент замера: вложенный цикл собирает все напряжения в один плоский список, дальше min, max и разброс; остаток считается по пакетной формуле: средняя банка, умноженная на число банок, — текущее напряжение пакета — против номинала 3.7 × банки, а доля ограничена диапазоном 0–100 через max(0.0, min(100.0, x)) из модуля 10 — перезаряд выше номинала не раздувает процент. mission_distance — накопление из модуля 3 на парах соседних точек; длина сегмента — math.dist(p, q), та же теорема Пифагора из модуля 3, собранная в готовую функцию стандартной библиотеки.
Финал проекта — отчёт, и у него главная мысль модуля: формат отчёта — это контракт. Отчёт собирается методом join из списка строк-секций (модуль 7), внутри f-строки со спецификаторами :.1f и :.2f из модуля 1; спецификатор держит ровно N знаков после точки, включая хвостовой ноль: f"{88:.1f}" — это "88.0", f"{0.1:.2f}" — это "0.10". Секции идут в фиксированном порядке — заголовок, длительность, высота, события, батарея, дистанция, — а события внутри строки выстраиваются по алфавиту сортировкой пар «(событие, счёт)»: sorted сравнивает кортежи по первому элементу, имена уникальны, поэтому порядок стабилен. Тест финального задания сверяет текст посимвольно: диспетчер разбирает отчёт машиной, и «почти тот же формат» для машины — другой. Три команды CLI бортового компьютера — report, altitude, battery — тот же контракт в терминале: фиксированный ввод, фиксированный вывод. И последний взгляд на собранное — признаки взрослого решения, по которым его принимает ревьюер: имена функций — глаголы с точным контрактом (parse_log_line, load_mission, mission_span); guard-проверки до вычислений; никаких магических чисел — журнал, замеры и маршрут проекта живут в именованных константах JOURNAL, CELL_SAMPLES, ROUTE; и тесты на краевые случаи — пустая строка парсера, одна точка маршрута, нулевая длительность. Это и есть проверка C14: сквозная задача доведена до конца и проверяется машиной.
Задания
0/11 заданий · 0/165 баллов
1. Этап 1 — парсинг: строка журнала в запись
15 балл(ов)Бортовой журнал «Модуля» пишет замер строкой вида "2026-05-01T12:00:35 | ALT | 48.0": метка времени, событие и число, разделённые " | ". Напишите функцию parse_log_line(line): срежьте пробелы по краям (strip), разрежьте по разделителю " | " (split из модуля 7); частей не ровно три — верните None; третье значение не приводится к float (try/except ValueError из модуля 8) — верните None; иначе соберите и верните словарь {"ts": ..., "event": ..., "value": ...} со значением-числом. Например, parse_log_line("2026-05-01T12:00:35 | ALT | 48.0") возвращает {"ts": "2026-05-01T12:00:35", "event": "ALT", "value": 48.0}, а строка события "2026-05-01T12:01:25 | WARN | low batt" — не замер: для неё вернётся None.