mirror of
https://github.com/M4TH1EU/DJI-FCC-HACK.git
synced 2025-07-05 11:03:19 +00:00
fixed serial connection, improved ui, improve error handling, ready for release
This commit is contained in:
parent
0942019869
commit
27204471c7
@ -23,16 +23,15 @@
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.Djiffchack">
|
||||
<intent-filter>
|
||||
<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"/>-->
|
||||
<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>
|
||||
|
||||
|
@ -1,9 +1,14 @@
|
||||
package ch.mathieubroillet.djiffchack
|
||||
|
||||
class Constants {
|
||||
companion object {
|
||||
// const val INTENT_ACTION_GRANT_USB_ACCESSORY = "ch.mathieubroillet.djiffchack.USB_ACCESSORY_PERMISSION"
|
||||
// const val INTENT_ACTION_GRANT_USB_DEVICE = "ch.mathieubroillet.djiffchack.USB_DEVICE_PERMISSION"
|
||||
const val INTENT_ACTION_GRANT_USB_PERMISSION = "ch.mathieubroillet.djiffchack.USB_PERMISSION"
|
||||
}
|
||||
|
||||
object Constants {
|
||||
private const val PACKAGE = "ch.mathieubroillet.djiffchack"
|
||||
const val INTENT_ACTION_GRANT_USB_PERMISSION = "$PACKAGE.USB_PERMISSION"
|
||||
|
||||
// The "magic bytes" that enable FCC mode
|
||||
// The credits goes to @galbb from https://mavicpilots.com/threads/mavic-air-2-switch-to-fcc-mode-using-an-android-app.115027/
|
||||
val BYTES_1 = byteArrayOf(85, 13, 4, 33, 42, 31, 0, 0, 0, 0, 1, -122, 32)
|
||||
val BYTES_2 = byteArrayOf(85, 24, 4, 32, 2, 9, 0, 0, 64, 9, 39, 0, 2, 72, 0, -1, -1, 2, 0, 0, 0, 0, -127, 31)
|
||||
|
||||
const val GITHUB_URL = "https://github.com/M4TH1EU/DJI-FCC-HACK"
|
||||
}
|
@ -6,7 +6,6 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.hardware.usb.UsbDevice
|
||||
import android.hardware.usb.UsbDeviceConnection
|
||||
import android.hardware.usb.UsbManager
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
@ -25,12 +24,13 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
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.MoreVert
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
@ -49,7 +49,10 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
@ -66,7 +69,7 @@ import kotlinx.coroutines.launch
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private lateinit var usbManager: UsbManager
|
||||
private var usbConnection by mutableStateOf(null as UsbDeviceConnection?)
|
||||
private var usbConnected by mutableStateOf(false)
|
||||
private var isPatching by mutableStateOf(false)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@ -81,10 +84,7 @@ class MainActivity : ComponentActivity() {
|
||||
addAction(Constants.INTENT_ACTION_GRANT_USB_PERMISSION)
|
||||
}
|
||||
ContextCompat.registerReceiver(
|
||||
this,
|
||||
usbReceiver,
|
||||
filter,
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
this, usbReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
)
|
||||
|
||||
// Initial check for USB connection
|
||||
@ -92,7 +92,7 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
setContent {
|
||||
DJI_FCC_HACK_Theme {
|
||||
MainScreen(usbConnection != null, ::refreshUsbConnection, ::sendPatch, isPatching)
|
||||
MainScreen(usbConnected, ::refreshUsbConnection, ::sendPatch, isPatching)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -112,66 +112,69 @@ class MainActivity : ComponentActivity() {
|
||||
// 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}")
|
||||
usbConnected = false
|
||||
return
|
||||
}
|
||||
|
||||
usbConnection = usbManager.openDevice(device)
|
||||
|
||||
|
||||
if (usbConnection == null) {
|
||||
if (usbManager.openDevice(device) == null) {
|
||||
Log.d("USB_CONNECTION", "Requesting USB Permission")
|
||||
requestUsbPermission(device)
|
||||
} else {
|
||||
usbConnected = true
|
||||
}
|
||||
} else {
|
||||
usbConnection = null
|
||||
usbConnected = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles sending the patch via USB communication
|
||||
* Sends the FCC patch to the DJI remote via USB
|
||||
*/
|
||||
private fun sendPatch() {
|
||||
isPatching = true
|
||||
|
||||
if (usbConnection == null) {
|
||||
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()
|
||||
isPatching = false
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
for (device in usbManager.deviceList.values) {
|
||||
try {
|
||||
val probeTable = ProbeTable()
|
||||
probeTable.addProduct(11427, 4128, CdcAcmSerialDriver::class.java)
|
||||
probeTable.addProduct(5840, 2174, CdcAcmSerialDriver::class.java)
|
||||
val probeTable = ProbeTable().apply {
|
||||
addProduct(11427, 4128, CdcAcmSerialDriver::class.java)
|
||||
|
||||
val usbSerialProber = UsbSerialProber(probeTable)
|
||||
val usbSerialPort = usbSerialProber.probeDevice(device).ports.firstOrNull()
|
||||
// TODO: not sure which device this is, might be the DJI remote before it's connected
|
||||
// to drone it seems to use a different device once connected to the drone
|
||||
// addProduct(5840, 2174, CdcAcmSerialDriver::class.java)
|
||||
}
|
||||
|
||||
if (usbSerialPort == null) {
|
||||
Toast.makeText(this, "No serial port found", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
// Retrieve the custom device (DJI remote) with the correct driver from the probe table above
|
||||
val driver = UsbSerialProber(probeTable).probeDevice(usbManager.deviceList.values.first())
|
||||
val deviceConnection = usbManager.openDevice(driver.device)
|
||||
if (deviceConnection == null) {
|
||||
Log.e("USB_PATCH", "Error opening USB device")
|
||||
Toast.makeText(this, "Error opening USB device", Toast.LENGTH_SHORT).show()
|
||||
return false
|
||||
}
|
||||
|
||||
usbSerialPort.open(usbConnection)
|
||||
usbSerialPort.setParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE)
|
||||
usbSerialPort.write(
|
||||
byteArrayOf(85, 13, 4, 33, 42, 31, 0, 0, 0, 0, 1, -122, 32),
|
||||
1000
|
||||
)
|
||||
usbSerialPort.write(
|
||||
byteArrayOf(
|
||||
85, 24, 4, 32, 2, 9, 0, 0, 64, 9, 39, 0, 2, 72, 0, -1, -1, 2, 0, 0, 0, 0,
|
||||
-127, 31
|
||||
), 1000
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("USB_PATCH", "Error sending patch: ${e.message}")
|
||||
Toast.makeText(this, "Patch failed: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
try {
|
||||
val deviceSerialPort = driver.ports.firstOrNull()
|
||||
if (deviceSerialPort == null) {
|
||||
Log.e("USB_PATCH", "Error opening USB port")
|
||||
Toast.makeText(this, "Error opening USB port", Toast.LENGTH_SHORT).show()
|
||||
return false
|
||||
}
|
||||
|
||||
deviceSerialPort.open(deviceConnection)
|
||||
deviceSerialPort.setParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE)
|
||||
deviceSerialPort.write(Constants.BYTES_1, 1000)
|
||||
deviceSerialPort.write(Constants.BYTES_2, 1000)
|
||||
|
||||
Toast.makeText(this, "Patched successfully", Toast.LENGTH_LONG).show()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
isPatching = false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
@ -221,32 +224,26 @@ class MainActivity : ComponentActivity() {
|
||||
fun MainScreen(
|
||||
usbConnected: Boolean,
|
||||
onRefresh: () -> Unit,
|
||||
onSendPatch: () -> Unit,
|
||||
onSendPatch: () -> Boolean,
|
||||
isPatching: Boolean = false
|
||||
) {
|
||||
var buttonText by remember { mutableStateOf("Send FCC Patch") }
|
||||
var buttonEnabled by remember { mutableStateOf(true) }
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("DJI FCC Hack") },
|
||||
actions = {
|
||||
IconButton(onClick = onRefresh, enabled = !isPatching) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = "Refresh USB Connection")
|
||||
}
|
||||
IconButton(onClick = { /* Open Settings */ }) {
|
||||
Icon(Icons.Default.MoreVert, contentDescription = "More Options")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
Scaffold(topBar = {
|
||||
TopAppBar(title = { Text("DJI FCC Hack") }, actions = {
|
||||
IconButton(onClick = onRefresh, enabled = !isPatching) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = "Refresh USB Connection")
|
||||
}
|
||||
})
|
||||
}) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
.padding(16.dp),
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState()), // Make the entire screen scrollable
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
@ -257,6 +254,61 @@ 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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Instructions Section
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = "Instructions",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Column {
|
||||
listOf(
|
||||
"Turn on the drone and remote and wait a few seconds for them to connect.",
|
||||
"Connect your phone to the bottom USB port of the remote.",
|
||||
"Click on 'Send FCC Patch'.",
|
||||
"Disconnect your phone from the bottom USB port of the remote and connect it to the top USB port."
|
||||
).forEachIndexed { index, instruction ->
|
||||
Text(
|
||||
text = "${index + 1}. $instruction",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Note: You have to repeat the process every time you turn on the remote and/or drone.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontStyle = FontStyle.Italic
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// USB Connection Status
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@ -285,9 +337,9 @@ fun MainScreen(
|
||||
// Send Patch Button
|
||||
Button(
|
||||
onClick = {
|
||||
buttonText = "Successfully patched"
|
||||
buttonEnabled = false
|
||||
onSendPatch()
|
||||
val result = onSendPatch()
|
||||
buttonText = if (result) "Successfully patched" else "Error patching"
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
delay(5000)
|
||||
@ -306,44 +358,38 @@ fun MainScreen(
|
||||
Text(buttonText)
|
||||
}
|
||||
|
||||
// Instructions Section
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = "Instructions",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "1. Connect your phone to the bottom port of the RC remote.\n" +
|
||||
"2. Tap 'Send FCC Patch'.\n" +
|
||||
"3. Disconnect from the bottom port and connect to the top port.",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
// Links
|
||||
Row {
|
||||
IconButton(onClick = { uriHandler.openUri(Constants.GITHUB_URL) }) {
|
||||
Image(
|
||||
painter = painterResource(id = isSystemInDarkTheme().let { if (it) R.drawable.github_light else R.drawable.github_dark }),
|
||||
contentDescription = "GitHub",
|
||||
modifier = Modifier.size(24.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,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "This app is provided as-is and is not affiliated with DJI. Use at your own risk.",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
// Footer
|
||||
Row {
|
||||
Text(
|
||||
text = "Made with ❤️ by ",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
Text(
|
||||
text = "Mathieu Broillet",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "based on the work of @galbb on MavicPilots",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
fontStyle = FontStyle.Italic
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -353,6 +399,6 @@ fun MainScreen(
|
||||
@Composable
|
||||
fun PreviewMainScreen() {
|
||||
DJI_FCC_HACK_Theme {
|
||||
MainScreen(usbConnected = true, onRefresh = {}, onSendPatch = {}, isPatching = false)
|
||||
MainScreen(usbConnected = true, onRefresh = {}, onSendPatch = { false }, isPatching = false)
|
||||
}
|
||||
}
|
1
app/src/main/res/drawable/github_dark.xml
Normal file
1
app/src/main/res/drawable/github_dark.xml
Normal file
@ -0,0 +1 @@
|
||||
<!-- drawable/github.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#000000" android:pathData="M12,2A10,10 0 0,0 2,12C2,16.42 4.87,20.17 8.84,21.5C9.34,21.58 9.5,21.27 9.5,21C9.5,20.77 9.5,20.14 9.5,19.31C6.73,19.91 6.14,17.97 6.14,17.97C5.68,16.81 5.03,16.5 5.03,16.5C4.12,15.88 5.1,15.9 5.1,15.9C6.1,15.97 6.63,16.93 6.63,16.93C7.5,18.45 8.97,18 9.54,17.76C9.63,17.11 9.89,16.67 10.17,16.42C7.95,16.17 5.62,15.31 5.62,11.5C5.62,10.39 6,9.5 6.65,8.79C6.55,8.54 6.2,7.5 6.75,6.15C6.75,6.15 7.59,5.88 9.5,7.17C10.29,6.95 11.15,6.84 12,6.84C12.85,6.84 13.71,6.95 14.5,7.17C16.41,5.88 17.25,6.15 17.25,6.15C17.8,7.5 17.45,8.54 17.35,8.79C18,9.5 18.38,10.39 18.38,11.5C18.38,15.32 16.04,16.16 13.81,16.41C14.17,16.72 14.5,17.33 14.5,18.26C14.5,19.6 14.5,20.68 14.5,21C14.5,21.27 14.66,21.59 15.17,21.5C19.14,20.16 22,16.42 22,12A10,10 0 0,0 12,2Z" /></vector>
|
1
app/src/main/res/drawable/github_light.xml
Normal file
1
app/src/main/res/drawable/github_light.xml
Normal file
@ -0,0 +1 @@
|
||||
<!-- drawable/github.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#ffffff" android:pathData="M12,2A10,10 0 0,0 2,12C2,16.42 4.87,20.17 8.84,21.5C9.34,21.58 9.5,21.27 9.5,21C9.5,20.77 9.5,20.14 9.5,19.31C6.73,19.91 6.14,17.97 6.14,17.97C5.68,16.81 5.03,16.5 5.03,16.5C4.12,15.88 5.1,15.9 5.1,15.9C6.1,15.97 6.63,16.93 6.63,16.93C7.5,18.45 8.97,18 9.54,17.76C9.63,17.11 9.89,16.67 10.17,16.42C7.95,16.17 5.62,15.31 5.62,11.5C5.62,10.39 6,9.5 6.65,8.79C6.55,8.54 6.2,7.5 6.75,6.15C6.75,6.15 7.59,5.88 9.5,7.17C10.29,6.95 11.15,6.84 12,6.84C12.85,6.84 13.71,6.95 14.5,7.17C16.41,5.88 17.25,6.15 17.25,6.15C17.8,7.5 17.45,8.54 17.35,8.79C18,9.5 18.38,10.39 18.38,11.5C18.38,15.32 16.04,16.16 13.81,16.41C14.17,16.72 14.5,17.33 14.5,18.26C14.5,19.6 14.5,20.68 14.5,21C14.5,21.27 14.66,21.59 15.17,21.5C19.14,20.16 22,16.42 22,12A10,10 0 0,0 12,2Z" /></vector>
|
Loading…
x
Reference in New Issue
Block a user