Android USB Accessory Mode raises IOError

127 views Asked by At

I've put together a simple app and accompanying Python script to exchange data between phone and PC over USB, using AOA Protocol v2. But it's throwing an error and I don't know why.

MainActivity.kt:

package com.example.amx2

import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.hardware.usb.UsbAccessory
import android.hardware.usb.UsbManager
import android.os.Bundle
import android.os.ParcelFileDescriptor
import android.util.Log
import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.example.amx2.ui.theme.AMX2Theme
import java.io.BufferedInputStream
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException


const val TAG_USB = "usb"
const val ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION"

class MainActivity : ComponentActivity() {
    var mText: TextView? = null
    var mFileDescriptor: ParcelFileDescriptor? = null
    var mUsbManager: UsbManager? = null
    var mAccessory: UsbAccessory? = null
    var mInputStream: FileInputStream? = null
    var mOutputStream: FileOutputStream? = null
    var permissionIntent: PendingIntent? = null
    var bStarted: Boolean = false
    var mThread: Thread? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.i(TAG_USB, "MainActivity onCreate")
        mUsbManager = getSystemService(Context.USB_SERVICE) as UsbManager
        setContent {
            AMX2Theme {
                // A surface container using the 'background' color from the theme
                Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                    Greeting("Android")
                }
            }
        }

        //val filter = IntentFilter(ACTION_USB_PERMISSION)
        //this.registerReceiver(usbReceiver, filter)
        //Log.i(TAG_USB, "registered broadcast receiver!")
        requestUsbPermissions()

        var permissionIntent =
            PendingIntent.getBroadcast(this, 0, Intent(ACTION_USB_PERMISSION), 0)
        val filter = IntentFilter(ACTION_USB_PERMISSION)
        registerReceiver(usbReceiver, filter)
    }

    override fun onDestroy() {
        unregisterReceiver(usbReceiver)
        super.onDestroy()
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        if (UsbManager.ACTION_USB_ACCESSORY_ATTACHED == intent.action) requestUsbPermissions()
    }

    private fun requestUsbPermissions() {
        if(bStarted) {
            Log.v(TAG_USB, "Already running")
            return
        }
        if(mUsbManager == null) {
            Log.e(TAG_USB, "mUsbManager is null")
            return
        }
        if(mUsbManager!!.accessoryList == null) {
            Log.e(TAG_USB, "accessoryList is null")
            return
        }
        val deviceList: Array<out UsbAccessory>? = mUsbManager!!.accessoryList
        if(deviceList == null || deviceList.isEmpty()) {
            Log.v(TAG_USB, "Device list is empty")
            return
        }

        Log.v(TAG_USB, "requesting permission")
        mAccessory = deviceList[0]
        if (!mUsbManager!!.hasPermission(mAccessory)) {
            Log.i(TAG_USB, "requesting permission for device $mAccessory")
            mUsbManager!!.requestPermission(mAccessory, permissionIntent)
        }
        else {
            Log.i(TAG_USB, "Already have permission for device $mAccessory")
            openDevice()
        }
    }

    private val usbReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            if (ACTION_USB_PERMISSION == intent.action) {
                synchronized(this) {
                    val accessory: UsbAccessory? = intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY)
                    if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                        Log.i(TAG_USB, "Permission granted, opening device")
                        openDevice()
                    } else {
                        Log.d(TAG_USB, "permission denied for accessory $accessory")
                        mUsbManager?.requestPermission(accessory, permissionIntent)
                    }
                }
            }
        }
    }

    private fun openDevice() {
        if(bStarted) {
            Log.i(TAG_USB, "Already running")
            return
        }
        Log.v(TAG_USB, "Opening USB device $mAccessory")
        mFileDescriptor = mUsbManager!!.openAccessory(mAccessory)
        if(mFileDescriptor == null) {
            Log.e(TAG_USB, "Open failed")
            return
        }
        mFileDescriptor?.fileDescriptor?.also { fd ->
            mInputStream  = FileInputStream(fd)
            mOutputStream = FileOutputStream(fd)
            mThread = Thread(null, mListenerTask, "AccessoryThread")
            mThread?.start()
            Log.v(TAG_USB, "Thread started")
            bStarted = true
        }
    }

    private var mListenerTask: Runnable = object : Runnable {
        override fun run() {
            Log.i(TAG_USB, "Thread running")
            val buffer = ByteArray(16384)
            val bufIn = BufferedInputStream(mInputStream)
            while(true) {
                try {
                    Log.i(TAG_USB, "About to read")
                    val ret: Int = bufIn.read(buffer)
                    Log.i(TAG_USB, "Read $ret bytes")
                    if (ret > 0) {
                        val msg = String(buffer)
                        Log.v(TAG_USB, "Got msg: $msg")
                    } else {
                        Log.e(TAG_USB, "Read error $ret")
                    }
                } catch (e: IOException) {
                    Log.e(TAG_USB, "Read failed: ${e.message}")
                    mInputStream?.close()
                    return
                    //e.printStackTrace()
                }
                try {
                    Thread.sleep(1000)
                } catch (e: InterruptedException) {
                    //e.printStackTrace()
                    mInputStream?.close()
                    return
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
            text = "Hello $name!",
            modifier = modifier
    )
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    AMX2Theme {
        Greeting("Android")
    }
}

client.py:

#!/usr/bin/python3

import usb.core
import sys
import time
import random

VID_ONEPLUS_7T_DEBUG = 0x2a70
PID_ONEPLUS_7T_DEBUG = 0x4ee7

VID_ANDROID_ACCESSORY = 0x18d1
PID_ANDROID_ACCESSORY = 0x2d01

def get_accessory() -> usb.core.Device:
    print('Looking for Android Accessory')
    print('VID: 0x%0.4x - PID: 0x%0.4x'
        % (VID_ANDROID_ACCESSORY, PID_ANDROID_ACCESSORY))
    dev = usb.core.find(idVendor=VID_ANDROID_ACCESSORY,
                        idProduct=PID_ANDROID_ACCESSORY)
    return dev

def get_android_device():
    print('Looking for Android device')
    print('VID: 0x%0.4x - PID: 0x%0.4x'
        % (VID_ONEPLUS_7T_DEBUG, PID_ONEPLUS_7T_DEBUG))
    android_dev = usb.core.find(idVendor=VID_ONEPLUS_7T_DEBUG,
        idProduct=PID_ONEPLUS_7T_DEBUG)
    if android_dev:
        print('Device found')
    else:
        sys.exit('No Android device found')
    return android_dev


def set_protocol(ldev):
    #ldev.reset()
    try:
        ldev.set_configuration()
    except usb.core.USBError as e:
        if  e.errno == 16:
            print('Device already configured, should be OK')
        else:
            sys.exit('Configuration failed')

    ret = ldev.ctrl_transfer(0xC0, 51, 0, 0, 2)

# Dunno how to translate: array('B', [2, 0])
    protocol = ret[0]
    print('Protocol version: %i' % protocol)
    if protocol < 2:
        sys.exit('Android Open Accessory protocol v1 not supported')

    return

def set_strings(ldev):
    send_string(ldev, 0, 'Segment 6') # manufacturer
    send_string(ldev, 1, 'AMX2') # model
    send_string(ldev, 2, 'AMX2 Android Interface') # description
    send_string(ldev, 3, '0.1.0-beta') # version
    send_string(ldev, 4,
            'https://github.com/Arn-O/py-android-accessory/') # URI
    send_string(ldev, 5, '4815162342') # serual
    return

def set_accessory_mode(ldev):
    # last value is timeout, others are magic
    ret = ldev.ctrl_transfer(0x40, 53, 0, 0, '', 0)
    if ret:
        sys.exit('Start-up failed')
    time.sleep(1)
    return

def send_string(ldev, str_id, str_val):
    ret = ldev.ctrl_transfer(0x40, 52, 0, str_id, str_val, 0)
    if ret != len(str_val):
        sys.exit('Failed to send string %i' % str_id)
    return

def start_accessory_mode():
    dev = get_accessory()
    if not dev:
        print('Android accessory not found')
        print('Try to start accessory mode')
        dev = get_android_device()
        set_protocol(dev)
        set_strings(dev)
        set_accessory_mode(dev)
        dev = get_accessory()
        if not dev:
            sys.exit('Unable to start accessory mode')
    print('Accessory mode started')
    return dev

def wait_for_command(ldev: usb.core.Device):
    while True:
        try:
            try:
                msg = "beans"
                ret = ldev.write(0x02, msg, 1000)
                if ret == len(msg):
                    print(' - Write OK')
            except usb.core.USBError as e:
                print("USB write error", e)

            #try:
            #    ret = ldev.read(0x81, 5, 1000)
            #    sret = ''.join([chr(x) for x in ret])
            #    print('>>> '),
            #    print(sret)
            #    if sret == "A1111":
            #        variation = -3
            #    else:
            #        if sret == "A0000":
            #            variation = 3
            #    sensor = sensor_output(sensor, variation)
            #except usb.core.USBError as e:
            #    if e.errno == 110:
            #        pass
            #    else:
            #        print("USB read error", e)
            time.sleep(0.2)
        except KeyboardInterrupt:
            print("Bye!")
            break
    return

def main():
    dev = start_accessory_mode()
    wait_for_command(dev)

if __name__ == '__main__':
    main()

First, I close the app, unplug the USB cable, plug it back in, and run the Python script. Sometimes it's able to launch the application (I've already checked "always open this app to handle this device") but usually I get "Unable to start accessory mode".

After that, if I re-run the app from Android Studio, it will connect, but fail to write. On the PC side, the script appears to succeed at writing once:

Accessory mode started
 - Write OK
USB write error [Errno 5] Input/Output Error
USB write error [Errno 5] Input/Output Error
USB write error [Errno 19] No such device (it may have been disconnected)

On the Android side, logcat shows that the line val ret: Int = bufIn.read(buffer) raises an IOException with the message "EIO (I/O error)" once the Python script has sent something. I can't find any explanation why this would happen.

0

There are 0 answers