Flashing firmware massal via USB DFU (Device Firmware Upgrade) pada mobile device farm sering mengalami kegagalan akibat race condition tingkat hardware dan bus controller. Ketika puluhan worker mengeksekusi flashing secara paralel, fluktuasi state koneksi USB memicu benturan alokasi perangkat, payload transfer terputus di tengah jalan, dan antrian mengalami desinkronisasi.

Akar Masalah Flashing USB DFU di Lingkungan Paralel

Interaksi low-level USB DFU tidak bekerja seperti request I/O jaringan standar. Tiga kegagalan utama yang sering terjadi meliputi:

  • Re-enumerasi Bus USB: Saat berpindah dari mode operasional (misal: ADB/fastboot) ke mode DFU, SoC memutus jalur data D+/D- lalu menyambungkannya kembali. Vendor ID (VID) dan Product ID (PID) berubah, serta nomor devpath Linux (/dev/bus/usb/XXX/YYY) di-reset. Worker lain mendeteksi ini sebagai perangkat baru dan langsung mencoba mengklaimnya.
  • Premature Lease Expiry: Control/bulk transfer file image firmware (ratusan megabyte hingga gigabyte) memblokir thread worker. Jika distributed lock hanya menggunakan time-to-live (TTL) statis, lock kedaluwarsa sebelum penulisan ROM tuntas, memungkinkan antrian mengirim job baru ke port yang sama.
  • Desync State Saat Device Hang: Kegagalan verifikasi bootloader atau kerusakan flash partition menyebabkan perangkat terjebak pada mode Emergency Download (EDL) atau brick. Worker mati karena uncaught timeout, meninggalkan zombie lock di antrian.

Strategi Penguncian Berbasis Physical USB Topology

Jangan pernah mengunci perangkat berdasarkan VID:PID, nomor serial software, atau path devfs yang dinamis. Serial number sering kali hilang saat perangkat berada dalam bootloader DFU low-level. Solusinya: kunci physical USB topology path.

Linux sysfs menyediakan struktur port hierarkis yang statis untuk setiap port pada USB hub, misalnya 1-1.4.2 (Bus 1, Hub port 1.4, port 2). Port fisik ini tidak berubah terlepas dari berapa kali perangkat re-enumerasi atau berganti VID:PID.

# Identifikasi physical port perangkat via udevadm
udevadm info -p /sys/bus/usb/devices/1-1.4.2 | grep -E 'DEVPATH|ID_PATH'

Arsitektur State Machine USB DFU

Siklus eksekusi worker harus diikat ke state machine deterministik:

  1. LOCK_ACQUIRED: Worker mengunci token port fisik di Redis.
  2. ENTER_DFU: Kirim perintah reboot bootloader/DFU via ADB atau hardware relay testpoint.
  3. WAIT_ENUMERATION: Worker menunggu udev event mendeteksi kemunculan VID:PID DFU pada port fisik yang sama (maksimum 15 detik).
  4. TRANSFER_PAYLOAD: Transfer binary firmware. Dynamic heartbeat thread memperpanjang Redis lock setiap interval tertentu selama progress I/O aktif.
  5. REBOOT_VERIFY: Tunggu re-enumerasi kembali ke runtime OS.
  6. RELEASE: Lepas lock Redis, tandai job selesai.

Implementasi Worker Lock dengan Dynamic Heartbeat

Implementasi Python minimal berikut menggunakan Redis distributed lock dengan background heartbeat thread dan validasi physical topology.

import time
import threading
import redis

class USBPortLock:
    def __init__(self, r_client: redis.Redis, hub_port_path: str, ttl_ms: int = 10000):
        self.r = r_client
        self.lock_key = f"lock:usb:port:{hub_port_path}"
        self.ttl_ms = ttl_ms
        self.lock_value = f"worker_{threading.get_ident()}_{time.time()}"
        self._running = False
        self._renew_thread = None

    def acquire(self) -> bool:
        # ponytail: pakai set NX sederhana, upgrade ke Redlock jika cluster Redis multi-node
        ok = self.r.set(self.lock_key, self.lock_value, nx=True, px=self.ttl_ms)
        if not ok:
            return False
        self._running = True
        self._renew_thread = threading.Thread(target=self._heartbeat, daemon=True)
        self._renew_thread.start()
        return True

    def _heartbeat(self):
        interval = (self.ttl_ms / 1000.0) / 2.0
        while self._running:
            time.sleep(interval)
            # Perpanjang lease hanya jika value masih cocok
            lua_renew = """
            if redis.call('get', KEYS[1]) == ARGV[1] then
                return redis.call('pexpire', KEYS[1], ARGV[2])
            else
                return 0
            end
            """
            if not self.r.eval(lua_renew, 1, self.lock_key, self.lock_value, self.ttl_ms):
                self._running = False
                break

    def release(self):
        self._running = False
        if self._renew_thread:
            self._renew_thread.join(timeout=1.0)
        lua_release = """
        if redis.call('get', KEYS[1]) == ARGV[1] then
            return redis.call('del', KEYS[1])
        else
            return 0
        end
        """
        self.r.eval(lua_release, 1, self.lock_key, self.lock_value)

# Runnable self-check test
def test_usb_lock():
    # Setup mock/isolated Redis client local
    r = redis.Redis(host='localhost', port=6379, db=15, decode_responses=True)
    try:
        r.ping()
    except redis.ConnectionError:
        print("Redis tidak terdeteksi, lewati live test.")
        return

    port_path = "1-1.3"
    lock1 = USBPortLock(r, port_path, ttl_ms=2000)
    lock2 = USBPortLock(r, port_path, ttl_ms=2000)

    assert lock1.acquire() is True, "Lock1 gagal acquire"
    assert lock2.acquire() is False, "Race condition: Lock2 berhasil acquire port yang sama"
    
    # Verifikasi dynamic heartbeat memperpanjang TTL
    time.sleep(2.5)
    assert r.exists(lock1.lock_key) == 1, "Heartbeat gagal memperpanjang lock"

    lock1.release()
    assert r.exists(lock1.lock_key) == 0, "Lock tidak terhapus setelah release"
    assert lock2.acquire() is True, "Lock2 gagal acquire setelah dilepas Lock1"
    lock2.release()
    print("Test Lock Logika: OK")

if __name__ == "__main__":
    test_usb_lock()

[code] → skipped: penanganan failover Redis cluster; add when node Redis di-deploy secara distributed master-replica.

Mekanisme Pembersihan Idempoten (Zombie Lock Recovery)

Ketika perangkat gagal booting dan masuk ke state mati total (DFU failure), worker harus menjalankan prosedur pemulihan hardware sebelum melepas lock:

  • USB Port Power Cycling: Gunakan utilitas seperti uhubctl untuk memutus dan menyambungkan kembali daya VBUS pada port fisik USB hub.
  • Fencing Token: Setiap job flashing membawa nomor versi inkremental. Perangkat keras yang terhubung lambat tidak boleh memproses paket firmware lama jika worker sudah menganggap task kedaluwarsa.
# Reset power VBUS pada port USB 2 di hub bus 1 secara programmatic
uhubctl -l 1-1 -p 2 -a cycle -d 2
Catatan Operasional: Pastikan smart USB hub di device farm Anda mendukung per-port power switching (PPPS). Hub komersial murah sering kali memutus daya ke seluruh port secara bersamaan saat satu port di-reset, yang akan mematikan flashing perangkat lain di hub tersebut.