Сквозной playbook разработки IoT-продуктов — Этап 4: Разработка firmware и облачного ПО
Этап 4 переводит продукт на правую сторону V: реализация и тестирование. В классической V-модели разработки продукта левая сторона — про дизайн (которым мы занялись на предыдущих этапах); здесь мы разрабатываем firmware (софт, работающий на вашем IoT-устройстве) и облачное ПО (серверы, базы данных, API, пользовательские интерфейсы), которые вместе оживляют ваш продукт. В отличие от разработки железа, которая относительно негибка после печати на плате, разработка софта высоко итеративна и agile. Фактически вы, возможно, уже писали куски firmware ещё на Этапе 3, пока тестировали ваш дизайн железа. Теперь пора наращивать темп. Софт — как глина: легко формовать и менять на лету, — так что мы можем быстро итерировать и непрерывно улучшать. Этот этап будет идти параллельно с оставшейся работой по железу, каждая сторона информирует другую по мере необходимости.
Этап 4 можно представить в двух основных частях: сначала планирование архитектуры и дизайна ПО (чтобы заложить прочный фундамент), а затем непрерывный цикл спринтов разработки, где мы строим, тестируем и повторяем, пока софт не станет твёрдым. Разберём по частям.
4.1 Архитектура и дизайн ПО
Прежде чем кто-то прыгнет в кодирование, разумно набросать план игры для архитектуры ПО — и для firmware на устройстве, и для облачных компонентов. Думайте об этом как о планировании автопутешествия. Вы не стали бы просто заводить мотор без карты; аналогично мы набрасываем основные компоненты и их взаимодействие до того, как напишем тысячи строк кода. Это аналогично системной архитектуре, которую мы делали на Этапе 2, но теперь на уровне кода. Хорошее планирование архитектуры ПО помогает избежать «спагетти-кода» в дальнейшем. Вот ключевые вещи, которые нужно установить в этой фазе:
Архитектура firmware (слоистый подход и вопросы RTOS)
Определите структуру вашего embedded-кода через расслоение. Распространённый подход в firmware — слоистая архитектура, которая помогает логически организовать код и упрощает его сопровождение. Например, на самом нижнем уровне у вас может быть Board Support Package (BSP) или слой аппаратной абстракции — он содержит низкоуровневые драйверы, специфичные для вашего микроконтроллера и аппаратной периферии. Выше могут быть драйверы устройств или модули для ваших датчиков и актуаторов (обрабатывающие такие вещи, как чтение датчика температуры или управление светодиодом). Над драйверами может быть сервисный слой — он может реализовывать коммуникационные протоколы, алгоритмы обработки данных или другую логику, обрабатывающую сырые данные. Наконец, наверху находится прикладной слой — мозг вашего устройства: он связывает всё вместе для выполнения назначения вашего продукта (например, чтение значений датчиков, принятие решений, отправка данных в облако и реагирование на команды). Подумайте о рисовании простой диаграммы (UML-диаграмма компонентов или даже просто прямоугольники и стрелки на доске), чтобы визуализировать эти слои. Это помогает всем в команде понять разделение ответственности — как дом с фундаментом, каркасом и крышей: у каждого слоя своя работа.
Ещё одно большое архитектурное решение на стороне firmware — использовать ли RTOS (операционную систему реального времени) или нет. RTOS — это как маленькая операционная система на вашем устройстве, которая может выполнять несколько задач как будто одновременно (через планирование). Если ваше устройство делает только одну вещь за раз, RTOS может и не понадобиться. Но у большинства IoT-гаджетов много всего происходит параллельно — например, периодическое чтение датчиков, прослушивание входящих сообщений (из облака или от пользователя), обновление дисплея или светодиода и обработка сетевой коммуникации. С RTOS вы можете спроектировать ваше firmware как набор задач или потоков. Например, одна задача может быть выделена под сэмплирование датчиков, другая — под обработку коммуникаций (отправка данных или приём команд), а третья — под служебные функции или обновления пользовательского интерфейса. В рамках дизайна архитектуры наметьте эти задачи и то, как они будут общаться друг с другом, — распространённые механизмы включают очереди сообщений, флаги событий или разделяемые данные, защищённые мьютексами (во избежание конфликтов). Решение этого рано поможет гарантировать, что ваше firmware не станет клубком переплетённого кода, где всё зависит от всего (также известного как спагетти-код). Вместо этого у вас будут ясные интерфейсы: может, задача датчика кладёт новые показания в очередь, а задача коммуникации забирает их для отправки в облако. Если вы спроектируете эти интерфейсы ясно сейчас, кодирование пойдёт гораздо глаже, и вы скажете себе спасибо позже, когда придёт время отлаживать или расширять firmware.
Зачем вся эта архитектурная суета для firmware? Потому что она упрощает вашу жизнь в дальнейшем. С хорошим слоистым дизайном и структурой задач вы можете изменить одну часть (скажем, заменить драйвер датчика или подкрутить алгоритм), не ломая всё остальное. Это также упрощает совместную работу нескольких разработчиков без наступания друг другу на ноги — один человек может разрабатывать драйвер датчика, пока другой работает над кодом облачной коммуникации, при условии, что вы договорились, как эти части стыкуются (например, формат данных сообщения с показанием датчика). Коротко: немного планирования здесь помогает избежать большой боли позже.
Облачная архитектура (структура сервера, базы данных и приложения)
Теперь рассмотрим облачную сторону. IoT-устройство не живёт в изоляции — оно обычно общается с каким-то облачным сервисом или сервером. Здесь нужно похожее архитектурное мышление: набросайте основные компоненты вашего облачного ПО. Например, у вас может быть API-сервер, который принимает данные от устройств, а также обслуживает запросы от пользовательских приложений. Скорее всего, будет база данных, где вы храните данные устройств (показания датчиков, статусы устройств, информацию о пользователях и т.д.). Могут быть дополнительные сервисы или микросервисы, выполняющие обработку данных или аналитику — например, анализ данных на аномалии, запуск алертов или действий при выполнении определённых условий (например, отправка email, если показание температуры слишком высокое) или агрегации для отчётности. Поверх этого рассмотрите клиентские приложения: возможно, веб-дашборд или мобильное приложение, с которым взаимодействуют пользователи. Эти клиенты будут общаться с сервером (часто через API), чтобы отображать данные и отправлять команды пользователя (например, пользователь нажимает кнопку, чтобы включить свою IoT-лампочку с телефона).
Полезно нарисовать диаграмму этой экосистемы — модель C4 (Client-Container-Component-Code) — популярный способ диаграммирования архитектуры ПО. Как минимум нарисуйте уровень Container: покажите устройство, сервер, базу данных и клиентское приложение и то, как данные текут между ними. Например, на вашей диаграмме может быть стрелка от Устройства к Облачному API («отправляет данные датчиков через HTTP/MQTT»), стрелка от Облака к Базе данных («хранит данные») и стрелка от Клиентского приложения к Облаку («пользователь запрашивает данные или отправляет команду») и так далее. Эта визуальная карта гарантирует, что у всех (разработчиков firmware, облачных инженеров, фронтенд-разработчиков и т.д.) есть ясная ментальная модель того, как вещи соединены. Гораздо проще обсуждать и ловить проблемы дизайна рано с такой диаграммой на руках. Возможно, вы обнаружите на этой стадии, что вам нужен дополнительный компонент — например, брокер сообщений или сервис IoT-платформы, — чтобы справляться с масштабированием до многих устройств. Лучше подумать об этом сейчас, чем когда вы глубоко в коде.
Ещё один аспект: подумайте о масштабируемости и надёжности в вашем облачном дизайне. Если вы ожидаете в итоге 100 000 подключающихся устройств, выдержит ли это выбранная вами база данных и серверный дизайн? Если сервер падает, что происходит (есть ли у вас резервирование)? Эти детали могут быть проработаны подробнее на более позднем этапе, но высокоуровневый дизайн должен учитывать такие крупные вопросы заранее. Например, вы можете решить использовать облачную IoT-платформу (AWS IoT, Azure IoT Hub и т.д.) вместо построения всего с нуля — это станет частью вашего архитектурного решения.
Коммуникационные протоколы и форматы данных (коммуникация устройство-облако)
IoT-продукт по своей сути включает коммуникацию между устройством и облаком. Часть дизайна ПО — решить, как они разговаривают друг с другом и на каком языке (протоколы и форматы данных). Есть несколько распространённых вариантов протоколов в IoT: например, будет ли ваше устройство использовать MQTT, HTTP/REST, CoAP или, может быть, что-то вроде WebSockets? У каждого свои плюсы и минусы. MQTT — лёгкий протокол публикации/подписки, отлично подходящий для IoT, потому что он эффективен и хорошо работает по ненадёжным сетям, — многие IoT-системы используют его для обмена сообщениями устройство-облако. HTTP/REST очень распространён и легко тестируется (поскольку к нему можно обратиться стандартными веб-инструментами), но может иметь чуть большие накладные расходы на сообщение. CoAP — специализированный IoT-протокол, похожий на облегчённый HTTP, часто используемый в ограниченных сетях. Ключ — выбрать тот, что подходит возможностям вашего устройства и вашей облачной инфраструктуре. Например, на сильно ограниченном устройстве (8-битный микроконтроллер на батарее) MQTT или CoAP часто подходят лучше, чем HTTP. Если ваше устройство более способное (работает на Linux или подобном), HTTP/REST — нормально и проще.
Как только протокол выбран, спроектируйте формат данных для сообщений. Многие проекты используют JSON, потому что он читаем человеком и с ним легко работать (в большинстве языков есть JSON-парсеры). Например, сообщение с показанием температуры в JSON может выглядеть так:
{ "device_id": "ABC123", "timestamp": 1692124800, "temperature_c": 22.5 }
Здесь поля разложены ясно. JSON, правда, немного многословен, так что если вашему устройству нужно отправлять данные очень эффективно (подумайте о тысячах сообщений или очень узкой полосе), вы можете использовать бинарный формат (вроде Protocol Buffers, CBOR или кастомной бинарной схемы), чтобы сэкономить байты. Этот дизайнерский выбор повлияет и на firmware, и на облако: обе стороны должны договориться о протоколе и формате. Это немного похоже на согласование языка перед началом разговора — если устройство «говорит» MQTT+JSON, а сервер ожидает что-то другое, они не поймут друг друга.
Полезный инструмент на этой стадии — нарисовать диаграмму последовательности или просто расписать примерный сценарий коммуникации. Например: «Устройство снимает показание датчика, форматирует JSON payload, отправляет его через MQTT в топик X; облачный брокер принимает его, облачный сервис обрабатывает и подтверждает его, затем сохраняет в базе данных; позже пользователь запрашивает данные через приложение, облачный API извлекает из базы данных и возвращает JSON в приложение». Расписывание этого потока гарантирует, что все (особенно разработчик firmware и разработчик облака) буквально на одной странице о том, как данные движутся. Это может предотвратить недопонимания вроде «О, я думал, мы используем UNIX-время в секундах, а не миллисекундах» или «Мы сжимаем данные или нет?». Поймайте это сейчас в дизайне — это сэкономит время отладки позже.
Стандарты кодирования и инструменты команды (качество с самого начала)
Теперь поговорим о том, как мы будем писать код и сотрудничать, потому что следование всей командой общим практикам необходимо. Договоритесь о стандартах кодирования для каждой части системы. Для разработки на embedded C многие команды принимают руководства MISRA-C — это набор правил для языка C, направленных на избежание небезопасных или склонных к багам конструкций (изначально из автомобильной индустрии, но широко полезных в любом критичном firmware). Если вы используете C++ в firmware, вы можете решить, какие возможности современного C++ разрешены или запрещены (чтобы держать вещи безопасными для embedded-систем — например, может, избегать динамических аллокаций во время выполнения и т.д.). Для высокоуровневых языков на облачной стороне вроде Python вы, вероятно, будете следовать руководству по стилю PEP 8, а для JavaScript/TypeScript, может быть, использовать линтер со стилем Airbnb или каким-то другим стандартным. Цель — не придираться, а обеспечить согласованность и ловить распространённые ловушки. Когда все пишут в согласованном стиле, кодовая база ощущается как один продукт, а не лоскутное одеяло, и легче читать и сопровождать код, написанный кем-то другим в команде.
В дополнение к стилю кода выберите инструменты и инфраструктуру, которые команда будет использовать. Это включает контроль версий и настройку репозитория (почти наверняка Git — если вы ещё не используете его, определённо начните, ведь это основа совместного кодирования). Решите вопрос стратегии ветвления: будете ли вы использовать простую trunk-based разработку или GitFlow с feature-ветками и т.д.? Также настройте систему отслеживания задач, если ещё не сделали (вроде Jira, Trello, Asana или GitHub Issues), чтобы управлять задачами и пользовательскими историями в ваших спринтах. Это также хорошее время настроить pipeline непрерывной интеграции (CI): выберите CI-сервис (GitHub Actions, GitLab CI, Jenkins, CircleCI и т.д.) и настройте его на автоматическую сборку и тестирование вашего кода при каждом пуше изменений. Мы поговорим о CI подробнее в следующем разделе, но именно на архитектурной стадии вы убеждаетесь, что все разработчики имеют доступ к этим инструментам и знают, как ими пользоваться.
Ещё одна часть «культуры кода» — решение таких вещей, как практики code review, — например, будет ли каждый pull request требовать одобрения как минимум ещё одного разработчика? Установление этих ожиданий рано — это здорово. Это может ощущаться формальностью, но это спасает вас от сценариев, где чей-то непроверенный код вносит неприятный баг или дыру в безопасности, которую другие ловят лишь гораздо позже. Договорившись, что «мы всегда делаем code review, следуем руководству по стилю и запускаем тесты перед слиянием», вы прививаете мышление качества с первого дня. Современная разработка — это в той же мере командное сотрудничество и дисциплина, что и гений кодирования.
Наконец, помните, что это планирование Этапа 4.1 — не про рисование красивых диаграмм, на которые никто никогда не смотрит, — оно про то, чтобы дать команде общее понимание и техническую дорожную карту. Это как планирование маршрута автопутешествия и упаковка карты: вы знаете, где большие повороты и как вы будете навигировать, но всё ещё можете быть гибкими с остановками или объездами. Потратьте разумное количество времени на этот дизайн архитектуры, но не стремитесь к совершенству и не детализируйте каждую последнюю функцию. Вам нужно ровно столько дизайна, чтобы люди могли начать кодировать скоординированно. С этим заложенным фундаментом вы готовы прыгнуть в разработку с уверенностью.
4.2 Спринты разработки — итерируй и побеждай
С архитектурным чертежом на руках начинается строительство — написание кода, его тестирование и наблюдение, как ваш IoT-продукт действительно делает вещи. Мы подходим к этому итерациями, обычно используя спринты, как в методологии Agile. Спринт — это короткий цикл фиксированной длины (обычно 2 недели, но может быть 1 неделя или до 4 недель в зависимости от предпочтений вашей команды), в котором команда обязуется поставить набор функций или улучшений к концу. Идея — разбить гигантскую работу «разработать весь софт» на кусочки по зубам, которые мы можем планомерно откусывать, делая осязаемый прогресс.
У этого итеративного подхода есть явные выгоды: он принуждает к непрерывной интеграции компонентов (так что firmware и облачное ПО тестируются вместе часто), и он даёт вам много возможностей получить обратную связь и скорректировать курс при необходимости. Это гораздо лучше, чем кодировать 6 месяцев и только потом пытаться посмотреть, работает ли всё (что является рецептом неприятных сюрпризов). Посмотрим, как может выглядеть типичный спринт-цикл для разработки нашего IoT-продукта:
Планирование спринта и пользовательские истории
Каждый спринт начинается со встречи планирования спринта (Sprint Planning). Команда смотрит на продуктовый backlog — это мастер-список всех функций, исправлений и задач, которые нужно сделать, изначально выведенный из требований, собранных на Этапе 1 (и пополнявшийся по мере того, как мы узнавали больше на поздних этапах). В планировании команда выбирает набор пользовательских историй или задач из backlog для спринта, исходя из приоритета и ёмкости команды.
Пользовательская история — это описание функции размером в один укус с точки зрения конечного пользователя, часто по шаблону «Как <тип пользователя>, я хочу <какую-то возможность>, чтобы <выгода>». Например, одна история может быть: «Как пользователь, я хочу, чтобы IoT-устройство отправляло данные о температуре в облако каждые 10 минут, чтобы я мог удалённо мониторить температуру в моей комнате». Из этой истории команда выяснит, что нужно сделать и на стороне устройства, и на стороне облака: возможно, в firmware устройства нужно читать датчик температуры и отправлять MQTT-сообщение каждые 600 секунд; в облаке нужен API или обработчик сообщений для приёма этих данных и место для их хранения, и, может быть, простой интерфейс для их отображения.
Во время планирования спринта команда обсуждает каждую выбранную историю, чтобы прибить критерии приёмки — по сути, откуда мы знаем, что эта история «сделана» и работает? Для примера выше критерии могут быть такими: «Дано устройство с датчиком температуры, когда оно работает 10 минут, как минимум одно показание температуры должно быть видимо в облачной базе данных и на дашборде пользователя». Конкретность помогает команде понять, что именно строить и как это проверять. Это время задавать вопросы и прояснять детали. Гораздо лучше выгладить неоднозначности сейчас, чем обнаружить позже, что у людей из firmware и облака были разные предположения (один разработчик думал, что это каждые 10 минут, когда активно, другой думал, что всегда, и т.д.). Как только планирование закончено, каждый должен знать, за какой кусок работы он отвечает в грядущем спринте.
Реализация и непрерывная интеграция (CI)
Теперь мы спускаемся к реализации — написанию кода, оживляющего эти пользовательские истории. Разработчики начнут кодировать функции firmware и облачные функции, определённые для спринта. Ключевая лучшая практика здесь — также писать автоматизированные тесты для каждой новой функции по мере её разработки. Для firmware это может включать написание unit-тестов для логики, которая может работать на хост-машине (например, если у вас есть функция, парсящая показание датчика, или конечный автомат для устройства, вы часто можете написать ПК-тест для неё). Для облачного ПО обычно проще: вы можете писать unit-тесты для ваших функций и интеграционные тесты для ваших API.
По мере того как разработчики пишут код, они коммитят его в систему контроля версий (Git). Здесь наша настройка непрерывной интеграции (CI) сияет. С CI каждый раз, когда код пушится или открывается pull request, на CI-сервере запускается автоматическая сборка. CI скомпилирует код firmware (возможно, в нескольких конфигурациях, если нужно), запустит любые ваши unit-тесты и, возможно, даже развернёт firmware на симуляторе или тестовом устройстве. Он также соберёт облачное ПО, запустит все его тесты, проверит стиль кода (линтеры), запустит инструменты статического анализа и даже измерит покрытие кода (какая часть кода задействована тестами). Цель — поймать любые проблемы немедленно. Если разработчик случайно внёс баг, из-за которого падает тест, система CI отметит это сразу — в течение минут после написания кода, — так что команда может исправить это до того, как это станет большей проблемой. Эта быстрая петля обратной связи критична. Гораздо проще исправить баг, который вы внесли час назад (вы ещё помните, что делали), чем баг месячной давности (который к тому времени запутался во множество других изменений).
Например, представьте, что разработчик меняет код чтения датчика и случайно вносит баг переполнения для очень высоких значений. Если у вас есть unit-тест, который проталкивает значение датчика к экстремуму, CI поймает это падение и оповестит команду. Разработчик может затем разобраться с этим немедленно. Без этого теста баг может затаиться до тех пор, пока не ударит жара, реальные датчики сообщат высокие температуры и устройство упадёт в поле. CI и тестирование существуют, чтобы ловить именно такие моменты — автоматически и в течение минут после написания кода.
Непрерывная интеграция — не только про тесты; команды часто интегрируют в CI также инструменты статического анализа. Инструменты статического анализа исследуют ваш исходный код на предмет потенциальных багов, уязвимостей безопасности или нарушений стиля без его запуска. Например, они могут ловить такие вещи, как «возможное разыменование null-указателя», или «неиспользуемые переменные», или другие запахи кода. Многие embedded-команды используют инструменты вроде PC-Lint, Cppcheck или clang-tidy для C/C++, а облачные команды используют линтеры (ESLint для JavaScript, Pylint для Python и т.д.) и, может быть, сканеры безопасности. CI может быть настроен на провал сборки, если эти инструменты находят серьёзные проблемы. Это устанавливает планку качества — например, «код firmware должен иметь 0 новых предупреждений, и все тесты должны проходить, чтобы слияние было разрешено». Может звучать строго, но это держит кодовую базу здоровой.
Дополнительно CI может отслеживать покрытие кода — какой процент вашего кода задействован автоматизированными тестами. Хотя 100% покрытие часто нереалистично (и не весь код одинаково легко тестировать), многие команды устанавливают цель (скажем, как минимум 80% покрытия) и отслеживают её. Если новая порция кода вдруг роняет покрытие с 80% до 50%, это красный флаг — может, тесты были пропущены или добавлено много непротестированного кода. Мониторя эти метрики спринт за спринтом, вы поддерживаете высокое качество кода на протяжении разработки вместо того, чтобы пытаться «прикрутить его сбоку» позже.
Подводя итог, реализация в итеративном контексте — это не просто «написать кучу кода». Это писать код, непрерывно интегрировать этот код в общую кодовую базу, непрерывно его тестировать и исправлять проблемы по ходу. Результат — программная система, которая всегда находится в полурабочем состоянии, становясь чуть лучше каждый день.
Инкрементальные сборки и демо в конце спринта
К концу каждого спринта вы стремитесь иметь инкрементальную сборку всей системы, которая осязаемо лучше предыдущей. В идеале это что-то, что вы действительно можете запустить на устройстве и тестовом облачном окружении, чтобы увидеть новые функции в действии. Хорошая мантра — «всегда быть готовым к демо» в конце спринта. Для нашего примера IoT-продукта, может быть, к концу Спринта 1 у нас работает базовый сквозной поток: устройство читает фиктивное значение датчика (возможно, фиксированное число или случайное тестовое значение) и отправляет его в облако, облако принимает его и сохраняет, и мы можем вручную проверить, что данные прошли (даже если это просто взгляд напрямую в базу данных или логи). Это уже значимая веха — она доказывает, что вся сантехника от устройства до облака работает!
К Спринту 2 мы можем заменить фиктивное значение реальным показанием датчика с железа и настроить простую веб-страницу, отображающую последнее показание. Теперь мы действительно можем видеть живые данные. К Спринту 3, возможно, мы добавим функцию алертов: если температура поднимается выше порога, устройство или облако запускает алерт (может, мигает светодиод или отправляется email). И так далее. Каждый спринт мы наслаиваем дополнительную функциональность: может, один спринт добавляет поддержку удалённого управления (отправка команд из облака на устройство), другой добавляет красивый UI-график исторических данных и т.д. После ряда спринтов у вас будет функционально полная система. Но что критично — на каждой стадии у вас была работающая система, к которой вы добавляли. Это значит, что если что-то идёт не так, вы знаете, что это, скорее всего, связано с самыми недавними добавлениями, что упрощает отладку. Это также значит, что стейкхолдеры (ваш босс, продакт-менеджер или даже тестовый пользователь) могут видеть прогресс и давать обратную связь на протяжении всего пути.
Регулярные демо — отличная практика. В конце каждого спринта (или даже чаще, например быстрое show-and-tell каждую неделю) соберите команду и любых заинтересованных стейкхолдеров и продемонстрируйте, что нового. Это может быть так просто, как «Устройство теперь отправляет реальные данные датчика — смотрите, я нагреваю датчик рукой, и число на этом дашборде растёт в реальном времени!» Эти демо создают ощущение достижения и импульса. Они также принуждают к дисциплине интеграции: вы не можете продемонстрировать что-то, что на самом деле не работает вместе, так что это толкает команду выглаживать проблемы интеграции по ходу, а не откладывать интеграцию до самого конца. Наблюдать, как продукт оживает по кусочку, спринт за спринтом, гораздо более мотивирует, чем работать в вакууме месяцами.
Тестирование и критерии Definition of Done
Мы говорили об автоматизированных тестах во время разработки, но стоит подчеркнуть, как тестирование вплетено в само определение прогресса в каждом спринте. Функция или пользовательская история считается «сделанной», только когда она соответствует определённым критериям за пределами просто «код был написан». Команды часто имеют чек-лист Definition of Done (DoD), который может включать такие пункты:
- Код завершён: код функции написан и компилируется без ошибок и предупреждений.
- Unit-тесты: написаны автоматизированные unit-тесты, покрывающие новый код, и все эти тесты проходят. (Например, если вы написали функцию для вычисления условия алерта, должны быть тесты для нормальных и граничных случаев этой функции.)
- Интеграционно протестировано: если функция взаимодействует с другими компонентами (а большинство взаимодействует), она была протестирована в более широком контексте — например, устройство действительно отправляет сообщение реальному облачному сервису, и мы проверили, что оно проходит сквозным потоком. Это может включать ручное тестирование или автоматизированные интеграционные тесты.
- Нет регрессий: существующие тесты (от более ранних функций) всё ещё проходят, то есть вы не сломали ничего, что работало раньше.
- Статический анализ чист: инструменты статического анализа не показывают новых критических предупреждений в коде. Иными словами, вы не внесли потенциальную утечку памяти или рискованную конструкцию.
- Прошло peer review: код был просмотрен как минимум ещё одним разработчиком в команде, и вся обратная связь была обработана. Это гарантирует, что вторая пара глаз искала логические ошибки, читаемость и соответствие стандартам.
- Документация обновлена: если эта функция требует изменений в документации или диаграммах, они сделаны. Например, если вы добавляете новый формат сообщения данных, вы соответственно обновляете документацию API или спецификацию коммуникационного протокола. Или если вы решили добавить новый микросервис в облако, вы обновляете диаграмму архитектуры.
- Принято стейкхолдером: в идеале вы продемонстрировали работающую функцию владельцу продукта или соответствующему стейкхолдеру, и он согласен, что она соответствует требованиям (критериям приёмки, которые мы определили ранее). Это финальное подтверждение, что история действительно сделана.
Это длинный список, и на первый взгляд он может выглядеть как куча галочек для проставления. Но эти практики спасают вас от огромного количества проблем позже. Гораздо лучше поймать и исправить баг или дефект дизайна сейчас, во время текущего спринта, чем позволить ему прокрасться до финального тестирования или до продакшена. Definition of Done по сути означает «эта функция не полусырая; она действительно интегрирована, протестирована и готова». Когда каждая часть вашей системы соответствует этим критериям по ходу дела, финальная фаза интеграции и тестирования (Этап 5) преподносит мало сюрпризов — вы всё это время проверяли.
Непрерывная документация (держите карты обновлёнными)
Вы, вероятно, создали некоторые диаграммы и документы в архитектурной фазе (Этап 4.1). Теперь, по мере продвижения разработки, важно держать эти проектные документы актуальными. У софта есть забавная склонность эволюционировать — может, вы обнаружите потребность в новой задаче в RTOS для функции watchdog, или облачная архитектура изменится добавлением слоя кэширования ради производительности. Если вы не обновляете архитектурные документы, они уплывут от реальности, и в итоге никто им не будет доверять. Чтобы избежать классической проблемы устаревшей документации, относитесь к документам как к живой части проекта. Некоторые команды даже пишут свои диаграммы как код, используя инструменты вроде PlantUML или Mermaid. Так «исходник» диаграммы живёт в репозитории рядом с кодом. Когда делается изменение, влияющее на архитектуру, команда обновляет код диаграммы и перегенерирует изображение.
Практичный вариант (если ваша команда готова) — интегрировать документацию также в процесс CI. Например, у вас может быть проверка: если изменились определённые ключевые файлы (вроде схемы сообщения или интерфейса компонента), а соответствующий файл документации не был обновлён, CI напоминает вам или проваливает сборку. Это подталкивает разработчиков: «Эй, вы изменили, как система работает, — пожалуйста, обновите диаграмму, чтобы все знали». Может звучать как перебор, но синхронизация документов окупается, когда присоединяется новый разработчик или когда вы возвращаетесь к проекту через шесть месяцев и пытаетесь вспомнить, как всё организовано. Даже если вы не автоматизируете это, как минимум назначьте быстрый review диаграмм каждые несколько спринтов, чтобы убедиться, что они всё ещё отражают реальность.
Суть: актуальная документация стоит усилий. Это значит, что любой в команде (или вне команды, вроде аудитора по соответствию или партнёра) может посмотреть на документы и доверять им. Вы избегаете ситуаций вроде «Не читай wiki, она старая — у Джо настоящая информация в голове (или в каких-то комментариях в коде)». Вместо этого знание зафиксировано и обновляется по ходу, что является признаком зрелой, сфокусированной на качестве команды.
Регулярные демо и петли обратной связи
Мы касались демо в контексте спринтов, но давайте подчеркнём ценность установления регулярного ритма демонстрации прогресса. В Agile это часто встреча Sprint Review: в конце спринта команда показывает, что они построили, стейкхолдерам (продакт-менеджерам, другим командам, иногда даже реальным пользователям или руководителям). Для IoT-проекта демо может быть буквальным — показ работающих вместе устройства и системы. Например, вы можете спроецировать веб-дашборд на экран и держать физическое устройство на столе. Затем сделать что-то вроде триггера устройства (нажать кнопку или создать условие) и показать, что облако обновляется в реальном времени. Эти моменты по-настоящему вдохновляют команду и ценны для стейкхолдеров. Это делает проект осязаемым и держит всех вовлечёнными. Это также шанс получить обратную связь: может, стейкхолдер скажет: «Это круто, но можем ли мы также отображать данные о влажности в следующей версии?» или «Алерт появился довольно долго, можем ли мы это ускорить?». Ранняя обратная связь вроде этой особенно ценна, потому что её можно подать в следующие спринты, задолго до того, как продукт финален, когда изменения гораздо проще вносить.
Не переживайте, если в ранних демо функциональность очень базовая — это ожидаемо. Ключ в том, что она итеративно улучшается. Первое демо может просто доказывать, что одно устройство может говорить с одним сервером. Следующее демо показывает простой UI. Более поздние демо показывают полировку и дополнительные функции. Если что-то не сработало как ожидалось в демо, это тоже нормально, потому что вы обнаружили проблему интеграции на глазах у всех, и теперь вы можете приоритизировать её исправление. (Фактически у некоторых команд есть поговорка: «Демонстрируй рано, демонстрируй часто — проваливайся часто, исправляй быстрее!») Демо также обеспечивает немного дисциплины: оно оказывает позитивное давление на команду, чтобы завершать работу до готового к отгрузке состояния каждый спринт, а не прокрастинировать на интеграцию до потом. Гораздо лучше узнать сейчас, что JSON устройства не парсится корректно на сервере, чем узнать это во время большой финальной фазы тестирования. Так что относитесь к демо не просто как к шоу, а как к части самого процесса разработки.
Наконец, празднуйте эти демо. Построение IoT-продукта — тяжёлая работа, включающая много частей, и каждая работающая часть — даже маленькая функция — заслуживает момента признания. Это поддерживает мораль и строит уверенность команды: энергия остаётся выше, когда все регулярно видят осязаемые результаты своей работы.
Подводя итог Этапа 4: разработка firmware и облачного ПО — это всё про итеративный прогресс и непрерывное улучшение. Инвестируя время в архитектуру ПО заранее, вы даёте вашей команде прочный чертёж (и избегаете закодировать себя в угол). Используя спринты разработки, непрерывную интеграцию и сильное определение готовности, вы гарантируете, что качество встроено на каждом шаге — баги ловятся рано, и софт всегда в одном шаге от работающего продукта. К концу Этапа 4 (который, вероятно, охватывает несколько спринт-циклов) у вас должно быть firmware, функционально полное и протестированное на реальном или репрезентативном железе, и облачная система, функционально полная и протестированная в реалистичном окружении. Иными словами, вся программная сторона вашего продукта готова к старту.
Благодаря этому итеративному подходу вы могли корректировать и улучшать непрерывно, а не ставить всё на один большой взрыв в конце. Это снижает риск и делает процесс разработки более предсказуемым. Это также просто лучший способ работать: вместо долгой полосы неопределённости вы наблюдали, как ваш IoT-продукт оживает шаг за шагом, от мигающего светодиода до полностью подключённого, интегрированного с облаком умного устройства. Тяжёлая работа на программной стороне сделана, и сделана она со строгостью и гибкостью. Вперёд, к следующему этапу, где мы берём это хорошо сработанное железо и софт и готовимся выпустить его в мир.