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

Сторожевой таймер и что он значит для кода

Суть в трёх абзацах

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

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

Это правильно, а не поломка. Постамат стоит без присмотра и выдаёт людям вещи. Если софт завис, единственная альтернатива перезагрузке — прислать человека за 20 километров. Простое объяснение всей истории — docs/WATCHDOG-PRIMER.md в репозитории прошивки.

Пустой лог здесь ничего не значит

Сброс аппаратный, система о нём не предупреждена. Сохранённый хвост предыдущей загрузки выглядит абсолютно нормальным. Единственный надёжный признак — периодичность: смотреть надо не на то, что было перед сбросом, а на аптайм в момент сброса. Два сброса подряд с почти одинаковым аптаймом (у нас 294 и 298 секунд) — это таймер, и гипотезы про событие можно даже не строить.

Почему нельзя просто выключить

Первая мысль у всех одна. Команда выключения существует, программа её посылает, микросхема отвечает «принято». Разобрано 2026-08-31: надзиратель команду игнорирует.

  1. Подобрать «правильный аргумент» невозможно — в ветке выключения аргумент вообще не читается, уходит всегда одна и та же неизменная посылка.
  2. «Успех» означает только доставку. Возвращается число переданных байтов — подтверждение, что микросхема приняла посылку по шине, а не что она что-то сделала.
  3. Прямой опыт. Кормление остановили, выключение послали, за аптаймом следили. Плата перезагрузилась через 288 секунд после последнего кормления — ровно штатный период.

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

Выбор сузился до одного варианта: кормить.

Как кормить

int fd = open("/dev/mcu_7502", O_RDWR);
ioctl(fd, 0x40024D03, N); // N — окно в секундах
  • N — окно 10…60 секунд. Меньше десяти микроконтроллер не принимает: 0 и 1 дают отказ, 10 и 60 проходят (проверено на живом). Смысл вызова — «я жив, дай мне ещё N секунд», а не «сбрось счётчик».
  • 🔴 Аргумент передаётся значением, а не указателем. Передача указателя даёт отказ.
  • ⚠️ Успешный код возврата не доказывает ничего — это подтверждение доставки по шине. Единственное доказательство, что таймер укрощён, — аптайм, уверенно переваливший за пять минут.
  • 🔴 Путь /sys/devices/platform/misc_power_en/mcuне кормление: запись туда проходит успешно, а плата всё равно сбрасывается на 293-й секунде. Не поддаваться.

Kotlin ioctl не умеет — нужен маленький нативный вызов, по образцу cpp/tty.c.

Почему приложение обязано жить в /system/priv-app

Узел /dev/mcu_7502 имеет метку SELinux mcu_device. Эта метка разрешена домену priv_app и запрещена untrusted_app. Приложение, поставленное обычным adb install, попадёт в untrusted_app и кормить таймер не сможет — причём сегодня, в permissive, оно бы «работало», а после залочки молча перестало.

Это и есть настоящее обоснование системной установки, а не украшательство.

Как устроено сейчас и куда переходим

Кормит отдельный системный сервис из образа:

service mcuwd /system/bin/mcuwd daemon 20 --guard net.thepragmaticdev.postamat --grace 240

Период кормления 20 секунд, окно 40 (вдвое больше периода — намеренный запас: пропущенный цикл ещё не роняет плату). --grace 240 — фора на загрузку, первые четыре минуты кормим безусловно, иначе плата сбрасывалась бы на каждом старте, не дождавшись киоска.

--guard — не украшение, а весь смысл. Демон, кормящий безусловно, превращает страховку в фикцию: киоск висит, экран мёртвый, вещь никто не выдаёт — а плата исправно рапортует надзирателю, что всё прекрасно. Со сторожем кормление привязано к жизни процесса киоска: пропал процесс — перестаём кормить — плата перезагружается сама.

Почему --guard мало и чем его заменяют

Сторож --guard видит только, что процесс существует. Зависшее приложение процесс не теряет: экран мёртвый, а сторож честно докладывает «процесс на месте». Страховка превращается в фикцию, только на шаг дальше.

Вопрос «откуда кормить» закрыт. У демона появился ключ --require-ping: он кормит микроконтроллер, только пока приложение шлёт признаки жизни. А приложение шлёт их, только если плата замков недавно ответила кадром со сошедшейся контрольной суммой — то есть «жив» наконец означает «ответил», а не «существует».

Цепочка целиком:

плата замков отвечает кадром с верным XOR


LockBus ставит отметку живого обмена


HealthSupervisor решает, что киоск жив


пинг сервису mcuwd (нативный модуль, сокет)


mcuwd кормит микроконтроллер — и только тогда

Приложение при этом не открывает /dev/mcu_7502: узел монопольный, и пока его держит системный сервис, открыть его нельзя. Схема с пингом даёт три вещи даром: нет борьбы за дескриптор, нет окна без кормления при перезапуске приложения, и загрузочное окно закрывает уже работающая фора сервиса.

✅ Проверено на живой плате — обе половины

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

Что делалиЧто получилось
демон с --require-ping, пинги идутаптайм 460 → 824 секунды, ни одной перезагрузки
пинги остановилиплата перезагрузилась через 281 секунду и поднялась сама

Вторая строка и есть доказательство: это не обещание в документе, а измеренное поведение. 824 секунды — почти три штатных периода таймера подряд; 281 секунда — ровно то самое окно «около пяти минут», отсчитанное от последнего пинга.

Чего этот опыт НЕ доказывает

Пинги слал скрипт из adb с правами root, а не приложение, и система была в permissive.

❓ Достучится ли до сервиса приложение — то есть хватит ли ему прав на сокет из своего домена, и что скажет на это enforcing, — вопрос открытый. Это гипотеза, закрытая рассуждением («узел разрешён домену priv_app»), а не измерением. Проверять придётся на живой плате, на настоящем APK, вместе со сбором отказов перед залочкой.

Порядок перехода нельзя переставлять

Сперва APK, умеющий пинговать, потом строка сервиса в образе.

  1. APK с пингом → залить → убедиться, что аптайм уверенно перевалил за 10 минут;
  2. только потом строка сервиса меняется на --require-ping;
  3. снова проверить аптайм.

Если залить образ с --require-ping раньше, чем APK, плата начнёт перезагружаться каждые пять минут — и в логах ядра при этом не будет ничего, лог просто оборвётся на полуслове. Этот симптом уже стоил проекту суток.

Имя сокета должно совпадать

Имя сокета в нативном модуле киоска обязано совпадать с ключом --socket в строке сервиса. Расхождение не даст ни ошибки сборки, ни падения: пинги будут исправно уходить в никуда, а плата — перезагружаться каждые пять минут. Артефакт: NATIVE-GATE.md.

Чем это грозит на практике

  • adb root убивает кормильца, запущенного руками. Эта команда перезапускает службу отладки, и кормилец, стартовавший из adb shell, умирает вместе с ней — даже под setsid. Плата остаётся без кормления и примерно через пять минут уходит в сброс. Коварство в том, что причина и следствие разнесены на пять минут: команда, которая всё сломала, к моменту перезагрузки давно отработала, и виноватым выглядит что-то другое. Сервис из образа — ребёнок init, а не отладочной службы, и задевать его это не должно, ❓ но на живом после 2026-09-01 не перепроверялось. Правило пока прежнее: adb root без нужды не звать, а если позвали — проверить adb shell getprop init.svc.mcuwd.
  • Таймер тикает и во время прошивки. Микроконтроллеру безразлично, что делает процессор: он считает и когда плата стоит в загрузчике. Отсюда жёсткое окно на заливку — около 295 секунд с последнего кормления, а запись раздела super занимает ~140 секунд. Подробности — сборка и заливка.
  • К залочке. Сейчас кормление идёт из домена init, и в логе отказов SELinux эти вызовы есть. В enforcing они будут отклонены, кормление прекратится молча, и плата уйдёт в цикл «пять минут — сброс». Нужна своя политика.

Как проверить, что всё хорошо

Единственное доказательство — аптайм, уверенно переваливший за пять минут.

adb shell getprop init.svc.mcuwd # ожидаем running
adb shell uptime # ожидаем заметно больше пяти минут

Контрольные измерения: 829 секунд аптайма при безусловном кормлении и 460 → 824 секунды при кормлении по пингу — оба без единого сброса, при том что без кормления сбросы шли стабильно на 281–298-й секунде.

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

Технический справочник со всеми номерами команд — docs/MCU-WATCHDOG.md в репозитории прошивки.