TL;DR: Рассказываю про свой опыт перепрошивки телефона и превращения его в маленький домашний сервер. Успешно перепрошил Oneplus 6 на Mobian, настроил ssh и прогнал бенчмарки. Спойлер: оказалось гораздо проще, чем я сам ожидал.

Для начала представлюсь – я Деревянкин Павел, менеджер продукта электронных визиток MyQRcards, в прошлом мобильный разработчик в этом же продукте.

В последнее время (в эпоху повального увлечения нейросетями) я столкнулся с тем, что API, которые я размещаю для всяких домашних экспериментов и микропроектов, уже не помещаются на крохотном арендованном облаке. Кроме того, пришло письмо о повышении арендной платы. Посмотрев на всё это, я решил, что пора обзавестись домашним сервером, который я уже давно хотел, и перенести на него то, что не требует вечного доступа из внешнего интернета.

Можно было для этой цели взять какой-нибудь из старых ноутбуков, что пылятся у меня за спиной, или купить Raspberry Pi, но я решил, что это будет слишком скучно, потому что, честно говоря, затеял всё это для развлечения, а не для гонки за реальным результатом, этого мне и на работе хватает. Я было полез на всем известный сайт в поисках б/у дешёвого Mac на M1 или чём повкуснее. Искал как сами miniPC, так и ноутбуки со сломанными дисплеями. Но, покопавшись в объявлениях с полчаса, понял, что это не такое дешёвое удовольствие, как хотелось бы. Да и что там будет веселее, кроме повышенной мощности?

И вот в этот момент я подумал: ну я же вырос из мобильной разработки, а M1 – это сильно ушедший вперёд мобильный процессор (не бейте ногами, я сильно упрощаю, да).

Почему бы не поискать возможность накатить сервак на телефон?

Главный минус ПК / ноутбука для меня был в том, что он довольно громоздкий, а зачастую ещё и шумный. Часть miniPC не имеют подобных проблем, но всё равно стоят дороже побитых бэушных смартфонов. Поэтому я решил изучить вопрос, а возможен ли вариант с телефоном.

Небольшой ресёрч привёл меня к проекту Mobian. Думаю, из названия более-менее понятно, что это за зверь. Debian для мобильных устройств, поддерживаемый энтузиастами. В вики проекта я нашёл, что среди аж двух (!) поддерживаемых Android-устройств притаился OnePlus 6. Как гордый и счастливый пользователь OnePlus 8, я решил, что это моя остановочка. Важной причиной, по которой я выбрал этот телефон на самом деле, – он имеет версию с 8 Гб оперативной памяти и накопителем на 128 Гб, при этом под капотом не самый плохой Snapdragon 845, который, хоть и устарел, но имеет 8 ядер и был довольно крут для 2017 года (ох, уже почти десять лет назад).

Телефон на руках, и вот тут-то и начинается моё приключение.

Свой путь я решил опять же начать с известного всем сайта объявлений, где начал искать подходящий девайс. Мне важно было, чтобы он работал и был по низу рынка, а его состояние / батарея / экран меня волновали мало. Да, подобных аппаратов оказалось очень мало: то ли уже слишком много времени прошло и все умерли, то ли всё уже разобрали другие энтузиасты. Но всё-таки я нашёл десяток устройств с 8 Гб оперативной памяти в разном состоянии. Среди них был даже один вариант в заводской упаковке и с полным комплектом дороже, чем за 20 тысяч, но я, естественно, выбрал самый дешёвый рабочий телефон с разбитыми экраном и задней крышкой за 3500. Споткнувшись об отмену первой доставки и доплатив ещё 500 за отправку “СДЭКом”, через неделю я наконец-то дотянулся до своего сокровища. Мне не терпелось начать ковыряться в этом молодце и окунуться в пучину мобильного серверного безумия.

Тот самый страдалец, над которым я решил поставить опыт.
Тот самый страдалец, над которым я решил поставить опыт.

К слову, я никогда до этого не углублялся в работу с Linux. Большую часть жизни я провёл в винде и бед не знал, а всё знакомство было через редкие ноутбуки без винды, куда я её поскорее накатывал, или, собственно, через облачные сервера. Я понимал, что выбранный путь вряд ли будет прост, но останавливаться не хотел. Почему-то у меня было радостное предвкушение предстоящих приключений, масштаб которых мне только предстояло узнать.

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

Подготовка к установке Mobian

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

Итак, у меня в руках свежеприобретённый телефон, сброшенный до заводских параметров. Я открыл гайд прошивки телефона на сайте проекта Mobian и приступил к делу. Плюс перед установкой я скачал стабильную версию дистрибутива для этого телефона (отсюда - https://images.mobian.org/qcom/) - mobian-sdm845-phosh-13.0.tar.xz.

Первое, что я сделал, – проверил, что на телефоне стоит последняя версия заводской ОС, потому что Mobian использует обращения к вендерному софту (драйвера и прочее) и, что не менее важно, стандартный bootloader, из которого происходит запуск системы.

Следующий шаг – нужно было разблокировать тот самый bootloader и fastboot OEM unlock, чтобы иметь возможность накатить на телефон стороннюю ОС. Инструкция для этого была не на сайте Mobian, а на сайте проекта postmarketOS. Чтобы получить доступ к загрузчику, сначала следует разблокировать режим разработчика, для чего достаточно найти номер сборки прошивки в настройках и кликнуть на неё несколько раз. Дальше нужно пойти уже в настройки «для разработчиков» и включить режим «Заводская разблокировка» (или «OEM-разблокировка» / «Enable OEM unlocking» – я смотрел название пункта в своём OnePlus 8, и оно может отличаться от того, что в последней версии OnePlus 6), чтобы разблокировать загрузчик.

После этого я сразу выключил телефон, отключил его от USB и зажал кнопку выключения и кнопку увеличения громкости звука, как говорила инструкция, чтобы попасть в flash mode и разблокировать fastboot.

Если всё сделать правильно, то при зажатых кнопке выключения и увеличения громкости через некоторое время появится надпись «fastboot», а ещё через пару секунд различные разноцветные режимы, которые у меня бесконечно сменялись, из-за неотпущенной кнопки громкости и информацию об устройстве, качелькой громкости нужно выставить режим START (зелёного цвета) и ничего не трогать. 

Наконец, можно подключить телефон к компьютеру, чтобы подготовить тот самый fastboot, который и позволит накатить прошивку. Для этого нужно установить на компьютер Android SDK Platform-Tools (это если на Windows / Mac, а на Linux – пакет android-tools). После чего открываем консоль / терминал и пишем команду

fastboot oem unlock

Если вы всё сделали правильно, на экране телефона появится запрос подтверждения. Нужно подтвердить включение fastboot, и устройство сбросится до заводского состояния, но произойдёт OEM unlock, который позволит накатывать сторонние прошивки. Снова выключаем телефон, отключаем USB-кабель, переходим в режим fastboot (снова зажимая увеличение громкости и кнопку выключения), выбираем режим start (не нажимая ничего). Подключаем телефон через USB-кабель. Пора начинать прошивку.

Установка Mobian

Тут уже все действия выполняются через командную строку / терминал.

Нам нужно выполнить следующую последовательность команд:

fastboot --set-active=a
fastboot flash boot mobian-sdm845-phosh-20251002.boot-enchilada.img
fastboot erase userdata
fastboot -S 100M flash userdata mobian-sdm845-phosh-20251002.boot-enchilada.img
fastboot erase dtbo
fastboot reboot 
Что они делают

fastboot --set-active=a – для начала необходимо выбрать, на каком слоте (A или B) установить прошивку. Если вы не знаете, на каком, советую выбрать слот A и не думать об этом. Как я понял, почти всегда прошивки ставятся на слот A.

fastboot flash boot mobian-<processor>-phosh-YYYYMMDD.boot-<model>.img – эта команда прошивает boot-раздел телефона, по сути подменяя ядро Android на ядро Mobian. Для каждого устройства идёт свой образ. Образ, который используется в моей команде выше, следует выгрузить из скачанного архива mobian-sdm845-phosh-13.0.tar.xz, внутри которого на самом деле больше, чем один образ. Для OnePlus 6 требуется именно образ enchilada.

fastboot erase userdata - Полностью очищает раздел пользовательских данных, чтобы данные Андроид не ломали поведение Mobian.

fastboot -S 100M flash userdata mobian-<processor>-phosh-YYYYMMDD.rootfs.img – записывает root filesystem (rootfs) в userdata. rootfs.img – это вся система Mobian (аналог / в Linux). -S 100M — разбивает передачу на куски по 100 Mб (нужно из-за ограничений fastboot).

fastboot erase dtbo – удаляет раздел DTBO (Device Tree Blob Overlay), так как DTBO от Android может конфликтовать с Linux.

fastboot reboot – собственно, перезагрузка устройства. Если все предыдущие шаги были сделаны правильно, то устройство при перезагрузке попытается загрузиться из выбранного слота с новым boot и новым rootfs, что в свою очередь, должно позволить запуститься Mobian.

Первый запуск может занять много времени, потому что под капотом будет происходить подготовка файловой системы. Но вот телефон включается и просит пароль. После установки на устройстве появляется пользователь mobian с паролем 1234. Очевидно, это небезопасно, и нужно подготовить Mobian к реальному использованию.

Ну а пока можно полюбоваться главным экраном устройства.
Ну а пока можно полюбоваться главным экраном устройства.

После загрузки Mobian следующий этап заключался в том, чтобы получить устойчивый удалённый доступ к системе по SSH и не набирать команды вручную на телефоне.

Для начала надо подключить телефон к сети. Пробуем смахнуть сверху вниз, открывается быстрое меню, но Wi-Fi там нет, идём в настройки (Settings) и обнаруживаем, что и там нет такого пункта. Что ж. Открываем консоль (Console) на устройстве и вводим серию команд:

sudo rfkill unblock all
nmcli radio wifi on
nmcli dev wifi list
sudo systemctl restart NetworkManager

После этих действий у меня в меню настроек появился пункт Wi-Fi, и подключиться к нему не составило труда.

Чтобы не мучиться с тем, что каждую минуту телефон уходит в сон, можно сходить в Settings – Power – снизу вкладка Power Saving. Здесь можно настроить Automatic Suspend. Я его просто выключил в режиме работы от сети.

Теперь нам нужно как-то с компьютера подключиться к телефону и настроить SSH. Запрашиваем через консоль на телефоне:

hostname -I

И в ответ получаем два IP:

- 10.66.0.1 — сетевой интерфейс по USB / альтернативному каналу

- 192.168.0.112 — IP телефона в локальной Wi-Fi сети

Теперь нужно на телефон накатить open-ssh-server:

sudo apt update
sudo apt install -y openssh-server
sudo systemctl enable --now ssh

Наконец, мы можем перестать писать запросы в консоли на телефоне (как же это было неудобно!). Можно подключиться компьютером через SSH, используя ip в локальной сети Wi-Fi и начать настраивать как обычный сервер.

Сначала проверяем обычный вход по рабочему Wi-Fi-адресу телефона:

ssh mobian@192.168.0.112

Если всё в порядке, отключаемся от ssh (или открываем новую консоль на ПК). Подготовим ключи, по которым будем подключаться к устройству:

ssh-keygen -t ed25519 -f "$env:USERPROFILE\.ssh\mobian_oneplus6_ed25519" -C "mobian-oneplus6-2026-04-05"

Пока старый доступ ещё работает, создаём на телефоне нового пользователя sshadmin:

ssh mobian@192.168.0.112 "sudo useradd -m -s /bin/bash sshadmin"

Сразу настраиваем для него .sshкаталог с правами:

ssh oneplus6-mobian "sudo install -d -m 700 -o sshadmin -g sshadmin /home/sshadmin/.ssh"

Добавляем новому пользователю публичный ключ:

ssh mobian@192.168.0.112 "printf '%s\n' 'PASTE_PUBLIC_KEY_FROM_oneplus6_admin_ed25519.pub_HERE' | sudo tee /home/sshadmin/.ssh/authorized_keys > /dev/null && sudo chown sshadmin:sshadmin /home/sshadmin/.ssh/authorized_keys && sudo chmod 600 /home/sshadmin/.ssh/authorized_keys"

Даём права администрирования:

ssh mobian@192.168.0.112 "printf 'sshadmin ALL=(ALL) NOPASSWD: ALL\n' | sudo tee /etc/sudoers.d/90-sshadmin > /dev/null && sudo chmod 440 /etc/sudoers.d/90-sshadmin"

Сразу блокируем пароль этого пользователя:

ssh mobian@192.168.0.112 "sudo passwd -l sshadmin"

Проверяем, что пользователь работает, если всё ок, переключаем sshd в режим: только ключи и только sshadmin

ssh sshadmin@192.168.0.112 "printf 'PubkeyAuthentication yes\nPasswordAuthentication no\nKbdInteractiveAuthentication no\nPermitRootLogin no\nAllowUsers sshadmin\n' | sudo tee /etc/ssh/sshd_config.d/99-sshadmin-only.conf > /dev/null"

Проверяем, что старый вход через mobian недоступен:

ssh mobian@192.168.0.112

И, если всё хорошо, можем считать, что базовая настройка уже готова. Доступ к устройству по ssh возможен только через пользователя sshadmin и только по ключу. Пароль выключен. Я не особо разбираюсь в сетевой безопасности, но посчитал, что на этом этапе я базово защитился от внешних угроз. После этого через настройки на устройстве я поменял пароль от основного профиля, чтобы было удобнее заходить в телефон с привычным паролем.

По большей части всё. Устройство подготовлено. Позже я накатил на него docker.io и поместил в него пару своих небольших API для домашних проектов, пересобрав их под arm64-linux, и даже смог настроить CI / CD, который собирает, а затем деплоит эти проекты на телефон. Телефон я подключил к розетке через адаптер на 10 Вт (чтобы не было лишнего нагрева, но питания хватало на всё время использования) и закинул поближе к роутеру. Теперь у меня есть маленький сервер.

Бенчмарки

Говоря про целесообразность этого решения, нельзя не задать вопрос: “А какие мощности оно может дать?”. Этот вопрос я задал Copilot и попросил того провести базовые бенчмарки получившегося сервера. Если кому-то будут интересны дополнительные подробности, спрашивайте в комментариях – может, проведу ещё опыты.

А пока немного голых цифр и интерпретаций от нейросети, так как я, честно скажу, в теме плаваю.

Исходные данные:

Устройство

  • модель: OnePlus 6;

  • SoC: Qualcomm Snapdragon 845;

  • CPU: 8 ядер;

  • производительные ядра: Kryo 385 Gold до 2.649 ГГц;

  • энергоэффективные ядра: Kryo 385 Silver до 1.766 ГГц;

  • доступная системе RAM: 7.4 GiB;

  • swap: 3.7 GiB через zram.

  • дистрибутив: Mobian на базе Debian Trixie;

  • ядро: 6.12-sdm845;

  • архитектура: aarch64.

Использованные инструменты

- sysbench 1.0.20;

- fio 3.39;

- stress-ng 0.19.02;

Сырые результаты

CPU: sysbench

| Тест | Результат |

| sysbench cpu --threads=1 --time=30 | 2777.18 events/s |

| sysbench cpu --threads=8 --time=30 | 14375.09 events/s |

Дополнительно:

- средняя latency в однопоточном тесте: около 0.36 ms;

- средняя latency в 8-поточном тесте: около 0.56 ms;

- 95-й перцентиль в 8-поточном тесте: около 1.12 ms.

Память: sysbench

| Тест | Результат |

| sysbench memory --threads=8 | 40407.88 MiB/s |

Это примерно 39.46 GiB/s эффективной скорости записи в рамках данного синтетического теста.

Диск: fio

| Тест | Результат |

| Последовательная запись, 1 GiB | 213 MiB/s |

| Последовательное чтение, 1 GiB | 339 MiB/s |

| Случайное смешанное чтение, 4K, 70/30 | 17.8 MiB/s |

| Случайная смешанная запись, 4K, 70/30 | 7.7 MiB/s |

| Random mixed read IOPS | около 4561 IOPS |

| Random mixed write IOPS | около 1960 IOPS |

По latency в random-тесте:

- средняя latency на random read: около 175 us;

- средняя latency на random write: около 90 us;

- 99-й перцентиль random read: около 734 us;

- 99-й перцентиль random write: около 161 us.

Длительная CPU-нагрузка: stress-ng

| Тест | Результат |

| stress-ng --cpu 8 --timeout 60 --metrics-brief | 38186 bogo ops total |

| Скорость в real time | 635.91 bogo ops/s |

Этот тест нельзя напрямую сравнивать с sysbench, но он показывает, что устройство выдержало минутную 8-поточную CPU-нагрузку без сбоев.

Сравнение с прочими решениями для серверов:

Для CPU удобнее всего смотреть на внешний CPU Mark, потому что именно он позволяет свести телефон, Raspberry Pi, серверный x86-хост и мини-PC в одну таблицу.

| Платформа | CPU-референс | CPU Mark | Относительно OnePlus 6 |

| OnePlus 6 | QTI SDM845 | 3713 | 1.00x |

| Raspberry Pi 4 | Broadcom BCM2711 | 852 | 0.23x |

| Бюджетный VPS-хост | Intel Xeon E5-2680 v4 class | 17219 | 4.64x |

| МiniPC | Intel N100 | 5341 | 1.44x |

Что это означает на практике:

- по суммарной CPU-мощности OnePlus 6 заметно сильнее Raspberry Pi 4;

- современный mini-PC на N100 уже быстрее телефона примерно на 44% по агрегированному CPU Mark;

- старый серверный Xeon-хост для VPS выглядит намного сильнее, но маленький VPS получает только долю этого CPU и ещё делит хост с соседями.

По памяти корректнее сравнивать не только объём, но и класс пропускной способности. Для OnePlus 6 у нас есть собственный sysbench memory, для остальных платформ ниже приведён ориентир по типичной памяти и её классу.

| Платформа | Типичный объём RAM | Числовой ориентир по памяти |

| OnePlus 6 | 7.4 GiB доступно | 40407.88 MiB/s в нашем sysbench memory |

| Raspberry Pi 4 | 4-8 GiB | LPDDR4-3200 class, теоретически около 12.8 GiB/s |

| Бюджетный VPS | 2-4 GiB | эффективная bandwidth гостя обычно находится в классе хостовой DDR4, грубо 20-40 GiB/s, но сильно зависит от гипервизора и соседей |

| Мини-PC на N100 | 8-16 GiB | DDR4-3200 / LPDDR5-4800 class, теоретически около 25.6-38.4 GiB/s |

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

- OnePlus 6 не выглядит урезанным по RAM для своей роли;

- против Raspberry Pi 4 телефон обычно имеет и больший объём памяти, и более сильный memory profile;

- против N100 телефон уже не выглядит слабо именно по памяти, там решающим фактором чаще становится не RAM, а диск и общая серверность платформы.

По диску различия ощущаются сильнее всего, поэтому здесь удобнее смотреть на последовательные скорости и random I/O отдельно.

| Платформа | Типичное хранилище | Seq write | Seq read | Random 4K / мелкие записи |

| OnePlus 6 | встроенное UFS | 213 MiB/s | 339 MiB/s | около 4561/1960 IOPS read/write в нашем mixed-тесте |

| Raspberry Pi 4 | хорошая microSD UHS-I | 20-50 MiB/s | 40-90 MiB/s | обычно порядка 500-1500 IOPS |

| Бюджетный VPS | SATA SSD class storage | 200-500 MiB/s | 200-550 MiB/s | обычно порядка 5000-20000 IOPS |

| Мини-PC на N100 | локальный NVMe SSD | 1500-3500 MiB/s | 1500-3500 MiB/s | обычно 30000-200000 IOPS |

Из этой таблицы уже видно главное:

  • против Raspberry Pi 4 на microSD телефон обычно выигрывает по последовательному диску и по общему ощущению от storage;

  • против обычного SSD-класса VPS телефон уже упирается в random I/O;

  • против miniPC с NVMe разрыв становится очень большим, особенно для контейнеров, логов и БД.

Короткий итог сравнения

Если свести сравнение к нескольким результатам:

  • по CPU OnePlus 6 находится выше Raspberry Pi 4, но ниже N100 miniPC;

  • по RAM OnePlus 6 выглядит нормально и конкурентноспособно, особенно учитывая растущие цены за оперативную память. 8Гб не самой медленной памяти — звучит неплохо;

  • по диску телефон для лёгких сервисов ещё жизнеспособен, но именно скорость хранилища сильнее всего отделяет его от VPS на SSD и тем более от miniPC с NVMe.

Заключение

Так стоило ли оно всё того?

Для меня – однозначно да. Потому что, потратив четыре часа, получил удовольствие, когда всё заработало, и теперь у меня есть недорогой маленький сервер, с которым я могу делать что хочу. На момент написания этих слов он уже трое суток крутит один маленький API-сервис (а на момент публикации уже почти неделю три API-сервиса и успел перегрузить роутер, из-за чего пришлось 11 часов перестраивать домашнюю сеть) и не испытывает с этим никаких проблем.

Даже во время тестов нагрузки перегрева не было замечено, максимальная замеченная температура была порядка 35 градусов по Цельсию. Думаю, что он может перегреваться, если загрузить его на 100%, но в моём сценарии такая долгосрочная нагрузка не планируется.

Я написал статью, чтобы поделиться с вами своим опытом. Возможно, кому-то ещё это будет интересно, а кому-то и полезно. Если есть какие-то вопросы или замечания, пишите, буду рад ответить в комментариях.

Комментарии (8)


  1. startsevdenis
    13.04.2026 20:38

    В прошлых похожих темах уже вставал вопрос, что делать с возможным вздутием батареи когда телефон постоянно подключен к ЗУ, как то прикидывали для себя этот вопрос?


    1. FA72 Автор
      13.04.2026 20:38

      Скажу честно, именно касательно вздутия особо не думал, но понимал, что деградация батареи и перегрев - довольно большие проблемы для данного решения. Я уже купил под него отдельный 10-ваттный адаптер, чтобы питание шло не с очень большим током и начал изучать вопрос ограничения заряда, чтобы батарея держалась в зоне 40-80 процентов. Но пока до конца это не реализовал.

      Вообще, честно говоря, я готов пойти даже на радикальные меры вплоть до перепайки платы питания, чтобы полностью удалить батарею из устройства (например, прямо сейчас нашёл, как это сделали с аналогичным устройством вот тут -https://blog.kedio.co/post/how-to-run-a-oneplus-6t-without-battery/ с примерно той же мотивацией). Это, конечно, чуть-чуть ухудшит "надёжность сервера", что в нём по сути есть встроенный бесперебойник.

      Но на текущий момент план - настроить потребление-разряд, чтобы держать в безопасном диапазоне, не перегревать, смотреть, что будет дальше. Думаю, что буду "ревизировать" устройство на состояние батареи раз в какое-то время (месяц) и, если проблема всё же возникнет, то уже ставить вопрос о "хирургическом вмешательстве". Либо с целью замены, либо с целью удаления батареи.

      Но опять же, глобально этот вопрос пока не продумывал и возможно первые поверхностные мысли неидеальны.


      1. FA72 Автор
        13.04.2026 20:38

        В общем, я чуть-чуть изучил вопрос, управление на стороне устройства зарядкой выглядит немного костыльным. Потому что, если начинаешь управлять зарядом программно, то драйвер зарядки "тупеет" и перестаёт показывать реальное состояние кабеля - подключен/отключен. Он стандартно всегда считает, что зарядка подключена (и на входе просто 0,01 А). Я накидал в итоге такой скрипт управления, который позволяет и состояние заряда проверять и температуру контролировать и при этом всём раз в 15 минут проверять идёт зарядка или нет.

        battery-limiter.service
        [Unit]
        Description=Battery charge limiter (40%%-80%%)
        After=multi-user.target
        
        [Service]
        Type=simple
        ExecStart=/usr/local/bin/battery-limiter.sh
        # Re-enable charging if service stops so phone doesn't die
        ExecStopPost=/bin/sh -c 'echo 500000 > /sys/class/power_supply/pmi8998-charger/current_max'
        Restart=always
        RestartSec=10
        StandardOutput=journal
        StandardError=journal
        
        [Install]
        WantedBy=multi-user.target
        battery-limiter.sh
        #!/bin/bash
        # battery-limiter.sh v9 -- keeps OnePlus 6 battery between 40% and 80%
        # Deployed as systemd service: battery-limiter.service
        #
        # CONSTRAINT: any write to pmi8998-charger/current_max permanently
        # corrupts the driver's readback (online/status/voltage_now freeze).
        # All sensing uses bq27411-0 fuel gauge (independent i2c chip).
        #
        # Three states:
        #   charging -- current_max=500mA, battery charging toward 80%
        #   paused   -- current_max=0, battery discharging toward 40%
        #               every 15 min: probe cable by restoring driver for 10s
        #   idle     -- driver has full control, cable is disconnected
        #               poll bq27411/status every minute to detect charger
        #
        # Temp safety: pause at 45C, resume below 40C
        set -uo pipefail
        
        CHARGE_MAX=80
        CHARGE_MIN=40
        CHARGE_CURRENT=500000   # 500mA
        TEMP_MAX=450             # 45.0C
        TEMP_RESUME=400          # 40.0C
        CHECK_INTERVAL=60        # 1 minute
        PROBE_CYCLES=15          # 15 cycles = 15 min between cable probes
        PROBE_SETTLE=10          # seconds to let driver settle during probe
        HEARTBEAT_CYCLES=5       # 5 cycles = 5 min
        
        SYS_CMAX="/sys/class/power_supply/pmi8998-charger/current_max"
        SYS_CAP="/sys/class/power_supply/bq27411-0/capacity"
        SYS_TEMP="/sys/class/power_supply/bq27411-0/temp"
        SYS_CUR="/sys/class/power_supply/bq27411-0/current_now"
        SYS_BQST="/sys/class/power_supply/bq27411-0/status"
        
        STATE="idle"
        TEMP_LOCKED=0
        PROBE_CTR=0
        HEARTBEAT_CTR=0
        
        log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $1"; }
        fmt_temp() { echo "$((${1}/10)).$((${1}%10))C"; }
        
        write_cmax() {
            if ! echo "$1" > "$SYS_CMAX" 2>/tmp/cmax_err; then
                log "WARN: write current_max=$1 failed: $(cat /tmp/cmax_err)"
            fi
        }
        
        enter_charging() {
            write_cmax "$CHARGE_CURRENT"
            STATE="charging"
            PROBE_CTR=0
            log "CHARGING (${CHARGE_CURRENT}uA, cap=${1}%)"
        }
        
        enter_paused() {
            write_cmax 0
            STATE="paused"
            PROBE_CTR=0
            log "PAUSED (cap=${1}%)"
        }
        
        enter_idle() {
            # Give control back to driver -- write a normal value so it
            # can re-negotiate, then hands off.
            write_cmax "$CHARGE_CURRENT"
            STATE="idle"
            PROBE_CTR=0
            log "IDLE -- driver has control (cable absent)"
        }
        
        # --- Validate sysfs ---
        for node in "$SYS_CMAX" "$SYS_CAP" "$SYS_TEMP" "$SYS_CUR" "$SYS_BQST"; do
            [ -e "$node" ] || { log "ERROR: missing $node"; exit 1; }
        done
        
        log "Starting: range ${CHARGE_MIN}%-${CHARGE_MAX}%, temp $(fmt_temp $TEMP_MAX), probe every ${PROBE_CYCLES}min"
        
        # --- Initial state: start idle, let first cycle decide ---
        CAP=$(cat "$SYS_CAP" 2>/dev/null) || CAP=50
        BQST=$(cat "$SYS_BQST" 2>/dev/null) || BQST="Unknown"
        if [ "$BQST" = "Charging" ]; then
            # Charger is connected right now
            if [ "$CAP" -ge "$CHARGE_MAX" ]; then
                enter_paused "$CAP"
            else
                enter_charging "$CAP"
            fi
        else
            log "IDLE -- no charge detected at start (bq=${BQST}, cap=${CAP}%)"
        fi
        
        # --- Main loop ---
        while true; do
            sleep "$CHECK_INTERVAL"
        
            CAP=$(cat "$SYS_CAP" 2>/dev/null)  || continue
            TEMP=$(cat "$SYS_TEMP" 2>/dev/null) || continue
            CUR=$(cat "$SYS_CUR" 2>/dev/null)  || continue
        
            # --- Heartbeat ---
            HEARTBEAT_CTR=$((HEARTBEAT_CTR + 1))
            if [ "$HEARTBEAT_CTR" -ge "$HEARTBEAT_CYCLES" ]; then
                HEARTBEAT_CTR=0
                BQST=$(cat "$SYS_BQST" 2>/dev/null) || BQST="?"
                log "STATUS state=${STATE} cap=${CAP}% temp=$(fmt_temp $TEMP) cur=${CUR}uA bq=${BQST}"
            fi
        
            # --- Temperature safety (always wins) ---
            if [ "$TEMP" -ge "$TEMP_MAX" ] && [ "$STATE" = "charging" ]; then
                log "OVERHEAT $(fmt_temp $TEMP) -- pausing"
                TEMP_LOCKED=1
                enter_paused "$CAP"
                continue
            fi
            if [ "$TEMP_LOCKED" = "1" ] && [ "$STATE" != "idle" ]; then
                if [ "$TEMP" -lt "$TEMP_RESUME" ]; then
                    log "COOLED $(fmt_temp $TEMP) -- temp lock off"
                    TEMP_LOCKED=0
                else
                    # Stay paused/enforce while hot
                    if [ "$STATE" = "paused" ] && [ "$CUR" -gt 0 ]; then
                        write_cmax 0
                        log "ENFORCE 0 (temp lock, cur=${CUR}uA)"
                    fi
                    continue
                fi
            fi
        
            # =====================
            # STATE: idle
            # Driver has full control. We just watch bq27411/status.
            # When charging detected -> take over.
            # =====================
            if [ "$STATE" = "idle" ]; then
                BQST=$(cat "$SYS_BQST" 2>/dev/null) || continue
                if [ "$BQST" = "Charging" ]; then
                    log "CHARGER DETECTED (bq=${BQST}, cap=${CAP}%)"
                    if [ "$CAP" -ge "$CHARGE_MAX" ]; then
                        enter_paused "$CAP"
                    else
                        enter_charging "$CAP"
                    fi
                fi
                continue
            fi
        
            # =====================
            # STATE: charging
            # current_max=500mA, charging toward 80%
            # =====================
            if [ "$STATE" = "charging" ]; then
                if [ "$CAP" -ge "$CHARGE_MAX" ]; then
                    enter_paused "$CAP"
                fi
                # If driver reset current_max on re-plug and current is
                # flowing but higher than expected, that's fine -- we're
                # charging anyway. No enforce needed in this state.
                continue
            fi
        
            # =====================
            # STATE: paused
            # current_max=0, discharging toward 40%
            # Enforce if driver resets current_max (detect via positive current).
            # Probe cable every PROBE_CYCLES.
            # =====================
            if [ "$STATE" = "paused" ]; then
        
                # Resume charging at 40%
                if [ "$CAP" -le "$CHARGE_MIN" ]; then
                    enter_charging "$CAP"
                    continue
                fi
        
                # Enforce: if battery is gaining charge, driver must have
                # reset current_max (USB re-negotiation on cable insert)
                if [ "$CUR" -gt 0 ]; then
                    write_cmax 0
                    log "ENFORCE 0 (driver reset, cur=${CUR}uA)"
                    PROBE_CTR=0   # just wrote -- reset probe timer
                    continue
                fi
        
                # Periodic cable probe
                PROBE_CTR=$((PROBE_CTR + 1))
                if [ "$PROBE_CTR" -ge "$PROBE_CYCLES" ]; then
                    PROBE_CTR=0
                    log "PROBE: restoring driver for ${PROBE_SETTLE}s ..."
                    write_cmax "$CHARGE_CURRENT"
                    sleep "$PROBE_SETTLE"
                    CUR2=$(cat "$SYS_CUR" 2>/dev/null) || CUR2=-1
                    if [ "$CUR2" -gt 0 ]; then
                        # Cable still connected -- re-pause
                        write_cmax 0
                        log "PROBE: cable connected (cur=${CUR2}uA) -- staying paused"
                    else
                        # Cable gone -- hand off to driver
                        enter_idle
                    fi
                fi
                continue
            fi
        done

        Мне кажется, лучшее и самое правильное решение для сохранения жизни аккумулятора - это такая же схема, но с умной розеткой. Путь такой:
        Телефон только контролирует своё состояние заряда. Если нужно зарядится (ниже 40%) - отправляется сигнал умной розетке, она врубается. Если нужно перейти в разрядку (выше 80% или перегрев) - тоже сигнал розетке, она вырубается. При этом розетка должна управляться по локальной сети, чтобы, если интернет-соединение потеряется, телефон не мог уйти в перегрев.

        Ну и да, всё же рано или поздно менять батарею или удалять её в любом случае придётся. Когда момент придёт - я задумаюсь, что именно из этого хочу сделать. Но пока пусть живёт в таком режиме.


        1. hochbar
          13.04.2026 20:38

          Вообще конечно не рекомендовал бы держать устройство с аккумулятором постоянно включенным в сеть. Вздутие это фигня, аккум выкинуть можно. Есть риск взрыва аккумулятора из-за деградации, а вот это опасно


          1. FA72 Автор
            13.04.2026 20:38

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

            Да, исключить такой сценарий полностью при постоянном подключении, наверное, нельзя, но, во-первых, в описанной схеме подключение именно что непостоянное (да, кабель воткнут, но батарея питается только, когда идёт заряд). Во-вторых, вероятность взрыва аккумулятора при удержании заряда в цикле 40/80 на низком питании с защитой по температуре, пожалуй, сопоставимо, если не ниже, чем вероятность взрыва аккумулятора обычного смартфона при зарядке, тем более с использованием всяких fastCharge. Да, такие случаи происходят, но это, скорее, исключение, чем правило.

            Мне кажется, что основное требование теперь - ревизия "сервера" раз в месяц на предмет отсутствия видимого вздутия. Если оно всё же будет обнаружено - в этот момент, да, риски кратно возрастают и стоит на это как-то реагировать. Как я и сказал выше, первостепенно рассматриваю, если такое произойдёт, просто замену батареи или, если будет желание поэкспериментировать, впаивание схемы-обманки для постоянного питания.


        1. FA72 Автор
          13.04.2026 20:38

          Нашёл один баг. Надо доработать кусок

          if [ "$STATE" = "idle" ];
          # =====================
              # STATE: idle
              # Driver has full control. We just watch bq27411/status.
              # When charging detected -> take over.
              # Safety: if cap drops to CHARGE_MIN, force charge ourselves
              #   (driver may be stuck after our previous writes).
              # =====================
              if [ "$STATE" = "idle" ]; then
                  # Safety net: don't let battery die while waiting for driver
                  if [ "$CAP" -le "$CHARGE_MIN" ]; then
                      log "SAFETY: cap=${CAP}% <= ${CHARGE_MIN}% in idle -- forcing charge"
                      enter_charging "$CAP"
                      continue
                  fi
                  BQST=$(cat "$SYS_BQST" 2>/dev/null) || continue
                  if [ "$BQST" = "Charging" ]; then
                      log "CHARGER DETECTED (bq=${BQST}, cap=${CAP}%)"
                      if [ "$CAP" -ge "$CHARGE_MAX" ]; then
                          enter_paused "$CAP"
                      else
                          enter_charging "$CAP"
                      fi
                  fi
                  continue
              fi


  1. hochbar
    13.04.2026 20:38

    Я прошивал postmarketOS на Samsung Galaxy Tab 10.1, и докер там поднимал. Но узнав какие риски при постоянно подключенном к питанию аккумуляторе убрал планшет в ящик. На днях вот реализовал другой вариант: взял Vontar X3 - ТВ приставка на amlogic s905x3 (antutu порядка 75К)- прошил на Armbian. 4gb ram, 32gb nand flash. Usb2+usb3+microSD. Поднял Docker + Portainer. Цена вопроса - меньше 3тыр с алика. На usb порт можно подцепить внешний жесткий диск и использовать как samba, dlna и прочие свистульки


    1. FA72 Автор
      13.04.2026 20:38

      Тоже интересный вариант! Но всё же, кажется, что ddr3 память и представленный процессор сильно слабее oneplus 6 (нашёл цифру на antutu - 276510), который мне встал немногим дороже. Но, честно говоря, основной идеей была в целом проверка того, насколько просто поставить сервак на телефон и что из этого получится. Я даже немного расстроился, что вышло так просто, потому что рассчитывал поковыряться.

      Так что можно сказать, что адекватные простые хорошие решения меня интересовали мало =)