РУБРИК · портал

← Ко всем документам

Тест-кейсы: приложение робота (android-bridge)

Ручное тестирование Kotlin-приложения, установленного на Android-телефон, закреплённый на роботе. Приложение: (1) принимает Bluetooth RFCOMM SPP-соединение от ноутбука ученика и гоняет по нему NDJSON-протокол run/stop/телеметрия; (2) выполняет программу ученика на Chaquopy (embedded CPython) прямо на телефоне; (3) форвардит команды моторов и опрашивает модуль STM32 по USB CDC 115200; (4) держит lease на модуль — единственный на сегодня механизм, который останавливает моторы, когда всё остальное замолчало; (5) показывает собственный экран состояния; (6) обновляет сам себя и несёт (но пока не прошивает) образ прошивки модуля.

Источники: mvp/android-bridge/README.md, mvp/android-bridge/PLAN.md, mvp/protocols/01-student-app--android-bridge.md (ноутбук ↔ телефон), mvp/protocols/05-android-bridge--controller.md (телефон ↔ модуль), исходники в mvp/android-bridge/app/src/main/kotlin/ и mvp/android-bridge/core/src/main/kotlin/.

Документация первична. Там, где протокол 05 прямо говорит, что модуль НЕ имеет собственного watchdog-а и продолжит крутить моторы после потери связи с телефоном — это не баг, а задокументированное и принятое владельцем продукта поведение пилота (D-079/D-080). Отмечено явно в каждом таком кейсе.

Как читать кейсы. «Шаги» и «Ожидаемый результат» написаны в том, что тестировщик видит глазами: экран приложения ученика, экран робота, поведение самого робота. Строка «Протокол» в конце кейса — техническая сверка для разработчика (какие сообщения ходят по проводу); при ручном прогоне её можно пропускать. Кейсы с пометкой [инструментальный] требуют отправки сырых сообщений скриптом — их выполняет разработчик, в ручной прогон они не входят.

Стенд тестера

Сводная таблица разделов

РазделНазваниеКейсов
AУстановка APK и первый запуск4
BРазрешения Android (BT, уведомления, USB)6
CBluetooth-сопряжение с ноутбуком8
DПриём и запуск программы10
EОстановка и cancellation9
FUSB-подключение модуля и permission-диалог7
GLease модуля, мотор-команды, реальное движение9
HТелеметрия обратно на ноутбук5
IЭкран состояния робота6
JОбновление приложения и прошивки модуля9
KОтказоустойчивость (критичные «железные» кейсы)12
LВерсии Android / разные телефоны4
Итого89

Приоритеты: P1 — блокирует урок или угрожает безопасности (моторы не останавливаются, робот не подключается вовсе); P2 — заметно мешает, но есть обход; P3 — косметика/редкий случай.


A. Установка APK и первый запуск

MB-001. Установка debug/release APK

Приоритет: P1 Предусловия: APK передан на телефон (ADB, файл, ссылка). Шаги:

Ожидаемый результат: Установка проходит без ошибок системы. Значок приложения появляется в списке приложений с названием из @string/app_name.

MB-002. Первый запуск — стартовый экран

Приоритет: P1 Предусловия: Приложение только что установлено, ранее не запускалось. Шаги:

Ожидаемый результат: Открывается экран робота (RobotActivity) — это launcher-активность, MainActivity («служебный экран») запускается только по отдельному переходу. Виден логотип, лицо робота (RubrickFaceView), панели «Состояние», «Обновление», «Прошивка модуля».

MB-003. Foreground-уведомление появляется сразу

Приоритет: P2 Предусловия: Приложение только что запущено. Шаги:

Ожидаемый результат: Видно постоянное (не смахиваемое свайпом) уведомление foreground-сервиса BridgeService — приложение держит FOREGROUND_SERVICE_CONNECTED_DEVICE, само уведомление может быть техническим (link/phase/tel/dropped), это ожидаемо для этой сборки.

MB-004. Переход на служебный экран и обратно

Приоритет: P3 Предусловия: Приложение запущено. Шаги:

Ожидаемый результат: Открывается инженерская консоль (MainActivity, счётчики байт, кнопки Ping/Connect USB и т. д.). «Назад» возвращает на экран робота, приложение не падает.


B. Разрешения Android (BT, уведомления, USB)

MB-005. Запрос Bluetooth-разрешений на Android 12+

Приоритет: P1 Предусловия: Android 12 (API 31) и новее, разрешения ещё не выданы (чистая установка). Шаги:

Ожидаемый результат: Системный диалог запрашивает BLUETOOTH_CONNECT, BLUETOOTH_SCAN, BLUETOOTH_ADVERTISE (запрашиваются из RobotActivity.requestBluetoothPermissionsIfNeeded). Диалог по-русски или системный — это нормально, локализация диалога не в зоне приложения.

MB-006. Отказ в Bluetooth-разрешении

Приоритет: P1 Предусловия: Тот же диалог, что в MB-005. Шаги:

Ожидаемый результат: Приложение не падает. BT SPP-сервер не запускается (в статусе телефон показывает «Ноутбук: жду подключения» бесконечно). Приложение остаётся откликающимся — панели «Обновление» и «Прошивка модуля» работают независимо от BT.

MB-007. Разрешение выдано позже, вручную из настроек Android

Приоритет: P2 Предусловия: Продолжение MB-006 — разрешения были отклонены. Шаги:

Ожидаемый результат: В разумное время (после следующего цикла обновления экрана, не более пары секунд) BT-сервер поднимается — статус «Ноутбук» уходит из состояния ожидания разрешения, ноутбук теперь может увидеть телефон. Перезапуск приложения не должен требоваться.

MB-008. Запрос уведомлений на Android 13+

Приоритет: P2 Предусловия: Android 13 (API 33) и новее. Шаги:

Ожидаемый результат: Системный диалог POST_NOTIFICATIONS. При отказе постоянное уведомление foreground-сервиса всё равно нужно системе для работы связанного сервиса — проверить, что сервис не падает и не останавливается из-за отсутствия разрешения на уведомления.

MB-009. Android ниже 13 — уведомления не запрашиваются

Приоритет: P3 Предусловия: Android 12 или ниже. Шаги:

Ожидаемый результат: Строка сообщает «Permissions: POST_NOTIFICATIONS N/A below API 33» — не предпринимается попытка запросить разрешение, которого не существует на этой версии.

MB-010. Отсутствие USB-хоста на устройстве

Приоритет: P3 Предусловия: Тестовый телефон без поддержки android.hardware.usb.host (редкий случай; если такого телефона нет — кейс пропустить и пометить «нет устройства для проверки»). Шаги:

Ожидаемый результат: Play Store (или ручная установка) не должны позволить установку — фича объявлена android:required="true". При ручной установке (adb install) приложение должно хотя бы не падать при старте, USB-функциональность просто недоступна.


C. Bluetooth-сопряжение с ноутбуком

MB-011. Первое сопряжение (system pairing) телефона и ноутбука

Приоритет: P1 Предусловия: Оба устройства с включённым Bluetooth, ранее не сопряжены. Шаги:

Ожидаемый результат: Устройства сопряжены на уровне ОС. Это системная операция, отдельная от приложения — обычный Android-диалог сопряжения. После сопряжения телефон виден ноутбуку как Bluetooth-порт.

MB-012. Ноутбук подключается к уже сопряжённому телефону

Приоритет: P1 Предусловия: MB-011 выполнен, приложение робота запущено и BT-разрешения выданы. Шаги:

Ожидаемый результат: Устанавливается RFCOMM-соединение (SPP UUID 00001101-0000-1000-8000-00805F9B34FB, имя сервиса RUBRICK). На экране робота «Ноутбук: подключён» (зелёным). В student-app робот показывается подключённым.

MB-013. Приветственный обмен при подключении

Приоритет: P1 Предусловия: MB-012 выполнен; нужен доступ к журналу student-app (кейс для тестировщика с разработчиком рядом или по журналу постфактум). Шаги:

Ожидаемый результат: Первым «здоровается» телефон, сразу за приветствием без запроса приходит статус робота. Протокол: телефон шлёт hello{proto:2, device, app:"rubrick-bridge", link, caps:[...]}, затем status; IDE отвечает своим hello{proto:2, ide, caps}.

MB-014. Второй ноутбук получает отказ «занято»

Приоритет: P1 Предусловия: Один ноутбук уже подключён к телефону (MB-012). Шаги:

Ожидаемый результат: Второй ноутбук получает понятный отказ — робот занят. Первый ноутбук ничего не замечает и продолжает нормально работать. Если второй ноутбук видит робота в сети, карточки робота в списке нет, а под списком строка «Занят другим ноутбуком или пультом: <имя робота>» (см. MB-017). Протокол: второе соединение получает error{code:"E_BUSY"} и закрывается; занятый робот отвечает на обнаружение с busy:true (§1.1 протокола 01).

MB-015. Обрыв и повторное подключение (без бега программы)

Приоритет: P2 Предусловия: Есть активное подключение (MB-012). Шаги:

Ожидаемый результат: Экран робота отражает «Ноутбук: жду подключения» после разрыва, затем снова «подключён» после повторного соединения. Приложение не требует перезапуска.

MB-016. TCP-транспорт вместо Bluetooth (порт 50507)

Приоритет: P2 Предусловия: Телефон и ноутбук в одной Wi-Fi/LAN-сети класса. Шаги:

Ожидаемый результат: Телефон находится в списке роботов по сети; подключение, запуск и остановка работают ровно так же, как по Bluetooth. Протокол: ответ на UDP-обнаружение {"t":"rubrick-robot", name, port:50507} и/или анонс DNS-SD _rubrick-robot._tcp; соединение по TCP:50507, дальше тот же hello/run/stop.

MB-017. Отказ «занято» действует и через другой транспорт

Приоритет: P2 Предусловия: Одно соединение активно по BT (MB-012). Шаги:

Ожидаемый результат: Телефон работает ровно с одним ноутбуком, независимо от способа подключения. Второй ноутбук не может подключиться и видит причину:

То же в обратную сторону: робот подключён по сети, второй ноутбук пробует Bluetooth. Когда первый ноутбук отключается, робот появляется в списке второго в течение ~4 с. Протокол: busy:true в ответе UDP-обнаружения; при подключении — error{code:"E_BUSY"} (§1.1 протокола 01). Решение владельца продукта 15.10.2026 (RUB-134): занятых не показываем, но называем.

MB-018. Discoverable-режим со служебного экрана

Приоритет: P3 Предусловия: Служебный экран открыт. Шаги:

Ожидаемый результат: Системный диалог ACTION_REQUEST_DISCOVERABLE на 300 секунд (константа DISCOVERABLE_SECONDS; запрос на большее Android молча урезает — это ожидаемо, не баг). После подтверждения телефон виден при Bluetooth-сканировании с других устройств.


D. Приём и запуск программы

MB-019. Обычный запуск программы — полный жизненный цикл

Приоритет: P1 Предусловия: Робот подключён к приложению ученика (раздел C), модуль подключён (раздел F). Шаги:

print('поехали')
robot.forward(200)

Ожидаемый результат: Кнопка сменяется на «■ Стоп». В консоли появляется «поехали». Робот проезжает вперёд и сам останавливается. После этого запуск помечается завершённым без ошибок, кнопка снова «Старт». Повторный запуск работает так же. Протокол: строгий порядок ack{re} → run_started{run_id} → out/tel → ровно один run_finished{ok:true, reason:"ok"}; ack значит «принято», не «успешно».

MB-020. Программа без обращения к роботу (только print)

Приоритет: P1 Предусловия: Робот подключён, модуль НЕ подключён (отсоединить USB-кабель модуля). Шаги:

Ожидаемый результат: Программа запускается и успешно завершается. В консоли — «hello». Отсутствие модуля не мешает: печатать можно и без него. Протокол: запуск не отклоняется при status.link == "none" (§4.2), run_finished{ok:true}.

MB-021. Программа обращается к роботу без модуля — понятная русская ошибка

Приоритет: P1 Предусловия: Робот подключён, модуль не подключён. Шаги:

Ожидаемый результат: Программа завершается с ошибкой, в консоли — понятное русское сообщение (про отсутствие модуля/подключения). Не молчаливое зависание, не разрыв связи с роботом.

MB-022. Запуск с некорректным lang [инструментальный]

Приоритет: P2 Шаги:

Ожидаемый результат / Протокол: error{code:"E_ARGS"}; код не пытается выполниться как Python (фолбэк «неизвестный enum» не применяется к диспетчеризующим полям).

MB-023. Второй запуск во время выполнения без force [инструментальный]

Приоритет: P1 Предусловия: Длинная программа выполняется (например robot.wait(30)). Из приложения ученика этот случай не воспроизвести: кнопка уже в состоянии «Стоп», а второй ноутбук получает отказ «занято» ещё на подключении (MB-014). Шаги:

Ожидаемый результат / Протокол: error{code:"E_RUNNING", re:<id второго run>}; первая программа продолжает выполняться, её завершение не приходит раньше времени.

MB-024. Новый запуск поверх выполняющейся программы

Приоритет: P1 Предусловия: Длинная программа с движением выполняется (robot.drive(40) + robot.wait(30)). Шаги:

Ожидаемый результат: Старая программа останавливается — робот тормозит; затем стартует новая. В консоли видно завершение старой («остановлена») и вывод новой. Никакой каши из двух программ. Протокол: run{force:true} выполняет полную процедуру остановки старого запуска (раздел E), включая его run_finished{reason:"stopped"}, и только затем стартует новый. Итого два run_finished — по одному на каждый run_id.

MB-025. Исходник длиннее лимита (128 КиБ) [инструментальный]

Приоритет: P3 Шаги:

Ожидаемый результат / Протокол: строка обрывается ридером телефона (лимит 256 КиБ на NDJSON-строку), приходит E_TOOLONG с re:0 — сопоставить с конкретным run нельзя, это задокументировано.

MB-026. Программа с опечаткой — ошибка показана человеку

Приоритет: P1 Шаги:

Ожидаемый результат: Программа завершается с ошибкой. В консоли — объяснение по-русски с номером строки. Приложение живо: опечатку можно исправить и нажать «Старт» снова. Протокол: err{s:"Traceback..."} (сырой traceback), затем run_finished{ok:false, reason:"error", exc:{type, msg, line, rubrick:false}}; русский текст — маппинг на стороне IDE.

MB-027. Ошибка самой библиотеки робота — текст уже русский

Приоритет: P2 Предусловия: Способ спровоцировать ошибку библиотеки: например, обратиться к мотору на порту, где мотор не подключён. Шаги:

Ожидаемый результат: В консоли — готовое русское сообщение о том, что случилось, как есть, без «перевода» и без сырого трейсбека в лицо ребёнку. Протокол: exc.rubrick == true, exc.msg — готовый русский текст; IDE показывает его без таблицы маппинга.

MB-028. Большой объём вывода — часть печати может потеряться, ошибки — никогда

Приоритет: P3 Шаги:

Ожидаемый результат: Программа завершается успешно. На медленном Bluetooth часть строк печати может не дойти — это допустимо, но потеря видна честно (счётчик потерянных строк в статусе/журнале), а не молча. Сообщения об ошибках не теряются никогда. Протокол: run_finished.dropped_out > 0 под перегрузкой; out роняется, err — нет.


E. Остановка и cancellation

Ключевой факт документа 05 §5.1 (важно для всех кейсов ниже): при получении stop телефон СНАЧАЛА шлёт #X на модуль (тормоз), и только потом — что-либо просит у Python-интерпретатора. Торможение НЕ ждёт ответа от программы.

MB-029. Обычная остановка кнопкой «Стоп»

Приоритет: P1 Предусловия: Выполняется программа с движением и длинным ожиданием (robot.forward(2000) + robot.wait(30)). Шаги:

Ожидаемый результат: Робот физически перестаёт ехать сразу — раньше, чем в приложении появится отметка о завершении. Затем запуск помечается остановленным, кнопка возвращается в «Старт». Протокол: #X уходит на модуль первым (бюджет телефон→провод p95 < 50 мс) → ack{re} → stop_ack{run_id, brake_confirmed:true, brake_ms} по #XACK → позже, отдельно — run_finished{ok:false, reason:"stopped"}.

MB-030. Торможение не ждёт программу

Приоритет: P1 Предусловия: Программа в тесном цикле (while True: x += 1), к роботу не обращается. Шаги:

Ожидаемый результат: Остановка принимается сразу, но отметка «завершена» может появиться заметно позже — на такой программе это ожидаемо, не зависание. Протокол: stop_ack приходит быстро (торможение не ждёт Python), run_finished — после срабатывания async-injection (_RubrickStopped) на ближайшей байткод-границе.

MB-031. stop с устаревшим run_id — не ошибка [инструментальный]

Приоритет: P2 Предусловия: Программа завершилась 5+ секунд назад (или не запускалась). Из приложения ученика этот случай не воспроизвести — кнопки «Стоп» без активного запуска нет. Шаги:

Ожидаемый результат / Протокол: ack + stop_ack без ошибки; #X всё равно уходит на модуль (тормозить уже остановленного робота бесплатно). Кода E_NO_RUN не существует вовсе.

MB-032. Двойное нажатие «Стоп» — без последствий

Приоритет: P2 Предусловия: Программа с движением выполняется. Шаги:

Ожидаемый результат: Робот останавливается, запуск помечается остановленным один раз. Никаких ошибок и странных состояний от второго нажатия. Протокол: второй stop получает свои ack + stop_ack (с независимым brake_ms), второй run_finished не приходит.

MB-033. «Стоп» во время time.sleep() завершает программу сразу

Приоритет: P1 Предусловия: программа содержит import time и time.sleep(60); робот 0.8.16 или новее. Шаги:

Ожидаемый результат: робот тормозит немедленно; запуск помечается остановленным за доли секунды (time.sleep в среде робота прерываемый); следующая программа запускается без «программа зависла — перезапустите приложение». На роботе старше 0.8.16 сон не прерывался — это и был баг «time.sleep… abandoned». Протокол: #X/stop_ack сразу; run_finished{reason:"stopped"} без ожидания grace-window.

MB-034. Заброшенный (abandoned) поток блокирует новые запуски

Приоритет: P1 Предусловия: MB-033 доведён до конца — grace-window истёк. Шаги:

Ожидаемый результат: Экран показывает «Программа зависла — перезапустите приложение» (красным, грустное лицо). Новый run получает E_RUNNING навсегда, до перезапуска приложения — это специфицированное, а не случайное поведение (§5.4 слой c).

MB-035. brake_confirmed:false — модуль не подтвердил тормоз

Приоритет: P1 Предусловия: Способ имитировать неответ модуля на #X (например временно отсоединить USB сразу после отправки stop, до получения #XACK — тайминг подобрать экспериментально). Шаги:

Ожидаемый результат: Приложение ученика показывает остановку иначе, чем обычную подтверждённую: команда торможения отправлена, но модуль её не подтвердил — робот может продолжать движение (см. раздел K про safety-риски без board watchdog). Это отличие видно в UI/журнале, а не скрыто. Протокол: stop_ack{brake_confirmed:false}.

MB-036. Остановка через физическую кнопку/действие на самом телефоне

Приоритет: P2 Предусловия: Программа выполняется; на экране робота нет отдельной кнопки «Стоп» (её нет в текущей реализации RobotActivity — только удалённая остановка через stop от IDE). Шаги:

Ожидаемый результат: Кнопки нет — это факт текущей реализации, не баг. Единственный путь остановить программу — stop от student-app. Зафиксировать как известное ограничение, если продукту это не соответствует ожиданиям.

MB-037. Латч фолта модуля — следующий run не глотает состояние молча

Приоритет: P2 Предусловия: Способ спровоцировать #EV фолт модуля (например перегрузка мотора / сам модуль даёт латч по своей логике — сверяться с прошивкой модуля отдельно, не с этим документом). Шаги:

Ожидаемый результат: Новый run либо отказывает с внятной причиной, либо латч сознательно снимается явным действием (Robot.clear_fault() / кнопка «Clear fault (#W,0)» на служебном экране) — никогда тихий автосброс без следа, что что-то случилось.


F. USB-подключение модуля и permission-диалог

MB-038. Первое подключение модуля — системный диалог разрешения USB

Приоритет: P1 Предусловия: Телефон запущен, приложение работает, модуль ранее не подключался к этому телефону. Шаги:

Ожидаемый результат: ATTACHED-широковещание доставляется приложению (в манифесте объявлен device_filter.xml). Системный диалог Android «Разрешить приложению Рубрик доступ к USB устройству?» появляется автоматически. Экран робота на это время показывает «Модуль: нужно разрешение на USB» (оранжевым).

MB-039. Подтверждение USB-разрешения

Приоритет: P1 Предусловия: MB-038, диалог на экране. Шаги:

Ожидаемый результат: Соединение поднимается, экран переходит «Модуль: соединяюсь» → «Модуль: подключена, прошивка X.Y.Z». Служебный экран показывает Board: <serial/proto> после первого успешного #GP/опроса.

MB-040. Отказ в USB-разрешении

Приоритет: P2 Шаги:

Ожидаемый результат: Приложение не падает. Экран остаётся в состоянии «нужно разрешение на USB». Отключение и повторное подключение кабеля снова показывает диалог (Android не запоминает отказ так же жёстко, как разрешение).

MB-041. Повторное подключение — разрешение не переспрашивается

Приоритет: P2 Предусловия: MB-039 выполнен и было отмечено «запомнить». Шаги:

Ожидаемый результат: Диалог не появляется повторно (usbManager.hasPermission() уже возвращает true для этого устройства). Соединение поднимается автоматически.

MB-042. Подключение модуля через служебный экран (ручная кнопка)

Приоритет: P3 Предусловия: Модуль физически подключён, но авто-detect почему-то не сработал. Шаги:

Ожидаемый результат: Та же процедура запроса разрешения/подключения запускается вручную, результат идентичен MB-038/039.

MB-043. Опрос присутствия моторов (#GP) отражается корректно

Приоритет: P2 Предусловия: Модуль подключён, к нему подсоединены только 2 из 4 моторов. Шаги:

Ожидаемый результат: Присутствие моторов переопрашивается периодически, не только при старте связи — статус должен отразить изменение состава моторов без переподключения USB.

MB-044. Ручное отключение модуля со служебного экрана

Приоритет: P2 Предусловия: Модуль подключён. Шаги:

Ожидаемый результат: Связь с модулем корректно закрывается, экран робота возвращается в «Модуль: не подключена». Если в этот момент выполнялась программа с движением — моторы должны быть остановлены (emergencyStop() вызывается при явном disconnectLink()).


G. Lease модуля, мотор-команды, реальное движение

MB-045. robot.forward() — позиционный шаг, робот проезжает и сам останавливается

Приоритет: P1 Предусловия: Модуль с двумя моторами (левый/правый) подключён и откалиброван. Шаги:

Ожидаемый результат: Робот физически проезжает вперёд и сам останавливается, без участия телефона в моменте окончания — это закрытый цикл на модуле (#F). Завершение определяется по неподвижности одометрии (2 последовательных опроса #GD каждые 150 мс без изменения, но только после того как одометрия хоть раз изменилась) — не по показаниям #GE (те возвращают на реальной модуле константу и НЕ являются признаком завершения шага, см. протокол 05 §3.1).

MB-046. robot.turn_left() / robot.turn_right() — поворот на месте

Приоритет: P1 Шаги:

Ожидаемый результат: Робот поворачивается примерно на заданный угол (калибровочные константы degPerMm/wheelDegPerRobotDeg — ориентировочные значения для колёс Ø43.2 мм, точность зависит от конкретной сборки) и останавливается сам.

MB-047. Позиционный шаг на незаполненном порту — таймаут без зависания

Приоритет: P2 Предусловия: Один из портов моторов физически пуст (не подключён). Шаги:

Ожидаемый результат: Шаг завершается по no-motion-abort таймауту (референс: 2000 мс), не по полному hard-таймауту — иначе экран «зависает» на десяток секунд, что для ребёнка читается как сломанный робот (задокументированный в протоколе 05 §3.2 реальный замер — 9.3 с при наивной реализации).

MB-048. Непрерывное движение — robot.drive(speed)

Приоритет: P1 Шаги:

Ожидаемый результат: Робот едет непрерывно. Пока программа жива и продолжает вызывать методы rubrick.py (в т.ч. robot.wait()), lease на модуль продолжает продлеваться (интервал keepalive-переотправки — 150 мс, референс), робот не останавливается сам по себе.

MB-049. Непрерывное движение останавливается через robot.stop()

Приоритет: P1 Шаги:

Ожидаемый результат: Робот немедленно тормозит (#M0,0,0,0;) — port-independent торможение всех четырёх каналов разом.

MB-050. Программа зависает во время непрерывного движения БЕЗ stop от IDE — 800 мс lease

Приоритет: P1 (safety) Предусловия: Программа: robot.drive(60), затем зависающий, не через rubrick.py, код — например тесный while True: pass без единого вызова robot.*. Шаги:

Ожидаемый результат: Робот останавливается сам — жёстким тормозом (не выбегом) — примерно через 800 мс (референсное значение lease, протокол 05 §5) после последней команды/продления. Сама программа не завершается (никакого run_finished до явного stop) — «зависшую» программу от «медленной» отличить нельзя, это по документации. Это тест-вектор №11 протокола 01 §12 и он должен проходить на реальном железе, а не только в юнит-тестах MotionTests.kt.

MB-051. Балансировочный триммер (если экспонирован в UI/API)

Приоритет: P3 Предусловия: Робот заметно уводит в сторону при robot.drive(). Шаги:

Ожидаемый результат: Триммер влияет только на непрерывное движение (#M), не на позиционные шаги (#F — они закрытый цикл на модуле и в трим не нуждаются). Если UI/API для этого пока не существует — пометить кейс как «не реализовано», не как провал.

MB-052. Направление и маппинг портов — «лево» и «право» настраиваемы

Приоритет: P3 Предусловия: Заведомо неправильно подключены моторы (левый в разъём правого и наоборот). Шаги:

Ожидаемый результат: После смены маппинга forward снова означает физически вперёд без переписывания программы ученика — по протоколу это конфигурация телефона, а не константа кода. Если настройка отсутствует в текущей сборке UI — пометить «не реализовано (если реализовано)».

MB-053. Одновременная попытка двух источников управлять моторами

Приоритет: P2 Предусловия: Программа выполняется и двигает робота. Шаги:

Ожидаемый результат: Ровно один источник владеет lease в моменте — «одиночный писатель» (§1 протокола 05). Конкурирующий источник не проникает на порт напрямую и не портит кадр — арбитраж на уровне MotionController/MotionArbiter, не на уровне порта.


H. Телеметрия обратно на ноутбук

MB-054. Показания робота идут и без запущенной программы

Приоритет: P2 Предусловия: Модуль подключён, программа не запущена. Шаги:

Ожидаемый результат: Показания обновляются и без запущенной программы — они описывают состояние модуля, а не программы. Протокол: tel/status идут независимо от запуска (протокол 01 §4.1 п.4).

MB-055. Частота телеметрии регулируется [инструментальный]

Приоритет: P2 Шаги:

Ожидаемый результат / Протокол: при hz:0 tel-кадры прекращаются (или сводятся к минимуму), при hz:10 возобновляются с прореживанием до ≤10 Гц. Допустимые значения {0,1,2,5,10}, всё остальное — E_ARGS.

MB-056. Показание батареи — это батарея МОДУЛЯ, не телефона

Приоритет: P2 Предусловия: Модуль и телефон на разных источниках питания с разным уровнем заряда. Шаги:

Ожидаемый результат: Показание соответствует именно модулю. Заряд телефона по этой связи не передаётся вовсе — это намеренное решение (протокол 01 §7.1), а не путаница.

MB-057. «NO SIGNAL» объявляется не раньше ~800 мс молчания

Приоритет: P2 Предусловия: Активное соединение с идущей телеметрией. Шаги:

Ожидаемый результат: Индикатор не должен схлопываться раньше ~800 мс простоя (порог из OVERVIEW.md §4) — слишком ранний срабатывающий индикатор будет ложно мигать на нормальных паузах в потоке.

MB-058. Дропы телеметрии под перегрузкой канала считаются честно

Приоритет: P3 Предусловия: Программа с большим потоком print() идёт одновременно с телеметрией (см. MB-028). Шаги:

Ожидаемый результат: Если данные реально теряются, счётчик потерь в статусе/журнале растёт, а не стоит на нуле. Bluetooth Classic документированно может «залипать» на 1–2 с при перегрузке (EXP-02 §6) — потери в этот момент нормальны, скрытые потери — нет. Протокол: status.dropped отражает реальный backpressure.


I. Экран состояния робота

MB-059. Лицо робота меняется по состоянию

Приоритет: P2 Предусловия: Экран робота открыт. Шаги:

Ожидаемый результат: Лицо (RubrickFaceView) визуально меняется в соответствии с состоянием, без задержки более одного цикла обновления экрана (референс — 500 мс, REFRESH_MS).

MB-060. Статусная панель «Ноутбук»

Приоритет: P2 Шаги:

Ожидаемый результат: Текст: «Ноутбук: жду подключения» (серый/приглушённый) → «Ноутбук: подключён» (зелёный). Никогда не показывает голый технический код состояния.

MB-061. Статусная панель «Модуль»

Приоритет: P2 Шаги:

Ожидаемый результат: Каждое состояние из refresh() отражено отдельной надписью и своим цветом; при подключённом модуле видна версия прошивки модуля (board.fw), а не версия приложения.

MB-062. Статусная панель «Программа»

Приоритет: P1 Шаги:

Ожидаемый результат: «Программа: не запущена» (серый) → «Программа: выполняется» (жёлтый) → «Программа зависла — перезапустите приложение» (красный, при abandoned-run из MB-034).

MB-063. Прошивка модуля — версия читается, запись честно недоступна

Приоритет: P1 Предусловия: Экран робота открыт, модуль может быть подключён или нет. Шаги:

Ожидаемый результат: Панель показывает версию и целостность образа, который несёт APK (Версия X.Y.Z несёт прошивку модуля... Записи в модуль пока нет.). Кнопка «Прошить модуль» не пишет ничего в модуль и открытым текстом сообщает об этом («Запись в модуль пока не реализована», flashBoardReport()) — заглушка, которая рапортует об успехе, была бы хуже отсутствующей кнопки, и здесь такой заглушки нет. Это ожидаемое поведение текущей сборки, не баг.

MB-064. Версия приложения и id устройства видны внизу экрана

Приоритет: P3 Шаги:

Ожидаемый результат: Строка вида «Версия X.Y.Z · android-...» — версия из packageManager.getPackageInfo(...).versionName и стабильный deviceId, сгенерированный один раз и хранящийся в filesDir приложения (не ANDROID_ID, не серийник — умышленно, из-за 152-ФЗ).


J. Обновление приложения и прошивки модуля

MB-065. Проверка обновления — порядок источников: сначала учитель, потом портал

Приоритет: P1 Предусловия: Телефон в сети класса с работающим ноутбуком учителя, и есть доступ в интернет. Шаги:

Ожидаемый результат: Телефон сперва пытается mDNS-обнаружить учителя (TeacherDiscovery), и только если учителя нет — обращается к порталу (rubrick.beys.tech). Источник обновления виден в диалоге установки («Источник: …»).

MB-066. Проверка обновления без интернета, но с учителем в сети

Приоритет: P1 Предусловия: Отключить интернет на телефоне (оставить только LAN/Wi-Fi класса без выхода наружу), учитель работает и раздаёт обновление в сети. Шаги:

Ожидаемый результат: Обновление находится через ретранслятор учителя. Отсутствие интернета не блокирует урок и не блокирует обновление — школа без интернета вообще должна оставаться рабочим сценарием (D-086).

MB-067. Полное отсутствие сети — «Обновлений сейчас не видно», не ошибка

Приоритет: P1 Предусловия: Ни учителя, ни интернета. Шаги:

Ожидаемый результат: «Обновлений сейчас не видно» нейтральным цветом (не красным/оранжевым как ошибка) — школа без сети это «обычный вторник», не поломка (см. AndroidUpdates.State.Unavailable). Приложение продолжает нормально работать (BT/USB/запуск программ не затронуты).

MB-068. Подпись проверяется перед установкой, независимо от источника

Приоритет: P1 Предусловия: Доступен файл с корректной Ed25519-подписью новой версии. Шаги:

Ожидаемый результат: Диалог явно говорит «Подпись проверена». Файл с любого источника проходит одинаковую проверку — учительский ноутбук в этой схеме не более доверенный источник, чем интернет (§7.1 плана android-bridge).

MB-069. Запрет понижения версии (downgrade ban)

Приоритет: P1 Предусловия: На телефоне версия новее, чем предлагаемая (например уже стоит 0.2.0, доступен подписанный .rbu с версией 0.1.0). Шаги:

Ожидаемый результат: Установка отклоняется с понятным сообщением про запрет понижения версии. Подпись сама по себе не даёт файлу права установиться поверх более новой версии (D-108).

MB-070. Обновление из файла (.rbu) — выбор внутри приложения

Приоритет: P1 Шаги:

Ожидаемый результат: Файл открывается и обрабатывается корректно именно через внутренний пикер — важно не пытаться открыть .rbu через файловый менеджер напрямую (внешний intent-filter по расширению *.rbu заведомо не сработает: провайдер Downloads отдаёт content:// URI без расширения в пути — задокументированная ловушка EXP-11).

MB-071. Разрешение «установка неизвестных приложений»

Приоритет: P2 Предусловия: Разрешение canRequestPackageInstalls() не выдано. Шаги:

Ожидаемый результат: Понятное русское сообщение с указанием, что открыть в настройках («Установка неизвестных приложений» → разрешить РУБРИКу), установка не проходит молча.

MB-072. Ответ пакетного установщика доходит до диалога подтверждения системы

Приоритет: P2 Предусловия: Обновление скачано и подписано корректно. Шаги:

Ожидаемый результат: Системный диалог подтверждения установки появляется автоматически (InstallResultReceiver перенаправляет EXTRA_INTENT), не требует ручного поиска уведомления.

MB-073. versionCode не растёт — Android сам отказывает

Приоритет: P3 Предусловия: Есть сборка с тем же или меньшим versionCode, что установлена. Шаги:

Ожидаемый результат: Установка отклоняется системой Android — независимый от приложения второй запрет понижения версии, задокументированный в README как обязательное требование к каждому релизу.


K. Отказоустойчивость (критичные «железные» кейсы)

> Читать перед выполнением. Протокол 05 §5/§6 прямо говорит: у модуля нет собственного > watchdog-а. Единственный барьер для непрерывного движения (#M, robot.drive()) — это > lease на телефоне (800 мс). Позиционные шаги (#F, robot.forward()/turn_left()/…) > самоограничены модулем и в любом случае доедут до цели и остановятся сами. Держи это в голове, > читая ожидаемые результаты ниже — часть из них специально описывает поведение, которое выглядит > пугающе, но задокументировано и принято владельцем продукта для пилота (D-079/D-080).

MB-074. Выдёргивание USB во время непрерывного движения — задокументированный риск

Приоритет: P1 (safety, ожидаемый результат — не «робот останавливается») Предусловия: Робот едет непрерывно (robot.drive(60), программа продолжает работать и продлевать lease — например в цикле с robot.wait()). Шаги:

Ожидаемый результат (по протоколу 05 §5/§6, D-079/D-080): Модуль продолжает крутить моторы — у него нет своего таймера, а команда телефона (#X/новый #M) физически не может дойти без провода. Робот останавливается только когда: USB переподключат и телефон снова получит возможность послать #X/новый кадр, либо сядет батарея модуля, либо кто-то вручную обесточит модуль. Это ожидаемое поведение текущего протокола, не баг — но обязательно к фиксации: если робот реально остановился сам без вмешательства, это расхождение с документацией и должно быть заведено как находка (спецификация могла разойтись с реализацией). Экран телефона при этом должен корректно показать «Модуль: не подключён» — то, что телефон корректно фиксирует потерю линка, не то же самое, что «моторы остановлены».

MB-075. Выдёргивание USB во время позиционного шага

Приоритет: P1 Предусловия: Робот выполняет robot.forward(500) (закрытый цикл на модуле). Шаги:

Ожидаемый результат: Модуль доводит шаг до конца и останавливается сам — позиционные шаги не зависят от связи с телефоном после того, как кадр #F уже отправлен (backstop 1, протокол 01 §1). На телефоне программа скорее всего зависнет в ожидании ответа/одометрии и в итоге упадёт с ошибкой связи (ConnectionError) — зафиксировать фактическое поведение и сверить с ожиданием.

MB-076. Обрыв Bluetooth во время выполнения программы — робот НЕ останавливается сам

Приоритет: P1 (ожидаемый результат — программа продолжает работать) Предусловия: Программа с движением выполняется, модуль подключён и работает штатно. Шаги:

Ожидаемый результат: По протоколу 01 §4.5: «телефон НЕ останавливает выполняющуюся программу из-за того, что IDE пропал». Робот продолжает выполнять программу как ни в чём не бывало — lease на модуле управляется программой напрямую через ProgramLink/MotionBridge, а не через BT-канал. При повторном подключении статус (status.run, status.run_since) должен показать, что программа всё это время выполнялась, и IDE должно «переподключиться» к её выводу. Вывод (out/err), случившийся во время разрыва, теряется и учитывается в dropped_out.

MB-077. Kill процесса приложения во время непрерывного движения

Приоритет: P1 (safety) Предусловия: Робот едет непрерывно. Шаги:

Ожидаемый результат: Как и в MB-074 — у мёртвого телефона lease больше некому продлевать, и это признанный владельцем продукта риск (D-080: «мёртвый телефон больше не останавливает непрерывное движение»). Робот может продолжать ехать до разряда батареи модуля. Позиционный шаг в процессе — довершится модулем самостоятельно (как в MB-075).

MB-078. Перезапуск приложения после kill — восстановление состояния

Приоритет: P2 Предусловия: Продолжение MB-077. Шаги:

Ожидаемый результат: Приложение поднимается с чистого состояния (никакой «памяти» о прерванном запуске — run_id был привязан к процессу). BT-сервер и попытка подключения к модулю стартуют заново, как при обычном первом запуске. Нет RECEIVE_BOOT_COMPLETED/авто-старта — только явное открытие приложения человеком (в манифесте такого ресивера нет, это ожидаемо).

MB-079. Экран погашен / Doze во время движения

Приоритет: P1 Предусловия: Робот едет непрерывно, программа продолжает работать. Шаги:

Ожидаемый результат: Foreground-сервис с FOREGROUND_SERVICE_CONNECTED_DEVICE должен пережить Doze/App Standby без остановки keepalive — так задумано (комментарий в BridgeService.kt про HW-24). Обязательно зафиксировать worstKeepaliveGapMs со служебного экрана до/после — если разрыв заметно превышает норму, это находка. Приложение не декларирует WakeLock — переживание Doze целиком полагается на foreground-сервис, стоит проверить на разных OEM (Xiaomi/Samsung с агрессивным энергосбережением — отдельные подозреваемые).

MB-080. Входящий звонок во время выполнения программы

Приоритет: P2 Предусловия: Программа выполняется, движение идёт. Шаги:

Ожидаемый результат: BT SPP-соединение и USB-связь с модулем не должны разрываться из-за звонка. Foreground-сервис должен продолжать работать. Если конкретный OEM всё же прерывает — зафиксировать модель телефона отдельно, это может быть особенность прошивки Android, а не баг приложения.

MB-081. Низкий заряд телефона

Приоритет: P3 Предусловия: Заряд телефона < 15%, включён режим энергосбережения ОС. Шаги:

Ожидаемый результат: Приложение продолжает работать (foreground-сервис имеет приоритет выше обычного фона), но энергосбережение ОС на некоторых OEM может душить фоновые потоки сильнее обычного — замерить worstKeepaliveGapMs, как в MB-079. bat_mv в телеметрии — это заряд модуля, не телефона (см. MB-056), не путать при анализе находки.

MB-082. Перезагрузка телефона

Приоритет: P2 Предусловия: Приложение работало, робот двигался. Шаги:

Ожидаемый результат: При перезагрузке процесс приложения умирает — то же самое, что MB-077/078 по последствиям для моторов. После загрузки Android приложение не запускается автоматически (нет boot-ресивера) — нужен ручной запуск. Это ожидаемое поведение текущей сборки.

MB-083. Повторное подключение ноутбука после любого разрыва — переподключение к живой программе

Приоритет: P1 Предусловия: Один из разрывов (MB-076) произошёл, программа всё ещё выполняется на телефоне. Шаги:

Ожидаемый результат: Новый hello/status содержит run:"ide:..." и корректный run_since — IDE должен суметь «подхватить» уже идущий запуск и его дальнейший вывод, а не считать, что ничего не происходит. Это прямое требование протокола 01 §4.5/§7.1.

MB-084. Отсоединение модуля во время старта позиционного шага (гонка)

Приоритет: P3 Предусловия: Способность выдернуть кабель ровно в момент отправки #F (по возможности воспроизвести несколько раз для повышения шанса поймать гонку). Шаги:

Ожидаемый результат: Приложение не падает и не зависает навсегда. Программа получает ошибку связи в разумный срок (ограничено hard-таймаутом шага, референс max(1000, estimate×2+1000) мс — секунды, не минуты). Повторный run после такой ошибки должен быть возможен.

MB-085. Экстренная остановка при активном соединении — таймбюджет

Приоритет: P2 Предусловия: Здоровое соединение BT + USB, робот едет. Шаги:

Ожидаемый результат: Специфицированные (не гарантированные без железа) бюджеты протокола 01 §5.5: телефон→модуль (#X на проводе) — p95 < 50 мс; телефон→#XACK — p95 < 200 мс. Полный путь клик в IDE → stop_ack в IDE — best effort, не является гейтом (BT может «залипать» на 1–2 с). Зафиксировать реальные цифры как первое настоящее измерение этого бюджета на живом железе.


L. Версии Android / разные телефоны

MB-086. Android 9 (minSdk 28) — самый старый поддерживаемый

Приоритет: P1 Предусловия: Физическое устройство с Android 9 (эмулятор годится только для UI и обновлений — Bluetooth и USB на нём не проверяются). Шаги:

Ожидаемый результат: Приложение работает. Рантайм-разрешения Bluetooth (API 31+) здесь не запрашиваются вовсе — они выдаются при установке; POST_NOTIFICATIONS тоже не запрашивается (нужен только с API 33). Самодиагностика первой строкой сообщает «Версия 9. Поддерживается с Android 9, но рекомендуется 12 и новее» жёлтым, и не считает это ошибкой.

MB-086b. 32-битный телефон (Android Go, armeabi-v7a)

Приоритет: P1 Предусловия: Телефон с 32-битной системой и 2–3 ГБ памяти, например Realme C30s (Android 12 Go). Приложение робота 0.8.21 или новее. Решение владельца продукта 15.10.2026: такие телефоны поддерживаются, APK несёт arm64-v8a и armeabi-v7a. Шаги:

Ожидаемый результат: Установка проходит без окна «Приложение несовместимо с вашим телефоном». Программа идёт, «Стоп» останавливает моторы сразу. Голос и камера работают. За 10 минут приложение не закрывается и не перезапускается. Если версия старше 0.8.21: установка отваливается с INSTALL_FAILED_NO_MATCHING_ABIS — в этих версиях был только arm64-v8a. Это не баг новой версии.

MB-086a. Android 9/10 — подтверждение установки при свёрнутом приложении

Приоритет: P1 Предусловия: Android 9 или 10, на портале есть версия новее установленной, разрешение «Установка неизвестных приложений» выдано. Шаги:

Ожидаемый результат: В шторке — уведомление «Обновление готово · Нажмите, чтобы установить»; по нажатию открывается системное окно установки. Android 10+ запрещает запуск окна из фона молча, без ошибки — уведомление и есть тот путь, по которому обновление всё-таки доходит. После успешной установки уведомление исчезает само.

MB-087. Android 12–14 — полный набор рантайм-разрешений

Приоритет: P1 Шаги:

Ожидаемый результат: На всех трёх — запрос BLUETOOTH_CONNECT/SCAN/ADVERTISE; на 13+ — дополнительно POST_NOTIFICATIONS. Поведение диалогов и порядок запроса не должны отличаться между минорными версиями одного мажорного релиза.

MB-088. Разные производители (OEM) — агрессивное энергосбережение

Приоритет: P1 Предусловия: Минимум два телефона разных производителей (например один «чистый» Android/Pixel, один Xiaomi/Samsung/Huawei с собственным менеджером энергосбережения). Шаги:

Ожидаемый результат: Записать worstKeepaliveGapMs для каждого производителя отдельно. Разница между устройствами — ожидаема и важна для решения по пилоту (2000 устройств, разные модели по школам) — не «баг одного телефона», а входные данные для риска.

MB-089. Планшет / нестандартное соотношение экрана

Приоритет: P3 Предусловия: Устройство с портретной ориентацией, но необычным соотношением сторон (или попытка повернуть экран, хотя RobotActivity заявлен как screenOrientation="portrait"). Шаги:

Ожидаемый результат: Ориентация принудительно портретная (объявлено в манифесте) — поворот не должен ломать вёрстку. На узких/широких экранах панели должны прокручиваться (ScrollView), не обрезаться.


Что не покрыто и почему