背景和使用场景
在物联网项目中需要通过手机应用初始化设备的网络连接,物联网终端使用的是Linux操作系统,配置为单应用启动模式,没有提供图形桌面,为了让普通用户方便的初始化设备,需要使用手机蓝牙连接设备配置无线网络连接。
手机应用开发蓝牙连接功能,通过近场蓝牙连接设备,配置WI-FI的SSID和密码,手机端对应蓝牙功能本文先不做介绍,可以通过nRF Connect 或 LightBlue 应用测试本文的实现。
为什么选择Golang来实现
设备端IOT客户端代码由Nodejs来实现,蓝牙服务也由Nodejs编写,由于bleno库已经不再维护,bleno的第三方库依赖和编译对Python和Nodejs的版本有诸多不便,所以转向使用Golang来实现,选择使用tinygo的bluetooth模块。
如果对Nodejs感兴趣也可以参考Bluetooth Nodejs实现
操作系统环境安装
在 Linux(Debian或Ubuntu)安装BlueZ
sudo apt update
sudo apt-get install bluetooth bluez bluez-tools rfkill
查看并启动Bluetooth服务
sudo systemctl enable bluetooth.service
sudo systemctl start bluetooth.service
sudo hciconfig hci0 up
rfkill unblock bluetooth
代码实现
功能需求
- 通过手机应用中设备搜索,查询主机提供的蓝牙发现服务,查询到目标设备,手动建立蓝牙连接
- 手机应用连接后,蓝牙服务返回设备IP地址和连接状态信息
- 手机端应用提交一个Form,输入SSID和Password,发送回设备蓝牙服务,设备修改WI-FI配置,连接新的无线网络
- 自动或手动刷新设备网络连接状态,有线或无线网络连接
搜索发现蓝牙服务刷新查看设备网络状态,设置无线网络参数
定义发送和接收的数据格式
//DeviceIPAddress model
type DeviceIPAddress struct {
Eth0 IPModel `json:"eth0"`
Wifi IPModel `json:"wifi"`
}
//IPModel for ip address
type IPModel struct {
IP string `json:"ip"`
Mac string `json:"mac"`
Name string `json:"name"`
}
//WIFIConfig data send to config device wifi
type WIFIConfig struct {
Ssid string `json:"ssid"`
Password string `json:"password"`
}
//NetworkStatus is the network health staus
type NetworkStatus struct {
Success bool `json:"success"`
Message string `json:"message"`
}
//WifiSettingStatus is the network health staus
type WifiSettingStatus struct {
Success bool `json:"success"`
}
Golang代码只实现Bluetooth服务的发现,数据的发送和接收,譬如主机网络地址,连接状态,Wi-Fi配置信息都交由本地Nodejs通过Localhost Http API 来实现
Nodejs 本地服务(http://localhost:3002/)
配置本地API服务路径
var localAPIHost = "http://127.0.0.1:3002/"
var getLocalIPAddressURL = localAPIHost + "getLocalIPAddress"
var internetHealthyCheckURL = localAPIHost + "internetHealthyCheck"
var setupNewWifiURL = localAPIHost + "setupNewWifi"
var factoryResetURL = localAPIHost + "factoryResetForBle"
//设备名称保存在主机本地txt文件中
var deviceBleFile = "/application/signage-device-application/db/device.txt"
定义服务UUID,可使用UUID在线生成工具或代码生成
var (
serviceUUID, _ = bluetooth.ParseUUID("d6cb1959-8010-43bd-8ef7-48dbd249b984")
refreshUUID, _ = bluetooth.ParseUUID("c537baa5-6201-4275-ab14-da353bde3dc3")
statusUUID, _ = bluetooth.ParseUUID("f9e9e098-77d4-4db3-a08f-8321c493431b")
ipUUID, _ = bluetooth.ParseUUID("2d75504c-b822-44b3-bb81-65d7b6cbdae1")
settingUUID, _ = bluetooth.ParseUUID("493ebfb0-b690-4ae8-a77a-329619c6f613")
resetTerminalUUID, _ = bluetooth.ParseUUID("2d75504c-b822-44b3-bb81-65d7b6cbdae3")
)
主机蓝牙服务提供3个可写的和2个只读的characteristic
//可写
var refreshChar bluetooth.Characteristic
var resetTerminalChar bluetooth.Characteristic
var settingChar bluetooth.Characteristic
//只读
var statusChar bluetooth.Characteristic
var ipChar bluetooth.Characteristic
主函数
func main() {
file, err := ioutil.ReadFile(deviceBleFile)
if err != nil {
log.Println(err)
}
bleName := string(file)
adapter := bluetooth.DefaultAdapter
must("enable BLE stack", adapter.Enable())
adv := adapter.DefaultAdvertisement()
must("config adv", adv.Configure(bluetooth.AdvertisementOptions{
LocalName: bleName, // kimacloud sevice
ServiceUUIDs: []bluetooth.UUID{serviceUUID},
}))
must("start adv", adv.Start())
var refreshChar bluetooth.Characteristic
var statusChar bluetooth.Characteristic
var ipChar bluetooth.Characteristic
var resetTerminalChar bluetooth.Characteristic
var settingChar bluetooth.Characteristic
must("add service", adapter.AddService(&bluetooth.Service{
UUID: serviceUUID,
Characteristics: []bluetooth.CharacteristicConfig{
{
Handle: &refreshChar,
UUID: refreshUUID,
Flags: bluetooth.CharacteristicWritePermission | bluetooth.CharacteristicWriteWithoutResponsePermission,
WriteEvent: func(client bluetooth.Connection, offset int, value []byte) {
ipaddresses, _ := getLocalIPAddresses()
ipString, _ := json.Marshal(ipaddresses)
ipChar.Write(ipString)
netState, _ := internetHealthyCheck()
if netState {
statusChar.Write([]byte("online"))
} else {
statusChar.Write([]byte("offline"))
}
},
},
{
Handle: &settingChar,
UUID: settingUUID,
Flags: bluetooth.CharacteristicWritePermission | bluetooth.CharacteristicWriteWithoutResponsePermission,
WriteEvent: func(client bluetooth.Connection, offset int, value []byte) {
setupNewWifi(value)
ipaddresses, _ := getLocalIPAddresses()
ipString, _ := json.Marshal(ipaddresses)
log.Println(ipString)
netState, _ := internetHealthyCheck()
if netState {
statusChar.Write([]byte("online"))
} else {
statusChar.Write([]byte("offline"))
}
},
},
{
Handle: &resetTerminalChar,
UUID: resetTerminalUUID,
Flags: bluetooth.CharacteristicWritePermission | bluetooth.CharacteristicWriteWithoutResponsePermission,
WriteEvent: func(client bluetooth.Connection, offset int, value []byte) {
log.Println("reset terminal")
resetTerminal(value)
},
},
{
Handle: &statusChar,
UUID: statusUUID,
Flags: bluetooth.CharacteristicNotifyPermission | bluetooth.CharacteristicReadPermission,
},
{
Handle: &ipChar,
UUID: ipUUID,
Flags: bluetooth.CharacteristicNotifyPermission | bluetooth.CharacteristicReadPermission,
},
},
}))
println("advertising...")
ipaddresses, _ := getLocalIPAddresses()
ipString, _ := json.Marshal(ipaddresses)
log.Println(ipString)
ipChar.Write(ipString)
address, _ := adapter.Address()
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
println("Kimacloud Bluetooth Service /", address.MAC.String())
time.Sleep(1 * time.Second)
}()