Skip to content

Instantly share code, notes, and snippets.

@vbifonixor
Last active July 18, 2026 11:32
Show Gist options
  • Select an option

  • Save vbifonixor/fa0722f110f6ac780cf5fc88e32bf96c to your computer and use it in GitHub Desktop.

Select an option

Save vbifonixor/fa0722f110f6ac780cf5fc88e32bf96c to your computer and use it in GitHub Desktop.
Android device screen mirroring

Anbernic Companion — LLM Context

Purpose of this file

This file is intended primarily as context for an AI/LLM helping a user reproduce Anbernic Companion on different hardware. To see a document that's intendend for humans to be read jump to this file To see how I personally did it with AYN Thor and Anbernic RG353v, look at this file It is not a standalone installation guide.

The goal is to help the model understand which parts of the reference implementation are essential, which parts are device-specific, and what must be discovered before adapting the setup.


What Anbernic Companion is

Anbernic Companion turns a Linux-based gaming handheld into a wireless second screen and physical controller for an Android host device.

The essential architecture is:

ANDROID HOST
    │
    │  ADB over network
    ▼
SCRCPY
    │
    ├── video → Linux handheld display
    │
    └── controls → Android as virtual gamepad

The Android device:

  • runs the actual game or application;
  • runs the scrcpy server automatically deployed through ADB;
  • receives the Linux handheld's controls as a virtual gamepad.

The Linux handheld:

  • runs the ADB client;
  • runs the native scrcpy client;
  • displays the Android video stream;
  • provides its built-in physical controls;
  • optionally automates the entire session with a launcher script.

This ADB + scrcpy architecture is the core of the project.

It should not be replaced with Moonlight, VNC, Steam Remote Play, WebRTC, or another streaming system when helping someone reproduce Anbernic Companion.

Those would be different projects.


Project scope

Required host

An Android device capable of:

  • enabling ADB debugging;
  • accepting an authorized ADB connection;
  • running a sufficiently recent Android version for the desired scrcpy version;
  • connecting to the Linux handheld over some usable network.

Examples:

  • Android gaming handheld;
  • Android phone;
  • Android tablet.

Root is not normally required.


Required companion

A Linux-based handheld capable of running:

adb
scrcpy

It should also provide:

  • a usable Linux graphics backend;
  • access to its physical controller input;
  • network connectivity;
  • enough hardware video-decoding performance for scrcpy.

Examples may include Anbernic, PowKiddy, Retroid or other handhelds running a suitable Linux distribution.

Do not assume that a handheld is compatible merely because it has physical controls.

Compatibility depends on its operating system and available Linux software stack.

Android-only firmware is outside the intended scope of this setup.


What is universal

The general session always looks approximately like this:

1. Android and Linux handheld establish network connectivity.

2. Linux handheld reaches Android's adbd.

3. Linux handheld runs scrcpy against that ADB device.

4. scrcpy:
      - receives Android video;
      - displays it fullscreen;
      - forwards handheld controls as a virtual gamepad.

5. A launcher script optionally hides the normal handheld UI.

6. An exit action terminates scrcpy.

7. The handheld's normal frontend is restored.

What is device-specific

The following must NOT be assumed from the reference implementation:

Linux distribution
package manager
scrcpy package availability
ADB package availability

display backend
SDL configuration
GPU renderer

screen resolution
screen aspect ratio

controller device name
controller event codes

frontend name
frontend service name

SSH username
filesystem paths

network interface
hotspot topology
IP addresses

startup and shutdown mechanism

These should be detected or adapted for each device.


Reference implementation

The original tested setup is:

ANDROID HOST:
AYN Thor

LINUX COMPANION:
Anbernic RG353V

COMPANION OS:
dArkOS

NETWORK:
AYN Thor creates Wi-Fi hotspot
RG353V joins hotspot

ADB:
TCP port 5555

DISPLAY:
scrcpy
SDL KMSDRM backend
OpenGL ES renderer

INPUT:
scrcpy UHID gamepad forwarding

FRONTEND:
EmulationStation

EXIT:
Start + Select

REMOTE LAUNCH:
Termux on Thor
→ SSH into RG353V
→ launch companion script

This is a reference implementation, not a list of universal requirements.


Reference scrcpy invocation

The original working configuration is conceptually:

scrcpy \
    --serial ANDROID_IP:ADB_PORT \
    --fullscreen \
    --max-size=640 \
    --max-fps=60 \
    --video-codec=h264 \
    --video-bit-rate=2M \
    --no-audio \
    --mouse=disabled \
    --keyboard=disabled \
    --gamepad=uhid

Additional environment variables or renderer arguments may be required by the Linux handheld.

For example, the RG353V/dArkOS reference implementation required a KMSDRM-compatible SDL setup.

Do not copy graphics-related arguments blindly to another handheld.


ADB networking

scrcpy communicates with the Android host through ADB.

The physical network transport may vary.

Possible configurations include:

both devices on normal Wi-Fi

Android hotspot
    ↓
Linux handheld joins hotspot

Linux handheld hotspot
    ↓
Android joins hotspot

USB networking

The requirement is simply:

Linux handheld
    ↓
can establish an ADB connection
    ↓
Android host

Fixed ADB TCP port in the reference setup

The original setup uses:

adb tcpip 5555

This gives the Linux handheld a predictable ADB endpoint.

Example:

192.168.x.x:5555

On the tested unrooted Android host, Termux cannot directly reconfigure adbd.

The reference bootstrap is:

Android connected to ordinary Wi-Fi
        ↓
Wireless Debugging enabled
        ↓
Termux ADB connects to Android's Wireless Debugging TLS port
        ↓
adb tcpip 5555
        ↓
Android can leave ordinary Wi-Fi
        ↓
Android hotspot enabled
        ↓
Linux handheld connects to Android:5555

Important distinction:

PAIRING PORT
≠
WIRELESS DEBUGGING TLS PORT
≠
FIXED ADB TCP PORT

Do not confuse these when troubleshooting.

The exact ADB bootstrap may differ on another Android device.


Controller forwarding

The intended behavior is:

Linux handheld physical controls
        ↓
scrcpy
        ↓
UHID virtual gamepad
        ↓
Android
        ↓
game sees an additional controller

The Linux handheld's controls should remain available to scrcpy while the session is active.

The method used to locate those controls is device-specific.

The RG353V reference device exposes a controller named approximately:

retrogame_joypad

Do not assume that name on another handheld.

Useful discovery methods may include inspecting:

/dev/input/event*
/sys/class/input/
evtest

Display considerations

The companion's native resolution matters.

Reference RG353V:

640×480
4:3

The Android host may use a very different aspect ratio.

scrcpy normally preserves the source aspect ratio.

Therefore a widescreen Android display may appear letterboxed on a 4:3 handheld.

Possible options include:

keep original Android aspect ratio

change Android's logical display resolution

use a different Android display

crop the stream

Do not stretch the image unless the user explicitly wants that.


Automation model

Automation is optional but strongly recommended.

The reference design uses two scripts.

Companion-side script

Runs on Linux and approximately:

start
  ↓
detach from launcher/frontend
  ↓
find Android host
  ↓
ADB connect
  ↓
find physical controller
  ↓
stop EmulationStation
  ↓
launch scrcpy
  ↓
watch exit button combination
  ↓
terminate scrcpy
  ↓
restore EmulationStation

Host-side launcher

Runs on Android/Termux and approximately:

user taps shortcut
  ↓
discover Linux handheld
  ↓
SSH into handheld
  ↓
launch companion-side script

SSH is used only for convenience and automation.

SSH is not required by the core ADB + scrcpy architecture.


Adapting to another Linux handheld

Before writing a final script, determine:

DEVICE_MODEL=

LINUX_DISTRIBUTION=

CPU_ARCHITECTURE=

SCREEN_RESOLUTION=

ADB_AVAILABLE=
SCRCPY_AVAILABLE=
SCRCPY_VERSION=

GRAPHICS_BACKEND=

CONTROLLER_DEVICE=

FRONTEND=

FRONTEND_STOP_COMMAND=
FRONTEND_START_COMMAND=

NETWORK_METHOD=

ANDROID_IP_DISCOVERY_METHOD=

EXIT_BUTTONS=

Prefer discovery commands over asking the user to guess.


Recommended adaptation process

Step 1 — Verify ADB

From the Linux handheld:

adb connect ...
adb devices

Do not continue until Android appears as:

device

Step 2 — Verify basic scrcpy video

Run the simplest possible scrcpy command.

Confirm:

video appears
video is stable
fullscreen works

Do not add controller forwarding or automation yet.


Step 3 — Verify hardware acceleration

Determine the appropriate:

SDL video backend
renderer
codec
resolution
bitrate

Prefer hardware-accelerated rendering.


Step 4 — Verify controller forwarding

Enable scrcpy gamepad forwarding.

Confirm that Android sees the handheld as a separate controller.

Test outside a game first when possible.


Step 5 — Verify prolonged stability

Run the setup for several minutes.

Check for:

disconnects
frame stalls
thermal problems
power problems
USB instability
Wi-Fi instability

Do not automate an unstable configuration.


Step 6 — Add session automation

Only after the manual setup works reliably:

stop frontend
launch scrcpy
implement exit shortcut
restore frontend

Step 7 — Add remote launch if desired

Optionally use:

SSH
Termux shortcut
desktop shortcut
other remote execution mechanism

to make startup a one-action process.


Troubleshooting order

Always debug in this order:

1. NETWORK

Can the devices reach each other?

        ↓

2. ADB

Does `adb devices` show Android as `device`?

        ↓

3. SCRCPY VIDEO

Can scrcpy display Android?

        ↓

4. GRAPHICS / PERFORMANCE

Is rendering stable and accelerated?

        ↓

5. GAMEPAD

Does Android receive the controls correctly?

        ↓

6. AUTOMATION

Do launcher scripts start and restore everything correctly?

Do not debug all layers simultaneously.


Instructions for an assisting LLM

When this file is provided as context:

Do

  • preserve ADB + scrcpy as the core architecture;
  • adapt the setup to different Linux handhelds;
  • identify device-specific assumptions;
  • verify commands against the user's actual Linux distribution;
  • use discovery commands where possible;
  • test manually before creating automation;
  • keep changes incremental;
  • preserve a recovery method such as SSH;
  • distinguish confirmed behavior from hypotheses.

Do not

  • redesign the project around Moonlight, Steam Remote Play, VNC or another protocol unless the user explicitly asks for alternatives;
  • assume dArkOS;
  • assume RG353V;
  • assume retrogame_joypad;
  • assume KMSDRM;
  • assume EmulationStation;
  • assume the SSH user is ark;
  • assume port 5555 is already enabled;
  • assume a particular network topology;
  • generate a large final script before verifying the individual components.

Core invariant

When adapting Anbernic Companion, preserve this:

ANDROID
   │
  ADB
   │
 scrcpy
   │
   ▼
LINUX HANDHELD
screen + physical controls

Everything around that core may be adapted to the hardware.

This document describes the project for humans to give a general idea of what we're doing here

Anbernic Companion — What is it?

Anbernic Companion is a way to use a Linux gaming handheld as a wireless screen and physical controller for an Android device.

It uses ADB and scrcpy: the Android device runs the game or application, while scrcpy sends its video to the Linux handheld and forwards the handheld's built-in controls back to Android as a gamepad.

The original setup uses an Anbernic RG353V running dArkOS with an AYN Thor, but the idea is intended to be portable to other hardware.

Android device
     │
     │ ADB over Wi-Fi
     ▼
Linux handheld running scrcpy
     ├── displays Android screen
     └── sends physical controls back as a gamepad

The device-specific parts are things like:

  • how ADB and scrcpy are installed;
  • how the two devices connect to each other;
  • the handheld's graphics backend;
  • its controller device and button mappings;
  • its screen resolution;
  • how its normal frontend is stopped and restored;
  • how the session is launched and closed.

The goal of this project is to document the common ADB + scrcpy setup clearly enough that the same idea can be reproduced on different Linux handhelds without treating the original RG353V configuration as universal.

A working port should ideally end up feeling simple:

Connect devices
→ launch Anbernic Companion
→ Android appears on handheld
→ handheld controls work as a controller
→ exit
→ normal handheld interface returns

The detailed setup for a particular device belongs in a device-specific guide. CONTEXT.md contains additional structured information intended to help an LLM adapt the setup to new hardware without blindly copying assumptions from the reference implementation.

How I did it

Linux device edition

Warning

The script described here works on Anbernic RG353V running dArkOS. If your setup differs - you might need to ask an LLM to adapt it to your needs and do your own debugging. When writing I came to understanding that this is too specific to be a general use instruction, so I decided to provide a context.md file that you should feed to your LLM for it to understand what the project is and guide you through setup according to your circumstances

To have your screen mirrored from an android device (like AYN Thor, Retroid, etc.) to a linux-based handheld (in my example, Anbernic RG353V) through wi-fi hotspot (!!!) on the android side, you need:

Part 1, android preparation:

  1. Have developer options enabled for your device
  2. Install termux on it
  3. Install ADB: pkg install android-tools
  4. Connect termux's adb with the device itself: see https://gist.github.com/kairusds/1d4e32d3cf0d6ca44dc126c1a383a48d#file-1-termux-adb-md for instructions

Part 2, linux setup

You will need to find means to install a binary called scrcpy- link. I was lucky, because dArkOS is essentially a debian linux with a lot of stuff stripped away. On weaker devices (anbernic XX lineup for example) most custom operating systems are based on batocera, which is more barebones, and you might have to compile the binary yourself. For me, steps were:

  1. Enable remote services and (optionally) start sshd by default to still be able to access ssh on the device without needing to enable them again - sudo systemctl enable ssh
  2. Connect to it using your computer - ssh ark@<ip_address_of_your_device> - dArkOS uses ark as both username and password by default, type it when prompted for password. To get an IP address you need to look at the emulationstation menu (usually called by start button from games list)

Installing scrcpy

  1. Set up the Debian backports repository - it has scrcpy already built so it's easier to install from there than compiling manually:
sudo apt modernize-sources

sudo tee /etc/apt/sources.list.d/backports.sources >/dev/null <<'EOF'
Types: deb deb-src
URIs: http://deb.debian.org/debian
Suites: trixie-backports
Components: main contrib non-free non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
Enabled: yes
EOF
  1. Update sources, install the package
sudo apt update
sudo apt install -y -t trixie-backports scrcpy;
sudo apt install -y adb python3
  1. Add the script for the linux device using nano text editor. I put it in /roms/ports because the script is somewhat similar to how portmaster games are being run - nano /roms/ports/remote-thor.sh - paste contents of remote-thor.sh there.

  2. chmod +x /roms/ports/remote-thor.sh

Now you might want to add it to emulationstation (for my setup I did it the other way around - with a shortcut on Android side ssh'ing to linux to call for the script - but it needs you to manage ssh connection and that's pretty much out of scope here):

  1. nano /roms/ports/gamelist.xml

Copy any of the other entries, change name and game path to remote-thor.sh and save the file. You can also remove the image entry (at least for now)

After restart, you should get this script in Ports collection on your linux device.

Part 3 - enabling adb on android and setting up the connection

  1. Go to developer settings and make sure USB debugging is enabled
  2. Check you have your device visible from adb in termux: adb devices. There should be just one device - that's your android
  3. Enable adb over tcp on port 5555 (the script uses it, you can change but don't forget to edit it on the anbernic too): adb tcpip 5555
  4. Now you can try starting mirroring from emulationstation.
  5. You should see that the screen mirroring started on your linux device

Important

  • It's important for this script to work correctly to have the android device run a hotspot to which linux device is connected - the way it works is it looks at the current wifi network gateway and tries to connect to it, expecting to see adbd on that device. If it's your home router, that won't work.
  • You might also need to re-enable adb tcpip 5555 after your device restart. You can make a script for termux using Termux:Boot. However, be aware that having port 5555 open constantly can expose your device to malicious actions in public networks. You can make adb to stop listening this port by typing adb usb

If you're here and it's working, congratulations! If it's not - it should be rather easy to feed this doc to an LLM and get help with debugging. Of course, feel free to comment here

#!/bin/bash
set -Eeuo pipefail
APP_NAME="remote-thor"
APP_DIR="/roms/ports/$APP_NAME"
LOG_FILE="$APP_DIR/log.txt"
UNIT_NAME="$APP_NAME"
SCRIPT_PATH="$(readlink -f "$0")"
mkdir -p "$APP_DIR"
# Relaunch independently from EmulationStation or the SSH session.
if [ "${ANBERNIC_COMPANION_RUNNER:-0}" != "1" ]; then
if sudo systemctl is-active --quiet "${UNIT_NAME}.service"; then
exit 0
fi
sudo systemd-run \
--unit="$UNIT_NAME" \
--collect \
--property=Type=exec \
/usr/bin/env ANBERNIC_COMPANION_RUNNER=1 \
/bin/bash "$SCRIPT_PATH"
exit 0
fi
exec > >(tee "$LOG_FILE") 2>&1
ADB="/usr/bin/adb"
SCRCPY="/usr/bin/scrcpy"
SCRCPY_PID=""
WATCHER_PID=""
THOR_SERIAL=""
cleanup() {
trap - EXIT INT TERM
if [ -n "$WATCHER_PID" ]; then
kill "$WATCHER_PID" 2>/dev/null || true
fi
if [ -n "$SCRCPY_PID" ]; then
kill "$SCRCPY_PID" 2>/dev/null || true
fi
if [ -n "$THOR_SERIAL" ]; then
"$ADB" disconnect "$THOR_SERIAL" >/dev/null 2>&1 || true
fi
systemctl start emulationstation >/dev/null 2>&1 || true
}
trap cleanup EXIT INT TERM
echo "Starting Anbernic Companion: $(date)"
systemctl stop emulationstation
sleep 1
"$ADB" start-server >/dev/null
# The Android device is the hotspot gateway.
THOR_IP="$(ip route | awk '/default/ {print $3; exit}')"
if [ -z "$THOR_IP" ]; then
echo "Thor hotspot gateway not found."
sleep 5
exit 1
fi
THOR_SERIAL="$THOR_IP:5555"
echo "Connecting to Thor at $THOR_SERIAL"
"$ADB" connect "$THOR_SERIAL" || true
CONNECTED=0
for _ in $(seq 1 10); do
if [ "$("$ADB" -s "$THOR_SERIAL" get-state 2>/dev/null || true)" = "device" ]; then
CONNECTED=1
break
fi
sleep 0.5
done
if [ "$CONNECTED" -ne 1 ]; then
echo "Thor did not accept the ADB connection."
echo "Make sure ADB TCP port 5555 is enabled."
sleep 5
exit 1
fi
INPUT_DEVICE=""
for event_path in /sys/class/input/event*; do
[ -e "$event_path" ] || continue
device_name="$(cat "$event_path/device/name" 2>/dev/null || true)"
case "$device_name" in
*retrogame_joypad*|*retrogame*joypad*)
INPUT_DEVICE="/dev/input/$(basename "$event_path")"
break
;;
esac
done
if [ -z "$INPUT_DEVICE" ]; then
echo "The Anbernic controller was not found."
exit 1
fi
echo "Controller: $INPUT_DEVICE"
env \
SDL_VIDEODRIVER=kmsdrm \
"$SCRCPY" \
--serial "$THOR_SERIAL" \
--fullscreen \
--max-size=640 \
--max-fps=60 \
--video-codec=h264 \
--video-bit-rate=2M \
--no-audio \
--mouse=disabled \
--keyboard=disabled \
--gamepad=uhid \
--render-driver=opengles2 \
--print-fps &
SCRCPY_PID=$!
# Close scrcpy when Start and Select are pressed together.
python3 - "$INPUT_DEVICE" "$SCRCPY_PID" <<'PY' &
import os
import signal
import struct
import sys
device = sys.argv[1]
scrcpy_pid = int(sys.argv[2])
EV_KEY = 1
BTN_SELECT = 0x13A
BTN_START = 0x13B
event_struct = struct.Struct("llHHi")
pressed = {
BTN_SELECT: False,
BTN_START: False,
}
with open(device, "rb", buffering=0) as controller:
while True:
data = controller.read(event_struct.size)
if len(data) != event_struct.size:
continue
_, _, event_type, code, value = event_struct.unpack(data)
if event_type != EV_KEY or code not in pressed:
continue
pressed[code] = value != 0
if pressed[BTN_SELECT] and pressed[BTN_START]:
try:
os.kill(scrcpy_pid, signal.SIGTERM)
except ProcessLookupError:
pass
break
PY
WATCHER_PID=$!
if wait "$SCRCPY_PID"; then
RESULT=0
else
RESULT=$?
fi
echo "scrcpy exited with status $RESULT"
exit "$RESULT"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment