updated for android 16 fixing some detection issues
This commit is contained in:
parent
f3e01a116c
commit
e7c34d0de5
@ -28,10 +28,6 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</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>
|
||||
</application>
|
||||
|
||||
|
||||
@ -19,6 +19,8 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.fillMaxWidth
|
||||
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.CheckCircle
|
||||
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.Star
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
@ -45,9 +49,11 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
@ -66,17 +72,44 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
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() {
|
||||
private lateinit var usbManager: UsbManager
|
||||
private var usbConnected 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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
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
|
||||
val filter = IntentFilter().apply {
|
||||
@ -85,15 +118,24 @@ class MainActivity : ComponentActivity() {
|
||||
addAction(Constants.INTENT_ACTION_GRANT_USB_PERMISSION)
|
||||
}
|
||||
ContextCompat.registerReceiver(
|
||||
this, usbReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
this, usbReceiver, filter, ContextCompat.RECEIVER_EXPORTED
|
||||
)
|
||||
|
||||
// Initial check for USB connection
|
||||
refreshUsbConnection()
|
||||
// Initial USB scan
|
||||
refreshUsbDeviceList()
|
||||
|
||||
setContent {
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the USB connection status
|
||||
*/
|
||||
private fun refreshUsbConnection() {
|
||||
if (usbManager.deviceList.isNotEmpty()) {
|
||||
val device: UsbDevice = usbManager.deviceList.values.first()
|
||||
Log.d("USB_CONNECTION", device.vendorId.toString() + ":" + device.productId.toString())
|
||||
private fun selectDevice(device: UsbDevice) {
|
||||
addDebugLog("User selected device: VID=${device.vendorId}, PID=${device.productId}")
|
||||
selectedDevice = device
|
||||
|
||||
// Check to be sure the device is the initialized DJI Remote (and not another USB device)
|
||||
if (device.productId != 4128) {
|
||||
Log.d("USB_CONNECTION", "Device not supported ${device.productId}")
|
||||
// Check permission status
|
||||
val hasPermission = usbManager.hasPermission(device)
|
||||
addDebugLog("Permission status: $hasPermission")
|
||||
|
||||
if (!hasPermission) {
|
||||
addDebugLog("Requesting permission for selected device...")
|
||||
usbConnected = false
|
||||
return
|
||||
}
|
||||
|
||||
if (usbManager.openDevice(device) == null) {
|
||||
Log.d("USB_CONNECTION", "Requesting USB Permission")
|
||||
requestUsbPermission(device)
|
||||
} else {
|
||||
addDebugLog("✓ Permission already granted for selected device")
|
||||
usbConnected = true
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the USB device list
|
||||
*/
|
||||
private fun refreshUsbDeviceList() {
|
||||
addDebugLog("=== Scanning for USB devices ===")
|
||||
addDebugLog("Android ${android.os.Build.VERSION.RELEASE} (API ${android.os.Build.VERSION.SDK_INT})")
|
||||
|
||||
// Check if USB host is supported
|
||||
val hasUsbHostFeature = packageManager.hasSystemFeature("android.hardware.usb.host")
|
||||
addDebugLog("USB Host feature available: $hasUsbHostFeature")
|
||||
|
||||
// Get device list
|
||||
val deviceList = usbManager.deviceList
|
||||
addDebugLog("UsbManager.deviceList size: ${deviceList.size}")
|
||||
|
||||
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 {
|
||||
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
|
||||
*/
|
||||
private fun sendPatch(): Boolean {
|
||||
// At this point, we assume the USB device is connected and we have permission to access it
|
||||
if (!usbConnected) {
|
||||
Toast.makeText(this, "No USB device connected!", Toast.LENGTH_SHORT).show()
|
||||
addDebugLog("=== Starting FCC Patch ===")
|
||||
|
||||
if (selectedDevice == null) {
|
||||
addDebugLog("ERROR: No device selected")
|
||||
Toast.makeText(this, "Please select a USB device first!", Toast.LENGTH_SHORT).show()
|
||||
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 {
|
||||
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
|
||||
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)
|
||||
if (deviceConnection == null) {
|
||||
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()
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
addDebugLog("Getting serial port...")
|
||||
val deviceSerialPort = driver.ports.firstOrNull()
|
||||
if (deviceSerialPort == null) {
|
||||
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()
|
||||
return false
|
||||
}
|
||||
|
||||
addDebugLog("Opening serial port...")
|
||||
deviceSerialPort.open(deviceConnection)
|
||||
addDebugLog("Setting parameters (19200 baud, 8N1)...")
|
||||
deviceSerialPort.setParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE)
|
||||
addDebugLog("Writing patch bytes (part 1)...")
|
||||
deviceSerialPort.write(Constants.BYTES_1, 1000)
|
||||
addDebugLog("Writing patch bytes (part 2)...")
|
||||
deviceSerialPort.write(Constants.BYTES_2, 1000)
|
||||
addDebugLog("SUCCESS: Patch sent successfully!")
|
||||
|
||||
Toast.makeText(this, "Patched successfully", Toast.LENGTH_LONG).show()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
addDebugLog("ERROR: ${e.javaClass.simpleName}: ${e.message}")
|
||||
Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
@ -183,14 +308,27 @@ class MainActivity : ComponentActivity() {
|
||||
* Requests USB permission for the device
|
||||
*/
|
||||
private fun requestUsbPermission(device: UsbDevice) {
|
||||
addDebugLog("Creating permission request intent...")
|
||||
addDebugLog("Package name: $packageName")
|
||||
|
||||
val permissionIntent = PendingIntent.getBroadcast(
|
||||
this,
|
||||
0,
|
||||
Intent(Constants.INTENT_ACTION_GRANT_USB_PERMISSION).apply { setPackage(packageName) },
|
||||
PendingIntent.FLAG_MUTABLE
|
||||
Intent(Constants.INTENT_ACTION_GRANT_USB_PERMISSION).apply {
|
||||
setPackage(packageName)
|
||||
putExtra(UsbManager.EXTRA_DEVICE, device)
|
||||
},
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
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) {
|
||||
UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
|
||||
Log.d("USB_EVENT", "USB Device Connected")
|
||||
refreshUsbConnection()
|
||||
addDebugLog("EVENT: USB device attached")
|
||||
refreshUsbDeviceList()
|
||||
}
|
||||
|
||||
UsbManager.ACTION_USB_DEVICE_DETACHED -> {
|
||||
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 -> {
|
||||
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")
|
||||
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,
|
||||
onRefresh: () -> Unit,
|
||||
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 buttonEnabled by remember { mutableStateOf(true) }
|
||||
var showDebugLog by remember { mutableStateOf(false) }
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
Scaffold(topBar = {
|
||||
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) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = "Refresh USB Connection")
|
||||
}
|
||||
@ -256,25 +438,25 @@ fun MainScreen(
|
||||
modifier = Modifier.size(75.dp),
|
||||
)
|
||||
|
||||
// Disclaimer Section
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = "Disclaimer",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "This app is provided as-is and is not affiliated with DJI. Use at your own risk.",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
// // Disclaimer Section
|
||||
// Card(
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// shape = RoundedCornerShape(16.dp),
|
||||
// colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)
|
||||
// ) {
|
||||
// Column(modifier = Modifier.padding(16.dp)) {
|
||||
// Text(
|
||||
// text = "Disclaimer",
|
||||
// style = MaterialTheme.typography.titleMedium,
|
||||
// fontWeight = FontWeight.Bold
|
||||
// )
|
||||
// Spacer(modifier = Modifier.height(4.dp))
|
||||
// Text(
|
||||
// text = "This app is provided as-is and is not affiliated with DJI. Use at your own risk.",
|
||||
// style = MaterialTheme.typography.bodyMedium
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
// Instructions Section
|
||||
Card(
|
||||
@ -311,29 +493,241 @@ fun MainScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// USB Connection Status
|
||||
// USB Device Selection Card
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (usbConnected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.errorContainer
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
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(16.dp),
|
||||
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.Center
|
||||
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
|
||||
)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
if (usbConnected) Icons.Default.CheckCircle else Icons.Default.Clear,
|
||||
contentDescription = "USB Status"
|
||||
Icons.Default.Info,
|
||||
contentDescription = "Debug Info",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = if (usbConnected) "Remote Connected" else "Remote Not Connected",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send Patch Button
|
||||
@ -353,13 +747,22 @@ fun MainScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
enabled = usbConnected && !isPatching && buttonEnabled
|
||||
enabled = selectedDevice != null && !isPatching && buttonEnabled
|
||||
) {
|
||||
Icon(Icons.Default.Build, contentDescription = "Patch")
|
||||
Spacer(Modifier.width(8.dp))
|
||||
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
|
||||
Row {
|
||||
IconButton(onClick = { uriHandler.openUri(Constants.GITHUB_URL) }) {
|
||||
@ -384,6 +787,17 @@ fun MainScreen(
|
||||
fontWeight = FontWeight.Bold,
|
||||
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(
|
||||
|
||||
84
dji_fcc_patch.py
Normal file
84
dji_fcc_patch.py
Normal 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()
|
||||
@ -27,7 +27,7 @@ androidx-material3 = { group = "androidx.compose.material3", name = "material3"
|
||||
|
||||
|
||||
[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-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
|
||||
|
||||
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user