Add bounded X4 Bluetooth wake and record Android recovery proof
This commit is contained in:
@@ -1044,3 +1044,218 @@ MISSIONCOR-77 синхронизирован через официальный O
|
||||
прежние блоки/checker, добавлены BLE01–03/B12/POWER03 и исправлен устаревший
|
||||
текст о невозможности SDK reopen. Предыдущие transport timeouts преодолены;
|
||||
автоматический wake/replug/preview checker остаётся открытым.
|
||||
|
||||
### BLE04 — чтение настроек при включённой X4 вне Android, 10.09.2026
|
||||
|
||||
Владелец сообщил: Power-off Charging установлен в «Только зарядка», доступны
|
||||
«Зарядка и включение» и «Зарядка и запись»; Wi-Fi Always On. Он нажал Bluetooth
|
||||
Wakeup, но конечное значение переключателя ещё уточняется. После USB out/in
|
||||
на дисплее осталось изображение с линз, Android не появился. Точное время
|
||||
нажатий неизвестно; это новый owner report, не автоматически измеренный цикл.
|
||||
|
||||
BLE04 intent: текущий diagnostic artifact a919d293c292935cfd636f0e, SHA-256
|
||||
bf1325b882d696314507f98fc627235ec579fa7d910035fb6155ce51f939b698,
|
||||
входящий в исходники model0.1.3-4. На существующей Ubuntu после read-only Core
|
||||
snapshot выполнить одну fresh20s scan/inspection для BLE03 кандидата с
|
||||
--read-options и прежним expected device ID. USB ownership guard запрещает
|
||||
Connect/GET при возвращении SDK USB. Иначе стандартные DIS reads и временная
|
||||
BE82 CCCD subscription, затем только GetOptions8: полные serial15/model48,
|
||||
после exact hash match95/97. Любая ошибка/timeout прекращает опыт без retries.
|
||||
Нет setters, Pair, off/on, записи на SD, изменения USB, служб или установки.
|
||||
Собственные notify/connection/filter снимаются в cleanup. Полный исходный USB
|
||||
serial, BLE адрес, ответы и frame bytes сохраняются только в private evidence.
|
||||
|
||||
Перед запуском владелец явно подтвердил Bluetooth Wakeup включён; затем
|
||||
повторно подтвердил Power-off Charging = «Только зарядка». Никакого изменения
|
||||
этого второго параметра ещё не было. Включение Wakeup выполнено владельцем на
|
||||
самой камере; это отдельная camera-side предпосылка, не автоматическая настройка
|
||||
из installer. Её программное чтение/установка пока не квалифицированы.
|
||||
|
||||
Core snapshot09:11:57.852449 UTC, Mac monotonic451997.875703666:
|
||||
Core operational, Rover006 online, D455 streaming, X4 offline/idle.
|
||||
Private before.json SHA-256
|
||||
26510f559498e7d02f86b578e0d4c09e51df8ceb931432f768e80af01941198c.
|
||||
BLE04 выполнен09:12:33.540701–09:12:59.133004 UTC,
|
||||
Ubuntu monotonic329861.421788818–329887.014153528. Один fresh candidate,
|
||||
GATT Connect/services resolution прошли; BE80 с BE81 read/write, BE82 notify,
|
||||
BE83 read. DIS180A отсутствует. Один GetOptions8 с15/48 отправлен; пришёл
|
||||
7-byte packet07000000050000 (total size7,type5). Текущий strict message reader
|
||||
не допускает такой тип и завершил опыт Unknown packet framing, returncode1.
|
||||
Это не подтверждение identity, не Options95/97 response и не доказательство
|
||||
отказа чтения на firmware. Полную семантику type5/handshake нужно установить
|
||||
до изменения reader или повторного опыта; ответов/команд наугад нет.
|
||||
Vendor read-request writes1, CCCD subscriptions1, Pair0, setters0,
|
||||
cleanup errors0, собственные notification/connection закрыты. Опыт автоматически
|
||||
не повторялся. Private report.json SHA-256
|
||||
c422244ff00a77cd6b6a08aaf84581269e231cc2d0c2baa36b224dcca9707186.
|
||||
|
||||
POWER04 intent: отдельный ручной тест штатного Power-off Charging.
|
||||
Первичный источник, прочитан10.09:
|
||||
https://onlinemanual.insta360.com/x5/en-us/troubleshooting/cannot-turn-on/auto-power-on
|
||||
Scenario2 явно применим к X4/X5: Charge and power on включает камеру при
|
||||
подключении питания; Android mode эта инструкция не гарантирует.
|
||||
Владелец отсоединяет USB, выбирает «Зарядка и включение», штатно выключает
|
||||
X4, затем подключает прежний USB к тому же Mini без нажатия Power. Результат
|
||||
экрана/Android/SDK наблюдается отдельно. SD autostart «Зарядка и запись» не
|
||||
используется. Если проверка не проходит, параметр можно вернуть в прежнее
|
||||
«Только зарядка»; обязательность ручной настройки фиксируется явно. Ubuntu,
|
||||
installer, порты, службы и SDK не меняются. Само намерение не является
|
||||
подтверждением выполнения владельцем или приёмкой автономности.
|
||||
|
||||
POWER04 observer: тот же a919d293c292935cfd636f0e, один --scan-seconds60 без
|
||||
--address/--read-options. Только discovery/sysfs; никакого Connect или vendor
|
||||
write. Запрос владельцу отправлен до старта observer, поэтому первое физическое
|
||||
событие может предшествовать окну; задержка от нажатия не вычисляется без
|
||||
совпавшей временной опоры. Отсутствие ответа владельца не считается тестом.
|
||||
|
||||
POWER04 observer complete09:15:09.540660–09:16:09.711625 UTC,
|
||||
Ubuntu monotonic330017.421746509–330077.592716198,58 USB samples,
|
||||
vendor writes0/Pair0/CCCD0/cleanup errors0. В09:15:52.130612 появился Mass Storage
|
||||
070a:4026/speed5000,09:15:57.238720 отсутствует,09:15:59.280009 появился SDK
|
||||
2e1a:0002/speed5000. Private observer.json SHA-256
|
||||
b8f58d9c5d356082514ab693e53b0fcb26ee9d162384cc449441784dfa38f401.
|
||||
Core readback09:16:46.738402 UTC, Mac monotonic452286.762178125: прежняя X4
|
||||
online/idle/preview0/recording0, новая session от09:15:59.915770 UTC,
|
||||
Rover006 online, D455 streaming с прежней session, Core operational.
|
||||
Private after.json SHA-256
|
||||
457bbec31bf2324161a7ee42a603ef0784c5a8504b1a632126e4eccff2f308fa.
|
||||
Это измеренный возврат USB/SDK; владелец ещё не подтвердил выполнение всей
|
||||
последовательности и отсутствие нажатия Power после подачи USB. До ответа
|
||||
результат не отмечается как доказанное Charge and power on → Android.
|
||||
Все собственные observers завершены; никакого BLE wake/off команды не было.
|
||||
|
||||
POWER04 owner confirmation получен после observer: владелец сообщил фактический
|
||||
порядок camera off → USB out → USB in, после подключения камера сама включилась
|
||||
в Android и приложение нашло её. Это подтверждает один цикл запуска от подачи
|
||||
USB в контексте предложенного Charge and power on, без отдельного включения
|
||||
Power после подключения. Порядок отличается от первоначальной инструкции
|
||||
(USB out до off), поэтому в результате фиксируется именно сообщённый порядок.
|
||||
Программное чтение выбранного camera-side charging flag не выполнялось.
|
||||
Warm replug включённой камеры и самостоятельное BLE off/on ещё не приняты.
|
||||
|
||||
REPLUG04 intent: после POWER04 один warm USB out/in без выключения камеры
|
||||
и без Power/выбора режима. Предполагается сохранённая настройка Charge and
|
||||
power on; если владелец не менял её, это должно быть отражено в результате.
|
||||
Тот же versioned a919 diagnostic, один scan-only60s; USB/SDK evidence отдельно
|
||||
от owner report, никаких BLE Connect/CCCD/vendor commands. Существующий
|
||||
X4 preview0/recording0; D455/Node/Core остаются рабочими. Успех POWER04 не
|
||||
закрывает этот другой сценарий и не обосновывает автоматический SD start.
|
||||
|
||||
REPLUG04 complete09:19:24.624291–09:20:24.792218 UTC,
|
||||
Ubuntu monotonic330272.505379028–330332.673308194. В начале SDK2e1a:0002/5000,
|
||||
09:19:27.846367 исчезла; до конца окна не вернулась. Владелец подтвердил
|
||||
USB out/in без camera off: карточка пропала, камера не перешла в Android.
|
||||
Warm replug не прошёл. Vendor writes0/Pair0/CCCD0/cleanup0; private observer
|
||||
SHA-256aeec9c36ebebe98b377286b82ea36381f60c283886fe290754761cb155525b49.
|
||||
|
||||
B13 / BLE05 intent: устраняется конкретная ошибка reader из BLE04.
|
||||
В pinned SDK статически проверены CameraSession::Start0x5e9dc0 (регистрация
|
||||
callback type5 в0x5e9e61) и callback0x5e94e0→OnHeartbeatHandler. Это доказывает
|
||||
класс полученного пустого7-byte type5; SDK не загружался и не исполнялся.
|
||||
Private disassembly SHA-256
|
||||
07c3a7223a4197cb67e740352a4f220c1a08512b17ba7040a4052a423258bcae.
|
||||
Reader принимает только exact07000000050000, считает максимум64 heartbeat,
|
||||
не считает их ответами Options и не посылает никаких ACK/новых команд.
|
||||
Добавлены2 synthetic tests split/combined/invalid/control flood. Ошибочные
|
||||
или неизвестные packets по-прежнему завершают опыт. Новый model0.1.3-5
|
||||
содержит правку; установленная0.1.3-3 и Node bundle не меняются.
|
||||
Diagnostic c67351f42c6f4d6185ce4083,24184 bytes,
|
||||
SHA-256d10d929539f30872c0af1e0b4d1752b07b69afa43dcf02932f818df67cd4c51a,
|
||||
source SHA-25627e9dc92974225bf3d00b506298a4fafe56fadddd92d6d4a75f78a5681cc3f46.
|
||||
Package build artifact2961928f986008d3a7980c74,58332445 bytes,
|
||||
SHA-256bf5bec58b924d0d92c5ef5a79c41095897e81c12ecf3cec4f38c3f139316aad4.
|
||||
Сначала штатная package qualification на существующей Ubuntu (1GiB/180CPU s/
|
||||
nice10, без SDK/device access/install). После успешной квалификации BLE05 —
|
||||
один новый read-options опыт при отсутствии USB SDK owner, с прежней identity,
|
||||
по тем же границам/cleanup BLE04. Это исправленная версия после сохранённого
|
||||
отказа, не автоматический retry неизвестного результата setter; setters0.
|
||||
|
||||
B13 complete09:23:27.614776–09:23:50.280562 UTC,
|
||||
Ubuntu monotonic330515.495862456,duration22.665818s.55 tests passed;
|
||||
package0.1.3-5 revisionc495a5bdc03c1121e10126ee,58204390 bytes,
|
||||
SHA-256262fb2cd0587822ac9f0bdf590f1a891a48d916832ddb62b7802ea664c8ae55e.
|
||||
Result archive SHA-256eb32de81df8950ebe5068ad31d574bba324a3dab566a41937cc8f8d35ebae414,
|
||||
скачан, hash/entries/package проверены, не установлен.
|
||||
BLE05 выполнен09:24:38.285204–09:24:58.488505 UTC,
|
||||
Ubuntu monotonic330586.166297987–330606.369597165: fresh candidates0,
|
||||
поэтому Connect/GET не выполнялись, vendor writes0/Pair0/CCCD0/cleanup0.
|
||||
Report SHA-2565954d7c2aaf87e21a77fb34a8dc61f5fda75470e44e11fc7abf03fa36fdd2082.
|
||||
Core snapshot09:26:24.785589 UTC, Mac monotonic452864.83879075: Core operational,
|
||||
Rover006 online,D455 streaming,X4 offline/idle. Handoff SHA-256
|
||||
3ad8d2468c42e059935e34ca2189eb859c7fd36c8c9289f03eda4a5013abd3b5.
|
||||
Владелец подтвердил тёмный дисплей, USB оставлен подключённым. Выключение
|
||||
или screen sleep не различены. Никакого нового аппаратного GET после heartbeat
|
||||
fix не получено; успешные55 tests остаются синтетической квалификацией.
|
||||
|
||||
### B14 / WAKE01 — адресная wake advertisement, 10.09.2026
|
||||
|
||||
Intent: отдельный packaged ble_wake.py, не generic GATT remote. Один20s
|
||||
connectable peripheral advertisement, Apple company0x004c отдельно от24-byte
|
||||
ORBIT payload с6 ASCII bytes serial suffix. Полный serial приходит в private
|
||||
stdin JSON и должен совпасть по hash с прежним instance; пользователь явно
|
||||
подтвердил Bluetooth Wakeup On. Не записывает настройки, не выключает камеру,
|
||||
не эмулирует CE80/GPS/buttons, не включает запись. До публикации guard запрещает
|
||||
существующий USB SDK owner/target bond/connection; adapter должен быть powered,
|
||||
не Pairable/Discoverable, без других advertising instances. Эти свойства
|
||||
проверяются, но не меняются. Нужные bluez/dbus/gi уже объявлены model package.
|
||||
|
||||
Основание: pinned X4 remote README7964f1133e5d0f2c7eb73aaaaf5d6ebdd2127199,
|
||||
SHA-256371ec3c3de881445ac5c7c32802f0ff965b54e286c5e03260407e56989cbf714;
|
||||
docs14 описывает BlueZ5.72 semantics и ограничения. Limited discoverable
|
||||
instance20s при non-discoverable adapter формирует flags05; никакого изменения
|
||||
глобальной discoverability. Запрошен interval100–150ms, BlueZ без experimental
|
||||
может его игнорировать; фактический on-air interval не измерен, Experimental
|
||||
не включается и systemd не меняется. No SDK return не доказывает невозможность
|
||||
wake-up. Radio selector6 не заменяет полную идентичность возвращённого USB.
|
||||
|
||||
Rollback scope: unregister только собственного advertisement path/private
|
||||
bus (BlueZ удаляет его и при разрыве owner), закрыть только новое соединение
|
||||
с ранее сопоставленным BLE address; чужие/new unknown connections сохраняются
|
||||
и отражаются как cleanup error. Переход камеры в on — желаемый результат,
|
||||
автоматического power-off rollback нет. Успех требует прежнего USB identity
|
||||
в SDK mode, затем отдельного Core readback. Pair/CCCD/GATT vendor writes0;
|
||||
сама wake-реклама — реальная команда пробуждения, не read-only операция.
|
||||
|
||||
B14 package build artifact888a7ea1f4be8f4f2672368b,58336992 bytes,
|
||||
SHA-256725631fe01a8966c70827950c4d35d6cbdb1fa748eba1ceaf2ec1ec60271598b.
|
||||
Wake artifact58bf450b04bb7da8c24ce327,33262 bytes,
|
||||
SHA-256522fea49ee253ba21a069fb646f09f65f8e788c7dcf3c19afccacddb6aac4556.
|
||||
Wake source SHA-256317377d3ca43dd4fb85c379f56fa2b25ba14158d9308b9d2a03ce7ecfdc7e6a7.
|
||||
Первоначальный локальный candidate3236/e836 уточнён до staging (добавлен
|
||||
guard non-discoverable для точного flags05), на Ubuntu не запускался.
|
||||
Сначала штатная package qualification0.1.3-6 через прежний bounded builder,
|
||||
затем один WAKE01. Никакой установки новой модели/Node bundle не выполняется.
|
||||
|
||||
B14 complete09:36:51.924266–09:37:14.790304 UTC,
|
||||
Ubuntu monotonic331319.805353548,duration22.866067622962873s.
|
||||
59 tests passed за4.832s; model0.1.3-6 revisionb28a02ee0b3c30a729cf1bd1,
|
||||
58206332 bytes, SHA-256
|
||||
8d62ec9465f3083ab2811d41e2749932bfe85ec80a1c7f409db280ca1a734ced.
|
||||
Result archive SHA-256
|
||||
bca9dc7418f0d8181222492540700b296963827f5fbe1c639eb31e68082e255f,
|
||||
скачан, hashes/entries/package проверены; пакет не установлен.
|
||||
|
||||
WAKE01 complete09:38:22.143427–09:38:38.276180 UTC,
|
||||
Ubuntu monotonic331410.024515616–331426.157269888,returncode0.
|
||||
Одна advertisement зарегистрирована09:38:22.454663. USB отсутствовал,
|
||||
затем Mass Storage09:38:32.197518 → отсутствует09:38:36.434160 →
|
||||
SDK2e1a:0002 с прежним полным serial hash09:38:38.274886.
|
||||
От регистрации до sampled SDK15.820223s; это не video latency.
|
||||
GATT vendor writes0,Pair0,CCCD0,cleanup0; advertisement снята,
|
||||
ActiveInstances после cleanup0, новых BLE connections нет.
|
||||
Private wake01/report.json SHA-256
|
||||
5065182d66ea8a69506687984be582cab97f09f871ecf128201c5fb4333295e1.
|
||||
Private target/report содержат serial/address, исключены из Git.
|
||||
|
||||
Core readback09:39:35.013180 UTC,Mac monotonic453655.039106375:
|
||||
Core operational,Rover006 online,X4 online/idle,preview0/recording0;
|
||||
новая SDK session09:38:40.287529+00:00, прежний instance.
|
||||
D455 streaming, прежняя session от06:01:30.304205Z.
|
||||
Private wake01/after.json SHA-256
|
||||
18be7e96afd663555a63abd8987fadce69d66101ca8b6ed16a832823816541ae.
|
||||
Владелец явно подтвердил: камера включилась и перешла в Android.
|
||||
Один адресный Bluetooth wake с оставленным USB принят. Automatic runtime
|
||||
trigger/retry/preview restore ещё не реализованы; immediate warm replug
|
||||
REPLUG04 не исправлен. Camera-side Wakeup On остаётся явной предпосылкой.
|
||||
Installed Node0.8.19/X40.1.3-3 сохранены; clean Ubuntu и несколько циклов
|
||||
ещё не квалифицированы. Новых OS packages/services/permissions не добавляли.
|
||||
|
||||
@@ -1,5 +1,44 @@
|
||||
# Insta360 X4 — состояние реализации 10.09.2026
|
||||
|
||||
## WAKE01 — первый успешный Bluetooth wake с возвратом Android
|
||||
|
||||
Ubuntu выполнила один адресный20s wake advertisement при оставленном USB.
|
||||
Прежняя X4 вернулась через Mass Storage в SDK примерно через16s; Core открыл
|
||||
новую сессию09:38:40 UTC без подготовки/перезапуска служб. Владелец подтвердил
|
||||
включение и Android на дисплее. Core/Node online, D455 продолжила streaming.
|
||||
Advertisement снята, cleanup0. Preview в этом опыте не запускался.
|
||||
|
||||
[Модуль пробуждения](14_INSTA360_X4_BLUETOOTH_WAKE.md) включён в исходники
|
||||
model0.1.3-6; B14 собран на существующей Ubuntu,59 tests passed. Новая модель
|
||||
не установлена: Node0.8.19 и X40.1.3-3 сохранены. Следующая работа — подключить
|
||||
bounded wake к восстановлению конкретного instance с сохранением намерения
|
||||
оператора, затем штатно обновить bundle и проверить несколько циклов.
|
||||
Это один успешный инженерный запуск, не готовый automatic runtime recovery.
|
||||
|
||||
REPLUG04 подтвердил отказ обычного USB out/in включённой камеры. BLE05 при
|
||||
тёмном дисплее не увидел fresh candidates и не отправлял GET. Reader теперь
|
||||
распознаёт точный heartbeat07000000050000 по статическому SDK proof; чтение
|
||||
Options95/97 на hardware не подтверждено. Bluetooth Wakeup On включён
|
||||
владельцем и остаётся camera-side prerequisite.
|
||||
|
||||
## BLE04 / POWER04 — чтение остановлено, один запуск от питания подтверждён
|
||||
|
||||
Владелец включил Bluetooth Wakeup и подтвердил On; до POWER04 charging flag
|
||||
был Charge only. После USB out/in включённая X4 снова не перешла в Android.
|
||||
BLE04 установил GATT connection, выполнил временную CCCD subscription и один
|
||||
GetOptions8 для identity15/48. Получен7-byte type5 packet, не поддержанный
|
||||
strict reader; опыт остановлен и соединение закрыто, без повторов/setters.
|
||||
Identity/Options95/97 не прочитаны. Это открытый transport вопрос, не firmware
|
||||
отказ параметра. Следующий отдельный опыт POWER04 — штатный Charge and power on:
|
||||
USB out → выбор настройки → выключение → USB in без кнопок. В09:15:52
|
||||
появился Mass Storage,09:15:59 — SDK USB и новая session прежней X4. Core/Node
|
||||
online,D455 streaming. Владелец подтвердил фактический порядок camera off →
|
||||
USB out → USB in и самостоятельное включение сразу в Android. Один цикл
|
||||
запуска выключенной X4 от подачи USB подтверждён; это ещё не warm replug
|
||||
включённой камеры. REPLUG04 отдельно подтвердил отказ такого переподключения.
|
||||
Обе camera-side настройки должны учитываться как явные предпосылки,
|
||||
их автоматическая установка из Core/Node не реализована.
|
||||
|
||||
## POWER03/BLE03 — пробуждение кнопкой и первый BLE candidate
|
||||
|
||||
Одиночное короткое Power при уже вставленном USB восстановило Android без
|
||||
@@ -11,8 +50,8 @@
|
||||
|
||||
BLE03 scan20s нашёл один свежий BE80 candidate с именем по USB serial suffix;
|
||||
полный USB serial hash совпал с прежним instance, USB2e1a:0002/speed5000.
|
||||
GATT identity/auth не проверялись. Следующий шаг — состояние Bluetooth Wakeup
|
||||
и отдельный адресный wake artifact; активный SDK owner не перехватывается.
|
||||
GATT identity/auth в BLE03 не проверялись. Последующие BLE04/POWER04 описаны
|
||||
выше; активный SDK owner не перехватывается.
|
||||
|
||||
## BLE01/BLE02 — первый модуль беспроводной диагностики
|
||||
|
||||
@@ -21,7 +60,9 @@ discovery, стандартное GATT inspection и отдельное чтен
|
||||
Те же модули и OS dependencies включены в исходники model package0.1.3-4.
|
||||
На Ubuntu initial scan20s и последующий scan60s не обнаружили X4; владелец
|
||||
сообщил о тёмном экране. Позднее короткое Power подтверждено в POWER03 выше.
|
||||
Read-options, pairing, setters и power-команды на hardware не запускались.
|
||||
В BLE01/02 read-options не запускался; позднейший BLE04 описан выше.
|
||||
В BLE01/02 pairing, setters и power-команды на hardware не выполнялись;
|
||||
позднейший активный wake advertisement принят отдельно в WAKE01 выше.
|
||||
Installed Node0.8.19 / X40.1.3-3 сохранены; bundled Node profile ещё не обновлялся.
|
||||
Автовозврат Android остаётся открытым; нельзя считать успешную сборку его приёмкой.
|
||||
|
||||
|
||||
@@ -30,6 +30,34 @@ Node0.8.19 bundle пока остаётся0.1.3-3; его обновление
|
||||
|
||||
## Очередь реализации
|
||||
|
||||
**Актуальный результат WAKE01:** один адресный Bluetooth wake с Ubuntu
|
||||
разбудил X4 при подключённом USB; камера сама вернулась в Android/SDK,
|
||||
владелец подтвердил экран. Node открыл прежний instance, D455 сохранила поток.
|
||||
Модуль упакован в0.1.3-6,59 Ubuntu tests passed, новая модель не установлена.
|
||||
REPLUG04 без wake остался неуспешным. Ближайший increment:
|
||||
|
||||
1. Сохранять полную identity и явное намерение восстановления отдельно для
|
||||
каждого instance; не будить намеренно выключенную оператором камеру.
|
||||
2. Подключить bounded wake и сериализацию adapter ownership; отменять при
|
||||
появлении SDK, ограничивать retries, не повторять SD-команды при unknown.
|
||||
3. Доставить через Node bundle и штатную подготовку из Core/Node, затем
|
||||
проверить возврат SDK/preview, несколько циклов и независимость камер.
|
||||
|
||||
Camera-side Bluetooth Wakeup On остаётся явной предпосылкой. Wake при тёмном
|
||||
дисплее не доказывает recovery зависшей включённой камеры или clean Ubuntu.
|
||||
Следующий абзац сохраняет порядок предыдущих опытов; их исходы уточнены ниже.
|
||||
|
||||
Уточнение BLE04/POWER04: Bluetooth Wakeup теперь включён владельцем. GATT
|
||||
Connect прошёл, но первый identity GetOptions получил неподдержанный7-byte
|
||||
type5 packet, поэтому без retries/setters закрыт; это отдельный вопрос framing.
|
||||
Обнаруженный штатный Power-off Charging → Charge and power on проверяется
|
||||
раньше следующей BLE-команды. Observer зафиксировал возврат Mass Storage→SDK
|
||||
и новой session; владелец подтвердил самостоятельный Android после camera off
|
||||
→ USB out → USB in. Один запуск выключенной X4 от подачи питания подтверждён.
|
||||
REPLUG04 включённой X4 завершился отказом; успех POWER04 его не закрывает.
|
||||
Ручные camera-side flags фиксируются как prerequisites, не скрытая доработка
|
||||
Ubuntu. «Зарядка и запись» не используется для операторского preview.
|
||||
|
||||
**Уточнение владельца10.09 после NET03:** ближайший приоритет после применения
|
||||
B11 — автономность подключения и питание. Проверить сохранение Android-режима,
|
||||
replug/cold start, автоматическое восстановление SDK и просмотра; определить
|
||||
|
||||
@@ -6,8 +6,19 @@
|
||||
существующего Ubuntu Mac mini. Ниже разделены выполненные проверки и оставшаяся
|
||||
приёмка; успешная полная автономность пока не доказана.
|
||||
|
||||
**Результат WAKE01:** один адресный Bluetooth wake с Ubuntu при оставленном
|
||||
USB включил прежнюю X4 и вернул Android/SDK примерно за 16 секунд. Владелец
|
||||
подтвердил экран, Core открыл новую сессию без подготовки, D455 сохранила поток.
|
||||
Модуль включён в model 0.1.3-6, B14 прошёл 59 Ubuntu tests; новая модель ещё
|
||||
не установлена. [Реализация и границы](14_INSTA360_X4_BLUETOOTH_WAKE.md).
|
||||
Обычный warm replug REPLUG04 остался неуспешным. Теперь ближайший шаг —
|
||||
подключить wake к bounded recovery конкретного instance с сохранением намерения
|
||||
оператора, затем штатно доставить bundle и проверить несколько циклов/preview.
|
||||
Bluetooth Wakeup On остаётся явной camera-side предпосылкой; ниже сохранена
|
||||
история исследований, а не утверждение готовности автоматического recovery.
|
||||
|
||||
**Уточнение RESEARCH01:** [исследование Android reconnect](12_INSTA360_X4_ANDROID_RECONNECT_RESEARCH.md)
|
||||
выявило внутренний udisk_mode и независимый BLE remote off/on. Ближайший шаг —
|
||||
выявило внутренний udisk_mode и независимый BLE remote off/on. На том этапе —
|
||||
проверка беспроводного управления и чтение настройки у точной X4 через versioned
|
||||
artifact. Отсутствие VBUS switching в PORT01 не доказывает невозможность
|
||||
программного восстановления камеры; внешний хаб остаётся альтернативной веткой.
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
[исследования reconnect](12_INSTA360_X4_ANDROID_RECONNECT_RESEARCH.md).
|
||||
Автоматический возврат Android ещё не принят.
|
||||
|
||||
Дополнение BLE04/BLE05/B14: первый GATT GetOptions получил только точный
|
||||
heartbeat `07000000050000`; reader теперь распознаёт его по статическому
|
||||
подтверждению SDK, не считает ответом Options и не отправляет ACK. B13 прошёл
|
||||
55 tests. BLE05 при тёмном экране не нашёл свежую X4, поэтому GET не выполнялся;
|
||||
аппаратное чтение identity/95/97 остаётся открытым. [Отдельный WAKE01](14_INSTA360_X4_BLUETOOTH_WAKE.md)
|
||||
успешно разбудил камеру и вернул Android/SDK. Это активная advertisement-команда,
|
||||
а не GATT GET. B14 model 0.1.3-6 включает все три модуля и прошёл 59 tests.
|
||||
Runtime automatic trigger и новая модель ещё не развёрнуты.
|
||||
|
||||
Финальный B12-R2: пакет 0.1.3-4 собран на Ubuntu, 53 tests passed, включая
|
||||
15 BLE cases. Проверены реальные содержимое .deb, зависимости, cold imports
|
||||
и статическая ELF closure. SHA-256 пакета:
|
||||
@@ -16,8 +25,9 @@
|
||||
отдельное GATT inspection и явный режим чтения настроек.
|
||||
- `ble_options.py`: только GET_OPTIONS, строгие packet/protobuf bounds и
|
||||
разбор ответов. Нет generic send, SET_OPTIONS, capture, reboot или power-off.
|
||||
- `build_ble_diagnostic.py`: артефакт с hashes обоих модулей; тот же код
|
||||
включён в model Debian package 0.1.3-4 через `build_deb.py`.
|
||||
- `build_ble_diagnostic.py`: артефакт с hashes трёх модулей; diagnostic —
|
||||
обычный entrypoint, wake выбирается явно через `--wake`. Тот же код
|
||||
включён в model Debian package 0.1.3-6 через `build_deb.py`.
|
||||
- `check_package_entry.py`: проверяет наличие/совпадение модулей в реальном
|
||||
.deb, объявленные зависимости и synthetic tests.
|
||||
|
||||
@@ -126,8 +136,9 @@ GetOptions не выполнялся. Vendor commands0, pairing0, CCCD subscript
|
||||
wake artifact. [Официальная X4 FAQ](https://onlinemanual.insta360.com/x4/en-us/operating-tutorials/connect/connect-app)
|
||||
требует включённую настройку Settings → General → Bluetooth Wakeup (если
|
||||
General отсутствует — непосредственно в Settings). Описанная там поддержка
|
||||
приложения iOS не является приёмкой Linux эмуляции. Значение настройки нашей
|
||||
камеры пока unknown. Ни выключение, ни wake advertisement не выполнялись.
|
||||
приложения iOS не является приёмкой Linux эмуляции. Позднее владелец включил
|
||||
Bluetooth Wakeup и явно подтвердил On. Это camera-side ручная предпосылка,
|
||||
не действие Ubuntu installer. Wake advertisement агентом ещё не выполнялся.
|
||||
Исходную точную USB identity использовать из private evidence; не объединять
|
||||
камеры по suffix. GetOptions — отдельная ветка без активного USB SDK owner.
|
||||
|
||||
@@ -137,3 +148,37 @@ Android setter. Если уже ANDROID=1, запись того же значе
|
||||
реализованного allowlist. BLE remote off/on требует отдельного артефакта и
|
||||
проверенного wake-up до выключения камеры. Ни один из этих исходов пока
|
||||
не установлен. Raw reports/hashes/UTC/monotonic — в installation ledger.
|
||||
|
||||
## BLE04 и следующий контроль Power-off Charging
|
||||
|
||||
После выхода владельца в настройки и USB out/in X4 осталась включённой
|
||||
с изображением на дисплее, USB SDK отсутствовал. BLE04 выполнил Connect,
|
||||
GATT service discovery, временный StartNotify и один GetOptions8 для15/48.
|
||||
DIS180A отсутствовал. Пришёл packet `07000000050000`, length7/type5;
|
||||
текущий reader ожидает message type4 и прекратил работу с Unknown packet framing.
|
||||
Это фактические bytes, но их семантика/handshake пока не установлены.
|
||||
Нельзя считать это успешным чтением, авторизацией или firmware отказом Options95.
|
||||
До95/97 дело не дошло, serial через GATT не подтверждён. Повторных записей не
|
||||
было; собственные notify/connection закрыты, cleanup0, Pair0, setters0.
|
||||
|
||||
Владелец обнаружил штатную настройку Power-off Charging со значениями Charge
|
||||
only / Charge and power on / Charge and record; выбрано Charge only.
|
||||
[Официальная инструкция, Scenario2 X4/X5](https://onlinemanual.insta360.com/x5/en-us/troubleshooting/cannot-turn-on/auto-power-on)
|
||||
подтверждает включение камеры при подаче питания для Charge and power on,
|
||||
но не обещает переход в Android. POWER04 выделен как отдельный ручной контроль:
|
||||
USB out → Charge and power on → штатное выключение → USB in без кнопок.
|
||||
Charge and record не используется. Дождаться фактического результата владельца
|
||||
и USB/SDK evidence; изменённая camera-side настройка должна быть явно описана
|
||||
как prerequisite первичной настройки устройства. При необходимости rollback —
|
||||
прежнее Charge only. Ни новый Ubuntu prerequisite, ни автоматическая настройка
|
||||
камеры из Core/Node этим не заявляются.
|
||||
|
||||
В POWER04 observer09:15:09–09:16:09 UTC зафиксированы Mass Storage09:15:52,
|
||||
SDK2e1a:0002/USB5000 в09:15:59 и новая SDK session09:15:59.915770.
|
||||
Core snapshot09:16:46 подтверждает прежний X4 instance online/idle,
|
||||
preview0/recording0, Node online и D455 streaming. Владелец затем подтвердил
|
||||
фактический порядок camera off → USB out → USB in и самостоятельный Android
|
||||
после подключения. Принят один запуск от подачи USB к выключенной X4 в
|
||||
контексте предложенного charging flag; его программного readback нет.
|
||||
Warm replug включённой X4 проверяется отдельно как REPLUG04. BLE wake
|
||||
advertisements и power commands агентом не выполнялись.
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# X4: ограниченная проверка пробуждения с Ubuntu
|
||||
|
||||
WAKE01/B14,10.09.2026. Предпосылки: владелец включил Bluetooth Wakeup;
|
||||
полный USB serial прежнего instance сохранён в private evidence, USB подключён,
|
||||
экран тёмный, USB SDK отсутствует. Sleep и power-off до опыта не различены.
|
||||
|
||||
## Реализация и упаковка
|
||||
|
||||
`plugins/insta360-x4/packaging/ble_wake.py` входит в model0.1.3-6 вместе с
|
||||
зависимостями BlueZ/dbus/gi. `build_ble_diagnostic.py --wake` создаёт versioned
|
||||
artifact с hash всех трёх модулей. Builder квалифицирует .deb на существующей
|
||||
Ubuntu до hardware запуска; установка, службы/права и Node bundle не меняются.
|
||||
Код ещё не подключён к автоматическому runtime recovery или UI.
|
||||
|
||||
Private stdin target содержит полный serial, ожидаемый device ID, ранее
|
||||
сопоставленный Bluetooth address и подтверждённый wakeup_enabled. Hash serial
|
||||
проверяется до Bluetooth; одного suffix недостаточно. Возврат SDK требует
|
||||
2e1a:0002 и совпадение полного USB serial hash, затем Core readback.
|
||||
|
||||
## Команда, основание и границы
|
||||
|
||||
[Первичный разбор физической X4](https://github.com/TheAngryRaven/insta360-ble-gps-spec/tree/7964f1133e5d0f2c7eb73aaaaf5d6ebdd2127199)
|
||||
описывает пробуждение по manufacturer advertisement до соединения с пультом.
|
||||
Company0x004c передаётся BlueZ отдельно; payload:
|
||||
`02 15 09 4f 52 42 49 54 09 ff 0f 00 <suffix:6 ASCII> 00 00 00 00 e4 01`.
|
||||
24 bytes + company2 + AD length/type2 + flags3 =31 bytes. Имя, appearance,
|
||||
service UUIDs и secondary PHY не добавляются. CE80/GPS/button события не
|
||||
эмулируются. Нет pairing, GATT writes, записи/фото, shutdown или настройки USB.
|
||||
Wake advertisement сама является активной командой; ноль GATT writes не
|
||||
означает отсутствие воздействия на камеру.
|
||||
|
||||
[BlueZ Advertising API](https://bluez.readthedocs.io/en/latest/advertising-api/)
|
||||
используется через private D-Bus owner и собственный object path. Асинхронный
|
||||
RegisterAdvertisement позволяет обрабатывать GetAll; deadline ограничен.
|
||||
По [BlueZ5.72 source](https://github.com/bluez/bluez/blob/5.72/src/advertising.c)
|
||||
DiscoverableTimeout задаёт limited mode; non-discoverable adapter добавляет
|
||||
BR/EDR-not-supported, ожидаемые flags05. Существующие adapter flags не меняются.
|
||||
Занятый advertiser, Pairable/Discoverable adapter или USB SDK owner прекращают
|
||||
опыт. Timeout20s и явный unregister ограничивают собственную рекламу; потеря
|
||||
owner bus также удаляет instance.
|
||||
|
||||
Min/MaxInterval100/150ms запрошены, но без experimental BlueZ может их
|
||||
игнорировать. Фактический radio interval/полный on-air packet в этом опыте не
|
||||
измерены; systemd Experimental не включается. Отрицательный результат не
|
||||
закрывает все возможные BLE wake transports.
|
||||
|
||||
Новые соединения с прежним сопоставленным адресом X4 закрываются; ранее
|
||||
существующие не принимаются в опыт. Неопознанные новые соединения сохраняются
|
||||
и отмечаются как cleanup error. Снятие advertisement не выполняет camera
|
||||
power-off: включённая камера — желаемый результат. Нет автоматических retries.
|
||||
|
||||
## Приёмка
|
||||
|
||||
Принять только фактическое появление прежней X4 в USB SDK и новую сессию Core
|
||||
без Power/выбора Android, при сохранении Node/D455. Registered advertisement
|
||||
не равна доставленному wake; SDK USB не равен движущемуся изображению.
|
||||
Пакет/тесты/UTC/monotonic/hash/raw и фактический результат — в ledger07.
|
||||
Автоматическая доставка camera-side настройки Bluetooth Wakeup через installer
|
||||
ещё не реализована. Clean Ubuntu acceptance и несколько камер остаются открыты.
|
||||
|
||||
## Фактический результат WAKE01
|
||||
|
||||
10.09.2026,09:38:22–09:38:38 UTC: одна advertisement, returncode0, cleanup0.
|
||||
После регистрации09:38:22.454663 USB появился как Mass Storage09:38:32.197518,
|
||||
затем SDK2e1a:0002 с прежним полным serial hash09:38:38.274886. Это15.820223s
|
||||
до sampled SDK, не задержка изображения. Advertisement снята, ActiveInstances0,
|
||||
Pair/CCCD/GATT vendor writes0, новых BLE connections нет.
|
||||
|
||||
Core readback09:39:35 подтвердил прежний X4 instance online/idle и новую SDK
|
||||
session09:38:40.287529. Node online, D455 streaming с прежней сессией.
|
||||
Владелец явно подтвердил: камера включилась и перешла в Android. B14 .deb
|
||||
прошёл59 tests; artifact/UTC/monotonic/SHA и private evidence записаны в ledger07.
|
||||
Установленные Node0.8.19 и X40.1.3-3 сохранены; preview в WAKE01 не запускался.
|
||||
|
||||
Принят один контролируемый wake с оставленным USB. Автоматическое включение
|
||||
этого модуля в runtime, сохранение identity/намерения восстановления для каждого
|
||||
instance, ограничение повторов, восстановление preview и повторяемость остаются
|
||||
следующим increment. Wake не доказывает немедленное восстановление включённой
|
||||
зависшей камеры: обычный warm replug REPLUG04 отдельно завершился отказом.
|
||||
@@ -283,6 +283,7 @@ class Inspector:
|
||||
self.report["notifications"].append({"hex": raw.hex(), **timestamp()})
|
||||
try:
|
||||
received.extend(reader.feed(raw))
|
||||
self.report["heartbeat_packets"] = reader.heartbeats
|
||||
except ValueError as error:
|
||||
errors.append(str(error))
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@ class PacketReader:
|
||||
|
||||
def __init__(self):
|
||||
self.buffer = bytearray()
|
||||
self.heartbeats = 0
|
||||
|
||||
def feed(self, chunk):
|
||||
if not chunk or len(self.buffer) + len(chunk) > MAX_PACKET * 2:
|
||||
@@ -91,12 +92,21 @@ class PacketReader:
|
||||
result = []
|
||||
while len(self.buffer) >= 4:
|
||||
size = struct.unpack_from("<I", self.buffer)[0]
|
||||
if not 16 <= size <= MAX_PACKET:
|
||||
if size != 7 and not 16 <= size <= MAX_PACKET:
|
||||
raise ValueError("Unknown packet framing")
|
||||
if len(self.buffer) < size:
|
||||
break
|
||||
raw = bytes(self.buffer[:size])
|
||||
del self.buffer[:size]
|
||||
if size == 7:
|
||||
# X4 BLE04 capture; SDK CameraSession::Start registers type5
|
||||
# with OnHeartbeatHandler. This is not an Options response.
|
||||
if raw != b"\x07\x00\x00\x00\x05\x00\x00":
|
||||
raise ValueError("Unsupported short control packet")
|
||||
self.heartbeats += 1
|
||||
if self.heartbeats > 64:
|
||||
raise ValueError("Heartbeat bound exceeded")
|
||||
continue
|
||||
_, kind, reserved, code, content, ident, tail = struct.unpack("<IBHHBIH", raw[:16])
|
||||
if kind != 4 or reserved or tail or content != 2 or ident & 0xC0000000 != 0xC0000000:
|
||||
raise ValueError("Unsupported message header/direction/fragmentation")
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
"""One bounded X4 wake advertisement; no camera GATT writes or power-off."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
if "__file__" in globals():
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
|
||||
from ble_diagnostic import ( # noqa: E402
|
||||
ADAPTER,
|
||||
DEVICE,
|
||||
PROPS,
|
||||
Inspector,
|
||||
assert_no_usb_owner,
|
||||
plain,
|
||||
timestamp,
|
||||
usb_snapshot,
|
||||
)
|
||||
|
||||
ADV = "org.bluez.LEAdvertisement1"
|
||||
ADV_MANAGER = "org.bluez.LEAdvertisingManager1"
|
||||
|
||||
|
||||
def wake_payload(target):
|
||||
if set(target) != {"device_id", "serial", "bluetooth_address", "wakeup_enabled"}:
|
||||
raise ValueError("Exact private target fields are required")
|
||||
serial = target["serial"]
|
||||
if not isinstance(serial, str) or not re.fullmatch(r"[A-Za-z0-9]{7,64}", serial):
|
||||
raise ValueError("Full USB serial is required, never a suffix alone")
|
||||
ident = "instax4_" + hashlib.sha256(serial.encode()).hexdigest()[:32]
|
||||
if target["device_id"] != ident or target["wakeup_enabled"] is not True:
|
||||
raise ValueError("USB identity and enabled camera wakeup must be confirmed")
|
||||
if not re.fullmatch(r"(?:[0-9A-F]{2}:){5}[0-9A-F]{2}", target["bluetooth_address"]):
|
||||
raise ValueError("Prior correlated Bluetooth address is required")
|
||||
# Company ID 0x004c is passed separately to BlueZ. X4 remote spec section1.
|
||||
return (
|
||||
bytes.fromhex("0215094f5242495409ff0f00")
|
||||
+ serial[-6:].encode()
|
||||
+ bytes.fromhex("00000000e401")
|
||||
)
|
||||
|
||||
|
||||
def matching_sdk(samples, expected):
|
||||
return any(
|
||||
d.get("idVendor") == "2e1a"
|
||||
and d.get("idProduct") == "0002"
|
||||
and "instax4_" + hashlib.sha256(d.get("serial", "").encode()).hexdigest()[:32] == expected
|
||||
for d in samples["devices"]
|
||||
)
|
||||
|
||||
|
||||
def run(target, report):
|
||||
payload = wake_payload(target)
|
||||
assert_no_usb_owner(target["device_id"])
|
||||
inspector = Inspector(report)
|
||||
advertisement = None
|
||||
manager = None
|
||||
registered = False
|
||||
attempted = False
|
||||
own_path = "/org/missioncore/x4/wake"
|
||||
initial = {}
|
||||
cleanup = []
|
||||
try:
|
||||
objects = inspector.objects()
|
||||
adapters = [
|
||||
(p, x)
|
||||
for p, x in objects.items()
|
||||
if x.get(ADAPTER, {}).get("Powered") and ADV_MANAGER in x
|
||||
]
|
||||
if len(adapters) != 1:
|
||||
raise RuntimeError("Exactly one powered advertising adapter is required")
|
||||
adapter_path, adapter = adapters[0]
|
||||
if (
|
||||
adapter[ADAPTER].get("Pairable", True)
|
||||
or adapter[ADAPTER].get("Discoverable", True)
|
||||
or adapter[ADV_MANAGER].get("ActiveInstances", -1) != 0
|
||||
):
|
||||
raise RuntimeError(
|
||||
"Require non-pairable/non-discoverable adapter with no advertisements"
|
||||
)
|
||||
if not adapter[ADV_MANAGER].get("SupportedInstances"):
|
||||
raise RuntimeError("No advertising instance is available")
|
||||
initial = {p: plain(x[DEVICE]) for p, x in objects.items() if DEVICE in x}
|
||||
for device in initial.values():
|
||||
if device.get("Address") == target["bluetooth_address"] and any(
|
||||
device.get(k) for k in ("Connected", "Paired", "Bonded")
|
||||
):
|
||||
raise RuntimeError("Preserve existing target connection/bond")
|
||||
report["adapter_before"] = plain(adapter)
|
||||
manager = inspector.iface(adapter_path, ADV_MANAGER)
|
||||
import dbus.service
|
||||
|
||||
class Advertisement(dbus.service.Object):
|
||||
released = False
|
||||
|
||||
@dbus.service.method(PROPS, in_signature="s", out_signature="a{sv}")
|
||||
def GetAll(self, interface):
|
||||
if interface != ADV:
|
||||
return dbus.Dictionary({}, signature="sv")
|
||||
return dbus.Dictionary(
|
||||
{
|
||||
"Type": dbus.String("peripheral"),
|
||||
"ManufacturerData": dbus.Dictionary(
|
||||
{dbus.UInt16(0x004C): dbus.Array(payload, signature="y")},
|
||||
signature="qv",
|
||||
),
|
||||
"Discoverable": dbus.Boolean(True),
|
||||
"DiscoverableTimeout": dbus.UInt16(20),
|
||||
"Timeout": dbus.UInt16(20),
|
||||
# Honored only with BlueZ experimental features. Never enable
|
||||
# that setting here; actual on-air interval is not asserted.
|
||||
"MinInterval": dbus.UInt32(100),
|
||||
"MaxInterval": dbus.UInt32(150),
|
||||
},
|
||||
signature="sv",
|
||||
)
|
||||
|
||||
@dbus.service.method(ADV, in_signature="", out_signature="")
|
||||
def Release(self):
|
||||
self.released = True
|
||||
report["advertisement_released"] = timestamp()
|
||||
|
||||
advertisement = Advertisement(inspector.bus, own_path)
|
||||
result, errors = [], []
|
||||
assert_no_usb_owner(target["device_id"])
|
||||
report["advertisement_intent"] = {
|
||||
"manufacturer_id": 0x004C,
|
||||
"bytes": len(payload),
|
||||
"timeout_seconds": 20,
|
||||
**timestamp(),
|
||||
}
|
||||
attempted = True
|
||||
manager.RegisterAdvertisement(
|
||||
own_path,
|
||||
dbus.Dictionary({}, signature="sv"),
|
||||
reply_handler=lambda: result.append(True),
|
||||
error_handler=lambda e: errors.append(str(e)),
|
||||
timeout=5,
|
||||
)
|
||||
inspector.pump(6, lambda: bool(result or errors))
|
||||
if errors or not result:
|
||||
raise RuntimeError(errors[0] if errors else "Advertisement registration timed out")
|
||||
registered = True
|
||||
report["advertisement_registered"] = timestamp()
|
||||
inspector.pump(20, lambda: matching_sdk(usb_snapshot(), target["device_id"]))
|
||||
report["sdk_usb_returned"] = matching_sdk(usb_snapshot(), target["device_id"])
|
||||
finally:
|
||||
if attempted and advertisement and not advertisement.released:
|
||||
done, errors = [], []
|
||||
try:
|
||||
manager.UnregisterAdvertisement(
|
||||
own_path,
|
||||
reply_handler=lambda: done.append(True),
|
||||
error_handler=lambda e: errors.append(str(e)),
|
||||
timeout=5,
|
||||
)
|
||||
inspector.pump(6, lambda: bool(done or errors or advertisement.released))
|
||||
if (errors and registered) or not (done or errors or advertisement.released):
|
||||
cleanup.append("Advertisement removal did not complete")
|
||||
except Exception as error:
|
||||
cleanup.append(str(error))
|
||||
# A wake beacon is connectable; disconnect only the prior correlated
|
||||
# X4 address when it acquired a connection during our advertisement.
|
||||
try:
|
||||
current = {p: plain(x[DEVICE]) for p, x in inspector.objects().items() if DEVICE in x}
|
||||
for path, device in current.items():
|
||||
if device.get("Connected") and not initial.get(path, {}).get("Connected"):
|
||||
if attempted and device.get("Address") == target["bluetooth_address"]:
|
||||
inspector.iface(path, DEVICE).Disconnect(timeout=5)
|
||||
report["wake_connection_closed"] = True
|
||||
else:
|
||||
cleanup.append("New uncorrelated Bluetooth connection preserved")
|
||||
if manager:
|
||||
report["adapter_after"] = plain(inspector.objects().get(adapter_path, {}))
|
||||
except Exception as error:
|
||||
cleanup.append(str(error))
|
||||
try:
|
||||
if advertisement:
|
||||
advertisement.remove_from_connection()
|
||||
except Exception as error:
|
||||
cleanup.append(str(error))
|
||||
finally:
|
||||
# Private bus owner disappearance also removes its advertisement.
|
||||
inspector.close()
|
||||
report["cleanup_errors"].extend(cleanup)
|
||||
|
||||
|
||||
def main():
|
||||
report = {
|
||||
"schema": "missioncore.insta360.ble-wake/v1",
|
||||
"started": timestamp(),
|
||||
"vendor_writes": 0,
|
||||
"pair_calls": 0,
|
||||
"notify_subscriptions": 0,
|
||||
"state": "running",
|
||||
}
|
||||
try:
|
||||
raw = sys.stdin.buffer.read(4097)
|
||||
if len(raw) > 4096:
|
||||
raise ValueError("Private wake target exceeds limit")
|
||||
run(json.loads(raw), report)
|
||||
report["state"] = "complete" if report.get("sdk_usb_returned") else "no_sdk_return"
|
||||
except Exception as error:
|
||||
report.update(state="error", error=str(error))
|
||||
if report.get("cleanup_errors"):
|
||||
report["state"] = "error"
|
||||
report["finished"] = timestamp()
|
||||
print(json.dumps(report), flush=True)
|
||||
return 0 if report["state"] == "complete" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,53 +1,62 @@
|
||||
"""Package the exact diagnostic entry point; never install host prerequisites."""
|
||||
"""Package exact model diagnostics; never install host prerequisites."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ENTRY = """import hashlib, json, sys, zipfile
|
||||
ENTRY = """import hashlib, json, sys, types, zipfile
|
||||
from pathlib import Path
|
||||
with zipfile.ZipFile(sys.argv[0]) as archive:
|
||||
manifest = json.loads(archive.read("manifest.json"))
|
||||
source = archive.read("ble_diagnostic.py")
|
||||
codec = archive.read("ble_options.py")
|
||||
if hashlib.sha256(source).hexdigest() != manifest["source_sha256"]:
|
||||
raise RuntimeError("Diagnostic source hash mismatch")
|
||||
if hashlib.sha256(codec).hexdigest() != manifest["codec_sha256"]:
|
||||
raise RuntimeError("Diagnostic codec hash mismatch")
|
||||
ident = hashlib.sha256(source + b"\\0" + codec).hexdigest()[:24]
|
||||
if Path(sys.argv[0]).name != "mission-core-x4-ble-" + ident + ".pyz":
|
||||
raw = archive.read("manifest.json")
|
||||
manifest = json.loads(raw)
|
||||
names = ("ble_options", "ble_diagnostic", "ble_wake")
|
||||
modules = {name: archive.read(name + ".py") for name in names}
|
||||
ident = hashlib.sha256(raw).hexdigest()[:24]
|
||||
entry = manifest["entrypoint"]
|
||||
if entry not in {"ble_diagnostic", "ble_wake"}:
|
||||
raise RuntimeError("Unknown diagnostic entry point")
|
||||
prefix = "mission-core-x4-wake-" if entry == "ble_wake" else "mission-core-x4-ble-"
|
||||
if Path(sys.argv[0]).name != prefix + ident + ".pyz":
|
||||
raise RuntimeError("Diagnostic artifact name mismatch")
|
||||
import types
|
||||
module = types.ModuleType("ble_options")
|
||||
exec(compile(codec, "ble_options.py", "exec"), module.__dict__)
|
||||
sys.modules["ble_options"] = module
|
||||
exec(compile(source, "ble_diagnostic.py", "exec"), {"__name__": "__main__"})
|
||||
for name, source in modules.items():
|
||||
if hashlib.sha256(source).hexdigest() != manifest["modules"][name]:
|
||||
raise RuntimeError("Diagnostic source hash mismatch")
|
||||
for name, source in modules.items():
|
||||
module = types.ModuleType(name)
|
||||
sys.modules[name] = module
|
||||
exec(compile(source, name + ".py", "exec"), module.__dict__)
|
||||
raise SystemExit(sys.modules[entry].main())
|
||||
"""
|
||||
|
||||
|
||||
def build():
|
||||
source = (ROOT / "packaging/ble_diagnostic.py").read_bytes()
|
||||
codec = (ROOT / "packaging/ble_options.py").read_bytes()
|
||||
sha = hashlib.sha256(source).hexdigest()
|
||||
def build(wake=False):
|
||||
modules = {
|
||||
name: (ROOT / "packaging" / (name + ".py")).read_bytes()
|
||||
for name in ("ble_options", "ble_diagnostic", "ble_wake")
|
||||
}
|
||||
manifest = {
|
||||
"schema": "missioncore.insta360.ble-diagnostic-artifact/v1",
|
||||
"source_sha256": sha,
|
||||
"codec_sha256": hashlib.sha256(codec).hexdigest(),
|
||||
"schema": "missioncore.insta360.ble-diagnostic-artifact/v2",
|
||||
"entrypoint": "ble_wake" if wake else "ble_diagnostic",
|
||||
"modules": {name: hashlib.sha256(data).hexdigest() for name, data in modules.items()},
|
||||
"dependencies": ["bluez", "python3-dbus", "python3-gi"],
|
||||
"persistent_system_changes": False,
|
||||
"vendor_commands": [8],
|
||||
"scope": "bounded-discovery-and-explicit-identity-options-read",
|
||||
"vendor_commands": [] if wake else [8],
|
||||
"scope": "single-addressed-wake-advertisement"
|
||||
if wake
|
||||
else "bounded-discovery-and-explicit-identity-options-read",
|
||||
}
|
||||
ident = hashlib.sha256(source + b"\0" + codec).hexdigest()[:24]
|
||||
output = ROOT / "build" / ("mission-core-x4-ble-" + ident + ".pyz")
|
||||
raw = json.dumps(manifest, sort_keys=True).encode()
|
||||
ident = hashlib.sha256(raw).hexdigest()[:24]
|
||||
prefix = "mission-core-x4-wake-" if wake else "mission-core-x4-ble-"
|
||||
output = ROOT / "build" / (prefix + ident + ".pyz")
|
||||
with output.open("xb") as stream, zipfile.ZipFile(stream, "w") as archive:
|
||||
for name, data in {
|
||||
"__main__.py": ENTRY.encode(),
|
||||
"ble_diagnostic.py": source,
|
||||
"ble_options.py": codec,
|
||||
"manifest.json": json.dumps(manifest, sort_keys=True).encode(),
|
||||
"manifest.json": raw,
|
||||
**{name + ".py": data for name, data in modules.items()},
|
||||
}.items():
|
||||
info = zipfile.ZipInfo(name, date_time=(2026, 9, 10, 0, 0, 0))
|
||||
info.external_attr = 0o600 << 16
|
||||
@@ -56,10 +65,12 @@ def build():
|
||||
return {
|
||||
"artifact": str(output),
|
||||
"sha256": hashlib.sha256(output.read_bytes()).hexdigest(),
|
||||
"source_sha256": sha,
|
||||
"bytes": output.stat().st_size,
|
||||
"modules": manifest["modules"],
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(json.dumps(build()))
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--wake", action="store_true")
|
||||
print(json.dumps(build(parser.parse_args().wake)))
|
||||
|
||||
@@ -16,7 +16,7 @@ sys.path.insert(0, str(REPOSITORY / "scripts/packaging"))
|
||||
from debian import package # noqa: E402
|
||||
from fetch_sdk import verify # noqa: E402
|
||||
|
||||
VERSION = "0.1.3-4"
|
||||
VERSION = "0.1.3-6"
|
||||
WHEELS = {
|
||||
"aiohappyeyeballs",
|
||||
"aiohttp",
|
||||
@@ -159,6 +159,7 @@ def build(output):
|
||||
"prepare.py",
|
||||
"ble_diagnostic.py",
|
||||
"ble_options.py",
|
||||
"ble_wake.py",
|
||||
):
|
||||
files.append(
|
||||
("usr/lib/mission-core-node/insta360/" + name, (PACKAGING / name).read_bytes(), 0o644)
|
||||
|
||||
@@ -23,7 +23,7 @@ data = subprocess.check_output(["/usr/bin/dpkg-deb", "--fsys-tarfile", str(packa
|
||||
with tarfile.open(fileobj=io.BytesIO(data)) as archive:
|
||||
payload = archive.extractfile("usr/share/mission-core-node/insta360/payload.zip").read()
|
||||
bundle = json.load(archive.extractfile("usr/share/mission-core-node/insta360/bundle.json"))
|
||||
for name in ("ble_diagnostic.py", "ble_options.py"):
|
||||
for name in ("ble_diagnostic.py", "ble_options.py", "ble_wake.py"):
|
||||
packaged = archive.extractfile("usr/lib/mission-core-node/insta360/" + name).read()
|
||||
if packaged != (ROOT / "packaging" / name).read_bytes():
|
||||
raise ValueError("Packaged BLE module differs from the qualified source")
|
||||
|
||||
@@ -53,6 +53,30 @@ class OptionsTests(unittest.TestCase):
|
||||
{"acknowledged": True, "present": False, "value": None},
|
||||
)
|
||||
|
||||
def test_captured_heartbeat_is_not_an_options_response(self):
|
||||
heartbeat = bytes.fromhex("07000000050000")
|
||||
raw = reply(field(1, 95) + field(2, field(95, 1)))
|
||||
reader = codec.PacketReader()
|
||||
self.assertEqual(reader.feed(heartbeat[:4]), [])
|
||||
self.assertEqual(reader.feed(heartbeat[4:]), [])
|
||||
self.assertEqual(reader.heartbeats, 1)
|
||||
packets = reader.feed(heartbeat + raw[:9])
|
||||
self.assertEqual(packets, [])
|
||||
packets = reader.feed(raw[9:] + heartbeat)
|
||||
self.assertEqual(len(packets), 1)
|
||||
self.assertEqual(reader.heartbeats, 3)
|
||||
self.assertEqual(codec.decode_options_response(packets[0], 1, [95])[95]["value"], 1)
|
||||
|
||||
def test_short_control_packets_are_exact_and_bounded(self):
|
||||
for raw in ("07000000040000", "07000000050100", "0800000005000000"):
|
||||
with self.assertRaises(ValueError):
|
||||
codec.PacketReader().feed(bytes.fromhex(raw))
|
||||
reader = codec.PacketReader()
|
||||
for _ in range(64):
|
||||
self.assertEqual(reader.feed(bytes.fromhex("07000000050000")), [])
|
||||
with self.assertRaises(ValueError):
|
||||
reader.feed(bytes.fromhex("07000000050000"))
|
||||
|
||||
def test_explicit_zero_and_packed_option_list(self):
|
||||
packet = codec.PacketReader().feed(reply(field(1, b"\x5f") + field(2, field(95, 0))))[0]
|
||||
self.assertEqual(codec.decode_options_response(packet, 1, [95])[95]["value"], 0)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Synthetic identity and exact wake payload; no host Bluetooth access."""
|
||||
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
directory = Path(__file__).resolve().parents[1] / "packaging"
|
||||
sys.path.insert(0, str(directory))
|
||||
spec = importlib.util.spec_from_file_location("wake_under_test", directory / "ble_wake.py")
|
||||
wake = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(wake)
|
||||
sys.path.pop(0)
|
||||
|
||||
|
||||
def target(serial="SYNTHETIC34UQG5"):
|
||||
return {
|
||||
"serial": serial,
|
||||
"device_id": "instax4_" + hashlib.sha256(serial.encode()).hexdigest()[:32],
|
||||
"bluetooth_address": "02:00:00:00:00:01",
|
||||
"wakeup_enabled": True,
|
||||
}
|
||||
|
||||
|
||||
class WakeTests(unittest.TestCase):
|
||||
def test_spec_vector_has_one_company_id_and_fits_legacy_advertisement(self):
|
||||
data = wake.wake_payload(target())
|
||||
self.assertEqual(data.hex(), "0215094f5242495409ff0f0033345551473500000000e401")
|
||||
self.assertEqual(len(data) + 7, 31)
|
||||
|
||||
def test_unknown_wakeup_suffix_only_and_wrong_identity_are_rejected(self):
|
||||
for value in (
|
||||
target("34UQG5"),
|
||||
dict(target(), device_id="instax4_" + "0" * 32),
|
||||
dict(target(), wakeup_enabled=False),
|
||||
dict(target(), bluetooth_address="unknown"),
|
||||
):
|
||||
with self.assertRaises(ValueError):
|
||||
wake.wake_payload(value)
|
||||
|
||||
def test_sdk_identity_requires_full_serial_and_correct_mode(self):
|
||||
value = target()
|
||||
device = {"idVendor": "2e1a", "idProduct": "0002", "serial": value["serial"]}
|
||||
self.assertTrue(wake.matching_sdk({"devices": [device]}, value["device_id"]))
|
||||
for changed in (
|
||||
dict(device, serial="34UQG5"),
|
||||
dict(device, idVendor="070a", idProduct="4026"),
|
||||
):
|
||||
self.assertFalse(wake.matching_sdk({"devices": [changed]}, value["device_id"]))
|
||||
|
||||
def test_present_sdk_owner_blocks_before_any_bluetooth_access(self):
|
||||
with (
|
||||
patch.object(wake, "assert_no_usb_owner", side_effect=RuntimeError("owned")),
|
||||
patch.object(wake, "Inspector") as inspector,
|
||||
):
|
||||
with self.assertRaisesRegex(RuntimeError, "owned"):
|
||||
wake.run(target(), {})
|
||||
inspector.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user