Перейти к основному содержимому

Что можно обновить по воздуху, а за чем придётся ехать

Вопрос не праздный: постаматы стоят по городу, а часть из них — за 20 километров. Цена ошибки в этом разделе измеряется поездками с ноутбуком и отвёрткой.

Граница проходит ровно по одному признаку: меняется только JavaScript и картинки — можно по сети; меняется что угодно нативное или системное — нужен новый образ и физический доступ к плате.

Слои и их цена

СлойЧто там лежитКак обновляется
Сервер и панельapps/api, apps/panelобычный деплой, устройства не трогаем
JS-бандл киоска и ассетыэкраны, тексты, правила из packages/domainпо сети — после того, как состоится нативная заливка с expo-updates
Нативные модули киоскаmodules/serial, modules/devstatsновый APK → новый образ → заливка на плате
Манифест, права, роль рабочего столаplugins/withHomeLauncher.jsто же
Системные сервисыкормление сторожевого таймера, запасной адрес eth0то же
Ядро и его командная строкаSELinux, параметры панелизаливка boot, отдельная процедура
Политика SELinux, залочказаливка, и проверять по одному шагу

Главное правило: нативная заливка одна

Отсюда растёт весь NATIVE-GATE.md в корне репозитория, и это самый важный документ для планирования.

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

Список того, что обязано попасть в ту сборку, живёт в NATIVE-GATE.md и меняется — здесь он не дублируется. Три блокера оттуда стоит знать наизусть:

  • expo-updates — сам механизм доставки JS. Без него следующая правка снова означает выезд.
  • Ключ подписи обновлений. Сертификат вшивается в APK; созданный позже означает переливку APK, то есть ту же поездку. ✅ Сам сертификат уже выпущен и проверен — подпись обновлений ниже.
  • runtimeVersion строкой. Обновление меняет JS, но не нативную часть. Если новый JS зовёт нативный модуль, которого в залитом APK нет, приложение упадёт на устройстве, до которого надо ехать. runtimeVersion не даёт обновлению доехать до сборки без нужного модуля, и задавать его надо руками: автоматика привяжет его к версии SDK, а у нас нативная часть меняется не тогда, когда меняется SDK.
Механизм выбран и подписан, но до устройств ещё не доехал

expo-updates выбран, сервер обновлений и подпись настроены, отдельное имя ota поднято. Чего не хватает — наличия самого механизма в залитом APK: это открытый пункт NATIVE-GATE.md (оперативный список, сверяйтесь с ним, а не с этой страницей), а не готовая возможность. До первой правильной заливки любая правка киоска — это выезд.

Подпись обновлений настроена

Обновление приезжает на постамат, который стоит на улице и ходит в сеть по Wi-Fi через неподписанный вендорский драйвер в ядре 4.19. Драйвер разбирает пакеты из эфира, до всякого фильтра на роутере. Без проверки подписи любой, кто сумеет подменить ответ сервера обновлений, получает исполнение своего кода на плате.

Схема обычная для expo-updates: сервер подписывает манифест приватным ключом, устройство проверяет публичной половиной, вшитой в APK.

Публичная половинаapps/kiosk/certs/code-signing.pem — секрета в ней нет, лежит в репозитории
Приватный ключтолько на сервере и в копии у владельца. В репозиторий не попадает
Годен до2036-08-29

Проверена вся цепочка, а не половина: подпись, сделанная сервером, этим сертификатом проверяется, а подменённое тело этой же подписью отвергается. То есть проверено и то, что законное обновление проходит, и то, что подделанное не проходит.

Истёкший сертификат МОЛЧА останавливает обновления

Не ошибкой, не тревогой — устройство просто перестаёт принимать обновления, и выглядит это как «обновления почему-то не доходят». Тем же свойством обладает и замена сертификата после сборки APK: новый не подойдёт к уже залитым терминалам, и починить это можно только выездом к каждому шкафу — сертификат живёт в нативной части.

Потеря приватного ключа — то же самое с другой стороны: подписать новое обновление станет нечем, и выход один — новая сборка APK с новым сертификатом, то есть объезд всех постаматов.

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

Два вопроса, которых на обычном телефоне не бывает

Сеть. В проде связь по Wi-Fi, и на роутере фильтр — наружу разрешено только до нашего backend. Значит обновления обязаны раздаваться с нашего сервера, а не со стороннего сервиса: схема, ходящая в чужое облако, здесь просто не поднимется. Под обновления отведено отдельное имя ota — и манифест, и ассеты живут на нём, почему так. В фильтр на роутере объекта заносятся ровно два имени: api и ota.

Страховка. Плохой JS-бандл на устройстве, у которого нет ни кнопки «назад», ни рабочего стола, ни клавиатуры, — это чёрный экран без выхода. Плата в этом случае будет перезагружаться каждые пять минут и снова показывать тот же чёрный экран. Значит обновление обязано откатываться само, а не по команде оператора.

Ловушка, которая уже сработала

осторожно
Копия в /data затеняет запечённую в образ при равном versionCode

Приложение живёт в образе (/system/priv-app), но установленная поверх копия перекрывает его, если номер версии совпадает. Один раз это уже сделало заливку холостой: код на плате был старый, а все были уверены в обратном.

При проверке новой версии — поднимать versionCode и делать adb uninstall.

Что требует именно выезда

Заливка образа идёт не через Android, а через загрузчик: клеммник UART → консоль U-Boot → режим внешнего диска → запись с ноутбука. Этот путь не зависит от того, что происходит внутри системы, — и именно поэтому он работает даже на «кирпиче».

Но он требует провода. Клеммник выведен на заднюю стенку, разбирать корпус не нужно, однако если постамат стоит запертым на объекте, «подключить провод» означает выезд человека с ключом. Это не техническое ограничение, а организационное, и учитывать его надо до того, как устройства разъедутся по городу.

Подробности процедуры — сборка и заливка.

Практический вывод для планирования

Всё, что может понадобиться менять часто — тексты, цены, тайминги, правила выдачи, вёрстка экранов, — держите в JS и в конфигурации с сервера. Всё, что уезжает в нативный слой или в системные сервисы, планируйте как редкое событие с выездом: батчами, с проверкой на одном устройстве и с готовым откатом.