Golang实现通过蓝牙配置Linux系统WI-FI
背景和使用场景
在物联网项目中需要通过手机应用初始化设备的网络连接,物联网终端使用的是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/)
- getLocalIPAddresses:返回设备IP信息
- internetHealthyCheck:查询设备是否已经连接网络
- setupNewWifi: 配置无线网络
- resetTerminal: 通过Shell救援重置设备
配置本地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)
}()
<-c
}
func must(action string, err error) {
if err != nil {
panic("failed to " + action + ": " + err.Error())
}
}
如下代码确保服务一直保持运行状态
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)
}()
<-c
通过调用本地HTTP API调用与Nodejs主服务交互设备状态和设置网络参数。
func getLocalIPAddresses() (DeviceIPAddress, error) {
res, err := http.Get(getLocalIPAddressURL)
if err != nil {
log.Println(err)
return DeviceIPAddress{}, err
}
defer res.Body.Close()
rbody, _ := ioutil.ReadAll(res.Body)
ipaddresses := DeviceIPAddress{}
err = json.Unmarshal(rbody, &ipaddresses)
if err != nil {
log.Println(err)
return DeviceIPAddress{}, err
}
return ipaddresses, nil
}
func internetHealthyCheck() (bool, error) {
res, err := http.Get(internetHealthyCheckURL)
if err != nil {
log.Println(err)
return false, err
}
defer res.Body.Close()
rbody, _ := ioutil.ReadAll(res.Body)
networkStatus := NetworkStatus{}
err = json.Unmarshal(rbody, &networkStatus)
if err != nil {
log.Println(err)
return false, err
}
return networkStatus.Success, nil
}
func setupNewWifi(wifiConfig []byte) (bool, error) {
request, _ := http.NewRequest("POST", setupNewWifiURL, bytes.NewBuffer(wifiConfig))
request.Header.Set("Content-Type", "application/json; charset=UTF-8")
client := &http.Client{}
res, err := client.Do(request)
if err != nil {
log.Println(err)
return false, err
}
defer res.Body.Close()
rbody, _ := ioutil.ReadAll(res.Body)
wifiSettingStatus := WifiSettingStatus{}
err = json.Unmarshal(rbody, &wifiSettingStatus)
if err != nil {
log.Println(err)
return false, err
}
log.Println(wifiSettingStatus)
return wifiSettingStatus.Success, nil
}
func resetTerminal(resetVersion []byte) (bool, error) {
request, _ := http.NewRequest("POST", factoryResetURL, bytes.NewBuffer(resetVersion))
request.Header.Set("Content-Type", "application/json; charset=UTF-8")
client := &http.Client{}
res, err := client.Do(request)
if err != nil {
log.Println(err)
return false, err
}
defer res.Body.Close()
return true, nil
}
编译程序
GOOS=linux GOARCH=arm64 go build -o bin/kimable
将蓝牙服务拷贝至设备端,配置为本地服务,确保服务在系统重启自行启动autostart
创建服务文件 /etc/systemd/system/kimacloud-ble.service
[Unit]
Description=Kimacloud Ble Service
Documentation=https://kimacloud.com/
After=pm2-root.service
[Install]
WantedBy=multi-user.target
[Service]
ExecStart=/home/player/kimable
WorkingDirectory=/home/player
User=root
Restart=always
RestartSec=5
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=%n
注册服务并确保服务正确启动
systemctl enable kimacloud-ble.service
systemctl start kimacloud-ble.service
systemctl status kimacloud-ble.service
journalctl -f -u kimacloud-ble.service
systemctl daemon-reload
完整代码请访问Github