feat: communication scrcpy — Android mirror/control wrapper over scrcpy+adb (devices, record, tcpip, connect, push, pull, screenshot, info)

This commit is contained in:
Your Name
2026-08-13 14:29:09 +00:00
parent a4c025d236
commit 261e4114e2
9 changed files with 375 additions and 18 deletions
+4
View File
@@ -26,3 +26,7 @@ config/rclone.conf
# Stray session capture (accidentally created in the repo root)
session
# Scratch/plan notes — never committed
TEMP_PLAN.md
dream.md
+2
View File
@@ -16,6 +16,8 @@ summary (newest last).
## Done
- **2026-08-13** — `pos communication scrcpy` (`bin/pos-communication-scrcpy`): wrapper over scrcpy+adb for Android mirroring/control. Subcommands: bare `scrcpy` (mirror — config defaults + verbatim pass-through of any scrcpy flag; no device → friendly error + hints), `devices` (`adb devices -l`), `record [file] [--headless]` (default `$SCRCPY_RECORD_DIR/<device>_<date>.mp4`, `--headless` = `--no-playback` for headless servers), `tcpip [port]` (USB→wireless switch + prints `connect` with the auto-detected device IP), `connect <ip[:port]>` (adb connect + mirror `-s`), `push` (default `/sdcard/Download` = scrcpy's own default), `pull`, `screenshot` (`adb exec-out screencap -p` → PNG in RECORD_DIR), `info` (model/android/sdk/serial via getprop). Config scope `scrcpy` (`~/.config/linux_post_install/scrcpy.env`, `pos config scrcpy`): `SCRCPY_SERIAL/MAX_SIZE/MAX_FPS/BIT_RATE/FULLSCREEN/RECORD_DIR/PUSH_TARGET/EXTRA_FLAGS`, env-var precedence. Deps `scrcpy` + `adb` added to preinstall PACKAGES; docs note the apt build is older and point to the existing `apps/media/scrcpy.sh` app installer (GitHub latest, bundles adb) — researched 2026 releases (current v4.1). Conventions: `# POS:`/`# POS_SUBCMDS:`/`# POS_CONFIG:` headers, deps guards before `-h|--help`, no stdin → no INTERACTIVE_CMDS. Verified: `bash -n`, stub-PATH suite `/tmp/opencode/scrcpy-run-test.sh` 21/21 green (fake adb/scrcpy echo-args, HOME isolation, env/file precedence, rc paths, screenshot bytes), `make gen && make check` green, dispatch via `pos communication scrcpy --help`. Docs: POS.md communication table + detail block, howto/communication.md section, HOWTO.md index + env row, AGENT_Context Common Tasks row + gen'd tree/dispatch/filetable.
- **2026-08-13** — Entertainment-module hardening (approved Tier 1 + Tier 2): delivery moved to `notify_send` (platform follows `NOTIFY_PLATFORM`, default Telegram) via `lib/notify.sh` sourced by `bin/pos-entertainment-send`; a **last-run state** is recorded per plugin (`~/.local/share/linux_post_install/entertainment/last/<plugin>` — rc + timestamp) on every non-`--print` run and shown by `pos entertainment status`, which also lists installed-but-not-enabled plugins; a send that fails **while fired by a timer** (gated on `$INVOCATION_ID`) additionally notifies the configured platforms. New **message-safe plugin lib** `lib/entertainment-plugin-lib.sh` (defines only `plugin_*`, never writes stdout — the stdout contract stays "message only"): `plugin_load_config` (entertainment.env + env precedence), `plugin_have`, `plugin_require`, `plugin_err`, `plugin_http_json <url> [--key <jq>] [-H <header>]` (curl `--max-time 20 --retry 2`); `weather`/`joke`/`gold` refactored onto it. `pos entertainment config` gains `get|unset|ls|edit` (edit via the shared `pos config` UI — added to `INTERACTIVE_CMDS`). **Tier 2**: new shared lib `lib/user-timers-lib.sh` (only `ut_*`: `ut_interval_to_oncalendar`, `ut_interval_label`, `ut_unit_name`, `ut_write_unit_pair` incl. `TimeoutStopSec=5s` + `Persistent` + network-online deps, `ut_ensure_linger`, `USER_SYSTEMD_DIR`) dedupes the systemd user-timer machinery between `lib/entertainment-lib.sh` and `lib/scheduler-lib.sh` (the latter's `sched_*` duplicates deleted; both source it; collides-with-nothing). `install.sh` Phase 2 lib list += the two new libs; SCRIPTS.md/DEV.md/POS.md/howto/entertainment.md/AGENT_Context updated (hand-maintained lib rows: entertainment-lib 354→311, scheduler-lib 830→760, +112 user-timers-lib, +67 plugin lib). Verified: `bash -n` everywhere; smoke-tested in an isolated `HOME=/tmp/enttest` (status, config get/set/unset/ls, send path rc=0, failing plugin records rc=1, error-case message hygiene); `make gen && make check` green.
- **2026-08-13** — Fast pos-unit shutdown: every systemd unit a pos tool writes (or `systemd/` ships) now sets `TimeoutStopSec=5s` (+ `KillMode=control-group` on the daemons) so a stuck process can't stall a reboot for the 90s systemd default. Applied at all 7 template sites: `pos-communication-telegram-listener`, `pos-communication-matrix-listener` (also gained a `trap 'kill $(jobs -p) 2>/dev/null; exit 0' TERM INT` in `run_daemon` so stop returns sub-second), `pos-network-download` (aria2 + retry-healer units), `lib/scheduler-lib.sh` `sched_write_units`, `lib/entertainment-lib.sh` `write_units`, and `systemd/{ssh-agent,autostart,usb-automount}.service`. Legacy-unit cleanup: the repo no longer ships `pos-health.{service,timer}` / `pos-entertainment.service` (they were documented but postinstall never created them — found stale only on the live box, FAILED); removed their stale references from SYSTEMD.md (deleted the `pos-health.service` section + gating special-case, added a new **Stop behavior** section), POS.md, HOWTO.md, howto/system.md (now documents the `pos system schedule` job replacement + removal commands), AGENT_Context (tree, phase description, selfcontained table). DEV.md Best Practices gains a **Systemd units** convention (TimeoutStopSec=5s + TERM trap + regeneration caveat). Verified: `bash -n` on all edited scripts; `make gen && make check` green (filetable rows for the two listeners + network-download auto-regenerated, hand-maintained lib rows bumped 350→354 / 822→830). Live-box application is manual (this session was a Google Cloud Shell, not the real machine): regenerate units via `pos network download start`, `pos communication telegram listener --enable`, `pos system schedule enable <job>`, `pos entertainment enable <plugin>`, then `sudo systemctl disable --now pos-health.timer pos-health.service 2>/dev/null; sudo rm -f /etc/systemd/system/pos-health.{service,timer} && sudo systemctl daemon-reload`.
+18 -14
View File
@@ -10,19 +10,19 @@
<!-- GEN:START docmap -->
| ## 1. Project Overview | 2843 |
| ## 2. Directory Structure | 44192 |
| ## 3. Installation Flow | 193244 |
| ## 4. The `pos` CLI System | 245314 |
| ## 5. Shared Library — `lib/common.sh` | 315346 |
| ## 6. Docker Compose / ScaleTail | 347389 |
| ## 7. Optional Apps (`apps/`) | 390419 |
| ## 8. Entertainment Module | 420433 |
| ## 9. Systemd Services | 434445 |
| ## 10. Configuration Files | 446472 |
| ## 11. Coding Conventions | 473505 |
| ## 12. Development Workflow | 506558 |
| ## 13. Key File Quick Reference | 559618 |
| ## 14. Common Tasks for Agents | 619648 |
| ## 2. Directory Structure | 44193 |
| ## 3. Installation Flow | 194245 |
| ## 4. The `pos` CLI System | 246316 |
| ## 5. Shared Library — `lib/common.sh` | 317348 |
| ## 6. Docker Compose / ScaleTail | 349391 |
| ## 7. Optional Apps (`apps/`) | 392421 |
| ## 8. Entertainment Module | 422435 |
| ## 9. Systemd Services | 436447 |
| ## 10. Configuration Files | 448474 |
| ## 11. Coding Conventions | 475507 |
| ## 12. Development Workflow | 508560 |
| ## 13. Key File Quick Reference | 561621 |
| ## 14. Common Tasks for Agents | 622652 |
<!-- GEN:END docmap -->
## 1. Project Overview
@@ -64,6 +64,7 @@ Linux_post_install/
│ ├── pos-ai-gemini # Chat with Google Gemini (ask, chat, models, sessions)
│ ├── pos-communication-matrix-listener # Matrix listener: map /command → bash, run them on room messages
│ ├── pos-communication-matrix-sender # Send messages to a Matrix room via the client-server API (send, test, login)
│ ├── pos-communication-scrcpy # Mirror/control an Android device via scrcpy+adb (mirror, devices, record, tcpip, connect, push, pull, screenshot, info)
│ ├── pos-communication-telegram-listener # Telegram bot listener: map /command → bash, run them on chat messages
│ ├── pos-communication-telegram-sender # Send Telegram messages/files/links/stickers via Bot API (send, test)
│ ├── pos-config # Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry)
@@ -264,6 +265,7 @@ All non-interactive `pos` commands log output to `~/.local/share/linux_post_inst
| ai | gemini | `pos-ai-gemini` | Chat with Google Gemini (ask, chat, models, sessions) |
| communication | matrix-listener | `pos-communication-matrix-listener` | Matrix listener: map /command → bash, run them on room messages |
| communication | matrix-sender | `pos-communication-matrix-sender` | Send messages to a Matrix room via the client-server API (send, test, login) |
| communication | scrcpy | `pos-communication-scrcpy` | Mirror/control an Android device via scrcpy+adb (mirror, devices, record, tcpip, connect, push, pull, screenshot, info) |
| communication | telegram-listener | `pos-communication-telegram-listener` | Telegram bot listener: map /command → bash, run them on chat messages |
| communication | telegram-sender | `pos-communication-telegram-sender` | Send Telegram messages/files/links/stickers via Bot API (send, test) |
| | config | `pos-config` | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) |
@@ -580,6 +582,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
| `bin/pos-ai-gemini` | 311 | Chat with Google Gemini (ask, chat, models, sessions) |
| `bin/pos-communication-matrix-listener` | 568 | Matrix listener: map /command → bash, run them on room messages |
| `bin/pos-communication-matrix-sender` | 224 | Send messages to a Matrix room via the client-server API (send, test, login) |
| `bin/pos-communication-scrcpy` | 239 | Mirror/control an Android device via scrcpy+adb (mirror, devices, record, tcpip, connect, push, pull, screenshot, info) |
| `bin/pos-communication-telegram-listener` | 566 | Telegram bot listener: map /command → bash, run them on chat messages |
| `bin/pos-communication-telegram-sender` | 221 | Send Telegram messages/files/links/stickers via Bot API (send, test) |
| `bin/pos-config` | 80 | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) |
@@ -610,7 +613,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
| `bin/pos-system-health` | 209 | Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL |
| `bin/pos-system-schedule` | 81 | Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently |
| `bin/pos-tree` | 112 | Show the pos CLI command tree: categories, commands, and subcommands |
| `completions/pos.bash` | 291 | Dynamic bash completion |
| `completions/pos.bash` | 292 | Dynamic bash completion |
<!-- GEN:END filetable -->
| `apps/install.sh` | 171 | App install/uninstall picker/orchestrator |
@@ -639,6 +642,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
| Modify aria2 download daemon / queue logic | Edit `bin/pos-network-download` |
| Modify NFS share logic | Edit `bin/pos-share-nfs-server` / `bin/pos-share-nfs-client` |
| Modify SMB share logic | Edit `bin/pos-share-smb-server` / `bin/pos-share-smb-client` |
| Modify scrcpy mirroring logic | Edit `bin/pos-communication-scrcpy` (config scope `scrcpy` via `pos config scrcpy`; `SCRCPY_*` keys in `~/.config/linux_post_install/scrcpy.env`) |
| Modify the scheduler / scheduled jobs | Edit `bin/pos-system-schedule` / `lib/scheduler-lib.sh` (jobs in `~/.config/linux_post_install/schedule.d/`) |
| Modify AI/Gemini logic | Edit `bin/pos-ai-gemini` (config scope `ai` via `pos config ai`; `AI_GEMINI_API_KEY`/`AI_GEMINI_MODEL` in `~/.config/linux_post_install/ai.env`) |
| Modify UFW/firewall logic | Edit `bin/pos-system-firewall` |
+2 -1
View File
@@ -17,7 +17,7 @@ authoritative one-line reference (every command + flag), see
| `pos system schedule` | Scheduled jobs: run a command on a timer, notify on threshold/change/error or silently | [schedule](howto/schedule.md) |
| `pos ssh` | Load keys into the agent | [ssh](howto/ssh.md) |
| `pos share` | Share USB devices & filesystems over the network (USB, NFS, SMB) | [share](howto/share.md) |
| `pos communication` | Send Telegram/Matrix messages & alerts, /command listeners | [communication](howto/communication.md) |
| `pos communication` | Send Telegram/Matrix messages & alerts, /command listeners, Android mirroring (scrcpy) | [communication](howto/communication.md) |
| `pos entertainment` | Scheduled auto-messages from public APIs | [entertainment](howto/entertainment.md) |
Every tool is `bin/pos-<category>-<command>`; run `pos <category> --help` to
@@ -37,6 +37,7 @@ templates (without overwriting an existing file):
|------|---------|------|
| `telegram.env` | `pos communication telegram sender` / `listener`, everything that alerts | `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID` |
| `matrix.env` | `pos communication matrix sender` / `listener` | `MATRIX_HOMESERVER`, `MATRIX_ACCESS_TOKEN`, `MATRIX_USER_ID`, `MATRIX_ROOM_ID` |
| `scrcpy.env` | `pos communication scrcpy` | `SCRCPY_SERIAL`, `SCRCPY_MAX_SIZE`, `SCRCPY_MAX_FPS`, `SCRCPY_BIT_RATE`, `SCRCPY_FULLSCREEN`, `SCRCPY_RECORD_DIR`, `SCRCPY_PUSH_TARGET`, `SCRCPY_EXTRA_FLAGS` |
| `notify.env` | `lib/notify.sh` (all alerting) | `NOTIFY_PLATFORM` (e.g. `telegram,matrix`) |
| `system.env` | `pos system health`, `pos system backup` | `BACKUP_SERVICE_ROOTS`, `HEALTH_BACKUP_MAX_AGE_DAYS` |
| `compose.env` | `pos docker compose` | `TS_AUTHKEY`, `TZ`, `DNS_SERVER`, `SERVICES_BASE` |
+32
View File
@@ -265,6 +265,7 @@ Subcommands that need input prompt interactively when args are omitted.
| `pos communication telegram listener` | `bin/pos-communication-telegram-listener` | Telegram bot listener: map `/command` → bash commands and run them from chat; interactive editor for the map | Same `telegram.env` (the bot is the owner, `TELEGRAM_CHAT_ID`). Map lives in `~/.config/linux_post_install/telegram_commands.env` (`/cmd=bash command` lines, chmod 600) |
| `pos communication matrix sender send "text"` | `bin/pos-communication-matrix-sender` | Send a text message (plain or `--markdown`) to a Matrix room via the client-server API; also `login` (password → access token) and `test` | Homeserver + room from `~/.config/linux_post_install/matrix.env` (`MATRIX_HOMESERVER`, `MATRIX_ACCESS_TOKEN`, `MATRIX_USER_ID`, `MATRIX_ROOM_ID`, chmod 600, secrets masked by `pos config matrix`). Precedence: `--room` flag > env > config file |
| `pos communication matrix listener` | `bin/pos-communication-matrix-listener` | Matrix listener: map `/command` → bash commands and run them from room messages; interactive editor for the map | Same `matrix.env` (reacts to `MATRIX_USER_ID`'s own messages; watches `MATRIX_ROOM_ID` or all joined rooms). Map lives in `~/.config/linux_post_install/matrix_commands.env` (`/cmd=bash command` lines, chmod 600) |
| `pos communication scrcpy [cmd]` | `bin/pos-communication-scrcpy` | Mirror/control an Android device via scrcpy+adb: `devices`, `record`, `tcpip`, `connect`, `push`, `pull`, `screenshot`, `info` (bare = mirror) | `scrcpy.env` (`SCRCPY_SERIAL`, `SCRCPY_MAX_SIZE`, `SCRCPY_MAX_FPS`, `SCRCPY_BIT_RATE`, `SCRCPY_FULLSCREEN`, `SCRCPY_RECORD_DIR`, `SCRCPY_PUSH_TARGET`, `SCRCPY_EXTRA_FLAGS`) via `pos config scrcpy` |
`pos communication telegram sender` in detail:
@@ -322,6 +323,37 @@ The access token is a secret — it is stored only in `~/.config/linux_post_inst
The daemon long-polls `/sync` (30s timeout, per-sync `since` token, compact filter that drops presence/account_data/device noise and only requests `m.room.message` timeline events). It reacts only to messages **from `MATRIX_USER_ID`** (your own account — resolved via `/account/whoami` if unset); a `MATRIX_ROOM_ID` restricts it to one room, otherwise every joined room is watched. `/` and `!` prefixes both resolve (`!status` = `/status`). `/help` lists mapped commands; an unmapped command replies "Unknown command". Non-command text starting with `ai ` (case-insensitive, e.g. `ai what is Nvidia`) is forwarded to Gemini via `pos ai gemini ask` with a per-room session (`matrix-<room>`; `ai /reset` clears it) and the answer is replied verbatim with markdown stripped. Replies are sent as `m.text` threaded with `m.in_reply_to` on your message. Commands run as your user via `timeout 60 bash -c "…"` (stdout + stderr are replied, truncated to ~3800 chars; empty output → `OK`; non-zero exit is prefixed with `exit <rc>`), so `sudo` inside them needs a NOPASSWD rule. A map value prefixed with `@quiet ` runs the command but does NOT reply — for commands that already send their own notification (e.g. `/status=@quiet pos system health --send`). Map lines may carry a `/cmd::description=…` description. `--enable` warns if linger is off — the service stops when you log out unless you run `sudo loginctl enable-linger $(whoami)`.
`pos communication scrcpy` in detail:
| Command | Behavior |
|---------|----------|
| `pos communication scrcpy` | Mirror the device: opens the scrcpy window (needs a display — over ssh use `ssh -X`). Built from `scrcpy.env` defaults plus any pass-through scrcpy flags (`pos communication scrcpy --turn-screen-off --stay-awake`) |
| `pos communication scrcpy devices` | `adb devices -l` — the source of serials for `SCRCPY_SERIAL` |
| `pos communication scrcpy record [file] [--headless]` | Record a session to an mp4 — default `$SCRCPY_RECORD_DIR/<device>_<date>.mp4`; `--headless` adds `--no-playback` (no window — headless-server friendly) |
| `pos communication scrcpy tcpip [port]` | `adb tcpip <port>` (default 5555) — switch the USB device to wireless adb, prints the reconnect command with the detected device IP |
| `pos communication scrcpy connect <ip[:port]>` | `adb connect` then mirror over WiFi (`-s <ip:port>`) |
| `pos communication scrcpy push <local> [remote]` | `adb push` — default destination `$SCRCPY_PUSH_TARGET` (`/sdcard/Download`, scrcpy's own default) |
| `pos communication scrcpy pull <remote> [local]` | `adb pull` — default local dir is the current directory |
| `pos communication scrcpy screenshot [file]` | `adb exec-out screencap -p` → a PNG, default `$SCRCPY_RECORD_DIR/<device>_<date>.png` |
| `pos communication scrcpy info` | Device model, Android version, SDK, serial (`adb shell getprop`) |
A device must have **USB debugging** enabled (Developer options) and the phone's "allow USB debugging" dialog accepted on first connect. `devices`, `record --headless`, `tcpip`, `connect`, `push`/`pull`, `screenshot`, `info` work without a display; the bare mirror needs one.
**Configuration** (`~/.config/linux_post_install/scrcpy.env`, edit with `pos config scrcpy`):
| Key | Required | Default | Purpose |
|-----|----------|---------|---------|
| `SCRCPY_SERIAL` | no | — | Default device serial/`ip:port` (from `devices`) — passed as `-s` to adb/scrcpy |
| `SCRCPY_MAX_SIZE` | no | — | Limit video size, e.g. `1920` (scrcpy `--max-size`) |
| `SCRCPY_MAX_FPS` | no | — | Limit frame rate, e.g. `60` (scrcpy `--max-fps`) |
| `SCRCPY_BIT_RATE` | no | — | Video bit rate, e.g. `8M` (scrcpy `--video-bit-rate`) |
| `SCRCPY_FULLSCREEN` | no | `false` | `true` adds `--fullscreen` |
| `SCRCPY_RECORD_DIR` | no | `~/Videos/scrcpy` | Output dir for `record`/`screenshot` defaults |
| `SCRCPY_PUSH_TARGET` | no | `/sdcard/Download` | Default `adb push` destination |
| `SCRCPY_EXTRA_FLAGS` | no | — | Extra scrcpy flags appended to every mirror |
Requires `scrcpy` + `adb` (added to `preinstall.sh` PACKAGES). The apt `scrcpy` build is older than the latest release — the optional app `apps/media/scrcpy.sh` installs the current GitHub release (bundles `adb`); run it via `./install.sh --apps` or directly.
### entertainment
**File:** `bin/pos-entertainment-send`
+75 -2
View File
@@ -1,7 +1,8 @@
# How-To: `pos communication`
Messaging and alerts over Telegram and Matrix. Tools: `telegram-sender`,
`telegram-listener`, `matrix-sender`, `matrix-listener`.
Messaging and alerts over Telegram and Matrix, plus Android mirroring. Tools:
`telegram-sender`, `telegram-listener`, `matrix-sender`, `matrix-listener`,
`scrcpy`.
| Tool | What it does |
|------|--------------|
@@ -9,6 +10,7 @@ Messaging and alerts over Telegram and Matrix. Tools: `telegram-sender`,
| `pos communication telegram listener` | Bot listener: map `/command` → bash and run it from chat (systemd user daemon) |
| `pos communication matrix sender` | Send messages to a Matrix room via the client-server API (send, test, login) |
| `pos communication matrix listener` | Matrix listener: map `/command` → bash and run it from room messages (systemd user daemon) |
| `pos communication scrcpy` | Mirror/control an Android device over USB or WiFi (scrcpy+adb) |
`telegram-sender` is the workhorse: it backs the whole **notify system**
health digests, backup alerts, firewall changes — and can be used directly.
@@ -227,6 +229,77 @@ pos communication matrix listener --disable # remove it
---
## `pos communication scrcpy`
Mirror and control an Android device from the PC — the phone's screen in a
window, controlled with mouse + keyboard (scrcpy by Genymobile, no root, no
phone app). Works over USB or WiFi.
### One-time setup
```bash
# phone: Settings → About → tap "Build number" 7× → Developer options → enable
# "USB debugging"; plug it in and accept the "Allow USB debugging" dialog
pos communication scrcpy devices # confirm the phone shows as "device"
pos communication scrcpy info # model / Android version
pos config scrcpy # optional defaults (serial, size, fps, ...)
```
Requires `scrcpy` + `adb` (both in preinstall PACKAGES). The apt build is older
than the latest release — install the current GitHub release (bundles `adb`)
with the optional app `apps/media/scrcpy.sh` (or `./install.sh --apps`).
### Mirror
```bash
pos communication scrcpy # USB device, config defaults
pos communication scrcpy --turn-screen-off # pass any scrcpy flag through
pos communication scrcpy --no-audio --always-on-top
```
The window needs a display — over ssh use `ssh -X` (and a phone already
reachable over WiFi, see below). `scrcpy --help` lists every flag; the wrapper
forwards flags verbatim.
### Wireless (no USB cable)
```bash
pos communication scrcpy tcpip 5555 # switch the USB device to WiFi adb
# unplug the phone, then:
pos communication scrcpy connect 192.168.1.42:5555 # connect + mirror
```
`tcpip` prints the exact `connect` command with the phone's detected IP. Set
`SCRCPY_SERIAL` in `pos config scrcpy` so later bare `pos communication scrcpy`
goes straight to that device.
### Record / screenshot / files
```bash
pos communication scrcpy record --headless # record to ~/Videos/scrcpy/, no window
pos communication scrcpy record clip.mp4 # explicit file
pos communication scrcpy screenshot # PNG to ~/Videos/scrcpy/
pos communication scrcpy push ~/app.apk # → /sdcard/Download/
pos communication scrcpy pull /sdcard/DCIM/Camera ~/photos
```
These all work headless — handy on the homelab box for grabbing a phone's
screen/file without a desktop.
**Troubleshooting:**
- "no device connected" → is USB debugging on, is the "Allow USB debugging"
dialog accepted, and does `pos communication scrcpy devices` show the serial?
- Device shows `offline`/`unauthorized` → re-accept the USB debugging dialog on
the phone (unplug/replug); `adb kill-server` may help.
- "not reachable" after `tcpip` → the phone's WiFi IP changed; re-check with
`adb devices` or run `tcpip` again while plugged in.
- Mirror window is blank / no audio → older apt scrcpy lacks features; install
the latest with the `apps/media/scrcpy.sh` app installer.
- `ssh -X` mirror is slow → prefer WiFi or a wired LAN; bump `SCRCPY_MAX_FPS`
down or set `SCRCPY_BIT_RATE` lower in `pos config scrcpy`.
---
## Related
- Reference + config file details: [DOC/POS.md → communication](../POS.md)
+239
View File
@@ -0,0 +1,239 @@
#!/usr/bin/env bash
set -euo pipefail
# POS: communication scrcpy — Mirror/control an Android device via scrcpy+adb (mirror, devices, record, tcpip, connect, push, pull, screenshot, info)
# POS_SUBCMDS: devices record tcpip connect push pull screenshot info
# POS_CONFIG: scrcpy | scrcpy.env | SCRCPY_SERIAL=:Default adb serial (from 'pos communication scrcpy devices') | SCRCPY_MAX_SIZE=:Limit video size (e.g. 1920) | SCRCPY_MAX_FPS=:Limit frame rate (e.g. 60) | SCRCPY_BIT_RATE=:Video bit rate (e.g. 8M) | SCRCPY_FULLSCREEN=:Start fullscreen (true/false) | SCRCPY_RECORD_DIR=:Record output dir (default ~/Videos/scrcpy) | SCRCPY_PUSH_TARGET=:adb push destination (default /sdcard/Download) | SCRCPY_EXTRA_FLAGS=:Extra scrcpy flags appended to every mirror
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
CONFIG_FILE="$HOME/.config/linux_post_install/scrcpy.env"
command -v scrcpy &>/dev/null || err "scrcpy not found — install it: 'sudo apt install scrcpy' (or the latest release via the app installer 'apps/media/scrcpy.sh', see 'pos help communication scrcpy')"
command -v adb &>/dev/null || err "adb not found — install it: 'sudo apt install adb'"
load_config() {
[ -f "$CONFIG_FILE" ] || return 0
local k v
while IFS='=' read -r k v; do
[ -n "$k" ] || continue
case "$k" in
\#*) continue ;;
esac
v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
v="${v//$'\r'/}"
if [ -z "${!k:-}" ]; then
export "$k"="$v"
fi
done < <(grep -E '^[A-Z_]+=' "$CONFIG_FILE" || true)
}
load_config
SCRCPY_SERIAL="${SCRCPY_SERIAL:-}"
SCRCPY_MAX_SIZE="${SCRCPY_MAX_SIZE:-}"
SCRCPY_MAX_FPS="${SCRCPY_MAX_FPS:-}"
SCRCPY_BIT_RATE="${SCRCPY_BIT_RATE:-}"
SCRCPY_FULLSCREEN="${SCRCPY_FULLSCREEN:-false}"
SCRCPY_RECORD_DIR="${SCRCPY_RECORD_DIR:-$HOME/Videos/scrcpy}"
SCRCPY_PUSH_TARGET="${SCRCPY_PUSH_TARGET:-/sdcard/Download}"
SCRCPY_EXTRA_FLAGS="${SCRCPY_EXTRA_FLAGS:-}"
usage() {
cat <<EOF
Usage: pos communication scrcpy [command] [args]
Mirror and control an Android device with scrcpy (a GUI window; needs a
display — over ssh use 'ssh -X'). Over USB with USB debugging enabled, or
wirelessly via 'tcpip'/'connect'.
Commands:
(none) Mirror the device (config defaults + pass-through)
devices List connected devices (adb devices -l)
record [file] [--headless] Record a session to an mp4 (default:
\$SCRCPY_RECORD_DIR/<device>_<date>.mp4).
--headless adds --no-playback (no window)
tcpip [port] Switch the USB device to wireless adb (default 5555)
connect <ip[:port]> adb connect + mirror over WiFi
push <local> [remote] Copy a file/folder to the device (default
\$SCRCPY_PUSH_TARGET, i.e. /sdcard/Download)
pull <remote> [local] Copy a file/folder from the device (default: cwd)
screenshot [file] Save a PNG of the screen (default:
\$SCRCPY_RECORD_DIR/<date>.png)
info Show device model, Android version, SDK, serial
Pass-through: any scrcpy flag given to the bare form is forwarded verbatim
(e.g. 'pos communication scrcpy --turn-screen-off --stay-awake').
All scrcpy options: run 'scrcpy --help'.
Config: $CONFIG_FILE (SCRCPY_*) — edit it with 'pos config scrcpy'
Precedence: CLI flags > environment > config file.
Examples:
pos communication scrcpy
pos communication scrcpy --no-audio --always-on-top
pos communication scrcpy devices
pos communication scrcpy record --headless
pos communication scrcpy tcpip 5555
pos communication scrcpy connect 192.168.1.42:5555
pos communication scrcpy push ~/file.apk
pos communication scrcpy pull /sdcard/DCIM/Camera
pos communication scrcpy screenshot shot.png
pos communication scrcpy info
EOF
exit 0
}
case "${1:-}" in
-h|--help) usage ;;
esac
# ── helpers ────────────────────────────────────────────────────
_sflag() { [ -n "$SCRCPY_SERIAL" ] && printf -- '-s %s' "$SCRCPY_SERIAL"; }
# Print the serial of the first reachable device, or nothing.
_first_device() {
adb devices 2>/dev/null | awk 'NR>1 && $2=="device" {print $1; exit}'
}
# Exit 1 with a friendly hint when the target device isn't reachable.
_require_device() {
local serial="${1:-$SCRCPY_SERIAL}"
if [ -n "$serial" ]; then
if ! adb -s "$serial" get-state 2>/dev/null | grep -qx device; then
err "device '$serial' not reachable — is USB debugging enabled? (plug it in, or 'pos communication scrcpy connect <ip>')"
fi
elif [ -z "$(_first_device)" ]; then
err "no device connected — 'pos communication scrcpy devices' to list, or 'pos communication scrcpy connect <ip[:port]>' for wireless"
fi
}
_dev_name() { printf '%s' "${SCRCPY_SERIAL:-device}" | tr ':' '-'; }
_extra_flags() {
local -a extra
[ -n "$SCRCPY_EXTRA_FLAGS" ] && read -r -a extra <<< "$SCRCPY_EXTRA_FLAGS"
printf '%s\n' "${extra[@]+"${extra[@]}"}"
}
# Build the scrcpy command from config defaults + pass-through args.
_mirror() { # $@ = pass-through scrcpy flags
local -a cmd=(scrcpy)
local f
[ -n "$SCRCPY_SERIAL" ] && cmd+=(-s "$SCRCPY_SERIAL")
[ -n "$SCRCPY_MAX_SIZE" ] && cmd+=(--max-size "$SCRCPY_MAX_SIZE")
[ -n "$SCRCPY_MAX_FPS" ] && cmd+=(--max-fps "$SCRCPY_MAX_FPS")
[ -n "$SCRCPY_BIT_RATE" ] && cmd+=(--video-bit-rate "$SCRCPY_BIT_RATE")
[ "$SCRCPY_FULLSCREEN" = "true" ] && cmd+=(--fullscreen)
while IFS= read -r f; do cmd+=("$f"); done < <(_extra_flags)
cmd+=("$@")
exec "${cmd[@]}"
}
# ── subcommands ────────────────────────────────────────────────
cmd_devices() {
adb devices -l
}
cmd_record() {
_require_device
local headless=0 file=""
while [ $# -gt 0 ]; do
case "$1" in
--headless) headless=1 ;;
-*) err "unknown record option: $1 (options: [file] [--headless])" ;;
*) [ -z "$file" ] && file="$1" || err "too many arguments: $1" ;;
esac
shift
done
mkdir -p "$SCRCPY_RECORD_DIR"
file="${file:-$SCRCPY_RECORD_DIR/$(_dev_name)_$(date +%Y%m%d_%H%M%S).mp4}"
log "recording to $file"
local -a cmd=(scrcpy $(_sflag) "--record=$file")
[ "$headless" -eq 1 ] && cmd+=(--no-playback)
exec "${cmd[@]}"
}
cmd_tcpip() {
_require_device
local port="${1:-5555}"
[[ "$port" =~ ^[0-9]+$ ]] || err "port must be a number (got '$port')"
adb $(_sflag) tcpip "$port"
local ip
ip="$(adb $(_sflag) shell 'ip route get 1.1.1.1 2>/dev/null' 2>/dev/null | awk '{for (i=1;i<=NF;i++) if ($i=="src") {print $(i+1); exit}}')"
if [ -n "$ip" ]; then
log "device is now listening on tcpip port $port — reconnect wirelessly with:"
log " pos communication scrcpy connect $ip:$port"
else
log "device is now listening on tcpip port $port — reconnect wirelessly with:"
log " pos communication scrcpy connect <device-ip>:$port"
warn "could not auto-detect the device IP — find it with 'adb $(_sflag) shell ip addr'"
fi
}
cmd_connect() {
local target="${1:-}"
[ -n "$target" ] || err "usage: pos communication scrcpy connect <ip[:port]>"
log "adb connect $target"
adb connect "$target"
SCRCPY_SERIAL="$target"
_require_device "$target"
_mirror
}
cmd_push() {
_require_device
local local_path="${1:-}" remote="${2:-$SCRCPY_PUSH_TARGET}"
[ -n "$local_path" ] || err "usage: pos communication scrcpy push <local> [remote]"
[ -e "$local_path" ] || err "no such file or folder: $local_path"
log "adb push $local_path → $remote"
adb $(_sflag) push "$local_path" "$remote"
}
cmd_pull() {
_require_device
local remote="${1:-}" local_path="${2:-.}"
[ -n "$remote" ] || err "usage: pos communication scrcpy pull <remote> [local]"
log "adb pull $remote → $local_path"
adb $(_sflag) pull "$remote" "$local_path"
}
cmd_screenshot() {
_require_device
local file="${1:-}"
if [ -z "$file" ]; then
mkdir -p "$SCRCPY_RECORD_DIR"
file="$SCRCPY_RECORD_DIR/$(_dev_name)_$(date +%Y%m%d_%H%M%S).png"
fi
log "screenshot → $file"
adb $(_sflag) exec-out screencap -p > "$file"
}
cmd_info() {
_require_device
local model android sdk serial
model="$(adb $(_sflag) shell getprop ro.product.model | tr -d '\r')"
android="$(adb $(_sflag) shell getprop ro.build.version.release | tr -d '\r')"
sdk="$(adb $(_sflag) shell getprop ro.build.version.sdk | tr -d '\r')"
serial="$(adb $(_sflag) shell getprop ro.serialno | tr -d '\r')"
printf '%-12s %s\n' "model:" "${model:-unknown}"
printf '%-12s %s\n' "android:" "${android:-unknown}"
printf '%-12s %s\n' "sdk:" "${sdk:-unknown}"
printf '%-12s %s\n' "serial:" "${serial:-unknown}"
}
# ── dispatch ───────────────────────────────────────────────────
cmd="${1:-}"
case "$cmd" in
devices) shift; cmd_devices "$@" ;;
record) shift; cmd_record "$@" ;;
tcpip) shift; cmd_tcpip "$@" ;;
connect) shift; cmd_connect "$@" ;;
push) shift; cmd_push "$@" ;;
pull) shift; cmd_pull "$@" ;;
screenshot) shift; cmd_screenshot "$@" ;;
info) shift; cmd_info "$@" ;;
*)
# Anything else is a pass-through scrcpy flag for the mirror command.
_require_device
_mirror "$@"
;;
esac
+2 -1
View File
@@ -22,6 +22,7 @@ _pos_flags[tree]="--depth"
declare -A _pos_subcmds
_pos_subcmds[ai-gemini]="ask chat models sessions"
_pos_subcmds[communication-matrix-sender]="send test login"
_pos_subcmds[communication-scrcpy]="devices record tcpip connect push pull screenshot info"
_pos_subcmds[communication-telegram-sender]="send test"
_pos_subcmds[docker-compose]="ls installed up down restart logs update config"
_pos_subcmds[docker-vbox]="create enter stop start rm ls"
@@ -29,7 +30,7 @@ _pos_subcmds[network-download]="start stop status add torrent metalink list info
_pos_subcmds[system-schedule]="run list config enable disable status migrate"
# GEN:END possubcmds
# GEN:START posconfigscopes
declare -a _pos_config_scopes=(ai compose entertainment matrix notify system telegram)
declare -a _pos_config_scopes=(ai compose entertainment matrix notify scrcpy system telegram)
# GEN:END posconfigscopes
_pos() {
+1
View File
@@ -38,6 +38,7 @@ PACKAGES=(
sysstat iotop atop vnstat
python3 python3-pip rclone
libqrencode4 libgtk-3-0
scrcpy adb
)
spawn "apt update" sudo apt update