用手机App控制你的鸿蒙WiFi小车:基于Android Studio的简易控制端开发实录
鸿蒙WiFi小车手机控制App开发全指南:从零构建Android遥控终端
去年夏天,我在工作室里捣鼓一台基于Hi3861的鸿蒙系统WiFi小车时,突然意识到一个问题:每次测试都需要连着笔记本电脑,用C#编写的控制端程序发送指令。这种开发体验实在太不"移动"了!于是我开始探索如何用手机App替代电脑端控制,最终完成了一个简洁高效的Android控制应用。本文将分享这个过程中的关键技术和踩坑经验,帮助开发者快速构建自己的移动端控制方案。
1. 开发环境与项目初始化
在开始编码之前,我们需要确保开发环境配置正确。Android Studio是当前最主流的Android应用开发IDE,建议使用最新稳定版本(本文基于Android Studio Giraffe编写)。创建一个新项目时,选择"Empty Activity"模板即可,因为我们不需要复杂的界面元素。
关键依赖配置 :在模块级build.gradle文件中添加以下依赖项:
dependencies {
implementation 'com.squareup.okhttp3:okhttp:4.9.3' // 网络通信库
implementation 'com.google.code.gson:gson:2.8.9' // JSON处理库
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' // 协程支持
}
提示:使用OkHttp而不是原生HttpURLConnection,因为它提供了更简洁的API和更好的UDP支持。
项目结构建议如下:
/app
├── src/main
│ ├── java/com/example/wificarcontroller
│ │ ├── CarController.kt # 小车控制逻辑
│ │ ├── UdpManager.kt # UDP通信管理
│ │ └── MainActivity.kt # 主界面
│ └── res
│ ├── layout/activity_main.xml
│ └── values/strings.xml
2. 控制界面设计与实现
好的控制界面应该直观且易于操作。我采用了游戏手柄式的布局,将方向控制放在中央,功能按钮环绕四周。这种设计符合大多数用户的直觉认知。
在activity_main.xml中定义如下布局结构:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageButton
android:id="@+id/btnForward"
android:layout_centerHorizontal="true"
android:src="@drawable/ic_up_arrow"/>
<LinearLayout
android:layout_below="@id/btnForward"
android:orientation="horizontal">
<ImageButton android:id="@+id/btnLeft" .../>
<ImageButton android:id="@+id/btnStop" .../>
<ImageButton android:id="@+id/btnRight" .../>
</LinearLayout>
<ImageButton
android:id="@+id/btnBackward"
android:layout_below="@id/btnLeftRightContainer"
.../>
</RelativeLayout>
交互优化技巧 :
- 使用
android:background="?attr/selectableItemBackgroundBorderless"让按钮有点击反馈 - 为按钮添加长按事件监听,实现持续控制功能
- 采用Material Design的触觉反馈API,增强操作体验
btnForward.setOnLongClickListener {
vibrate(50) // 50ms的震动反馈
sendCommand("FORWARD")
true
}
3. UDP通信核心实现
与鸿蒙小车的通信采用UDP协议,这是物联网设备控制的常见选择。我们需要处理三个关键问题:消息格式、连接管理和异常处理。
3.1 消息格式设计
与小车端的通信采用JSON格式,保持与PC端控制程序的兼容性。基本命令结构如下:
{
"command": "FORWARD",
"duration": 1000,
"speed": 75
}
在Kotlin中,我们定义对应的数据类:
data class CarCommand(
val command: String,
val duration: Int? = null,
val speed: Int? = null
) {
fun toJson(): String = Gson().toJson(this)
}
3.2 UDP管理器实现
创建一个UdpManager单例类处理所有网络操作:
object UdpManager {
private const val PORT = 50001
private lateinit var socket: DatagramSocket
fun initialize() {
socket = DatagramSocket().apply {
broadcast = true
}
}
fun sendCommand(ip: String, command: CarCommand) {
try {
val bytes = command.toJson().toByteArray()
val address = InetAddress.getByName(ip)
val packet = DatagramPacket(bytes, bytes.size, address, PORT)
socket.send(packet)
} catch (e: Exception) {
Log.e("UDP", "Send failed", e)
}
}
fun discoverCar(): String? {
// 广播发现逻辑...
}
}
注意:Android 9及以上版本对UDP广播有限制,需要在AndroidManifest.xml中添加
<uses-permission android:name="android.permission.INTERNET"/>和<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>权限。
4. 小车IP自动发现与手动配置
可靠的IP发现机制是提升用户体验的关键。我们实现两种方式:广播发现和手动配置。
4.1 广播发现实现
fun discoverCar(): String? {
val discoverPacket = DatagramPacket(
"DISCOVER".toByteArray(),
"DISCOVER".length,
InetAddress.getByName("255.255.255.255"),
PORT
)
socket.send(discoverPacket)
val buffer = ByteArray(1024)
val response = DatagramPacket(buffer, buffer.size)
socket.soTimeout = 2000 // 2秒超时
try {
socket.receive(response)
return response.address.hostAddress
} catch (e: SocketTimeoutException) {
return null
}
}
4.2 IP配置管理
对于需要手动配置的情况,我们使用SharedPreferences保存历史IP:
class IpManager(private val context: Context) {
private val prefs = context.getSharedPreferences("car_ip", Context.MODE_PRIVATE)
fun saveIp(ip: String) {
prefs.edit().putString("last_ip", ip).apply()
}
fun getLastIp(): String? = prefs.getString("last_ip", null)
}
在界面中添加IP输入对话框:
fun showIpDialog() {
MaterialAlertDialogBuilder(this)
.setTitle("输入小车IP")
.setView(EditText(this).apply {
setText(ipManager.getLastIp())
})
.setPositiveButton("连接") { _, _ ->
val ip = (dialog as EditText).text.toString()
ipManager.saveIp(ip)
connectToCar(ip)
}
.show()
}
5. 控制逻辑优化与性能调优
基础功能实现后,我们需要关注控制延迟和用户体验优化。
5.1 命令队列系统
为避免快速连续点击导致的命令丢失,实现一个简单的命令队列:
class CommandQueue {
private val scope = CoroutineScope(Dispatchers.IO)
private val channel = Channel<CarCommand>(capacity = 10)
init {
scope.launch {
for (cmd in channel) {
udpManager.sendCommand(currentIp, cmd)
delay(50) // 最小命令间隔
}
}
}
fun enqueue(command: CarCommand) {
scope.launch { channel.send(command) }
}
}
5.2 运动控制算法
为小车实现平滑的加速/减速曲线:
fun calculateSpeedProfile(
currentSpeed: Int,
targetSpeed: Int,
steps: Int = 5
): List<Int> {
val diff = targetSpeed - currentSpeed
return (1..steps).map { currentSpeed + (diff * it/steps) }
}
应用在控制逻辑中:
fun sendGradualCommand(command: String, targetSpeed: Int) {
val speeds = calculateSpeedProfile(lastSpeed, targetSpeed)
speeds.forEach { speed ->
commandQueue.enqueue(CarCommand(command, speed = speed))
delay(20)
}
}
6. 高级功能扩展
基础控制稳定后,可以考虑添加以下增强功能:
6.1 传感器数据显示
如果小车配备了传感器,可以在App中显示实时数据:
// 小车端需要发送传感器数据
{
"type": "SENSOR",
"distance": 25.4,
"speed": 30
}
// App端解析显示
fun handleSensorData(json: String) {
val type = JsonParser.parseString(json).asJsonObject["type"].asString
if (type == "SENSOR") {
val data = Gson().fromJson(json, SensorData::class.java)
runOnUiThread {
textDistance.text = "${data.distance} cm"
// 更新其他UI...
}
}
}
6.2 轨迹记录与回放
实现简单的运动轨迹记录:
class PathRecorder {
private val path = mutableListOf<CarCommand>()
fun startRecording() { path.clear() }
fun recordCommand(cmd: CarCommand) { path.add(cmd) }
fun replay() {
path.forEach { cmd ->
udpManager.sendCommand(currentIp, cmd)
delay(cmd.duration ?: 100)
}
}
}
6.3 语音控制集成
利用Android的语音识别API添加语音控制:
private fun setupVoiceControl() {
val recognizer = SpeechRecognizer.createSpeechRecognizer(this).apply {
setRecognitionListener(object : RecognitionListener {
override fun onResults(results: Bundle) {
val matches = results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
matches?.firstOrNull()?.let { processVoiceCommand(it) }
}
// 其他回调方法...
})
}
btnVoice.setOnClickListener {
recognizer.startListening(Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, "en-US")
})
}
}
private fun processVoiceCommand(command: String) {
when {
command.contains("前进") -> sendCommand("FORWARD")
command.contains("后退") -> sendCommand("BACKWARD")
// 其他命令...
}
}
7. 调试与问题排查
开发过程中难免遇到各种连接和控制问题,以下是我总结的常见问题及解决方法:
连接不稳定问题 :
- 检查手机和小车是否在同一WiFi网络
- 确认路由器没有开启客户端隔离功能
- 测试网络延迟:
ping <小车IP>
控制响应延迟高 :
- 减少JSON数据体积,去掉不必要的字段
- 适当增加UDP发送缓冲区大小
- 在路由器设置中为小车设备分配静态IP
Android权限问题排查清单 :
- 确认AndroidManifest.xml已声明所有必要权限
- 检查Android 6.0+运行时权限是否已获取
- 在开发者选项中开启"无线调试日志"
- 使用Android Studio的Profiler工具监控网络活动
一个实用的调试技巧是在App中添加日志查看界面:
class DebugConsoleActivity : AppCompatActivity() {
private val logEntries = mutableListOf<String>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_debug)
val logView = findViewById<TextView>(R.id.logView)
val scrollView = findViewById<ScrollView>(R.id.scrollView)
GlobalScope.launch(Dispatchers.Main) {
while (true) {
logView.text = logEntries.joinToString("\n")
scrollView.fullScroll(View.FOCUS_DOWN)
delay(500)
}
}
}
companion object {
fun log(message: String) {
logEntries.add("${SimpleDateFormat("HH:mm:ss").format(Date())}: $message")
if (logEntries.size > 100) logEntries.removeAt(0)
}
}
}
在需要的地方调用 DebugConsoleActivity.log("发送前进命令") 即可记录调试信息。
更多推荐


所有评论(0)