여러 폰 운영하기

폰에 이름을 붙이고, Python, Node.js, 셸에서 동시에 실행하고, 모바일 IP 주소를 새로 받고, 선반 가득한 폰을 며칠씩 연결된 상태로 유지합니다.

번역된 페이지입니다. 영어 문서와 내용이 다르면 영어 문서가 기준입니다. English

droidline serve 하나로 PC와 네트워크가 감당하는 만큼 폰을 다룰 수 있습니다. 폰마다 명령 큐가 따로 있어서, 한 폰에 보낸 명령은 순서대로 실행되고 서로 다른 폰은 동시에 실행됩니다.

폰마다 이름 붙이기

모든 폰에는 k7d2q9xa 같은 무작위 ID가 있습니다. 페어링할 때 기억하기 쉬운 이름을 붙이거나, 나중에 이름을 바꾸세요.

droidline pair 482913 --name shelf-01
droidline rename k7d2q9xa shelf-02
droidline devices

이름에는 문자, 숫자, 점, 하이픈, 밑줄을 쓸 수 있습니다. 같은 이름을 적은 라벨을 폰에 붙여 두면 어느 폰인지 헷갈릴 일이 크게 줄어듭니다.

동시에 실행하기

서버에 어떤 폰이 온라인인지 물은 다음, 폰마다 스레드, 태스크, 백그라운드 작업을 하나씩 주세요.

from concurrent.futures import ThreadPoolExecutor
from droidline import Droidline

dl = Droidline()

def morning(name: str) -> str:
    d = dl.device(name)
    d.wake()
    d.launch("com.example.app")
    d.touch("text", "Check in", timeout=20)
    return d.get_text("id", "status")

names = [p["name"] for p in dl.devices() if p["online"]]
with ThreadPoolExecutor(len(names) or 1) as pool:
    for name, status in zip(names, pool.map(morning, names)):
        print(name, status)
import { Droidline } from "droidline";

const dl = new Droidline();
const phones = (await dl.devices()).filter((p) => p.online);

const results = await Promise.allSettled(phones.map(async (p) => {
  const d = dl.device(p.name);
  await d.wake();
  await d.launch("com.example.app");
  await d.touch("text", "Check in", { timeout: 20 });
  return d.getText("id", "status");
}));

results.forEach((r, i) => console.log(phones[i].name, r.status === "fulfilled" ? r.value : r.reason.message));
for name in $(droidline --json devices | jq -r '.value[] | select(.online) | .name'); do
  (
    droidline -d "$name" wake
    droidline -d "$name" launch com.example.app
    droidline -d "$name" touch text "Check in" --timeout 20
    echo "$name: $(droidline -d "$name" get_text id status)"
  ) &
done
wait

한 폰이 실패해도 다른 폰은 멈추지 않습니다. Python에서는 그 폰의 결과에 이르렀을 때 pool.map에서 예외가 나오고, Node.js에서는 allSettled가 실패를 기록하며, 셸에서는 폰마다 별도의 서브셸에서 실행됩니다.

폰마다 다른 네트워크 쓰기

앱별 프록시를 쓰면 폰마다 한 앱을 서로 다른 업스트림으로 보낼 수 있습니다. 업스트림을 한 번 저장해 두고 이름으로 지정하세요.

droidline proxy add kr1 socks5://user:pass@203.0.113.10:1080
droidline proxy add kr2 socks5://user:pass@203.0.113.11:1080
plan = {"shelf-01": "@kr1", "shelf-02": "@kr2"}
for name, upstream in plan.items():
    dl.device(name).proxy(upstream, app="com.android.chrome")
const plan = { "shelf-01": "@kr1", "shelf-02": "@kr2" };
for (const [name, upstream] of Object.entries(plan)) {
  await dl.device(name).proxy(upstream, { app: "com.android.chrome" });
}
droidline -d shelf-01 proxy @kr1 --app com.android.chrome
droidline -d shelf-02 proxy @kr2 --app com.android.chrome

새 모바일 IP 주소

모바일 데이터에서는 비행기 모드를 켰다 끄면 보통 통신사에서 새 주소를 받습니다. 폰은 오프라인인 동안 명령을 받을 수 없으므로, 켜기와 끄기를 하나의 batch로 보내세요. batch는 폰이 혼자 끝까지 실행합니다.

d.batch([("airplane", True), ("sleep", 3000), ("airplane", False)], wait=True)
await d.batch([["airplane", true], ["sleep", 3000], ["airplane", false]], { wait: true });
droidline -d shelf-01 batch '[["airplane",true],["sleep",3000],["airplane",false]]' --wait

wait가 없으면 호출은 바로 {"accepted": true}를 돌려주고, 결과는 나중에 result 이벤트로 도착합니다.

꺼지는 화면

폰 화면이 꺼져 있으면 탭과 스와이프가 실패합니다. 작업마다 먼저 wake()를 호출하세요. 화면을 켜고, PIN이 없는 잠금 화면을 닫습니다. 사람이 지켜보지 않는 폰이라면 폰 설정에서 PIN, 패턴, 비밀번호 잠금을 해제해 두세요. Droidline은 이런 잠금을 풀 수 없습니다.

며칠씩 연결 유지하기

  • Droidline의 배터리 최적화를 제한 없음으로 설정하고, 폰 제조사가 따로 두는 백그라운드 제한에서도 Droidline을 허용하세요(흔히 있는 위치는 문제 해결에 정리되어 있습니다).
  • 페어링한 뒤에는 폰이 부팅될 때 앱이 자동으로 시작됩니다.
  • 다시 접속하면 폰은 PC가 놓친 응답을 다시 보내고, 반복된 명령은 두 번 실행되지 않습니다. 개념을 보세요.
  • droidline devices는 폰마다 연결 경로와 빠진 권한을 보여 줍니다. 오프라인 폰이 마지막으로 접속한 시각은 devices()의 last_seen으로 알 수 있습니다.
  • 폰은 계속 충전해 두세요. USB 충전기면 충분하고, 데이터 연결은 필요 없습니다.

선반 지켜보기

device 이벤트를 구독하면 폰이 끊기거나 돌아오는 순간을 알 수 있습니다.

from droidline import Droidline

dl = Droidline()
dl.subscribe(["device"])
dl.on("device", lambda e: print(e["name"], e["state"], e.get("route")))
input("Watching. Press Enter to stop.\n")
droidline --json devices | jq -r '.value[] | "\(.name)\t\(if .online then "online" else "offline" end)\t\(.route)"'

업데이트

앱은 하루에 한 번 GitHub Releases를 확인하고, 새 버전이 있으면 알림을 띄웁니다. 기존 앱 위에 업데이트를 설치하면 페어링은 그대로 유지됩니다.