Runbook восстановления OPi на новой SD-карте + systemd-юнит

Инцидент 22.07: ФС ушла в read-only — захват кадра и SSH отвалились,
heartbeat и MQTT-вес продолжали работать. В репозитории не хватало
юнита и инструкции подъёма с нуля.

- orange_pi/scales.service (восстановлен по systemctl status)
- RECOVERY.md: установка, проверки, продление жизни карты,
  диагностика через MQTT без SSH
This commit is contained in:
ZeroDay
2026-07-23 09:09:21 +03:00
parent 94044fde75
commit aafbbc64d3
2 changed files with 134 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
# Восстановление Orange Pi на новой SD-карте
Runbook на случай, когда карта деградировала и устройство надо поднять с нуля.
Составлен во время инцидента 22.07.2026: ФС ушла в read-only — захват кадра и SSH
отвалились, при этом heartbeat и публикация веса в MQTT продолжали работать.
## Что взять на пост
- новая SD-карта, **high-endurance** (Samsung PRO Endurance / SanDisk Max Endurance);
32 ГБ хватает с запасом — занято ~12%
- образ Armbian для Orange Pi PC, записанный заранее
- `orange_pi/scales.py`, `orange_pi/outbox.py`, `orange_pi/scales.service` из этого репозитория
- старую карту НЕ выбрасывать: с неё может понадобиться вытащить остатки очереди outbox
## 1. Система
apt update
apt install -y python3 python3-pip ffmpeg
pip3 install paho-mqtt # на свежих Debian/Armbian добавить --break-system-packages
Больше внешних зависимостей нет — всё остальное в скрипте из stdlib.
## 2. Файлы
Рабочий файл на устройстве — именно `/root/scales.py`; репозиторий на OPi не клонируется,
файл доставляется вручную. `outbox.py` кладётся рядом, в `/root`.
cp scales.py outbox.py /root/
Актуальную версию можно скачать прямо с сервера:
curl -s -o /root/scales.py https://scales.zeroday.su/testphotos/scales.py
## 3. Сервис
cp scales.service /etc/systemd/system/scales.service
systemctl daemon-reload
systemctl enable --now scales.service
systemctl status scales.service --no-pager
## 4. Проверки после старта
# в логе должны появиться MQTT1/MQTT2, GPIO, OUTBOX, HEARTBEAT
journalctl -u scales.service -n 30 --no-pager
# весовой терминал
ls -l /dev/ttyUSB0
# камера — тот же вызов, что внутри grab_frame()
ffmpeg -y -loglevel error -rtsp_transport tcp -i "<RTSP_URL из scales.py>" \
-frames:v 1 -update 1 -q:v 2 -f image2 /tmp/scale_frame.jpg && ls -l /tmp/scale_frame.jpg
# офлайн-распознавание на боксе
curl -s http://192.168.20.4:5010/health
# сервер принимает (ожидаемый ответ: {"error":"event_id required"})
curl -s -X POST -H "Content-Type: application/json" -d '{}' https://scales.zeroday.su/api/local-plate
Финальная проверка — кнопка «Тест-снимок» в панели: фото должно появиться в заездах.
## 5. Чтобы карта прожила дольше
Главный убийца SD-карт — постоянная запись логов.
mkdir -p /etc/systemd/journald.conf.d
printf '[Journal]\nStorage=volatile\nRuntimeMaxUse=32M\n' > /etc/systemd/journald.conf.d/volatile.conf
systemctl restart systemd-journald
Убедиться, что `/tmp` — tmpfs (туда пишется кадр перед отправкой):
findmnt /tmp
Если нет — добавить в `/etc/fstab`:
tmpfs /tmp tmpfs defaults,nosuid,nodev,size=128M 0 0
На перспективу: перенос системы на USB-SSD или eMMC снимает проблему целиком.
## Диагностика без SSH
Устройство управляется через MQTT-брокер даже когда SSH недоступен
(на сервере: `localhost:1883`, пользователь `scales-server`).
# живой heartbeat раз в 30 с — значит процесс жив
mosquitto_sub -h 127.0.0.1 -p 1883 -u scales-server -P '<пароль>' -t 'scale/device/status' -v
# команды: ping (мгновенный статус), test-shot (проверка камеры), reboot
mosquitto_pub -h 127.0.0.1 -p 1883 -u scales-server -P '<пароль>' -t 'scale/device/cmd' -m '{"cmd":"ping"}'
Как читать симптомы:
| признак | что значит |
|---|---|
| heartbeat идёт, на `ping` отвечает мгновенно | процесс жив, ресурсы в порядке |
| `outbox_pending: 0`, но заездов на сервере нет | `capture_and_send` вышел до постановки в очередь — не получен кадр |
| `test-shot` не даёт фото, а RTSP из VLC играет | причина локальная на OPi: запись в `/tmp` или ffmpeg |
| SSH рвёт соединение на key exchange | sshd не может создать сессию — характерно для read-only ФС |
Совпадение последних трёх — почти наверняка карта.
## Известная недоработка
`capture_and_send()` при неудачном захвате кадра выходит по `return` **до** постановки
события в очередь. Значит отказ камеры теряет не фото, а всю запись о взвешивании —
именно так были потеряны заезды 20.07.2026. Вес должен доезжать до сервера и без фото;
поправить при ближайшем доступе к устройству.