2025-02-08 17:05:17 +01:00

286 lines
10 KiB
Kotlin

package ch.mathieubroillet.djiffchack
import android.app.PendingIntent
import android.content.BroadcastReceiver
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
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
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.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Build
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import ch.mathieubroillet.djiffchack.ui.theme.DJI_FCC_HACK_Theme
import com.hoho.android.usbserial.driver.CdcAcmSerialDriver
import com.hoho.android.usbserial.driver.ProbeTable
import com.hoho.android.usbserial.driver.UsbSerialPort
import com.hoho.android.usbserial.driver.UsbSerialProber
class MainActivity : ComponentActivity() {
private lateinit var usbManager: UsbManager
private var usbConnection by mutableStateOf(null as UsbDeviceConnection?)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
usbManager = getSystemService(Context.USB_SERVICE) as UsbManager
// Register receiver to detect USB plug/unplug events
val filter = IntentFilter().apply {
addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED)
addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
addAction(Constants.INTENT_ACTION_GRANT_USB_PERMISSION)
}
ContextCompat.registerReceiver(
this,
usbReceiver,
filter,
ContextCompat.RECEIVER_NOT_EXPORTED
)
// Initial check for USB connection
refreshUsbConnection()
setContent {
MainScreen(usbConnection != null, ::refreshUsbConnection, ::sendPatch)
}
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(usbReceiver)
}
/**
* Refreshes the USB connection status
*/
private fun refreshUsbConnection() {
if (usbManager.deviceList.isNotEmpty()) {
val device: UsbDevice = usbManager.deviceList.values.first()
// 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}")
return
}
usbConnection = usbManager.openDevice(device)
if (usbConnection == null) {
Log.d("USB_CONNECTION", "Requesting USB Permission")
requestUsbPermission(device)
}
} else {
usbConnection = null
}
}
/**
* Handles sending the patch via USB communication
*/
private fun sendPatch() {
if (usbConnection == null) {
Toast.makeText(this, "No USB device connected!", Toast.LENGTH_SHORT).show()
return
}
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 usbSerialProber = UsbSerialProber(probeTable)
val usbSerialPort = usbSerialProber.probeDevice(device).ports.firstOrNull()
if (usbSerialPort == null) {
Toast.makeText(this, "No serial port found", Toast.LENGTH_SHORT).show()
return
}
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()
}
}
}
/**
* Requests USB permission for the device
*/
private fun requestUsbPermission(device: UsbDevice) {
val permissionIntent = PendingIntent.getBroadcast(
this,
0,
Intent(Constants.INTENT_ACTION_GRANT_USB_PERMISSION).apply { setPackage(packageName) },
PendingIntent.FLAG_MUTABLE
)
usbManager.requestPermission(device, permissionIntent)
}
/**
* BroadcastReceiver to handle USB events
*/
private val usbReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
Log.d("USB_EVENT", "USB Device Connected")
refreshUsbConnection()
}
UsbManager.ACTION_USB_DEVICE_DETACHED -> {
Log.d("USB_EVENT", "USB Device Disconnected")
refreshUsbConnection()
}
Constants.INTENT_ACTION_GRANT_USB_PERMISSION -> {
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
Log.d("USB_EVENT", "USB Permission Granted")
refreshUsbConnection()
}
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(usbConnected: Boolean, onRefresh: () -> Unit, onSendPatch: () -> Unit) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("DJI FCC Hack") },
actions = {
IconButton(onClick = onRefresh) {
Icon(Icons.Default.Refresh, contentDescription = "Refresh USB Connection")
}
IconButton(onClick = { /* Open Settings */ }) {
Icon(Icons.Default.MoreVert, contentDescription = "More Options")
}
}
)
}
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// App Logo
Image(
painter = painterResource(id = R.drawable.dji_innovations_logo),
contentDescription = "DJI Logo",
modifier = Modifier.size(100.dp)
)
// USB Connection Status
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = if (usbConnected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.errorContainer
)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Image(
painter = painterResource(id = R.drawable.usb_c_port),
contentDescription = "USB Status",
modifier = Modifier.size(24.dp),
)
Spacer(Modifier.width(8.dp))
Text(
text = if (usbConnected) "Remote Connected" else "Remote Not Connected",
style = MaterialTheme.typography.bodyMedium
)
}
}
// Send Patch Button
Button(
onClick = onSendPatch,
shape = RoundedCornerShape(24.dp),
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
enabled = usbConnected
) {
Icon(Icons.Default.Build, contentDescription = "Patch")
Spacer(Modifier.width(8.dp))
Text("Send FCC Patch")
}
}
}
}
@Preview(showBackground = true)
@Composable
fun PreviewMainScreen() {
DJI_FCC_HACK_Theme {
MainScreen(usbConnected = true, onRefresh = {}, onSendPatch = {})
}
}