Android开发笔记[1]-蓝牙BLE控制esp32
摘要
基于Android通过低功耗蓝牙(BLE)控制esp32小灯亮灭.
超链接
esp32解析蓝牙ble数据并作出响应:
esp32笔记[6]-蓝牙(BLE)控制小灯
平台信息
- Android Studio: Electric Eel | 2022.1.1 Patch 2
- Gradle:distributionUrl=https://services.gradle.org/distributions/gradle-7.5-bin.zip
- jvmTarget = '1.8'
- minSdk 24
- targetSdk 34
- compileSdk 34
- 开发语言:Kotlin
源码
[https://gitee.com/qsbye/AndTheStone/tree/mnist]
安卓蓝牙BLE
Android BLE 蓝牙开发入门
[https://developer.android.google.cn/reference/android/bluetooth/BluetoothGattService]
[https://developer.android.google.cn/guide/topics/connectivity/bluetooth-le?hl=zh-cn]
一个低功耗蓝牙设备可以定义许多 Service, Service 可以理解为一个功能的集合。设备中每一个不同的 Service 都有一个 128 bit 的 UUID 作为这个 Service 的独立标志。蓝牙核心规范制定了两种不同的UUID,一种是基本的UUID,一种是代替基本UUID的16位UUID。所有的蓝牙技术联盟定义UUID共用了一个基本的UUID:
0x0000xxxx-0000-1000-8000-00805F9B34FB
为了进一步简化基本UUID,每一个蓝牙技术联盟定义的属性有一个唯一的16位UUID,以代替上面的基本UUID的‘x’部分。例如,心率测量特性使用0X2A37作为它的16位UUID,因此它完整的128位UUID为:
0x00002A37-0000-1000-8000-00805F9B34FB
在 Service 下面,又包括了许多的独立数据项,我们把这些独立的数据项称作 Characteristic。同样的,每一个 Characteristic 也有一个唯一的 UUID 作为标识符。在 Android 开发中,建立蓝牙连接后,我们说的通过蓝牙发送数据给外围设备就是往这些 Characteristic 中的 Value 字段写入数据;外围设备发送数据给手机就是监听这些 Charateristic 中的 Value 字段有没有变化,如果发生了变化,手机的 BLE API 就会收到一个监听的回调。
包括如下关键组件:
- BluetoothAdapter
- BluetoothDevice
- BluetoothGatt
- BluetoothGattService
- BluetoothGattCharacteristic
低功耗蓝牙(BLE)封装库
RxBLE库
[https://github.com/Belolme/RxBLE]
使用方法
clone 下来,复制 ble 包到本地项目即可使用(确保当前开发的项目有依赖 RxJava2)。可根据自己的需求进行二次开发。
这是一个使用 RxJava 封装的低功耗蓝牙类库。封装了低功耗蓝牙的连接,写入数据,读取数据和监听硬件特定通道数据改变的功能.
Rxjava2
[https://www.jianshu.com/p/061f23ecc19a]
[https://github.com/nanchen2251/RxJava2Examples]
响应式编程是一种基于异步数据流概念的编程模式。
数据流就像一条河:它可以被观测,被过滤,被操作,或者为新的消费者与另外一条流合并为一条新的流。
响应式编程的一个关键概念是事件。事件可以被等待,可以触发过程,也可以触发其它事件。事件是唯一的以合适的方式将我们的现实世界映射到我们的软件中:如果屋里太热了我们就打开一扇窗户。同样的,当我们的天气app从服务端获取到新的天气数据后,我们需要更新app上展示天气信息的UI;汽车上的车道偏移系统探测到车辆偏移了正常路线就会提醒驾驶者纠正,就是是响应事件。
响应式编程最通用的一个场景是UI:我们的移动App必须做出对网络调用、用户触摸输入和系统弹框的响应。
RxJava 2.x 已经按照 Reactive-Streams specification 规范完全的重写了,maven也被放在了io.reactivex.rxjava2:rxjava:2.x.y 下,所以 RxJava 2.x 独立于 RxJava 1.x 而存在,而随后官方宣布的将在一段时间后终止对 RxJava 1.x 的维护,所以对于熟悉 RxJava 1.x 的老司机自然可以直接看一下 2.x 的文档和异同就能轻松上手了。
Android 添加断点调试
[https://blog.csdn.net/acxingyun/article/details/73992112]
androidstudio里的debug功能,点击后程序会重新运行然后停留在断点处。
实现
项目结构
关键代码
MainActivity.kt
package com.mbeddev.androidkotlinvirtualjoystick
import android.Manifest
import android.app.Activity
import android.app.Service
import android.content.Intent
import android.content.pm.PackageManager
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.bluetooth.*
import android.bluetooth.le.*
import android.content.BroadcastReceiver
import android.content.Context
import android.os.Handler
import android.os.IBinder
//时间显示相关
import java.text.SimpleDateFormat
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.widget.AppCompatButton
import androidx.appcompat.widget.AppCompatTextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.app.ActivityCompat
import androidx.core.view.doOnLayout
import java.util.*
import kotlin.properties.Delegates
//蓝牙Rxble库
import com.billin.www.rxble.ble.*
import com.billin.www.rxble.ble.bean.BLEDevice
import com.billin.www.rxble.ble.callback.BaseResultCallback
import com.billin.www.rxble.ble.originV2.BluetoothLeInitialization
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.ObservableSource
import io.reactivex.functions.Function
/*
蓝牙相关:
- 设备名:ESP BLE Uart
- SERVICE_UUID:"6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
- CHARACTERISTIC_UUID_RX:"6E400002-B5A3-F393-E0A9-E50E24DCCA9E",这里是发送
- CHARACTERISTIC_UUID_TX:"6E400003-B5A3-F393-E0A9-E50E24DCCA9E",这里是接收
*/
class MainActivity : AppCompatActivity(), JoystickView.JoyStickListener,BluetoothActivity.BluetoothActivityListener{
//时间戳显示
private lateinit var logArea: TextView
private lateinit var enterTime: String
//手柄消抖
private var joystickLastResponseTime by Delegates.notNull<Long>()
private var joystickDebounceInterval by Delegates.notNull<Long>() // 1秒
init {
joystickLastResponseTime = 0
joystickDebounceInterval = 1000
}
//动态申请权限
private var permission = false
//蓝牙相关
private val REQUEST_BLUETOOTH_TURN_ON = 1
private lateinit var bleAdapter: BluetoothAdapter
private lateinit var bleManager: BluetoothManager
private lateinit var bleScanHandler:Handler
val BLE_DEVICE_NAME = "ESP BLE Uart"
val BLE_DEVICE_MAC = "7C:9E:BD:62:32:1E"
val BLE_SERVICE_UUID = UUID.fromString("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
val BLE_CHARACTERISTIC_UUID_RX = UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E")
val BLE_CHARACTERISTIC_UUID_TX = UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E")
//end 蓝牙相关
// 蓝牙命令相关
val cmd_led_toggle = "3c3100006c656420746f67676c653b00000000000000000000000000000000000000000001003e00"//led_toggle的完整16进制命令
val cmd_led_on = "3c3100006c6564206f6e3b0000000000000000000000000000000000000000000000000001003e00"//led_on的完整16进制命令
val cmd_led_off = "3c3100006c6564206f66663b00000000000000000000000000000000000000000000000001003e00"//led_off的完整16进制命令
val cmd_left = "3c3100006c6566743b00000000000000000000000000000000000000000000000000000001003e00"//left的完整16进制命令
val cmd_right = "3c31000072696768743b000000000000000000000000000000000000000000000000000001003e00"//right的完整16进制命令
val cmd_forward = "3c310000666f72776172643b00000000000000000000000000000000000000000000000001003e00" //forward的完整16进制命令
val cmd_backward = "3c3100006261636b776172643b000000000000000000000000000000000000000000000001003e00"//backward的完整16进制命令
// end 蓝牙命令相关
// RxBle相关
lateinit var mClient: BluetoothClient
//end RxBle相关
//可供外部访问
companion object {
// Global screen dimensions.
var screenHeight: Int = 0
var screenWidth: Int = 0
const val REQUEST_PERMISSION = 1
private const val PERMISSION_REQUEST_CODE = 123
}
var joystickId = -1
private fun getCurrentTime(): String {
val currentTime = SimpleDateFormat("HH:mm:ss").format(Date())
return currentTime
}
private fun displayTimestamp(timestamp: String): String {
val inputFormat = SimpleDateFormat("HH:mm:ss")
val outputFormat = SimpleDateFormat("hh:mm:ss a")
val date = inputFormat.parse(timestamp)
return outputFormat.format(date)
}
private fun displayLogMessage(logText: String) {
val logArea = findViewById<TextView>(R.id.log_area_text)
val time = this.getCurrentTime()
val logMessage = "$time $logText"
val newText = "${logArea.text}\n$logMessage"
// 设置日志区最大行数
val maxLines = 25
val lines = newText.lines()
val trimmedLines = lines.takeLast(maxLines)
val trimmedText = trimmedLines.joinToString("\n")
logArea.text = trimmedText
}
fun convertToHexArray(input: String): String {
val hexArray = "0123456789ABCDEF".toCharArray()
val byteArray = ByteArray(input.length / 2)
for (i in input.indices step 2) {
val firstDigit = hexArray.indexOf(input[i])
val secondDigit = hexArray.indexOf(input[i + 1])
val byteValue = (firstDigit shl 4) + secondDigit
byteArray[i / 2] = byteValue.toByte()
}
val hexArrayString = byteArray.joinToString(",") { "%02X".format(it) }
return "[$hexArrayString]"
}
//转换16进制字符到字符串
fun hexToString(hex: String): String {
val sb = StringBuilder()
val len = hex.length
var i = 0
while (i < len) {
val hexChar = hex.substring(i, i + 2)
val char = hexChar.toInt(16).toChar()
sb.append(char)
i += 2
}
return sb.toString()
}
//转换字符到16进制
fun convertHexStringToByteArray(hexString: String): ByteArray {
val byteArray = ByteArray(hexString.length / 2)
for (i in hexString.indices step 2) {
val hex = hexString.substring(i, i + 2)
val byteValue = hex.toInt(16).toByte()
byteArray[i / 2] = byteValue
}
return byteArray
}
//转换接收到的16进制到字符
fun convertIntArrayToCharArray(intArray: ByteArray): CharArray {
val charArray = CharArray(intArray.size)
for (i in intArray.indices) {
charArray[i] = intArray[i].toChar()
}
return charArray
}
// 写入数据函数
fun writeData(my_data : String) {
Log.e("RxBle", "connect test: on write")
mClient.write(
BLE_DEVICE_MAC,
BLE_SERVICE_UUID,
BLE_CHARACTERISTIC_UUID_RX,
my_data.toByteArray()
)
.subscribe(object : Observer<String> {
override fun onSubscribe(d: Disposable) {
Log.e("RxBle", "connect test onSubscribe: ")
}
override fun onNext(value: String) {
Log.e("RxBle", "connect test onNext: ")
//执行下一次发送
//返回true
}
override fun onError(e: Throwable) {
Log.e("RxBle", "connect test onError: ", e)
}
override fun onComplete() {
Log.e("RxBle", "connect test onComplete: ")
runOnUiThread {
displayLogMessage("已发送:"+my_data)
}
}
})
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//日志区显示进入
displayLogMessage("🎉进入应用!")
//日志区显示欢迎信息
displayLogMessage("👏欢迎!")
displayLogMessage("帮助:\n1. 连接蓝牙; \n2. 移动右边的手柄以控制机器人;")
//监听'蓝牙'按钮
val connectBluetoothTextView = findViewById<AppCompatTextView>(R.id.connect_bluetooth)
connectBluetoothTextView.setOnClickListener {
displayLogMessage("连接蓝牙...")
bleScanHandler = Handler()
//蓝牙管理,这是系统服务可以通过getSystemService(BLUETOOTH_SERVICE)的方法获取实例
bleManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
//通过蓝牙管理实例获取适配器,然后通过扫描方法(scan)获取设备(device)
bleAdapter = bleManager.adapter
if (!bleAdapter.isEnabled) {
val bluetoothTurnOn = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
try{startActivityForResult(bluetoothTurnOn, REQUEST_BLUETOOTH_TURN_ON)}
catch(e: SecurityException){
Log.e("Bluetooth", "蓝牙没有打开!")
}
} else {
//检查BLE权限
this.requestPermission()
}
}
//监听'关于本应用'按钮
val appAboutTextView = findViewById<AppCompatTextView>(R.id.app_about)
appAboutTextView.setOnClickListener {
displayLogMessage("作者:qsbye")
}
//监听'进入设置'按钮
val gotoSettingTextView = findViewById<AppCompatButton>(R.id.btn_goto)
gotoSettingTextView.setOnClickListener {
displayLogMessage("进入设置...")
}
// This lambda is for specific procedures that need to be done when the layout is created.
val cLayout: LinearLayout = findViewById(R.id.constraintLayout)
cLayout.doOnLayout {
screenHeight = it.measuredHeight
screenWidth = it.measuredWidth
}
val joystick: JoystickView = findViewById(R.id.my_joystick)
joystick.setJoystickListener(this)
val lp: ConstraintLayout.LayoutParams = ConstraintLayout.LayoutParams(
ConstraintLayout.LayoutParams.WRAP_CONTENT,
ConstraintLayout.LayoutParams.WRAP_CONTENT
)
val set = ConstraintSet()
// 获取各个TextView和对应的数值TextView
val batteryStatusTitle = findViewById<TextView>(R.id.battery_status_title)
val batteryStatusValue = findViewById<TextView>(R.id.battery_status_value)
val speedAccelerationTitle = findViewById<TextView>(R.id.speed_acceleration_title)
val speedAccelerationValue = findViewById<TextView>(R.id.speed_acceleration_value)
val rotationAngleTitle = findViewById<TextView>(R.id.rotation_angle_title)
val rotationAngleValue = findViewById<TextView>(R.id.rotation_angle_value)
// 设置数值TextView的文本内容
batteryStatusValue.text = "100%"
speedAccelerationValue.text = "0 m/s, 0 m/s²"
rotationAngleValue.text = "0°"
}
override fun onJoystickMoved(xPercent: Float, yPercent: Float, id: Int) {
//记录上一次响应的时间
val currentTime = System.currentTimeMillis()
val elapsedTime = currentTime - joystickLastResponseTime
//符合消抖规则
if (elapsedTime >= joystickDebounceInterval) {
//记录时间
joystickLastResponseTime = System.currentTimeMillis()
if (id == R.id.my_joystick) {
val threshold = 0.1 // 阈值,用于判断方向
if (xPercent < -threshold) {
if (yPercent < -threshold) {
// 左上角
if (xPercent < yPercent) {
displayLogMessage("Forward") // 更接近上方,显示"Forward"
try{
writeData(hexToString(cmd_forward))
}
catch(e:Exception){
}
} else {
displayLogMessage("Left") // 更接近左方,显示"Left"
try{
writeData(hexToString(cmd_left))
}
catch(e:Exception){
}
}
} else if (yPercent > threshold) {
// 左下角
if (xPercent < -yPercent) {
displayLogMessage("Backward") // 更接近下方,显示"Backward"
try{
writeData(hexToString(cmd_backward))
}
catch(e:Exception){
}
} else {
displayLogMessage("Left") // 更接近左方,显示"Left"
try{
writeData(hexToString(cmd_left))
}
catch(e:Exception){
}
}
} else {
displayLogMessage("Left") // 向左移动,显示"Left"
try{
writeData(hexToString(cmd_left))
}
catch(e:Exception){
}
}
} else if (xPercent > threshold) {
if (yPercent < -threshold) {
// 右上角
if (-xPercent < yPercent) {
displayLogMessage("Forward") // 更接近上方,显示"Forward"
try{
writeData(hexToString(cmd_forward))
}
catch(e:Exception){
}
} else {
displayLogMessage("Right") // 更接近右方,显示"Right"
try{
writeData(hexToString(cmd_right))
}
catch(e:Exception){
}
}
} else if (yPercent > threshold) {
// 右下角
if (-xPercent < -yPercent) {
displayLogMessage("Backward") // 更接近下方,显示"Backward"
try{
writeData(hexToString(cmd_backward))
}
catch(e:Exception){
}
} else {
displayLogMessage("Right") // 更接近右方,显示"Right"
try{
writeData(hexToString(cmd_right))
}
catch(e:Exception){
}
}
} else {
displayLogMessage("Right") // 向右移动,显示"Right"
try{
writeData(hexToString(cmd_right))
}
catch(e:Exception){
}
}
} else {
if (yPercent < -threshold) {
displayLogMessage("Forward") // 向上移动,显示"Forward"
try{
writeData(hexToString(cmd_forward))
}
catch(e:Exception){
}
} else if (yPercent > threshold) {
displayLogMessage("Backward") // 向下移动,显示"Backward"
try{
writeData(hexToString(cmd_backward))
}
catch(e:Exception){
}
}
}
}
}
}
// start 权限检查
//动态申请权限
private fun requestPermission() {
displayLogMessage( "申请权限中...")
val state1 =
ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
val state2 =
ActivityCompat.checkSelfPermission(this,Manifest.permission.BLUETOOTH_SCAN)
val state3 =
ActivityCompat.checkSelfPermission(this,Manifest.permission.BLUETOOTH)
val state4 =
ActivityCompat.checkSelfPermission(this,Manifest.permission.BLUETOOTH_CONNECT)
val state5 =
ActivityCompat.checkSelfPermission(this,Manifest.permission.BLUETOOTH_ADMIN)
val state6 =
ActivityCompat.checkSelfPermission(this,Manifest.permission.ACCESS_COARSE_LOCATION)
if (state1 == PackageManager.PERMISSION_GRANTED) {
displayLogMessage( "已开启定位权限")
permission = true
if (state2 == PackageManager.PERMISSION_GRANTED) {
displayLogMessage( "已开启蓝牙扫描权限")
if (state3 == PackageManager.PERMISSION_GRANTED) {
displayLogMessage( "已开启蓝牙权限")
permission = true
if (state4 == PackageManager.PERMISSION_GRANTED) {
displayLogMessage( "已开启蓝牙连接权限")
//调用蓝牙扫描函数
displayLogMessage("调用蓝牙扫描函数")
//******这里是蓝牙入口*******
//1. 初始化RxBle
mClient = BluetoothClientBLEV2Adapter(
BluetoothLeInitialization.getInstance(this)
)
mClient.openBluetooth()
//2. RxBle扫描设备
mClient.search(3000, false)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<BLEDevice> {
override fun onSubscribe(d: Disposable) {
Log.e("RxBle", "start")
//mTextView.text = "start\n"
}
override fun onNext(value: BLEDevice) {
Log.e("RxBle", "device $value")
//mTextView.text = "${mTextView.text}\n\n$value"
//判断有没有需要的设备
if (value.toString().contains(BLE_DEVICE_NAME)) {
this@MainActivity.displayLogMessage("已查找到所需的设备:\n"+BLE_DEVICE_NAME)
/***/
// 注册通知的操作,只执行一次
fun registerNotify() {
mClient.registerNotify(
BLE_DEVICE_MAC,
BLE_SERVICE_UUID,
BLE_CHARACTERISTIC_UUID_TX,
object : BaseResultCallback<ByteArray> {
override fun onSuccess(data: ByteArray) {
Log.e("RxBle", "I have receive a new message: " + data.contentToString())
val result = "E/RxBle: I have receive a new message: ${convertIntArrayToCharArray(data).contentToString()}"
runOnUiThread {
displayLogMessage("接收到:$result")
}
}
override fun onFail(msg: String) {
Log.e("RxBle", "oop! setting register is failed!")
}
}
)
}
// 连接成功后的回调函数
fun onConnectSuccess() {
Log.e("RxBle", "connect test: on connect success")
// 注册通知
registerNotify()
}
// 连接并注册通知
mClient.connect(BLE_DEVICE_MAC)
.subscribe(object : Observer<String> {
override fun onSubscribe(d: Disposable) {
Log.e("RxBle", "connect test onSubscribe: ")
}
override fun onNext(value: String) {
Log.e("RxBle", "connect test onNext: ")
// 连接成功后注册通知
onConnectSuccess()
// 执行多次写入数据
writeData(hexToString(cmd_led_toggle))
//writeData("Fine")
// 可以根据需要继续写入数据
}
override fun onError(e: Throwable) {
Log.e("RxBle", "connect test onError: ", e)
}
override fun onComplete() {
Log.e("RxBle", "connect test onComplete: ")
}
})
/***/
}
}
override fun onError(e: Throwable) {
Log.e("RxBle","onError"+e)
}
override fun onComplete() {
Log.e("RxBle","onComplete: search")
}// end onComplete
})
//******结束蓝牙入口*******
}else{
displayLogMessage( "请授予权限后重新点击按钮!")
ActivityCompat.requestPermissions(
this, arrayOf(Manifest.permission.BLUETOOTH_CONNECT),
REQUEST_PERMISSION)
}
}
else{
displayLogMessage( "请授予权限后重新点击按钮!")
ActivityCompat.requestPermissions(
this, arrayOf(Manifest.permission.BLUETOOTH),
REQUEST_PERMISSION)
}
}else{
displayLogMessage( "请授予权限后重新点击按钮!")
ActivityCompat.requestPermissions(
this, arrayOf(Manifest.permission.BLUETOOTH_SCAN),
REQUEST_PERMISSION)
}
} else {
displayLogMessage( "请授予权限后重新点击按钮!")
ActivityCompat.requestPermissions(
this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
REQUEST_PERMISSION)
}
}
//检查权限
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String?>,
grantResults: IntArray
) {
if (requestCode == REQUEST_PERMISSION) {
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) { //蓝牙权限开启失败
displayLogMessage( "未开启蓝牙权限")
} else {
startActivity(Intent(this, MainActivity::class.java))
finish()
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
//end 权限检查
}// end MainActivity
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.mbeddev.androidkotlinvirtualjoystick">
<!-- 标注需要的权限 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<!-- end 需要的权限 -->
<!-- 需要的特性 -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false"/>
<!-- end 需要的特性 -->
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AndroidKotlinVirtualJoystick"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
build.gradle(app)
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
compileSdk 34
defaultConfig {
applicationId "com.mbeddev.androidkotlinvirtualjoystick"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'com.google.android.material:material:1.6.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core-ktx:+'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation 'io.reactivex.rxjava2:rxjava:2.1.1'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
}
效果
手机端 | esp32端 | 总体效果 |
---|---|---|
![]() |
![]() |
![]() |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」