350 lines
8.7 KiB
Go
350 lines
8.7 KiB
Go
package ble
|
|
|
|
import "C"
|
|
|
|
import (
|
|
"fmt"
|
|
"runtime"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"git.wow.st/gmp/ble/gatt"
|
|
"git.wow.st/gmp/ble/ns"
|
|
)
|
|
|
|
// Types required for ble.go
|
|
|
|
type bleState ns.CBManagerState
|
|
|
|
type bleHandle struct {
|
|
cd *ns.CBDelegate
|
|
cm *ns.CBCentralManager
|
|
}
|
|
|
|
type Peripheral struct {
|
|
Name string
|
|
RSSI int
|
|
Identifier string
|
|
|
|
p *ns.CBPeripheral
|
|
}
|
|
|
|
type Service *ns.CBService
|
|
type Characteristic *ns.CBCharacteristic
|
|
|
|
// Internal global variables
|
|
|
|
var cdLookup map[unsafe.Pointer]*BLE
|
|
var pdLookup map[unsafe.Pointer]*BLE
|
|
var pcache map[unsafe.Pointer]*Peripheral
|
|
|
|
// Functions required by API
|
|
|
|
//Init needs to be called before the BLE library can be used. No setup needed
|
|
//on Darwin.
|
|
func Init() {
|
|
|
|
}
|
|
|
|
//peripheralLookup returns a pointer to a BLE struct related to the given
|
|
//Peripheral.
|
|
func peripheralLookup(p Peripheral) *BLE {
|
|
return pdLookup[p.p.Ptr()]
|
|
}
|
|
|
|
//newPeripheral creates a new Peripheral struct
|
|
func newPeripheral(x *ns.CBPeripheral) Peripheral {
|
|
if pcache == nil {
|
|
pcache = make(map[unsafe.Pointer]*Peripheral)
|
|
} else {
|
|
if ret,ok := pcache[x.Ptr()]; ok {
|
|
return *ret
|
|
}
|
|
}
|
|
ret := Peripheral{
|
|
Name: peripheralName(x),
|
|
Identifier: x.Identifier().UUIDString().String(),
|
|
p: x,
|
|
}
|
|
pcache[x.Ptr()] = &ret
|
|
return ret
|
|
}
|
|
|
|
func (p Peripheral) IsIncomplete() bool {
|
|
return p.p == nil
|
|
}
|
|
|
|
func (x Peripheral) Retain() {
|
|
//NOTE: ns.ObjectAtIndex() calls SetFinalizer for us, which will be a problem
|
|
//later when we call GC(), so we always first clear the finalizer before
|
|
//setting a new one.
|
|
runtime.SetFinalizer(x.p, nil)
|
|
x.p.Retain()
|
|
x.p.GC()
|
|
}
|
|
|
|
//stringState returns a string version of the BLE state
|
|
func (b *BLE) stringState() string {
|
|
x := b.state
|
|
switch (ns.NSInteger)(x) {
|
|
case ns.CBManagerStateResetting:
|
|
return "resetting"
|
|
case ns.CBManagerStateUnsupported:
|
|
return "unsupported"
|
|
case ns.CBManagerStateUnauthorized:
|
|
return "unauthorized"
|
|
case ns.CBManagerStatePoweredOff:
|
|
return "powered off"
|
|
case ns.CBManagerStatePoweredOn:
|
|
return "powered on"
|
|
case ns.CBManagerStateUnknown:
|
|
return "unknown"
|
|
default:
|
|
return "no state"
|
|
}
|
|
}
|
|
|
|
//readyToScan returns true if the hardware is ready to initiate a scan
|
|
func (b *BLE) readyToScan() bool {
|
|
return b.state == (bleState)(ns.CBManagerStatePoweredOn)
|
|
}
|
|
|
|
//scan puts the BLE hardware into scanning mode
|
|
func (b *BLE) scan() {
|
|
b.handle.cm.ScanForPeripheralsWithServices(nil, nil)
|
|
}
|
|
|
|
//stopScan stops a scan in progress
|
|
func (b *BLE) stopScan() {
|
|
b.handle.cm.StopScan()
|
|
}
|
|
|
|
|
|
//connectPeripheral attempts to connect to a Peripheral
|
|
func (b *BLE) connectPeripheral(x Peripheral) {
|
|
fmt.Printf("BLE.Connect(): calling cm.ConnectPeripheral(%p)\n", x.p.Ptr())
|
|
b.handle.cm.ConnectPeripheral(x.p, nil)
|
|
}
|
|
|
|
//cancelConnection cancels an in-progress connection attempt
|
|
func (b *BLE) cancelConnection(p Peripheral) {
|
|
b.handle.cm.CancelPeripheralConnection(p.p)
|
|
}
|
|
|
|
//knownPeripheral returns a Peripheral that is known to the system without
|
|
//scanning
|
|
func (b *BLE) knownPeripheral(p Peripheral) (Peripheral, bool) {
|
|
fmt.Printf("RetrievePeripheralsWithIdentifiers\n")
|
|
ps := b.handle.cm.RetrievePeripheralsWithIdentifiers(ns.NSArrayWithObjects(ns.NSUUIDAlloc().InitWithUUIDString(ns.NSStringWithGoString(p.Identifier))))
|
|
if x := (int)(ps.Count()); x > 0 {
|
|
fmt.Printf("--found %d\n", x)
|
|
cbp := ps.ObjectAtIndex(0).CBPeripheral()
|
|
return newPeripheral(cbp), true
|
|
} else {
|
|
fmt.Printf("--none found\n")
|
|
return p, false
|
|
}
|
|
}
|
|
|
|
//DiscoverServices asks a Peripheral for its Services
|
|
func (x Peripheral) DiscoverServices() {
|
|
fmt.Printf("Discovering services on %s\n", x.Name)
|
|
|
|
// discover all services on this device
|
|
x.p.DiscoverServices(nil)
|
|
}
|
|
|
|
//DiscoverCharacteristics asks a Peripheral for the Characteristics related
|
|
//to a Service
|
|
func (p Peripheral) DiscoverCharacteristics(serv Service) {
|
|
p.p.DiscoverCharacteristics(nil, serv)
|
|
}
|
|
|
|
//SetNotifyValue subscribes to a characteristic
|
|
func (p Peripheral) SetNotifyValue(c Characteristic) {
|
|
p.p.SetNotifyValue(1, c)
|
|
}
|
|
|
|
//NewBLE returns a pointer to a BLE struct after setting up the OS
|
|
//Bluetooth API.
|
|
func NewBLE() *BLE {
|
|
ps := Peripherals{items: make([]PeripheralListItem, 0)}
|
|
ble := &BLE{events: make(chan interface{}), peripherals: ps}
|
|
|
|
queue := ns.DispatchQueueCreate(ns.CharWithGoString("go_hrm_queue"), nil)
|
|
|
|
cd := ns.CBDelegateAlloc()
|
|
|
|
cd.CentralManagerDidUpdateStateCallback(didUpdateState)
|
|
cd.CentralManagerDidDiscoverPeripheralCallback(didDiscoverPeripheral)
|
|
cd.CentralManagerDidConnectPeripheralCallback(didConnectPeripheral)
|
|
cd.PeripheralDidDiscoverServicesCallback(didDiscoverServices)
|
|
cd.PeripheralDidDiscoverCharacteristicsForServiceCallback(didDiscoverCharacteristics)
|
|
cd.PeripheralDidUpdateValueForCharacteristicCallback(didUpdateValue)
|
|
|
|
|
|
ble.handle.cd = cd
|
|
if cdLookup == nil {
|
|
cdLookup = make(map[unsafe.Pointer]*BLE, 0)
|
|
}
|
|
if pdLookup == nil {
|
|
pdLookup = make(map[unsafe.Pointer]*BLE, 0)
|
|
}
|
|
|
|
// We defined our own queue because this won't work on the main queue.
|
|
ble.handle.cm = ns.CBCentralManagerAlloc().InitWithDelegateQueue(cd, queue)
|
|
cdLookup[ble.handle.cm.Ptr()] = ble
|
|
|
|
// For debugging purposes, run GC every second to make sure things are
|
|
// not over-released.
|
|
go func() {
|
|
for {
|
|
runtime.GC()
|
|
time.Sleep(time.Second * 30)
|
|
}
|
|
}()
|
|
return ble
|
|
}
|
|
|
|
// Core Bluetooth callback functions
|
|
|
|
func didUpdateState(c *ns.CBCentralManager) {
|
|
b := cdLookup[c.Ptr()]
|
|
st := c.CBManager.State()
|
|
if st == (ns.CBManagerState)(ns.CBManagerStatePoweredOn) {
|
|
b.ready = true
|
|
b.connections.close = make(chan struct{})
|
|
} else {
|
|
if b.ready {
|
|
close(b.connections.close)
|
|
b.connections.Lock()
|
|
for _, item := range b.connections.items {
|
|
fmt.Printf("Closing connection to %s\n", item.p.Name)
|
|
b.handle.cm.CancelPeripheralConnection(item.p.p)
|
|
}
|
|
b.connections.items = b.connections.items[:0]
|
|
b.connections.Unlock()
|
|
b.ready = false
|
|
}
|
|
}
|
|
b.setState(st)
|
|
b.events <- UpdateStateEvent{State: b.stringState()}
|
|
}
|
|
|
|
func didDiscoverPeripheral(c *ns.CBCentralManager, p *ns.CBPeripheral, d *ns.NSDictionary, rssi *ns.NSNumber) {
|
|
b := cdLookup[c.Ptr()]
|
|
peripheral := newPeripheral(p)
|
|
peripheral.RSSI = (int)(rssi.IntValue())
|
|
_didDiscoverPeripheral(b, peripheral)
|
|
}
|
|
|
|
func _didDiscoverPeripheral(b *BLE, peripheral Peripheral) {
|
|
if peripheral.Name == "" {
|
|
return
|
|
}
|
|
if ok := b.peripherals.Add(peripheral); ok {
|
|
pdLookup[peripheral.p.Ptr()] = b
|
|
b.events <- DiscoverPeripheralEvent{Peripheral: peripheral}
|
|
}
|
|
}
|
|
|
|
func didConnectPeripheral(c *ns.CBCentralManager, p *ns.CBPeripheral) {
|
|
fmt.Printf("Did connect peripheral\n")
|
|
b := cdLookup[c.Ptr()]
|
|
|
|
// set ourselves up as a peripheral delegate
|
|
p.SetDelegate(b.handle.cd)
|
|
|
|
uuidstring := p.Identifier().UUIDString().String()
|
|
peripheral := newPeripheral(p)
|
|
found := false
|
|
|
|
b.peripherals.Lock()
|
|
for _, item := range b.peripherals.items {
|
|
if item.p.Identifier == uuidstring {
|
|
peripheral = item.p
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
b.peripherals.Unlock()
|
|
|
|
if !found {
|
|
peripheral.Name = peripheralName(p)
|
|
if ok := b.peripherals.Add(peripheral); ok {
|
|
pdLookup[p.Ptr()] = b
|
|
}
|
|
}
|
|
|
|
b.connections.UpdateState(peripheral, "connected")
|
|
b.events <- ConnectEvent{peripheral}
|
|
fmt.Printf("Go: didConnectPeripheral returning\n")
|
|
}
|
|
|
|
func didDiscoverServices(p *ns.CBPeripheral, e *ns.NSError) {
|
|
b := pdLookup[p.Ptr()]
|
|
fmt.Printf("Did discover services\n")
|
|
p.Services().ObjectEnumerator().ForIn(func(o *ns.Id) bool {
|
|
serv := o.CBService()
|
|
uuid := serv.UUID().UUIDString().String()
|
|
b.events <-DiscoverServiceEvent{
|
|
Peripheral: newPeripheral(p),
|
|
Gatt: gatt.Service{uuid},
|
|
Service: serv,
|
|
}
|
|
return true
|
|
})
|
|
fmt.Printf("Go: didDiscoverServices returning\n")
|
|
}
|
|
|
|
func didDiscoverCharacteristics(p *ns.CBPeripheral, s *ns.CBService, e *ns.NSError) {
|
|
b := pdLookup[p.Ptr()]
|
|
fmt.Printf("Did discover characteristics\n")
|
|
s.Characteristics().ObjectEnumerator().ForIn(func(o *ns.Id) bool {
|
|
chr := o.CBCharacteristic()
|
|
chuuid := chr.UUID()
|
|
fmt.Printf("------%s\n", chuuid.UUIDString())
|
|
b.events <-DiscoverCharacteristicEvent{
|
|
Peripheral: newPeripheral(p),
|
|
Service: s,
|
|
Characteristic: chr,
|
|
Gatt: gatt.Characteristic{chuuid.UUIDString().String()},
|
|
}
|
|
return true
|
|
})
|
|
fmt.Printf("Go: didDiscoverCharacteristics returning\n")
|
|
}
|
|
|
|
func didUpdateValue(p *ns.CBPeripheral, chr *ns.CBCharacteristic, e *ns.NSError) {
|
|
b := pdLookup[p.Ptr()]
|
|
v := chr.Value()
|
|
b.events <-UpdateValueEvent{
|
|
Peripheral: newPeripheral(p),
|
|
Characteristic: chr,
|
|
Data: C.GoBytes(v.Bytes(), (C.int)(v.Length())),
|
|
}
|
|
}
|
|
|
|
// internal convenience functions
|
|
|
|
func peripheralName(p *ns.CBPeripheral) string {
|
|
var ret string
|
|
nsname := p.Name()
|
|
if nsname.Ptr() != nil {
|
|
ret = nsname.String()
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func (b *BLE) setState(x ns.CBManagerState) {
|
|
b.Lock()
|
|
defer b.Unlock()
|
|
b.state = (bleState)(x)
|
|
if b.ready && b.wantScan {
|
|
fmt.Printf("Go: Scanning\n")
|
|
b.scan()
|
|
b.wantScan = false
|
|
}
|
|
}
|
|
|