文档概述
本文档基于 Kotlin 语言,详细阐述 Android App 为充电桩实现 WiFi 局域网配网的完整流程、指令规范及可直接复用的代码实例。核心原理与之前一致:充电桩进入配网模式后创建临时 WiFi 热点,App 接入该热点后通过 UDP 广播发送目标 WiFi 参数,充电桩接收后连接目标 WiFi 并反馈结果。
Kotlin 适配特性:采用协程(Coroutine)处理异步通信、空安全(Null Safety)避免空指针、扩展函数简化 WiFi 信息获取,符合 Android Jetpack 最佳实践。
一、配网前提条件
1. 硬件条件
- 充电桩:支持 WiFi 模块,长按配网按键进入配网模式(指示灯快闪),创建临时热点(如
ChargingPile_XXXX); - 手机:Android 8.0+,已连接充电桩临时热点,开启 WiFi 和位置服务(Android 10 + 读取 SSID 必需)。
2. 软件权限(Kotlin 申请方式)
App 需在AndroidManifest.xml声明以下权限,并在运行时申请危险权限:
xml
<!-- 基础权限 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<!-- Android 10+ 读取SSID必需 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- UDP广播必需 -->
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
二、配网完整流程
预览
查看代码
否
是
充电桩进入配网模式
手机连接充电桩临时热点
App申请权限并获取目标WiFi参数
App通过协程封装UDP配网指令并广播
充电桩解析指令并尝试连接目标WiFi
连接成功?
充电桩发送失败反馈
充电桩保存WiFi参数并退出配网模式
充电桩发送成功反馈
App解析反馈并提示失败
App解析反馈并提示成功
flowchart TD
A[充电桩进入配网模式] --> B[手机连接充电桩临时热点]
B --> C[App申请权限并获取目标WiFi参数]
C --> D[App通过协程封装UDP配网指令并广播]
D --> E[充电桩解析指令并尝试连接目标WiFi]
E --> F{连接成功?}
F -- 否 --> G[充电桩发送失败反馈]
F -- 是 --> H[充电桩保存WiFi参数并退出配网模式]
H --> I[充电桩发送成功反馈]
G --> J[App解析反馈并提示失败]
I --> K[App解析反馈并提示成功]
否
是
充电桩进入配网模式
手机连接充电桩临时热点
App申请权限并获取目标WiFi参数
App通过协程封装UDP配网指令并广播
充电桩解析指令并尝试连接目标WiFi
连接成功?
充电桩发送失败反馈
充电桩保存WiFi参数并退出配网模式
充电桩发送成功反馈
App解析反馈并提示失败
App解析反馈并提示成功

流程分步说明
- 充电桩配网模式启动:用户长按充电桩配网键 3-5 秒,指示灯快闪即进入配网模式;
- 手机网络连接:用户在 WiFi 设置中连接充电桩临时热点;
- 权限与参数获取:App 引导用户授权位置 / 网络权限,自动读取当前连接的 WiFi SSID(可选),或让用户手动输入目标 WiFi 的 SSID 和密码;
- UDP 指令发送:App 通过 Kotlin 协程封装 JSON 格式配网指令,以 UDP 广播形式发送至局域网;
- 充电桩处理:充电桩监听指定 UDP 端口,解析指令后尝试连接目标 WiFi;
- 结果反馈:充电桩将连接结果封装为 JSON 指令,通过 UDP 反馈给 App;
- App 结果展示:App 解析反馈指令,通过 Toast / 弹窗提示用户配网成功 / 失败及原因。
三、配网指令详细说明
1. 通用规则
- 传输协议:UDP 广播(优先,无需知道充电桩 IP),广播地址
255.255.255.255,端口6666; - 数据格式:JSON(UTF-8 编码),敏感字段(密码)建议 AES-128 加密(示例中暂不加密);
- 超时规则:App 监听反馈超时时间 15 秒,充电桩连接 WiFi 超时时间 10 秒。
2. 核心指令定义
(1)配网指令(App → 充电桩)
表格
| 字段名 | 类型 | 必选 | 说明 |
|---|---|---|---|
| cmd_type | String | 是 | 固定值:wifi_config |
| device_id | String | 是 | 充电桩 SN / 设备 ID |
| wifi_ssid | String | 是 | 目标 WiFi SSID |
| wifi_pwd | String | 是 | 目标 WiFi 密码(空 = 无密码) |
| timestamp | Long | 是 | 发送时间戳(毫秒) |
示例 JSON:
json
{
"cmd_type":"wifi_config",
"device_id":"CP20240318001",
"wifi_ssid":"Home_WiFi",
"wifi_pwd":"12345678",
"timestamp":1710768000000
}
(2)反馈指令(充电桩 → App)
表格
| 字段名 | 类型 | 必选 | 说明 |
|---|---|---|---|
| cmd_type | String | 是 | 固定值:wifi_config_result |
| device_id | String | 是 | 充电桩 SN / 设备 ID |
| result_code | Int | 是 | 结果码:0 = 成功;1 = 密码错误;2=SSID 不存在;3 = 解析失败;4 = 超时 |
| result_msg | String | 是 | 结果描述(如 "配网成功""密码错误") |
| timestamp | Long | 是 | 反馈时间戳(毫秒) |
成功示例:
json
{
"cmd_type":"wifi_config_result",
"device_id":"CP20240318001",
"result_code":0,
"result_msg":"配网成功,已连接Home_WiFi",
"timestamp":1710768010000
}
四、Kotlin 核心代码实例
1. 依赖配置(build.gradle)
添加 Gson(JSON 解析)和协程依赖:
gradle
dependencies {
// Gson解析JSON
implementation 'com.google.code.gson:gson:2.10.1'
// Kotlin协程
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
// 权限申请(可选,简化权限处理)
implementation 'com.permissionx.guolindev:permissionx:1.7.1'
}
2. 工具类:WiFi 信息扩展函数
kotlin
import android.content.Context
import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager
import android.os.Build
/**
* Context扩展函数:获取WiFi管理器
*/
fun Context.getWifiManager(): WifiManager {
return applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
}
/**
* 获取当前连接的WiFi SSID(去除双引号,处理空安全)
*/
fun Context.getCurrentWifiSsid(): String? {
val wifiManager = getWifiManager()
if (!wifiManager.isWifiEnabled) return null
val wifiInfo: WifiInfo = wifiManager.connectionInfo ?: return null
val ssid = wifiInfo.ssid ?: return null
// 移除Android默认给SSID加的双引号
return if (ssid.startsWith("\"") && ssid.endsWith("\"")) {
ssid.substring(1, ssid.length - 1)
} else ssid
}
/**
* 转义JSON特殊字符(避免解析失败)
*/
fun String.escapeJsonChars(): String {
return this.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
}
3. 核心类:WiFi 配网管理器
kotlin
import android.content.Context
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.DatagramPacket
import java.net.DatagramSocket
import java.net.InetAddress
/**
* 充电桩WiFi配网管理器(基于Kotlin协程)
*/
class ChargingPileWifiConfigManager(private val context: Context) {
// 常量定义
private companion object {
const val UDP_BROADCAST_IP = "255.255.255.255"
const val UDP_PORT = 6666
const val LISTEN_TIMEOUT = 15000 // 监听反馈超时时间(15秒)
val gson = Gson()
}
// 配网结果回调
interface OnConfigResultListener {
fun onSuccess(message: String)
fun onFailed(errorCode: Int, errorMsg: String)
}
/**
* 发送配网指令
* @param targetSsid 目标WiFi SSID
* @param targetPwd 目标WiFi密码
* @param deviceId 充电桩设备ID
* @param listener 结果回调
*/
fun sendConfigCommand(
targetSsid: String,
targetPwd: String,
deviceId: String,
listener: OnConfigResultListener
) {
// 协程处理异步操作(IO线程)
GlobalScope.launch(Dispatchers.IO) {
try {
// 1. 封装配网指令
val configCmd = ConfigCommand(
cmd_type = "wifi_config",
device_id = deviceId,
wifi_ssid = targetSsid.escapeJsonChars(),
wifi_pwd = targetPwd.escapeJsonChars(),
timestamp = System.currentTimeMillis()
)
val cmdJson = gson.toJson(configCmd)
val cmdBytes = cmdJson.toByteArray(Charsets.UTF_8)
// 2. 创建UDP Socket并发送广播
val socket = DatagramSocket(UDP_PORT)
socket.broadcast = true // 开启广播模式
socket.soTimeout = LISTEN_TIMEOUT
val broadcastAddress = InetAddress.getByName(UDP_BROADCAST_IP)
val packet = DatagramPacket(
cmdBytes,
cmdBytes.size,
broadcastAddress,
UDP_PORT
)
socket.send(packet)
// 3. 监听充电桩反馈
val buffer = ByteArray(1024)
val resultPacket = DatagramPacket(buffer, buffer.size)
socket.receive(resultPacket)
// 4. 解析反馈结果
val resultJson = String(
resultPacket.data,
0,
resultPacket.length,
Charsets.UTF_8
)
val result = gson.fromJson(resultJson, ConfigResult::class.java)
// 5. 回调结果(切换到主线程)
withContext(Dispatchers.Main) {
if (result.result_code == 0) {
listener.onSuccess(result.result_msg)
} else {
listener.onFailed(result.result_code, result.result_msg)
}
}
socket.close()
} catch (e: Exception) {
// 异常处理(切换到主线程)
withContext(Dispatchers.Main) {
listener.onFailed(999, "配网失败:${e.message ?: "未知错误"}")
}
}
}
}
// 配网指令数据类(对应JSON结构)
data class ConfigCommand(
val cmd_type: String,
val device_id: String,
val wifi_ssid: String,
val wifi_pwd: String,
val timestamp: Long
)
// 配网结果数据类(对应JSON结构)
data class ConfigResult(
val cmd_type: String,
val device_id: String,
val result_code: Int,
val result_msg: String,
val timestamp: Long
)
}
4. 页面示例:配网 Activity
kotlin
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.permissionx.guolindev.PermissionX
import kotlinx.android.synthetic.main.activity_config.*
class WifiConfigActivity : AppCompatActivity() {
private lateinit var configManager: ChargingPileWifiConfigManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_config)
// 初始化配网管理器
configManager = ChargingPileWifiConfigManager(this)
// 自动填充当前连接的SSID
tvCurrentSsid.text = "当前连接WiFi:${getCurrentWifiSsid() ?: "未检测到"}"
etSsid.setText(getCurrentWifiSsid())
// 配网按钮点击事件
btnConfig.setOnClickListener {
val ssid = etSsid.text.toString().trim()
val pwd = etPwd.text.toString().trim()
val deviceId = etDeviceId.text.toString().trim()
// 校验输入
if (ssid.isEmpty()) {
Toast.makeText(this, "请输入WiFi名称", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
if (deviceId.isEmpty()) {
Toast.makeText(this, "请输入充电桩设备ID", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
// 申请权限
requestPermissions {
// 权限通过后发送配网指令
configManager.sendConfigCommand(ssid, pwd, deviceId, object :
ChargingPileWifiConfigManager.OnConfigResultListener {
override fun onSuccess(message: String) {
Toast.makeText(this@WifiConfigActivity, message, Toast.LENGTH_SHORT).show()
// 配网成功,跳转结果页
// startActivity(Intent(this@WifiConfigActivity, SuccessActivity::class.java))
}
override fun onFailed(errorCode: Int, errorMsg: String) {
Toast.makeText(this@WifiConfigActivity, "失败:$errorMsg", Toast.LENGTH_SHORT).show()
}
})
}
}
}
/**
* 申请必要权限(位置+网络)
*/
private fun requestPermissions(onGranted: () -> Unit) {
PermissionX.init(this)
.permissions(
android.Manifest.permission.ACCESS_COARSE_LOCATION,
android.Manifest.permission.ACCESS_FINE_LOCATION,
android.Manifest.permission.INTERNET
)
.onExplainRequestReason { scope, deniedList ->
scope.showRequestReasonDialog(deniedList, "需要位置权限读取WiFi名称,网络权限发送配网指令", "确定", "取消")
}
.onForwardToSettings { scope, deniedList ->
scope.showForwardToSettingsDialog(deniedList, "请在设置中开启权限", "去设置", "取消")
}
.request { allGranted, _, _ ->
if (allGranted) {
onGranted()
} else {
Toast.makeText(this, "权限申请失败,无法进行配网", Toast.LENGTH_SHORT).show()
}
}
}
}
5. 布局文件(activity_config.xml)
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:id="@+id/tvCurrentSsid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:marginBottom="10dp"
android:text="当前连接WiFi:未检测到" />
<EditText
android:id="@+id/etDeviceId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入充电桩设备ID"
android:marginBottom="10dp" />
<EditText
android:id="@+id/etSsid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入目标WiFi名称"
android:marginBottom="10dp" />
<EditText
android:id="@+id/etPwd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入目标WiFi密码(无密码留空)"
android:inputType="textPassword"
android:marginBottom="20dp" />
<Button
android:id="@+id/btnConfig"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="开始配网" />
</LinearLayout>
五、测试与验证步骤
- 环境准备 :
- 充电桩进入配网模式,创建临时热点;
- 测试手机连接该热点;
- 安装 App 并授予位置、网络权限。
- 操作步骤 :
- 输入充电桩设备 ID、目标 WiFi SSID 和密码;
- 点击 "开始配网" 按钮;
- 观察 App 提示(成功 / 失败),同时查看充电桩指示灯(常亮 = 配网成功,快闪 = 失败)。
- 验证结果 :
- 配网成功后,充电桩会自动连接目标 WiFi,可通过路由器管理页面查看设备是否上线;
- App 可再次发送心跳指令,验证充电桩是否在线。
六、常见异常及处理方案
表格
| 异常场景 | 错误码 | Kotlin 侧处理方式 |
|---|---|---|
| WiFi 密码错误 | 1 | 清空密码输入框,提示用户 "密码错误,请重新输入" |
| SSID 不存在 | 2 | 校验 SSID 格式,提示用户 "未找到该 WiFi,请检查名称或确保 WiFi 已开启" |
| UDP 发送失败 | 999 | 检查手机是否连接充电桩热点,通过wifiManager.isWifiEnabled校验 WiFi 是否开启 |
| 监听反馈超时 | 999 | 提示用户 "未收到充电桩响应,请确认设备处于配网模式,或重试" |
| 权限未授予 | - | 使用 PermissionX 引导用户授权,未授权时禁用配网按钮 |
| JSON 解析失败 | 999 | 校验指令 JSON 格式,通过escapeJsonChars处理特殊字符 |
七、总结
核心要点
- Kotlin 实现配网的核心是协程 + UDP 广播:协程替代传统 Thread 处理异步通信,避免主线程阻塞;UDP 广播无需提前知道充电桩 IP,适配局域网内多设备场景;
- 指令规范:JSON 格式需包含
cmd_type(指令类型)、device_id(设备标识)、result_code(结果码)等核心字段,特殊字符需转义; - 权限处理:Android 10 + 必须申请位置权限才能读取 WiFi SSID,需引导用户授权并处理权限拒绝场景。
Kotlin 开发注意事项
- 空安全:所有可能为空的字段(如 SSID、反馈结果)需添加
?,避免空指针异常; - 协程作用域:建议使用
lifecycleScope替代GlobalScope,避免内存泄漏; - 加密传输:正式环境需对 WiFi 密码进行 AES 加密,可扩展
ConfigCommand添加加密逻辑; - 兼容性:适配 Android 12 + 的 WiFi 权限变更(如
NEARBY_WIFI_DEVICES权限)。