Files
artemis_control/internal/adb/discover.go
Paperclip Builder 1115531684 feat: Phase 2 act primitives
Add the Artemis Go CLI and its packages:
- internal/adb: timeout-bounded adb wrapper, device list, wireless-debugging
  port scan and auto-connect (never hardcodes the moving port).
- internal/ui: uiautomator dump parsing, find-by-text, tap the centre of a
  text match (FR-4), type, swipe, scroll, back, home, launch, wake.
- cmd/artemis: CLI for the primitives plus 'demo', the Phase 2 DoD run.

Failures are loud: a missing or disabled element errors instead of tapping a
guessed coordinate. The screen is woken before acting, and every adb call has
a deadline.

Also record the section 13 Q4 decision (adb lives in the user profile on .13,
wireless ADB needs no udev rules; service packaging still open until Phase 6),
add unit tests, and document the phase evidence.
2026-10-16 11:36:03 +11:00

158 lines
3.9 KiB
Go

package adb
import (
"fmt"
"sort"
"strings"
"sync"
"time"
"net"
)
// The wireless-debugging port changes on every phone reboot or toggle of
// wireless debugging, so it is never hardcoded. Android opens two TCP ports:
// a pairing port (useless here) and the connect port. `adb connect` succeeds on
// the connect port and is refused on the pairing port.
// DefaultScanRange is the range Android picks wireless-debugging ports from.
const (
DefaultScanFrom = 30000
DefaultScanTo = 65535
)
// Connect asks the adb server to connect to host:port.
func (r *Runner) Connect(address string) (string, error) {
out, err := r.run(15*time.Second, "connect", address)
msg := strings.TrimSpace(string(out))
if err != nil {
return msg, err
}
// adb connect can exit 0 while reporting failure in its output.
lower := strings.ToLower(msg)
if strings.Contains(lower, "failed") || strings.Contains(lower, "cannot") {
return msg, fmt.Errorf("adb connect %s: %s", address, msg)
}
return msg, nil
}
// DeviceOnline reports whether serial is present and in state "device".
func (r *Runner) DeviceOnline(serial string) bool {
devices, err := r.Devices()
if err != nil {
return false
}
for _, d := range devices {
if d.Serial == serial && d.State == "device" {
return true
}
}
return false
}
// ScanPorts does a concurrent TCP connect scan of host:from-to and returns the
// open ports in ascending order.
func ScanPorts(host string, from, to int, timeout time.Duration) []int {
if timeout <= 0 {
timeout = 500 * time.Millisecond
}
const workers = 512
type result struct {
port int
open bool
}
jobs := make(chan int)
results := make(chan result)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for port := range jobs {
addr := net.JoinHostPort(host, fmt.Sprintf("%d", port))
conn, err := net.DialTimeout("tcp", addr, timeout)
if err != nil {
results <- result{port: port, open: false}
continue
}
conn.Close()
results <- result{port: port, open: true}
}
}()
}
go func() {
for p := from; p <= to; p++ {
jobs <- p
}
close(jobs)
}()
go func() {
wg.Wait()
close(results)
}()
var open []int
for res := range results {
if res.open {
open = append(open, res.port)
}
}
sort.Ints(open)
return open
}
// ConnectAuto brings the phone online. It reuses an already-connected device if
// one is present, otherwise it scans host for open ports and tries
// `adb connect` on each one until a port comes up in state "device".
//
// It returns the serial it connected to.
func (r *Runner) ConnectAuto(host string) (string, error) {
// Already connected? Find a device whose serial is host:port.
for _, d := range mustDevices(r) {
if d.State == "device" && strings.HasPrefix(d.Serial, host+":") {
r.Serial = d.Serial
return d.Serial, nil
}
}
ports := ScanPorts(host, DefaultScanFrom, DefaultScanTo, 0)
if len(ports) == 0 {
return "", fmt.Errorf("no open TCP ports on %s in %d-%d; is wireless debugging on?",
host, DefaultScanFrom, DefaultScanTo)
}
var errs []string
for _, port := range ports {
serial := fmt.Sprintf("%s:%d", host, port)
if _, err := r.Connect(serial); err != nil {
errs = append(errs, fmt.Sprintf("%d: %v", port, err))
continue
}
// Give the adb server a moment to move the device to "device".
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
if r.DeviceOnline(serial) {
r.Serial = serial
return serial, nil
}
time.Sleep(250 * time.Millisecond)
}
errs = append(errs, fmt.Sprintf("%d: connect accepted but device never came online", port))
}
return "", fmt.Errorf("open ports on %s were %v but none accepted adb connect: %s",
host, ports, strings.Join(errs, "; "))
}
func mustDevices(r *Runner) []Device {
devices, err := r.Devices()
if err != nil {
return nil
}
return devices
}