ARTICLE AD BOX
I have created an electron app to provide the sync service that my business logic will be hosted from, and it would control an expo app (barebone) to accept info from the host.
The problem is I keep getting these errors:
[syncService] Listening for AOA devices...
[syncService] Device attached ΓÇö VID: 0x18D1 PID: 0x4EE7
[syncService] Target device found ΓÇö switching to AOA mode...
[syncService] AOA switch failed: LIBUSB_ERROR_ACCESS
I have tried everything possible and I keep getting this error. I am looking for either assistance in how my code is set up or any leads to have the host open the device to communicate.
// electron/src/usb/syncService.js import { usb } from 'usb' const AOA_VENDOR_ID = 0x18D1 const AOA_ACCESSORY_PID = 0x2D00 const AOA_ACCESSORY_ADB_PID = 0x2D01 // ─── Your device's original IDs (before AOA switch) ────────────────────────── const TARGET_VENDOR_ID = 0x18D1 const TARGET_PRODUCT_ID = 0x4EE7 const AOA_GET_PROTOCOL = 51 const AOA_SEND_STRING = 52 const AOA_START = 53 const NEXT_API_BASE = process.env.NEXT_API_BASE || 'https://yourserver.com' // ─── Serial number reader ───────────────────────────────────────────────────── function getSerialNumber(device) { return new Promise((resolve, reject) => { const idx = device.deviceDescriptor.iSerialNumber if (!idx) return resolve(null) device.getStringDescriptor(idx, (err, serial) => { if (err) return reject(err) resolve(serial) }) }) } // ─── Fetch approved data from your Entity Portal API ───────────────────────── async function fetchApprovedData(serialNumber) { const res = await fetch( `${NEXT_API_BASE}/api/devices/${encodeURIComponent(serialNumber)}/approved`, { headers: { 'Content-Type': 'application/json' } } ) if (!res.ok) { throw new Error(`API rejected serial ${serialNumber} — status ${res.status}`) } return res.json() // { tracks: [...], playlists: [...], restrictions: {...} } } // ─── Send payload to device over USB ───────────────────────────────────────── function sendPayload(device, data) { return new Promise((resolve, reject) => { const iface = device.interfaces[0] try { iface.claim() } catch (err) { return reject(new Error(`Could not claim USB interface: ${err.message}`)) } const outEndpoint = iface.endpoints.find(e => e.direction === 'out') if (!outEndpoint) { return reject(new Error('No OUT endpoint found on device interface')) } const payload = Buffer.from(JSON.stringify(data), 'utf8') // Send 4-byte length prefix first so Android knows total bytes incoming const lengthPrefix = Buffer.alloc(4) lengthPrefix.writeUInt32BE(payload.length, 0) console.log(`[syncService] Sending ${payload.length} bytes to device...`) outEndpoint.transfer(lengthPrefix, (err) => { if (err) return reject(new Error(`Length prefix transfer failed: ${err.message}`)) // Send payload in chunks to avoid USB buffer overflow sendInChunks(outEndpoint, payload, 0, (err) => { if (err) return reject(err) console.log(`[syncService] Sync complete.`) iface.release(true, () => resolve()) }) }) }) } // Chunk size of 16KB is safe for most Android AOA implementations const CHUNK_SIZE = 16 * 1024 function sendInChunks(endpoint, buffer, offset, callback) { if (offset >= buffer.length) return callback(null) const chunk = buffer.slice(offset, offset + CHUNK_SIZE) endpoint.transfer(chunk, (err) => { if (err) return callback(new Error(`Chunk transfer failed at offset ${offset}: ${err.message}`)) sendInChunks(endpoint, buffer, offset + CHUNK_SIZE, callback) }) } // ─── Main sync entry point ──────────────────────────────────────────────────── // ─── Control transfer helper ─────────────────────────────────────────────────── function controlTransfer(device, bmRequestType, bRequest, wValue, wIndex, data) { return new Promise((resolve, reject) => { device.controlTransfer(bmRequestType, bRequest, wValue, wIndex, data, (err, res) => { if (err) return reject(err) resolve(res) }) }) } // ─── Switch to AOA mode ────────────────────────────────────────────────────── async function switchToAOA(device) { device.open() // 1. Check AOA protocol version const buf = await controlTransfer(device, 0xC0, AOA_GET_PROTOCOL, 0, 0, 2) const version = buf.readUInt16LE(0) console.log(`[syncService] AOA protocol version: ${version}`) if (version === 0) throw new Error('Device does not support AOA') // 2. Send accessory strings const strings = [ [0, 'YourCompany'], [1, 'IpodSystem'], [2, 'iPod Sync System'], [3, '1.0'], [4, 'https://yoursite.com'], [5, 'IpodSystemSerial'], ] for (const [index, str] of strings) { await controlTransfer(device, 0x40, AOA_SEND_STRING, 0, index, Buffer.from(str + '\0')) } // 3. Tell device to switch to AOA mode — pass empty Buffer instead of 0 await controlTransfer(device, 0x40, AOA_START, 0, 0, Buffer.alloc(0)) console.log('[syncService] AOA switch sent — device will reconnect...') device.close() } // ─── USB attach listener — call this once from main.js ─────────────────────── function startListening() { usb.on('attach', (device) => { const { idVendor, idProduct } = device.deviceDescriptor console.log(`[syncService] Device attached — VID: 0x${idVendor.toString(16).toUpperCase()} PID: 0x${idProduct.toString(16).toUpperCase()}`) // If it's your device in normal mode — switch it to AOA if (idVendor === TARGET_VENDOR_ID && idProduct === TARGET_PRODUCT_ID) { console.log('[syncService] Target device found — switching to AOA mode...') switchToAOA(device).catch(err => console.error('[syncService] AOA switch failed:', err.message)) return } // If it's already in AOA mode — sync it const isAccessory = idVendor === AOA_VENDOR_ID && (idProduct === AOA_ACCESSORY_PID || idProduct === AOA_ACCESSORY_ADB_PID) if (!isAccessory) { console.log('[syncService] Not a target device — skipping.') return } console.log('[syncService] AOA device attached — starting sync...') syncDevice(device) }) console.log('[syncService] Listening for AOA devices...') } const usb = require('usb') const AOA_VENDOR_ID = 0x18D1 const AOA_ACCESSORY_PID = 0x2D00 // accessory only const AOA_ACCESSORY_ADB_PID = 0x2D01 // accessory + adb // AOA identification strings (shown to user on device prompt) const AOA_STRINGS = { manufacturer: 'ipodSystem', modelName: 'MusicDevice', description: 'Music Sync', version: '1.0', uri: 'https://yourapp.com', serialNumber: '00000001' } async function findAndConnectDevice() { const devices = usb.getDeviceList() for (const device of devices) { try { device.open() const serial = await getSerial(device) // Step 1 — send AOA handshake to switch device into accessory mode await switchToAccessoryMode(device, serial) // device will disconnect and reconnect with AOA PID // listen for reconnection } catch (e) { device.close() } } } async function switchToAccessoryMode(device, serial) { // Check AOA protocol version const versionBuffer = Buffer.alloc(2) device.controlTransfer( 0xC0, 51, 0, 0, 2, (err, data) => { if (err) throw err const version = data.readUInt16LE(0) console.log('AOA version:', version) // should be 1 or 2 // Send identification strings sendAOAString(device, 0, AOA_STRINGS.manufacturer) sendAOAString(device, 1, AOA_STRINGS.modelName) sendAOAString(device, 2, AOA_STRINGS.description) sendAOAString(device, 3, AOA_STRINGS.version) sendAOAString(device, 4, AOA_STRINGS.uri) sendAOAString(device, 5, AOA_STRINGS.serialNumber) // Start accessory mode device.controlTransfer(0x40, 53, 0, 0, Buffer.alloc(0), (err) => { if (err) throw err // device will re-enumerate — wait for reconnect }) } ) } function sendAOAString(device, index, value) { const buf = Buffer.from(value + '\0', 'utf8') device.controlTransfer(0x40, 52, 0, index, buf, (err) => { if (err) console.error(`String ${index} failed:`, err) }) } async function getSerial(device) { return new Promise((resolve, reject) => { device.controlTransfer( 0x80, 6, 0x0303, 0x0409, 255, (err, data) => { if (err) return reject(err) resolve(data.slice(2).toString('utf16le').replace(/\0/g, '')) } ) }) }These two are the host (electron app) ^
Below are the module inside an expo app to register the intent and what not
package expo.modules.mymodule import android.content.Intent import android.hardware.usb.UsbAccessory import android.hardware.usb.UsbManager import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition import java.io.FileInputStream class UsbAccessoryModule : Module() { companion object { val accessoryObservers = mutableListOf<(UsbAccessory) -> Unit>() } override fun definition() = ModuleDefinition { Name("UsbAccessory") AsyncFunction("startListening") { val context = appContext.reactContext ?: throw Exception("React context is not available") val usbManager = context.getSystemService(android.content.Context.USB_SERVICE) as UsbManager val activity = appContext.currentActivity ?: throw Exception("No current activity") val accessory: UsbAccessory? = activity.intent?.getParcelableExtra(UsbManager.EXTRA_ACCESSORY) ?: throw Exception("No USB accessory found") val pfd = usbManager.openAccessory(accessory) val inputStream = FileInputStream(pfd.fileDescriptor) // Read length prefix val lenBytes = ByteArray(4) inputStream.read(lenBytes) val length = ((lenBytes[0].toInt() and 0xFF) shl 24) or ((lenBytes[1].toInt() and 0xFF) shl 16) or ((lenBytes[2].toInt() and 0xFF) shl 8) or (lenBytes[3].toInt() and 0xFF) // Read full payload val data = ByteArray(length) var offset = 0 while (offset < length) { val read = inputStream.read(data, offset, length - offset) if (read < 0) break offset += read } String(data, Charsets.UTF_8) // returned as resolved value to JS } } } package expo.modules.mymodule import android.app.Activity import android.content.Intent import android.hardware.usb.UsbAccessory import android.hardware.usb.UsbManager import android.os.Bundle import expo.modules.core.interfaces.ReactActivityLifecycleListener class MyReactActivityLifecycleListener : ReactActivityLifecycleListener { override fun onCreate(activity: Activity?, savedInstanceState: Bundle?) { handleIntent(activity?.intent) } override fun onNewIntent(intent: Intent?): Boolean { handleIntent(intent) return true } private fun handleIntent(intent: Intent?) { val accessory: UsbAccessory? = intent?.getParcelableExtra(UsbManager.EXTRA_ACCESSORY) if (accessory != null) { // Notify all observers registered by the module UsbAccessoryModule.accessoryObservers.forEach { observer -> observer(accessory) } } } }this will be injected into the main manifest:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-feature android:name="android.hardware.usb.host" /> <uses-permission android:name="android.permission.USB_PERMISSION" /> </manifest> <?xml version="1.0" encoding="utf-8"?> <resources> <usb-accessory manufacturer="ipodSystem" model="MusicDevice" version="1.0" /> </resources>`;can anyone tell me what is wrong or if they need more info ?
