Why a Phone Farm Needs an Agent Loop
Running tasks across 10–50 Android devices manually is chaos. Apps crash, sessions expire, CAPTCHAs appear. A naive script-per-device approach collapses. What you need is a persistent agent loop per device.
Architecture
Task Scheduler → Device Manager → Agent Loop (UiAutomator2 + OCR) → scrcpy monitor
1. Device Discovery
def list_devices():
result = subprocess.run(["adb", "devices", "-l"], capture_output=True, text=True)
devices = []
for line in result.stdout.strip().splitlines()[1:]:
if not line.strip(): continue
parts = line.split()
serial, state = parts[0], parts[1]
devices.append(Device(serial=serial, state=state))
return [d for d in devices if d.state == "device"]
2. Agent Loop
async def agent_loop(serial: str, task_queue: asyncio.Queue):
d = u2.connect(serial)
while True:
task = await task_queue.get()
try:
await execute_task(d, task)
except Exception:
await handle_recovery(d, serial)
finally:
task_queue.task_done()
3. OCR Fallback
async def ocr_tap(d, target_text: str):
img = d.screenshot(format="opencv")
result = ocr.ocr(img, cls=True)
for line in result[0]:
bbox, (text, conf) = line
if target_text.lower() in text.lower() and conf > 0.8:
cx = int((bbox[0][0] + bbox[2][0]) / 2)
cy = int((bbox[0][1] + bbox[2][1]) / 2)
d.click(cx, cy)
return
4. Recovery
async def handle_recovery(d, serial):
for fn in [lambda: d.press("back"), lambda: d.press("home"), lambda: d.reboot()]:
try:
fn()
await asyncio.sleep(2)
if is_responsive(d): return
except: continue
Key Takeaways
- One agent loop per device — never share UiAutomator2 across threads
- OCR fallback for canvas-rendered UIs
- Build recovery path before the happy path
- scrcpy on a separate channel from automation