Тест-кейсы: приложение робота (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). Отмечено явно в каждом таком кейсе.
Как читать кейсы. «Шаги» и «Ожидаемый результат» написаны в том, что тестировщик видит глазами: экран приложения ученика, экран робота, поведение самого робота. Строка «Протокол» в конце кейса — техническая сверка для разработчика (какие сообщения ходят по проводу); при ручном прогоне её можно пропускать. Кейсы с пометкой [инструментальный] требуют отправки сырых сообщений скриптом — их выполняет разработчик, в ручной прогон они не входят.
Стенд тестера
- Android-телефон (реальный, не эмулятор), закреплённый на собранном роботе;
- модуль контроллера STM32 с подключёнными моторами (минимум 2, в идеале 4 порта, часть можно оставить пустой сознательно — см. раздел G);
- USB-OTG кабель телефон↔модуль;
- ноутбук со student-app (Windows и/или macOS), Bluetooth включён;
- отдельная батарея/аккумулятор питания модуля, независимая от телефона (модуль не питается от USB — по USB идут только данные,
bat_mvв статусе — это напряжение батареи МОДУЛЯ, не телефона); - собранный APK приложения робота (
app-debug.apkилиapp-release.apk); - техника безопасности: разделы про обрыв связи во время движения (K1–K5) намеренно воспроизводят ситуацию «робот может продолжать ехать, никто его не остановит программно». Тестируй их на столе с бортиками/ограждением, или будь готов(а) физически поймать робота или выключить питание модуля вручную.
Сводная таблица разделов
| Раздел | Название | Кейсов |
|---|---|---|
| A | Установка APK и первый запуск | 4 |
| B | Разрешения Android (BT, уведомления, USB) | 6 |
| C | Bluetooth-сопряжение с ноутбуком | 8 |
| D | Приём и запуск программы | 10 |
| E | Остановка и cancellation | 9 |
| F | USB-подключение модуля и permission-диалог | 7 |
| G | Lease модуля, мотор-команды, реальное движение | 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, файл, ссылка). Шаги:
- Установить APK обычным способом Android (открыть файл /
adb install). - Дождаться завершения установки.
Ожидаемый результат: Установка проходит без ошибок системы. Значок приложения появляется в списке приложений с названием из @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 — разрешения были отклонены. Шаги:
- Открыть настройки Android → Приложения → Рубрик (или как называется APK) → Разрешения.
- Включить Bluetooth-разрешения вручную.
- Вернуться в приложение.
Ожидаемый результат: В разумное время (после следующего цикла обновления экрана, не более пары секунд) BT-сервер поднимается — статус «Ноутбук» уходит из состояния ожидания разрешения, ноутбук теперь может увидеть телефон. Перезапуск приложения не должен требоваться.
MB-008. Запрос уведомлений на Android 13+
Приоритет: P2 Предусловия: Android 13 (API 33) и новее. Шаги:
- Открыть служебный экран (
MainActivity). - Нажать «Request notifications».
Ожидаемый результат: Системный диалог 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 (редкий случай; если такого телефона нет — кейс пропустить и пометить «нет устройства для проверки»). Шаги:
- Установить приложение на телефон без USB-host.
Ожидаемый результат: Play Store (или ручная установка) не должны позволить установку — фича объявлена android:required="true". При ручной установке (adb install) приложение должно хотя бы не падать при старте, USB-функциональность просто недоступна.
C. Bluetooth-сопряжение с ноутбуком
MB-011. Первое сопряжение (system pairing) телефона и ноутбука
Приоритет: P1 Предусловия: Оба устройства с включённым Bluetooth, ранее не сопряжены. Шаги:
- На ноутбуке открыть системные настройки Bluetooth, найти телефон робота, запросить сопряжение.
- Подтвердить код сопряжения (если запрашивается) на обеих сторонах.
Ожидаемый результат: Устройства сопряжены на уровне ОС. Это системная операция, отдельная от приложения — обычный Android-диалог сопряжения. После сопряжения телефон виден ноутбуку как Bluetooth-порт.
MB-012. Ноутбук подключается к уже сопряжённому телефону
Приоритет: P1 Предусловия: MB-011 выполнен, приложение робота запущено и BT-разрешения выданы. Шаги:
- В student-app нажать поиск/подключение робота.
- Выбрать сопряжённый телефон.
Ожидаемый результат: Устанавливается 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). Шаги:
- Выключить Bluetooth на ноутбуке.
- Подождать несколько секунд.
- Снова включить Bluetooth и подключиться повторно.
Ожидаемый результат: Экран робота отражает «Ноутбук: жду подключения» после разрыва, затем снова «подключён» после повторного соединения. Приложение не требует перезапуска.
MB-016. TCP-транспорт вместо Bluetooth (порт 50507)
Приоритет: P2 Предусловия: Телефон и ноутбук в одной Wi-Fi/LAN-сети класса. Шаги:
- В student-app выбрать подключение по сети (если BT недоступен/не подходит на Windows).
- Дождаться обнаружения телефона (DNS-SD
_rubrick-robot._tcpили UDP-широковещание на 50506).
Ожидаемый результат: Телефон находится в списке роботов по сети; подключение, запуск и остановка работают ровно так же, как по 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, а под списком строка «Занят другим ноутбуком или пультом: <имя робота>» (для нескольких — «Заняты другими ноутбуками или пультами: …»); знакомый COM-порт робота на Windows тоже не показывается; если других роботов нет, над строкой «Роботов не нашлось»;
- если робота видно только по Bluetooth (сеть не отвечает), карточка есть, нажатие на неё даёт «Rubrick занят — им управляет другой компьютер или пульт. Попроси отключиться или выбери другого робота.»;
- «Показать, что делать» при одних занятых роботах ведёт к случаю «<имя> уже подключён к другому ноутбуку или пульту», без шагов про запуск приложения на телефоне.
То же в обратную сторону: робот подключён по сети, второй ноутбук пробует Bluetooth. Когда первый ноутбук отключается, робот появляется в списке второго в течение ~4 с. Протокол: busy:true в ответе UDP-обнаружения; при подключении — error{code:"E_BUSY"} (§1.1 протокола 01). Решение владельца продукта 15.10.2026 (RUB-134): занятых не показываем, но называем.
MB-018. Discoverable-режим со служебного экрана
Приоритет: P3 Предусловия: Служебный экран открыт. Шаги:
- Нажать «Make discoverable».
Ожидаемый результат: Системный диалог 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-кабель модуля). Шаги:
- Запустить программу
print('hello')— без единого обращения кrobot.
Ожидаемый результат: Программа запускается и успешно завершается. В консоли — «hello». Отсутствие модуля не мешает: печатать можно и без него. Протокол: запуск не отклоняется при status.link == "none" (§4.2), run_finished{ok:true}.
MB-021. Программа обращается к роботу без модуля — понятная русская ошибка
Приоритет: P1 Предусловия: Робот подключён, модуль не подключён. Шаги:
- Запустить программу с
robot.forward(200).
Ожидаемый результат: Программа завершается с ошибкой, в консоли — понятное русское сообщение (про отсутствие модуля/подключения). Не молчаливое зависание, не разрыв связи с роботом.
MB-022. Запуск с некорректным lang [инструментальный]
Приоритет: P2 Шаги:
- Скриптом отправить
run{lang:"javascript", ...}(приложение ученика такое не присылает).
Ожидаемый результат / Протокол: error{code:"E_ARGS"}; код не пытается выполниться как Python (фолбэк «неизвестный enum» не применяется к диспетчеризующим полям).
MB-023. Второй запуск во время выполнения без force [инструментальный]
Приоритет: P1 Предусловия: Длинная программа выполняется (например robot.wait(30)). Из приложения ученика этот случай не воспроизвести: кнопка уже в состоянии «Стоп», а второй ноутбук получает отказ «занято» ещё на подключении (MB-014). Шаги:
- Скриптом отправить второй
runбезforce.
Ожидаемый результат / Протокол: 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 Шаги:
- Скриптом отправить
runсsource~200 КиБ.
Ожидаемый результат / Протокол: строка обрывается ридером телефона (лимит 256 КиБ на NDJSON-строку), приходит E_TOOLONG с re:0 — сопоставить с конкретным run нельзя, это задокументировано.
MB-026. Программа с опечаткой — ошибка показана человеку
Приоритет: P1 Шаги:
- Запустить программу с опечаткой:
robot.forwrd(200).
Ожидаемый результат: Программа завершается с ошибкой. В консоли — объяснение по-русски с номером строки. Приложение живо: опечатку можно исправить и нажать «Старт» снова. Протокол: err{s:"Traceback..."} (сырой traceback), затем run_finished{ok:false, reason:"error", exc:{type, msg, line, rubrick:false}}; русский текст — маппинг на стороне IDE.
MB-027. Ошибка самой библиотеки робота — текст уже русский
Приоритет: P2 Предусловия: Способ спровоцировать ошибку библиотеки: например, обратиться к мотору на порту, где мотор не подключён. Шаги:
- Запустить программу, дёргающую пустой порт (например
robot.motor('A').run_angle(360, 50)— буква разъёма, число4значит то же — при пустом разъёме A).
Ожидаемый результат: В консоли — готовое русское сообщение о том, что случилось, как есть, без «перевода» и без сырого трейсбека в лицо ребёнку. Протокол: exc.rubrick == true, exc.msg — готовый русский текст; IDE показывает его без таблицы маппинга.
MB-028. Большой объём вывода — часть печати может потеряться, ошибки — никогда
Приоритет: P3 Шаги:
- Запустить программу с тесным циклом из ~500
print().
Ожидаемый результат: Программа завершается успешно. На медленном 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)). Шаги:
- Через 1–2 секунды после старта нажать «■ Стоп».
Ожидаемый результат: Робот физически перестаёт ехать сразу — раньше, чем в приложении появится отметка о завершении. Затем запуск помечается остановленным, кнопка возвращается в «Старт». Протокол: #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+ секунд назад (или не запускалась). Из приложения ученика этот случай не воспроизвести — кнопки «Стоп» без активного запуска нет. Шаги:
- Скриптом отправить
stop{run_id:"ide:какой-то-старый"}.
Ожидаемый результат / Протокол: 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 или новее. Шаги:
- Запустить, дождаться входа в
sleep, нажать «Стоп». - Сразу запустить следующую программу.
Ожидаемый результат: робот тормозит немедленно; запуск помечается остановленным за доли секунды (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.
Ожидаемый результат: Экран показывает «Программа зависла — перезапустите приложение» (красным, грустное лицо). Новый run получает E_RUNNING навсегда, до перезапуска приложения — это специфицированное, а не случайное поведение (§5.4 слой c).
MB-035. brake_confirmed:false — модуль не подтвердил тормоз
Приоритет: P1 Предусловия: Способ имитировать неответ модуля на #X (например временно отсоединить USB сразу после отправки stop, до получения #XACK — тайминг подобрать экспериментально). Шаги:
- Спровоцировать ситуацию, где
#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 Предусловия: Телефон запущен, приложение работает, модуль ранее не подключался к этому телефону. Шаги:
- Подключить модуль через OTG-кабель.
Ожидаемый результат: ATTACHED-широковещание доставляется приложению (в манифесте объявлен device_filter.xml). Системный диалог Android «Разрешить приложению Рубрик доступ к USB устройству?» появляется автоматически. Экран робота на это время показывает «Модуль: нужно разрешение на USB» (оранжевым).
MB-039. Подтверждение USB-разрешения
Приоритет: P1 Предусловия: MB-038, диалог на экране. Шаги:
- Нажать «OK»/«Разрешить», опционально — отметить «запомнить для этого устройства».
Ожидаемый результат: Соединение поднимается, экран переходит «Модуль: соединяюсь» → «Модуль: подключена, прошивка X.Y.Z». Служебный экран показывает Board: <serial/proto> после первого успешного #GP/опроса.
MB-040. Отказ в USB-разрешении
Приоритет: P2 Шаги:
- Повторить MB-038, на диалоге нажать «Запретить».
Ожидаемый результат: Приложение не падает. Экран остаётся в состоянии «нужно разрешение на USB». Отключение и повторное подключение кабеля снова показывает диалог (Android не запоминает отказ так же жёстко, как разрешение).
MB-041. Повторное подключение — разрешение не переспрашивается
Приоритет: P2 Предусловия: MB-039 выполнен и было отмечено «запомнить». Шаги:
- Отсоединить кабель.
- Подключить кабель снова.
Ожидаемый результат: Диалог не появляется повторно (usbManager.hasPermission() уже возвращает true для этого устройства). Соединение поднимается автоматически.
MB-042. Подключение модуля через служебный экран (ручная кнопка)
Приоритет: P3 Предусловия: Модуль физически подключён, но авто-detect почему-то не сработал. Шаги:
- Открыть служебный экран.
- Нажать «Connect USB».
Ожидаемый результат: Та же процедура запроса разрешения/подключения запускается вручную, результат идентичен MB-038/039.
MB-043. Опрос присутствия моторов (#GP) отражается корректно
Приоритет: P2 Предусловия: Модуль подключён, к нему подсоединены только 2 из 4 моторов. Шаги:
- Дождаться нескольких циклов опроса
#GP(опрашивается периодически, не один раз). - Отключить один из подключённых моторов «на горячую» (если конструкция позволяет безопасно).
Ожидаемый результат: Присутствие моторов переопрашивается периодически, не только при старте связи — статус должен отразить изменение состава моторов без переподключения USB.
MB-044. Ручное отключение модуля со служебного экрана
Приоритет: P2 Предусловия: Модуль подключён. Шаги:
- Нажать «Disconnect» на служебном экране.
Ожидаемый результат: Связь с модулем корректно закрывается, экран робота возвращается в «Модуль: не подключена». Если в этот момент выполнялась программа с движением — моторы должны быть остановлены (emergencyStop() вызывается при явном disconnectLink()).
G. Lease модуля, мотор-команды, реальное движение
MB-045. robot.forward() — позиционный шаг, робот проезжает и сам останавливается
Приоритет: P1 Предусловия: Модуль с двумя моторами (левый/правый) подключён и откалиброван. Шаги:
- Запустить
robot.forward(300)(мм).
Ожидаемый результат: Робот физически проезжает вперёд и сам останавливается, без участия телефона в моменте окончания — это закрытый цикл на модуле (#F). Завершение определяется по неподвижности одометрии (2 последовательных опроса #GD каждые 150 мс без изменения, но только после того как одометрия хоть раз изменилась) — не по показаниям #GE (те возвращают на реальной модуле константу и НЕ являются признаком завершения шага, см. протокол 05 §3.1).
MB-046. robot.turn_left() / robot.turn_right() — поворот на месте
Приоритет: P1 Шаги:
- Запустить
robot.turn_left(90).
Ожидаемый результат: Робот поворачивается примерно на заданный угол (калибровочные константы degPerMm/wheelDegPerRobotDeg — ориентировочные значения для колёс Ø43.2 мм, точность зависит от конкретной сборки) и останавливается сам.
MB-047. Позиционный шаг на незаполненном порту — таймаут без зависания
Приоритет: P2 Предусловия: Один из портов моторов физически пуст (не подключён). Шаги:
- Отправить позиционную команду на порт без мотора (через
raw/служебный экран, либо через именованный мотор в программе, если API это позволяет).
Ожидаемый результат: Шаг завершается по no-motion-abort таймауту (референс: 2000 мс), не по полному hard-таймауту — иначе экран «зависает» на десяток секунд, что для ребёнка читается как сломанный робот (задокументированный в протоколе 05 §3.2 реальный замер — 9.3 с при наивной реализации).
MB-048. Непрерывное движение — robot.drive(speed)
Приоритет: P1 Шаги:
- Запустить программу с
robot.drive(60)безwait/stopсразу после.
Ожидаемый результат: Робот едет непрерывно. Пока программа жива и продолжает вызывать методы rubrick.py (в т.ч. robot.wait()), lease на модуль продолжает продлеваться (интервал keepalive-переотправки — 150 мс, референс), робот не останавливается сам по себе.
MB-049. Непрерывное движение останавливается через robot.stop()
Приоритет: P1 Шаги:
- Продолжение MB-048: вызвать
robot.stop().
Ожидаемый результат: Робот немедленно тормозит (#M0,0,0,0;) — port-independent торможение всех четырёх каналов разом.
MB-050. Программа зависает во время непрерывного движения БЕЗ stop от IDE — 800 мс lease
Приоритет: P1 (safety) Предусловия: Программа: robot.drive(60), затем зависающий, не через rubrick.py, код — например тесный while True: pass без единого вызова robot.*. Шаги:
- Запустить, дать роботу поехать, дождаться зависания.
- Замерить, сколько робот едет после последнего вызова
rubrick.py.
Ожидаемый результат: Робот останавливается сам — жёстким тормозом (не выбегом) — примерно через 800 мс (референсное значение lease, протокол 05 §5) после последней команды/продления. Сама программа не завершается (никакого run_finished до явного stop) — «зависшую» программу от «медленной» отличить нельзя, это по документации. Это тест-вектор №11 протокола 01 §12 и он должен проходить на реальном железе, а не только в юнит-тестах MotionTests.kt.
MB-051. Балансировочный триммер (если экспонирован в UI/API)
Приоритет: P3 Предусловия: Робот заметно уводит в сторону при robot.drive(). Шаги:
- Если есть способ задать trim (−0.5..0.5) — применить ненулевое значение.
Ожидаемый результат: Триммер влияет только на непрерывное движение (#M), не на позиционные шаги (#F — они закрытый цикл на модуле и в трим не нуждаются). Если UI/API для этого пока не существует — пометить кейс как «не реализовано», не как провал.
MB-052. Направление и маппинг портов — «лево» и «право» настраиваемы
Приоритет: P3 Предусловия: Заведомо неправильно подключены моторы (левый в разъём правого и наоборот). Шаги:
- Запустить
robot.forward()— убедиться, что робот едет назад/криво. - Если в приложении есть настройка маппинга портов/знака «вперёд» — поменять её.
Ожидаемый результат: После смены маппинга forward снова означает физически вперёд без переписывания программы ученика — по протоколу это конфигурация телефона, а не константа кода. Если настройка отсутствует в текущей сборке UI — пометить «не реализовано (если реализовано)».
MB-053. Одновременная попытка двух источников управлять моторами
Приоритет: P2 Предусловия: Программа выполняется и двигает робота. Шаги:
- Параллельно (если есть такая возможность в служебном экране — джойстик/кнопки) попытаться выдать ручную команду на моторы.
Ожидаемый результат: Ровно один источник владеет lease в моменте — «одиночный писатель» (§1 протокола 05). Конкурирующий источник не проникает на порт напрямую и не портит кадр — арбитраж на уровне MotionController/MotionArbiter, не на уровне порта.
H. Телеметрия обратно на ноутбук
MB-054. Показания робота идут и без запущенной программы
Приоритет: P2 Предусловия: Модуль подключён, программа не запущена. Шаги:
- На странице робота в приложении ученика наблюдать показания (моторы, батарея модуля).
Ожидаемый результат: Показания обновляются и без запущенной программы — они описывают состояние модуля, а не программы. Протокол: tel/status идут независимо от запуска (протокол 01 §4.1 п.4).
MB-055. Частота телеметрии регулируется [инструментальный]
Приоритет: P2 Шаги:
- Скриптом отправить
set_telemetry{hz:0}, затемset_telemetry{hz:10}.
Ожидаемый результат / Протокол: при 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 Предусловия: Активное соединение с идущей телеметрией. Шаги:
- Отключить BT на ноутбуке резко (не штатным способом).
- Замерить, через сколько времени student-app показывает потерю сигнала.
Ожидаемый результат: Индикатор не должен схлопываться раньше ~800 мс простоя (порог из OVERVIEW.md §4) — слишком ранний срабатывающий индикатор будет ложно мигать на нормальных паузах в потоке.
MB-058. Дропы телеметрии под перегрузкой канала считаются честно
Приоритет: P3 Предусловия: Программа с большим потоком print() идёт одновременно с телеметрией (см. MB-028). Шаги:
- Понаблюдать за
status.droppedво время интенсивного вывода.
Ожидаемый результат: Если данные реально теряются, счётчик потерь в статусе/журнале растёт, а не стоит на нуле. Bluetooth Classic документированно может «залипать» на 1–2 с при перегрузке (EXP-02 §6) — потери в этот момент нормальны, скрытые потери — нет. Протокол: status.dropped отражает реальный backpressure.
I. Экран состояния робота
MB-059. Лицо робота меняется по состоянию
Приоритет: P2 Предусловия: Экран робота открыт. Шаги:
- Провести через состояния: нет BT-сервера (
sleepy) → ожидание ноутбука (neutral) → программа выполняется (happy) → программа зависла/abandoned (sad).
Ожидаемый результат: Лицо (RubrickFaceView) визуально меняется в соответствии с состоянием, без задержки более одного цикла обновления экрана (референс — 500 мс, REFRESH_MS).
MB-060. Статусная панель «Ноутбук»
Приоритет: P2 Шаги:
- Пройти по состояниям: не подключён → подключён.
Ожидаемый результат: Текст: «Ноутбук: жду подключения» (серый/приглушённый) → «Ноутбук: подключён» (зелёный). Никогда не показывает голый технический код состояния.
MB-061. Статусная панель «Модуль»
Приоритет: P2 Шаги:
- Пройти по состояниям: не подключена → нужно разрешение на USB → соединяюсь → подключена (с версией прошивки).
Ожидаемый результат: Каждое состояние из refresh() отражено отдельной надписью и своим цветом; при подключённом модуле видна версия прошивки модуля (board.fw), а не версия приложения.
MB-062. Статусная панель «Программа»
Приоритет: P1 Шаги:
- Пройти по состояниям: не запущена → выполняется → зависла (abandoned).
Ожидаемый результат: «Программа: не запущена» (серый) → «Программа: выполняется» (жёлтый) → «Программа зависла — перезапустите приложение» (красный, при 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-файл через системный выбор документа (ACTION_OPEN_DOCUMENT).
Ожидаемый результат: Файл открывается и обрабатывается корректно именно через внутренний пикер — важно не пытаться открыть .rbu через файловый менеджер напрямую (внешний intent-filter по расширению *.rbu заведомо не сработает: провайдер Downloads отдаёт content:// URI без расширения в пути — задокументированная ловушка EXP-11).
MB-071. Разрешение «установка неизвестных приложений»
Приоритет: P2 Предусловия: Разрешение canRequestPackageInstalls() не выдано. Шаги:
- Попытаться установить проверенное обновление.
Ожидаемый результат: Понятное русское сообщение с указанием, что открыть в настройках («Установка неизвестных приложений» → разрешить РУБРИКу), установка не проходит молча.
MB-072. Ответ пакетного установщика доходит до диалога подтверждения системы
Приоритет: P2 Предусловия: Обновление скачано и подписано корректно. Шаги:
- Дождаться
STATUS_PENDING_USER_ACTION.
Ожидаемый результат: Системный диалог подтверждения установки появляется автоматически (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()). Шаги:
- Физически выдернуть USB-кабель между телефоном и модулем во время движения.
- Наблюдать за роботом не менее 10 секунд, готовясь остановить его вручную.
Ожидаемый результат (по протоколу 05 §5/§6, D-079/D-080): Модуль продолжает крутить моторы — у него нет своего таймера, а команда телефона (#X/новый #M) физически не может дойти без провода. Робот останавливается только когда: USB переподключат и телефон снова получит возможность послать #X/новый кадр, либо сядет батарея модуля, либо кто-то вручную обесточит модуль. Это ожидаемое поведение текущего протокола, не баг — но обязательно к фиксации: если робот реально остановился сам без вмешательства, это расхождение с документацией и должно быть заведено как находка (спецификация могла разойтись с реализацией). Экран телефона при этом должен корректно показать «Модуль: не подключён» — то, что телефон корректно фиксирует потерю линка, не то же самое, что «моторы остановлены».
MB-075. Выдёргивание USB во время позиционного шага
Приоритет: P1 Предусловия: Робот выполняет robot.forward(500) (закрытый цикл на модуле). Шаги:
- Выдернуть USB в середине движения.
Ожидаемый результат: Модуль доводит шаг до конца и останавливается сам — позиционные шаги не зависят от связи с телефоном после того, как кадр #F уже отправлен (backstop 1, протокол 01 §1). На телефоне программа скорее всего зависнет в ожидании ответа/одометрии и в итоге упадёт с ошибкой связи (ConnectionError) — зафиксировать фактическое поведение и сверить с ожиданием.
MB-076. Обрыв Bluetooth во время выполнения программы — робот НЕ останавливается сам
Приоритет: P1 (ожидаемый результат — программа продолжает работать) Предусловия: Программа с движением выполняется, модуль подключён и работает штатно. Шаги:
- Выключить Bluetooth на ноутбуке (или унести ноутбук за пределы связи) во время движения.
Ожидаемый результат: По протоколу 01 §4.5: «телефон НЕ останавливает выполняющуюся программу из-за того, что IDE пропал». Робот продолжает выполнять программу как ни в чём не бывало — lease на модуле управляется программой напрямую через ProgramLink/MotionBridge, а не через BT-канал. При повторном подключении статус (status.run, status.run_since) должен показать, что программа всё это время выполнялась, и IDE должно «переподключиться» к её выводу. Вывод (out/err), случившийся во время разрыва, теряется и учитывается в dropped_out.
MB-077. Kill процесса приложения во время непрерывного движения
Приоритет: P1 (safety) Предусловия: Робот едет непрерывно. Шаги:
- Через «Настройки Android → Приложения → Рубрик → Принудительная остановка» (или через диспетчер задач) убить процесс приложения во время движения.
- Наблюдать за роботом.
Ожидаемый результат: Как и в MB-074 — у мёртвого телефона lease больше некому продлевать, и это признанный владельцем продукта риск (D-080: «мёртвый телефон больше не останавливает непрерывное движение»). Робот может продолжать ехать до разряда батареи модуля. Позиционный шаг в процессе — довершится модулем самостоятельно (как в MB-075).
MB-078. Перезапуск приложения после kill — восстановление состояния
Приоритет: P2 Предусловия: Продолжение MB-077. Шаги:
- Заново открыть приложение.
Ожидаемый результат: Приложение поднимается с чистого состояния (никакой «памяти» о прерванном запуске — run_id был привязан к процессу). BT-сервер и попытка подключения к модулю стартуют заново, как при обычном первом запуске. Нет RECEIVE_BOOT_COMPLETED/авто-старта — только явное открытие приложения человеком (в манифесте такого ресивера нет, это ожидаемо).
MB-079. Экран погашен / Doze во время движения
Приоритет: P1 Предусловия: Робот едет непрерывно, программа продолжает работать. Шаги:
- Заблокировать экран телефона (кнопка питания) на 1–2 минуты, не выключая приложение.
- Разблокировать и проверить состояние.
Ожидаемый результат: 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) произошёл, программа всё ещё выполняется на телефоне. Шаги:
- Восстановить BT-соединение с ноутбука.
Ожидаемый результат: Новый hello/status содержит run:"ide:..." и корректный run_since — IDE должен суметь «подхватить» уже идущий запуск и его дальнейший вывод, а не считать, что ничего не происходит. Это прямое требование протокола 01 §4.5/§7.1.
MB-084. Отсоединение модуля во время старта позиционного шага (гонка)
Приоритет: P3 Предусловия: Способность выдернуть кабель ровно в момент отправки #F (по возможности воспроизвести несколько раз для повышения шанса поймать гонку). Шаги:
- Запустить
robot.forward(). - Выдернуть кабель максимально быстро после старта.
Ожидаемый результат: Приложение не падает и не зависает навсегда. Программа получает ошибку связи в разумный срок (ограничено hard-таймаутом шага, референс max(1000, estimate×2+1000) мс — секунды, не минуты). Повторный run после такой ошибки должен быть возможен.
MB-085. Экстренная остановка при активном соединении — таймбюджет
Приоритет: P2 Предусловия: Здоровое соединение BT + USB, робот едет. Шаги:
- Замерить время от нажатия «Стоп» в student-app до фактической остановки моторов (секундомер/ видео с двух точек — экран и робот).
Ожидаемый результат: Специфицированные (не гарантированные без железа) бюджеты протокола 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 на нём не проверяются). Шаги:
- Установить и пройти минимум разделы A–G на этом устройстве.
Ожидаемый результат: Приложение работает. Рантайм-разрешения 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. Шаги:
- Скачать приложение робота со страницы «Загрузки» портала и установить.
- Подключиться к роботу из приложения ученика по Bluetooth.
- Запустить программу с моторами. Во время движения нажать «Стоп».
- Проверить голосовые команды и камеру (лицо, распознавание).
- Оставить приложение работать 10 минут с запущенной программой.
Ожидаемый результат: Установка проходит без окна «Приложение несовместимо с вашим телефоном». Программа идёт, «Стоп» останавливает моторы сразу. Голос и камера работают. За 10 минут приложение не закрывается и не перезапускается. Если версия старше 0.8.21: установка отваливается с INSTALL_FAILED_NO_MATCHING_ABIS — в этих версиях был только arm64-v8a. Это не баг новой версии.
MB-086a. Android 9/10 — подтверждение установки при свёрнутом приложении
Приоритет: P1 Предусловия: Android 9 или 10, на портале есть версия новее установленной, разрешение «Установка неизвестных приложений» выдано. Шаги:
- Нажать «Установить» в плашке обновления.
- Сразу свернуть приложение (Home) и погасить экран.
- Через минуту разблокировать телефон и открыть шторку уведомлений.
Ожидаемый результат: В шторке — уведомление «Обновление готово · Нажмите, чтобы установить»; по нажатию открывается системное окно установки. Android 10+ запрещает запуск окна из фона молча, без ошибки — уведомление и есть тот путь, по которому обновление всё-таки доходит. После успешной установки уведомление исчезает само.
MB-087. Android 12–14 — полный набор рантайм-разрешений
Приоритет: P1 Шаги:
- Повторить установку и разделы A–C на устройстве с Android 12, 13 и 14 (по возможности — три разных устройства/версии).
Ожидаемый результат: На всех трёх — запрос BLUETOOTH_CONNECT/SCAN/ADVERTISE; на 13+ — дополнительно POST_NOTIFICATIONS. Поведение диалогов и порядок запроса не должны отличаться между минорными версиями одного мажорного релиза.
MB-088. Разные производители (OEM) — агрессивное энергосбережение
Приоритет: P1 Предусловия: Минимум два телефона разных производителей (например один «чистый» Android/Pixel, один Xiaomi/Samsung/Huawei с собственным менеджером энергосбережения). Шаги:
- Повторить MB-079 (Doze/экран погашен) на каждом устройстве.
Ожидаемый результат: Записать worstKeepaliveGapMs для каждого производителя отдельно. Разница между устройствами — ожидаема и важна для решения по пилоту (2000 устройств, разные модели по школам) — не «баг одного телефона», а входные данные для риска.
MB-089. Планшет / нестандартное соотношение экрана
Приоритет: P3 Предусловия: Устройство с портретной ориентацией, но необычным соотношением сторон (или попытка повернуть экран, хотя RobotActivity заявлен как screenOrientation="portrait"). Шаги:
- Открыть экран робота на таком устройстве.
- Попробовать повернуть телефон.
Ожидаемый результат: Ориентация принудительно портретная (объявлено в манифесте) — поворот не должен ломать вёрстку. На узких/широких экранах панели должны прокручиваться (ScrollView), не обрезаться.
Что не покрыто и почему
- Реальная прошивка модуля через приложение — функциональность не реализована и намеренно (README/
flashBoardReport()): физической прошивки нет, кнопка честно об этом сообщает. Раздел J проверяет только то, что кнопка не врёт об успехе (MB-063), а не саму прошивку. - «Запустить последнюю программу» с телефона — упомянута в
PLAN.md§2 п.7 и в протоколе 01 (run_idс префиксомphone:) как часть архитектуры, но кнопки для неё в текущейRobotActivityнет — в тест-кейсы не включена, чтобы не тестировать несуществующий UI. Если появится в билде — дополнить раздел D. - Регулировка балансировочного трима и маппинга портов из UI — протокол 05 §2.1/§2.3 описывает эти параметры как конфигурацию телефона «per-robot», но явного экрана настроек для них в просмотренных исходниках не найдено — соответствующие кейсы (MB-051, MB-052) помечены «если реализовано».
- Джойстик/ручное управление с самого телефона — протокол 05 §5 упоминает джойстик как один из возможных источников lease в общем виде, конкретной реализации в
RobotActivity/MainActivityне найдено — MB-053 сформулирован осторожно, без предположений о конкретном UI. - Хард-килл интерпретатора через отдельный процесс — прямо задокументирован как не входящий в v26 (протокол 01 §14, non-normative appendix). Не тестируется — это осознанно отложенная функциональность, а не баг MVP.
- Нагрузочное/долговременное тестирование (endurance) — в
experiments/exp-02-android-bt-bridge/ scripts/run-endurance.shесть отдельный скрипт для многочасовых прогонов; в этот документ ручные многочасовые кейсы не включены как непрактичные для ручного прохождения тестировщиком — предложить отдельным авто-прогоном, не вручную. - Реальные значения таймингов из протокола 01 §5.5 помечены в документе как «специфицированные бюджеты, не измерения» — раздел K (MB-085) первый раз просит реально их замерить; до этого их корректность не подтверждена ни на каком железе (в репозитории явно записано: физического стенда не было).