Compare commits
No commits in common. "e7c34d0de5a5eb267a5e3b69448c2e542c687062" and "10aff2c404a75d2511b66a2a5ec69417490fe955" have entirely different histories.
e7c34d0de5
...
10aff2c404
@ -31,11 +31,6 @@ This app should work on any Android device running Android 8 and above.
|
|||||||
* DJI Mavic Air 2
|
* DJI Mavic Air 2
|
||||||
* DJI Mini 4K
|
* DJI Mini 4K
|
||||||
* DJI Mini 2
|
* DJI Mini 2
|
||||||
* DJI Air 2S
|
|
||||||
* DJI Neo 2
|
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> Many people reported that this hack doesn't work with the Mini 3, if anyone finds a working hack I could take a look at reverse-engineering it.
|
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> Please let me know if you have tested this app on another drone so I can update this README.
|
> Please let me know if you have tested this app on another drone so I can update this README.
|
||||||
@ -78,7 +73,6 @@ No, this app only switches to FCC mode. To switch back to CE, turn off the drone
|
|||||||
## Goggles Support
|
## Goggles Support
|
||||||
>[!WARNING]
|
>[!WARNING]
|
||||||
> This app is not related to the following FCC file-based hacks for goggles; they are included here for reference only.
|
> This app is not related to the following FCC file-based hacks for goggles; they are included here for reference only.
|
||||||
|
|
||||||
Steps to enable higher power output for DJI Goggles:
|
Steps to enable higher power output for DJI Goggles:
|
||||||
|
|
||||||
**DJI Goggles V1/V2**
|
**DJI Goggles V1/V2**
|
||||||
|
|||||||
@ -13,7 +13,7 @@ android {
|
|||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "1.1"
|
versionName = "1.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,10 @@
|
|||||||
<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>
|
||||||
|
|
||||||
|
|||||||
@ -19,8 +19,6 @@ 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
|
||||||
@ -34,9 +32,7 @@ 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
|
||||||
@ -49,11 +45,9 @@ 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
|
||||||
@ -72,44 +66,17 @@ 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 {
|
||||||
@ -118,24 +85,15 @@ 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_EXPORTED
|
this, usbReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED
|
||||||
)
|
)
|
||||||
|
|
||||||
// Initial USB scan
|
// Initial check for USB connection
|
||||||
refreshUsbDeviceList()
|
refreshUsbConnection()
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
DJI_FCC_HACK_Theme {
|
DJI_FCC_HACK_Theme {
|
||||||
MainScreen(
|
MainScreen(usbConnected, ::refreshUsbConnection, ::sendPatch, isPatching)
|
||||||
usbConnected = usbConnected,
|
|
||||||
onRefresh = ::refreshUsbDeviceList,
|
|
||||||
onSendPatch = ::sendPatch,
|
|
||||||
isPatching = isPatching,
|
|
||||||
debugLogs = debugLogs,
|
|
||||||
usbDevices = usbDevices,
|
|
||||||
selectedDevice = selectedDevice,
|
|
||||||
onDeviceSelected = ::selectDevice
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -145,90 +103,28 @@ 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 device list
|
* Refreshes the USB connection status
|
||||||
*/
|
*/
|
||||||
private fun refreshUsbDeviceList() {
|
private fun refreshUsbConnection() {
|
||||||
addDebugLog("=== Scanning for USB devices ===")
|
if (usbManager.deviceList.isNotEmpty()) {
|
||||||
addDebugLog("Android ${android.os.Build.VERSION.RELEASE} (API ${android.os.Build.VERSION.SDK_INT})")
|
val device: UsbDevice = usbManager.deviceList.values.first()
|
||||||
|
|
||||||
// Check if USB host is supported
|
// Check to be sure the device is the initialized DJI Remote (and not another USB device)
|
||||||
val hasUsbHostFeature = packageManager.hasSystemFeature("android.hardware.usb.host")
|
if (device.productId != 4128) {
|
||||||
addDebugLog("USB Host feature available: $hasUsbHostFeature")
|
Log.d("USB_CONNECTION", "Device not supported ${device.productId}")
|
||||||
|
usbConnected = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Get device list
|
if (usbManager.openDevice(device) == null) {
|
||||||
val deviceList = usbManager.deviceList
|
Log.d("USB_CONNECTION", "Requesting USB Permission")
|
||||||
addDebugLog("UsbManager.deviceList size: ${deviceList.size}")
|
requestUsbPermission(device)
|
||||||
|
} else {
|
||||||
if (deviceList.isEmpty()) {
|
usbConnected = true
|
||||||
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 {
|
||||||
addDebugLog("Found ${deviceList.size} USB device(s):")
|
usbConnected = false
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,23 +132,12 @@ 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 {
|
||||||
addDebugLog("=== Starting FCC Patch ===")
|
// At this point, we assume the USB device is connected and we have permission to access it
|
||||||
|
if (!usbConnected) {
|
||||||
if (selectedDevice == null) {
|
Toast.makeText(this, "No USB device connected!", Toast.LENGTH_SHORT).show()
|
||||||
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)
|
||||||
|
|
||||||
@ -262,41 +147,30 @@ 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
|
||||||
addDebugLog("Probing USB device...")
|
val driver = UsbSerialProber(probeTable).probeDevice(usbManager.deviceList.values.first())
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -308,27 +182,14 @@ 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 {
|
Intent(Constants.INTENT_ACTION_GRANT_USB_PERMISSION).apply { setPackage(packageName) },
|
||||||
setPackage(packageName)
|
PendingIntent.FLAG_MUTABLE
|
||||||
putExtra(UsbManager.EXTRA_DEVICE, device)
|
|
||||||
},
|
|
||||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
)
|
)
|
||||||
|
|
||||||
addDebugLog("Calling usbManager.requestPermission()...")
|
usbManager.requestPermission(device, permissionIntent)
|
||||||
try {
|
|
||||||
usbManager.requestPermission(device, permissionIntent)
|
|
||||||
addDebugLog("Permission request sent successfully")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
addDebugLog("ERROR requesting permission: ${e.message}")
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -340,51 +201,18 @@ 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")
|
||||||
addDebugLog("EVENT: USB device attached")
|
refreshUsbConnection()
|
||||||
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")
|
||||||
addDebugLog("EVENT: USB device detached")
|
refreshUsbConnection()
|
||||||
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 -> {
|
||||||
val device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java)
|
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
|
||||||
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")
|
||||||
addDebugLog("✓ USB permission granted by user")
|
refreshUsbConnection()
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -398,25 +226,14 @@ 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")
|
||||||
}
|
}
|
||||||
@ -438,25 +255,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(
|
||||||
@ -493,240 +310,28 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// USB Device Selection Card
|
// USB Connection Status
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
containerColor = if (usbConnected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.errorContainer
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
Row(
|
||||||
Row(
|
modifier = Modifier.padding(16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxWidth()
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
|
||||||
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
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
Icon(
|
||||||
Row(
|
if (usbConnected) Icons.Default.CheckCircle else Icons.Default.Clear,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
contentDescription = "USB Status"
|
||||||
modifier = Modifier.fillMaxWidth()
|
)
|
||||||
) {
|
Spacer(Modifier.width(8.dp))
|
||||||
Icon(
|
Text(
|
||||||
Icons.Default.Info,
|
text = if (usbConnected) "Remote Connected" else "Remote Not Connected",
|
||||||
contentDescription = "Debug Info",
|
style = MaterialTheme.typography.bodyMedium
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -747,22 +352,13 @@ fun MainScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(56.dp),
|
.height(56.dp),
|
||||||
enabled = selectedDevice != null && !isPatching && buttonEnabled
|
enabled = usbConnected && !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) }) {
|
||||||
@ -787,17 +383,6 @@ 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(
|
||||||
|
|||||||
@ -1,84 +0,0 @@
|
|||||||
# 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()
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.11.0"
|
agp = "8.8.0"
|
||||||
kotlin = "2.0.0"
|
kotlin = "2.0.0"
|
||||||
coreKtx = "1.10.1"
|
coreKtx = "1.10.1"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
@ -27,7 +27,7 @@ androidx-material3 = { group = "androidx.compose.material3", name = "material3"
|
|||||||
|
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version = "8.6.0" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
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" }
|
||||||
|
|
||||||
|
|||||||
7
gradle/wrapper/gradle-wrapper.properties
vendored
7
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,7 +0,0 @@
|
|||||||
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
3
gradlew
vendored
@ -174,8 +174,7 @@ fi
|
|||||||
|
|
||||||
# Escape application args
|
# Escape application args
|
||||||
save () {
|
save () {
|
||||||
for i do printf %s\\n "$i" | sed "s/'/'\\''/g;1s/^/'/;
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
" ; done
|
|
||||||
echo " "
|
echo " "
|
||||||
}
|
}
|
||||||
APP_ARGS=`save "$@"`
|
APP_ARGS=`save "$@"`
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user