updated for android 16 fixing some detection issues

This commit is contained in:
Your Name 2026-01-16 17:32:53 +01:00
parent f3e01a116c
commit e7c34d0de5
34 changed files with 1955 additions and 1453 deletions

View File

@ -28,10 +28,6 @@
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"/>
</intent-filter>
<meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" android:resource="@xml/device_filter"/>
</activity> </activity>
</application> </application>

View File

@ -19,6 +19,8 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@ -32,7 +34,9 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Build import androidx.compose.material.icons.filled.Build
import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
@ -45,9 +49,11 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
@ -66,17 +72,44 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
data class UsbDeviceInfo(
val device: UsbDevice,
val isDjiDevice: Boolean,
val hasPermission: Boolean
)
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private lateinit var usbManager: UsbManager private lateinit var usbManager: UsbManager
private var usbConnected by mutableStateOf(false) private var usbConnected by mutableStateOf(false)
private var isPatching by mutableStateOf(false) private var isPatching by mutableStateOf(false)
private val debugLogs = mutableStateListOf<String>()
private val maxLogs = 50
private val usbDevices = mutableStateListOf<UsbDeviceInfo>()
private var selectedDevice by mutableStateOf<UsbDevice?>(null)
private fun addDebugLog(message: String) {
val timestamp = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault()).format(Date())
val logEntry = "[$timestamp] $message"
debugLogs.add(0, logEntry) // Add to the beginning
Log.d("DEBUG_LOG", message)
// Keep only the most recent logs
if (debugLogs.size > maxLogs) {
debugLogs.removeAt(debugLogs.lastIndex)
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
usbManager = getSystemService(Context.USB_SERVICE) as UsbManager usbManager = getSystemService(Context.USB_SERVICE) as UsbManager
addDebugLog("App started")
addDebugLog("Android ${android.os.Build.VERSION.RELEASE} (API ${android.os.Build.VERSION.SDK_INT})")
// Register receiver to detect USB plug/unplug events // Register receiver to detect USB plug/unplug events
val filter = IntentFilter().apply { val filter = IntentFilter().apply {
@ -85,15 +118,24 @@ class MainActivity : ComponentActivity() {
addAction(Constants.INTENT_ACTION_GRANT_USB_PERMISSION) addAction(Constants.INTENT_ACTION_GRANT_USB_PERMISSION)
} }
ContextCompat.registerReceiver( ContextCompat.registerReceiver(
this, usbReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED this, usbReceiver, filter, ContextCompat.RECEIVER_EXPORTED
) )
// Initial check for USB connection // Initial USB scan
refreshUsbConnection() refreshUsbDeviceList()
setContent { setContent {
DJI_FCC_HACK_Theme { DJI_FCC_HACK_Theme {
MainScreen(usbConnected, ::refreshUsbConnection, ::sendPatch, isPatching) MainScreen(
usbConnected = usbConnected,
onRefresh = ::refreshUsbDeviceList,
onSendPatch = ::sendPatch,
isPatching = isPatching,
debugLogs = debugLogs,
usbDevices = usbDevices,
selectedDevice = selectedDevice,
onDeviceSelected = ::selectDevice
)
} }
} }
} }
@ -103,29 +145,90 @@ class MainActivity : ComponentActivity() {
unregisterReceiver(usbReceiver) unregisterReceiver(usbReceiver)
} }
private fun selectDevice(device: UsbDevice) {
addDebugLog("User selected device: VID=${device.vendorId}, PID=${device.productId}")
selectedDevice = device
// Check permission status
val hasPermission = usbManager.hasPermission(device)
addDebugLog("Permission status: $hasPermission")
if (!hasPermission) {
addDebugLog("Requesting permission for selected device...")
usbConnected = false
requestUsbPermission(device)
} else {
addDebugLog("✓ Permission already granted for selected device")
usbConnected = true
}
}
/** /**
* Refreshes the USB connection status * Refreshes the USB device list
*/ */
private fun refreshUsbConnection() { private fun refreshUsbDeviceList() {
if (usbManager.deviceList.isNotEmpty()) { addDebugLog("=== Scanning for USB devices ===")
val device: UsbDevice = usbManager.deviceList.values.first() addDebugLog("Android ${android.os.Build.VERSION.RELEASE} (API ${android.os.Build.VERSION.SDK_INT})")
Log.d("USB_CONNECTION", device.vendorId.toString() + ":" + device.productId.toString())
// Check to be sure the device is the initialized DJI Remote (and not another USB device) // Check if USB host is supported
if (device.productId != 4128) { val hasUsbHostFeature = packageManager.hasSystemFeature("android.hardware.usb.host")
Log.d("USB_CONNECTION", "Device not supported ${device.productId}") addDebugLog("USB Host feature available: $hasUsbHostFeature")
usbConnected = false
return
}
if (usbManager.openDevice(device) == null) { // Get device list
Log.d("USB_CONNECTION", "Requesting USB Permission") val deviceList = usbManager.deviceList
requestUsbPermission(device) addDebugLog("UsbManager.deviceList size: ${deviceList.size}")
} else {
usbConnected = true if (deviceList.isEmpty()) {
addDebugLog("WARNING: No USB devices found")
addDebugLog("Possible reasons:")
addDebugLog(" - No USB device connected")
addDebugLog(" - Android 16 USB enumeration bug")
addDebugLog(" - USB Protection enabled (unlock phone)")
addDebugLog(" - Try unplugging and re-plugging USB")
// Don't clear usbDevices here - keep showing last known devices
// Only clear connection status
if (selectedDevice != null) {
val stillExists = deviceList.values.any { it == selectedDevice }
if (!stillExists) {
addDebugLog("Selected device no longer in list")
usbConnected = false
selectedDevice = null
}
} }
} else { } else {
usbConnected = false addDebugLog("Found ${deviceList.size} USB device(s):")
// Build new list without clearing immediately
val newDevices = mutableListOf<UsbDeviceInfo>()
deviceList.values.forEachIndexed { index, device ->
val isDji = device.vendorId == 11427 && device.productId == 4128
val hasPerm = usbManager.hasPermission(device)
addDebugLog(" [$index] VID=0x${device.vendorId.toString(16)} PID=0x${device.productId.toString(16)}")
addDebugLog(" Name: ${device.deviceName}")
addDebugLog(" DJI Device: $isDji")
addDebugLog(" Has Permission: $hasPerm")
newDevices.add(UsbDeviceInfo(device, isDji, hasPerm))
// Update selected device permission status if it's in the new list
if (device == selectedDevice) {
usbConnected = hasPerm
addDebugLog("Selected device found, permission: $hasPerm")
}
// Auto-select DJI device if found and has permission and nothing selected
if (isDji && hasPerm && selectedDevice == null) {
selectedDevice = device
usbConnected = true
addDebugLog("✓ Auto-selected DJI device with permission")
}
}
// Update the list only after building complete list
usbDevices.clear()
usbDevices.addAll(newDevices)
} }
} }
@ -133,12 +236,23 @@ class MainActivity : ComponentActivity() {
* Sends the FCC patch to the DJI remote via USB * Sends the FCC patch to the DJI remote via USB
*/ */
private fun sendPatch(): Boolean { private fun sendPatch(): Boolean {
// At this point, we assume the USB device is connected and we have permission to access it addDebugLog("=== Starting FCC Patch ===")
if (!usbConnected) {
Toast.makeText(this, "No USB device connected!", Toast.LENGTH_SHORT).show() if (selectedDevice == null) {
addDebugLog("ERROR: No device selected")
Toast.makeText(this, "Please select a USB device first!", Toast.LENGTH_SHORT).show()
return false return false
} }
if (!usbManager.hasPermission(selectedDevice!!)) {
addDebugLog("ERROR: No permission for selected device")
Toast.makeText(this, "No USB permission!", Toast.LENGTH_SHORT).show()
return false
}
val device = selectedDevice!!
addDebugLog("Using device: VID=${device.vendorId}, PID=${device.productId}")
val probeTable = ProbeTable().apply { val probeTable = ProbeTable().apply {
addProduct(11427, 4128, CdcAcmSerialDriver::class.java) addProduct(11427, 4128, CdcAcmSerialDriver::class.java)
@ -148,30 +262,41 @@ class MainActivity : ComponentActivity() {
} }
// Retrieve the custom device (DJI remote) with the correct driver from the probe table above // Retrieve the custom device (DJI remote) with the correct driver from the probe table above
val driver = UsbSerialProber(probeTable).probeDevice(usbManager.deviceList.values.first()) addDebugLog("Probing USB device...")
val driver = UsbSerialProber(probeTable).probeDevice(device)
addDebugLog("Opening USB device...")
val deviceConnection = usbManager.openDevice(driver.device) val deviceConnection = usbManager.openDevice(driver.device)
if (deviceConnection == null) { if (deviceConnection == null) {
Log.e("USB_PATCH", "Error opening USB device") Log.e("USB_PATCH", "Error opening USB device")
addDebugLog("ERROR: Failed to open USB device")
Toast.makeText(this, "Error opening USB device", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Error opening USB device", Toast.LENGTH_SHORT).show()
return false return false
} }
try { try {
addDebugLog("Getting serial port...")
val deviceSerialPort = driver.ports.firstOrNull() val deviceSerialPort = driver.ports.firstOrNull()
if (deviceSerialPort == null) { if (deviceSerialPort == null) {
Log.e("USB_PATCH", "Error opening USB port") Log.e("USB_PATCH", "Error opening USB port")
addDebugLog("ERROR: No serial port found")
Toast.makeText(this, "Error opening USB port", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Error opening USB port", Toast.LENGTH_SHORT).show()
return false return false
} }
addDebugLog("Opening serial port...")
deviceSerialPort.open(deviceConnection) deviceSerialPort.open(deviceConnection)
addDebugLog("Setting parameters (19200 baud, 8N1)...")
deviceSerialPort.setParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE) deviceSerialPort.setParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE)
addDebugLog("Writing patch bytes (part 1)...")
deviceSerialPort.write(Constants.BYTES_1, 1000) deviceSerialPort.write(Constants.BYTES_1, 1000)
addDebugLog("Writing patch bytes (part 2)...")
deviceSerialPort.write(Constants.BYTES_2, 1000) deviceSerialPort.write(Constants.BYTES_2, 1000)
addDebugLog("SUCCESS: Patch sent successfully!")
Toast.makeText(this, "Patched successfully", Toast.LENGTH_LONG).show() Toast.makeText(this, "Patched successfully", Toast.LENGTH_LONG).show()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
addDebugLog("ERROR: ${e.javaClass.simpleName}: ${e.message}")
Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_LONG).show() Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_LONG).show()
} }
@ -183,14 +308,27 @@ class MainActivity : ComponentActivity() {
* Requests USB permission for the device * Requests USB permission for the device
*/ */
private fun requestUsbPermission(device: UsbDevice) { private fun requestUsbPermission(device: UsbDevice) {
addDebugLog("Creating permission request intent...")
addDebugLog("Package name: $packageName")
val permissionIntent = PendingIntent.getBroadcast( val permissionIntent = PendingIntent.getBroadcast(
this, this,
0, 0,
Intent(Constants.INTENT_ACTION_GRANT_USB_PERMISSION).apply { setPackage(packageName) }, Intent(Constants.INTENT_ACTION_GRANT_USB_PERMISSION).apply {
PendingIntent.FLAG_MUTABLE setPackage(packageName)
putExtra(UsbManager.EXTRA_DEVICE, device)
},
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
) )
usbManager.requestPermission(device, permissionIntent) addDebugLog("Calling usbManager.requestPermission()...")
try {
usbManager.requestPermission(device, permissionIntent)
addDebugLog("Permission request sent successfully")
} catch (e: Exception) {
addDebugLog("ERROR requesting permission: ${e.message}")
e.printStackTrace()
}
} }
@ -202,18 +340,51 @@ class MainActivity : ComponentActivity() {
when (intent.action) { when (intent.action) {
UsbManager.ACTION_USB_DEVICE_ATTACHED -> { UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
Log.d("USB_EVENT", "USB Device Connected") Log.d("USB_EVENT", "USB Device Connected")
refreshUsbConnection() addDebugLog("EVENT: USB device attached")
refreshUsbDeviceList()
} }
UsbManager.ACTION_USB_DEVICE_DETACHED -> { UsbManager.ACTION_USB_DEVICE_DETACHED -> {
Log.d("USB_EVENT", "USB Device Disconnected") Log.d("USB_EVENT", "USB Device Disconnected")
refreshUsbConnection() addDebugLog("EVENT: USB device detached")
val device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java)
if (device == selectedDevice) {
addDebugLog("Selected device was detached")
selectedDevice = null
usbConnected = false
}
refreshUsbDeviceList()
} }
Constants.INTENT_ACTION_GRANT_USB_PERMISSION -> { Constants.INTENT_ACTION_GRANT_USB_PERMISSION -> {
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { val device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java)
val granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
addDebugLog("Permission response for device: VID=${device?.vendorId}, PID=${device?.productId}")
addDebugLog("Permission granted: $granted")
if (granted) {
Log.d("USB_EVENT", "USB Permission Granted") Log.d("USB_EVENT", "USB Permission Granted")
refreshUsbConnection() addDebugLog("✓ USB permission granted by user")
// Verify permission with hasPermission
if (device != null) {
val verified = usbManager.hasPermission(device)
addDebugLog("Permission verified with hasPermission(): $verified")
if (device == selectedDevice && verified) {
usbConnected = true
addDebugLog("✓ Selected device now connected")
}
}
// Refresh immediately
refreshUsbDeviceList()
} else {
addDebugLog("❌ USB permission denied by user")
if (device == selectedDevice) {
usbConnected = false
}
} }
} }
} }
@ -227,14 +398,25 @@ fun MainScreen(
usbConnected: Boolean, usbConnected: Boolean,
onRefresh: () -> Unit, onRefresh: () -> Unit,
onSendPatch: () -> Boolean, onSendPatch: () -> Boolean,
isPatching: Boolean = false isPatching: Boolean = false,
debugLogs: SnapshotStateList<String> = mutableStateListOf(),
usbDevices: SnapshotStateList<UsbDeviceInfo> = mutableStateListOf(),
selectedDevice: UsbDevice? = null,
onDeviceSelected: (UsbDevice) -> Unit = {}
) { ) {
var buttonText by remember { mutableStateOf("Send FCC Patch") } var buttonText by remember { mutableStateOf("Send FCC Patch") }
var buttonEnabled by remember { mutableStateOf(true) } var buttonEnabled by remember { mutableStateOf(true) }
var showDebugLog by remember { mutableStateOf(false) }
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
Scaffold(topBar = { Scaffold(topBar = {
TopAppBar(title = { Text("DJI FCC Hack") }, actions = { TopAppBar(title = { Text("DJI FCC Hack") }, actions = {
androidx.compose.material3.TextButton(onClick = { showDebugLog = !showDebugLog }) {
Text(
text = if (showDebugLog) "Hide Log" else "Show Log",
style = MaterialTheme.typography.labelMedium
)
}
IconButton(onClick = onRefresh, enabled = !isPatching) { IconButton(onClick = onRefresh, enabled = !isPatching) {
Icon(Icons.Default.Refresh, contentDescription = "Refresh USB Connection") Icon(Icons.Default.Refresh, contentDescription = "Refresh USB Connection")
} }
@ -256,25 +438,25 @@ fun MainScreen(
modifier = Modifier.size(75.dp), modifier = Modifier.size(75.dp),
) )
// Disclaimer Section // // Disclaimer Section
Card( // Card(
modifier = Modifier.fillMaxWidth(), // modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp), // shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer) // colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)
) { // ) {
Column(modifier = Modifier.padding(16.dp)) { // Column(modifier = Modifier.padding(16.dp)) {
Text( // Text(
text = "Disclaimer", // text = "Disclaimer",
style = MaterialTheme.typography.titleMedium, // style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold // fontWeight = FontWeight.Bold
) // )
Spacer(modifier = Modifier.height(4.dp)) // Spacer(modifier = Modifier.height(4.dp))
Text( // Text(
text = "This app is provided as-is and is not affiliated with DJI. Use at your own risk.", // text = "This app is provided as-is and is not affiliated with DJI. Use at your own risk.",
style = MaterialTheme.typography.bodyMedium // style = MaterialTheme.typography.bodyMedium
) // )
} // }
} // }
// Instructions Section // Instructions Section
Card( Card(
@ -311,28 +493,240 @@ fun MainScreen(
} }
} }
// USB Connection Status // USB Device Selection Card
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = if (usbConnected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.errorContainer containerColor = MaterialTheme.colorScheme.surfaceVariant
) )
) { ) {
Row( Column(modifier = Modifier.padding(16.dp)) {
modifier = Modifier.padding(16.dp), Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center modifier = Modifier.fillMaxWidth()
) {
Icon(
Icons.Default.Build,
contentDescription = "USB Devices",
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "USB Devices (${usbDevices.size})",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.height(8.dp))
if (usbDevices.isEmpty()) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = "No USB devices found",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.error
)
// Text(
// text = "Troubleshooting steps:",
// style = MaterialTheme.typography.bodySmall,
// fontWeight = FontWeight.Bold,
// color = MaterialTheme.colorScheme.onSurfaceVariant
// )
// Column(modifier = Modifier.padding(start = 8.dp)) {
// listOf(
// "Make sure phone is UNLOCKED",
// "Try unplugging and re-plugging USB",
// "Check USB notification - change to 'File Transfer' mode",
// "Enable USB debugging in Developer options",
// "Known bug in Android 16 - may not work reliably"
// ).forEach { tip ->
// Row(modifier = Modifier.padding(vertical = 2.dp)) {
// Text("• ", style = MaterialTheme.typography.bodySmall)
// Text(
// text = tip,
// style = MaterialTheme.typography.bodySmall,
// color = MaterialTheme.colorScheme.onSurfaceVariant
// )
// }
// }
// }
// Spacer(modifier = Modifier.height(4.dp))
// Text(
// text = "Tap refresh (↻) after trying each step",
// style = MaterialTheme.typography.bodySmall,
// fontStyle = FontStyle.Italic,
// color = MaterialTheme.colorScheme.primary
// )
}
} else {
usbDevices.forEach { deviceInfo ->
val isSelected = deviceInfo.device == selectedDevice
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
.clickable { onDeviceSelected(deviceInfo.device) }
.then(
if (isSelected) {
Modifier.border(
3.dp,
MaterialTheme.colorScheme.primary,
RoundedCornerShape(8.dp)
)
} else {
Modifier
}
),
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(
containerColor = if (isSelected)
MaterialTheme.colorScheme.primaryContainer
else
MaterialTheme.colorScheme.surfaceContainerHighest
)
) {
Row(
modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = "VID: 0x${deviceInfo.device.vendorId.toString(16)} / PID: 0x${deviceInfo.device.productId.toString(16)}",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
if (deviceInfo.isDjiDevice) {
Spacer(modifier = Modifier.width(8.dp))
Icon(
Icons.Default.Star,
contentDescription = "DJI Device",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp)
)
}
}
Text(
text = deviceInfo.device.deviceName,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
if (deviceInfo.isDjiDevice) {
Text(
text = "✓ DJI Remote Controller",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = if (deviceInfo.hasPermission) "✓ Has Permission" else "⚠ No Permission",
style = MaterialTheme.typography.bodySmall,
color = if (deviceInfo.hasPermission)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.error
)
if (!deviceInfo.hasPermission) {
androidx.compose.material3.TextButton(
onClick = { onDeviceSelected(deviceInfo.device) },
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 8.dp, vertical = 0.dp),
modifier = Modifier.height(24.dp)
) {
Text(
text = "Grant",
style = MaterialTheme.typography.labelSmall
)
}
}
}
}
if (isSelected) {
Icon(
Icons.Default.CheckCircle,
contentDescription = "Selected",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp)
)
}
}
}
}
}
}
}
// Debug Log Section
if (showDebugLog) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) { ) {
Icon( Column(modifier = Modifier.padding(16.dp)) {
if (usbConnected) Icons.Default.CheckCircle else Icons.Default.Clear, Row(
contentDescription = "USB Status" verticalAlignment = Alignment.CenterVertically,
) modifier = Modifier.fillMaxWidth()
Spacer(Modifier.width(8.dp)) ) {
Text( Icon(
text = if (usbConnected) "Remote Connected" else "Remote Not Connected", Icons.Default.Info,
style = MaterialTheme.typography.bodyMedium contentDescription = "Debug Info",
) tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Debug Log",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.height(8.dp))
if (debugLogs.isEmpty()) {
Text(
text = "No logs yet...",
style = MaterialTheme.typography.bodySmall,
fontStyle = FontStyle.Italic,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
} else {
Card(
modifier = Modifier
.fillMaxWidth()
.height(200.dp),
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
)
) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(8.dp)
) {
debugLogs.forEach { log ->
Text(
text = log,
style = MaterialTheme.typography.bodySmall,
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace,
modifier = Modifier.padding(vertical = 2.dp)
)
}
}
}
}
}
} }
} }
@ -353,13 +747,22 @@ fun MainScreen(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(56.dp), .height(56.dp),
enabled = usbConnected && !isPatching && buttonEnabled enabled = selectedDevice != null && !isPatching && buttonEnabled
) { ) {
Icon(Icons.Default.Build, contentDescription = "Patch") Icon(Icons.Default.Build, contentDescription = "Patch")
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text(buttonText) Text(buttonText)
} }
if (selectedDevice == null && usbDevices.isNotEmpty()) {
Text(
text = "Please select a USB device above",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
fontStyle = FontStyle.Italic
)
}
// Links // Links
Row { Row {
IconButton(onClick = { uriHandler.openUri(Constants.GITHUB_URL) }) { IconButton(onClick = { uriHandler.openUri(Constants.GITHUB_URL) }) {
@ -384,6 +787,17 @@ fun MainScreen(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
) )
Text(
text = " updated by ",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.secondary
)
Text(
text = "luhf",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.secondary,
)
} }
Text( Text(

84
dji_fcc_patch.py Normal file
View File

@ -0,0 +1,84 @@
# DJI FCC Patch Script for Windows
#
# This script sends a "magic packet" to a DJI controller
# to enable FCC mode, which can increase the drone's signal range.
#
# Credits:
# - Based on the Android app by Mathieu Broillet: https://github.com/M4TH1EU/DJI-FCC-HACK
# - Original research by @galbb on MavicPilots.com
#
# HOW TO USE:
# 1. Install Python from the Microsoft Store or python.org.
# 2. Install the pyserial library by opening a Command Prompt or PowerShell and running:
# pip install pyserial
# 3. Turn on your drone and controller, and wait for them to connect.
# 4. Connect your PC to the bottom USB port of the controller.
# 5. Run this script.
# 6. If successful, disconnect the PC and connect your phone to the top USB port of the controller.
#
# DISCLAIMER: Use at your own risk. This script modifies your device's operation.
# The authors are not responsible for any damage or legal issues that may arise.
import serial
import serial.tools.list_ports
import time
# DJI Controller USB identifiers
VENDOR_ID = 11427
PRODUCT_ID = 4128
# The "magic bytes" that enable FCC mode
# (from ch.mathieubroillet.djiffchack.Constants.kt)
BYTES_1 = bytes([85, 13, 4, 33, 42, 31, 0, 0, 0, 0, 1, -122 & 0xFF, 32])
BYTES_2 = bytes([85, 24, 4, 32, 2, 9, 0, 0, 64, 9, 39, 0, 2, 72, 0, -1 & 0xFF, -1 & 0xFF, 2, 0, 0, 0, 0, -127 & 0xFF, 31])
def find_dji_controller():
"""Finds the COM port of the connected DJI controller."""
ports = serial.tools.list_ports.comports()
for port in ports:
if port.vid == VENDOR_ID and port.pid == PRODUCT_ID:
print(f"Found DJI Controller at {port.device}")
return port.device
return None
def send_patch(com_port):
"""Opens the serial port and sends the patch bytes."""
try:
with serial.Serial(com_port, baudrate=19200, bytesize=8, parity='N', stopbits=1, timeout=1) as ser:
print("Sending patch...")
# Send first byte array
ser.write(BYTES_1)
print(f"Sent {len(BYTES_1)} bytes (Packet 1).")
time.sleep(0.1) # Small delay between writes
# Send second byte array
ser.write(BYTES_2)
print(f"Sent {len(BYTES_2)} bytes (Packet 2).")
print("\nPatch sent successfully!")
print("You can now disconnect the controller from your PC.")
return True
except serial.SerialException as e:
print(f"Error: Could not open or write to serial port {com_port}.")
print(f"Details: {e}")
print("\nPlease ensure the controller is properly connected and no other software is using the COM port.")
return False
if __name__ == "__main__":
print("--- DJI FCC Patch Script ---")
print("Searching for DJI Controller...")
controller_port = find_dji_controller()
if controller_port:
send_patch(controller_port)
else:
print("\nDJI Controller not found.")
print("Please make sure:")
print("1. The controller is turned on and connected to the drone.")
print("2. Your PC is connected to the BOTTOM USB port of the controller.")
print("3. The necessary USB drivers are installed.")
print("\nPress Enter to exit.")
input()

View File

@ -27,7 +27,7 @@ androidx-material3 = { group = "androidx.compose.material3", name = "material3"
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version = "8.6.0" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

View File

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

3
gradlew vendored
View File

@ -174,7 +174,8 @@ fi
# Escape application args # Escape application args
save () { save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done for i do printf %s\\n "$i" | sed "s/'/'\\''/g;1s/^/'/;
" ; done
echo " " echo " "
} }
APP_ARGS=`save "$@"` APP_ARGS=`save "$@"`