package ble import "C" import ( "encoding/binary" "fmt" "runtime" "sync" "time" "unsafe" "git.wow.st/gmp/ble/gatt" "git.wow.st/gmp/ble/ns" ) type BLE struct { state ns.CBManagerState events chan interface{} peripherals Peripherals cd *ns.CBDelegate cm *ns.CBCentralManager ready, wantScan bool sync.Mutex connections Connections } var cdLookup map[unsafe.Pointer]*BLE var pdLookup map[unsafe.Pointer]*BLE var pcache map[unsafe.Pointer]*Peripheral type State string type Peripheral struct { Name string RSSI int Identifier string p *ns.CBPeripheral } func peripheralName(p *ns.CBPeripheral) string { var ret string nsname := p.Name() if nsname.Ptr() != nil { ret = nsname.String() } return ret } 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 } type PeripheralListItem struct { p Peripheral seen time.Time } type Peripherals struct { items []PeripheralListItem sync.Mutex } type ConnectionListItem struct { p Peripheral state chan string } type Connections struct { items []ConnectionListItem close chan struct{} sync.Mutex } func (c *Connections) UpdateState(p Peripheral, s string) { var ch chan string c.Lock() for _, item := range c.items { if p.Identifier == item.p.Identifier { ch = item.state } } c.Unlock() ch <- s } func (ps *Peripherals) Add(x Peripheral) bool { ps.Lock() defer ps.Unlock() found := false for n, item := range ps.items { if item.p.Identifier == x.Identifier { item.p = x item.seen = time.Now() ps.items[n] = item found = true break } } if found { return false } //take ownership of this Objective-C object x.p.Retain() runtime.SetFinalizer(x.p, nil) x.p.GC() item := PeripheralListItem{p: x, seen: time.Now()} ps.items = append(ps.items, item) return true } func (cs *Connections) Add(x ConnectionListItem) bool { cs.Lock() defer cs.Unlock() found := false for _, item := range cs.items { if item.p.Identifier == x.p.Identifier { // are we allowed to have multiple connections to the same peripheral? return false } } if found { return false } cs.items = append(cs.items, x) return true } type UpdateStateEvent struct { State string } type DiscoverPeripheralEvent struct { Peripheral Peripheral } type DiscoverServiceEvent struct { Peripheral Peripheral Gatt gatt.Service Service *ns.CBService } type DiscoverCharacteristicEvent struct { Peripheral Peripheral Gatt gatt.Characteristic Service *ns.CBService Characteristic *ns.CBCharacteristic } type UpdateValueEvent struct { Data []byte } type ConnectEvent struct { Peripheral Peripheral } type ConnectTimeoutEvent struct { Peripheral Peripheral } type UpdageValueEvent struct { Peripheral Peripheral Characteristic gatt.Characteristic } func (b *BLE) Events() chan interface{} { return b.events } func stringState(x ns.CBManagerState) string { switch x { case (ns.CBManagerState)(ns.CBManagerStateResetting): return "resetting" case (ns.CBManagerState)(ns.CBManagerStateUnsupported): return "unsupported" case (ns.CBManagerState)(ns.CBManagerStateUnauthorized): return "unauthorized" case (ns.CBManagerState)(ns.CBManagerStatePoweredOff): return "powered off" case (ns.CBManagerState)(ns.CBManagerStatePoweredOn): return "powered on" case (ns.CBManagerState)(ns.CBManagerStateUnknown): return "unknown" default: return "no state" } } func (b *BLE) State() string { b.Lock() defer b.Unlock() return stringState(b.state) } func (b *BLE) setState(x ns.CBManagerState) { b.Lock() defer b.Unlock() b.state = x if b.ready && b.wantScan { fmt.Printf("Go: Scanning\n") b.cm.ScanForPeripheralsWithServices(nil, nil) b.wantScan = false } } func (b *BLE) Scan() { b.Lock() defer b.Unlock() if b.state != (ns.CBManagerState)(ns.CBManagerStatePoweredOn) { b.wantScan = true } else { b.peripherals.Lock() b.peripherals.items = b.peripherals.items[:0] b.peripherals.Unlock() fmt.Printf("Go: Scanning\n") b.cm.ScanForPeripheralsWithServices(nil, nil) } } func (b *BLE) StopScan() { b.Lock() if !b.ready { b.Unlock() return } b.Unlock() fmt.Printf("Go: stopping scan\n") b.cm.StopScan() } func CancelConnection(p Peripheral) { b := pdLookup[p.p.Ptr()] b.connections.Lock() var ch chan string; for _, item := range b.connections.items { if item.p.Identifier == p.Identifier { ch = item.state break } } b.connections.Unlock() if ch != nil { ch <- "cancel" } } func connectTracker(b *BLE, x ConnectionListItem) bool { fmt.Printf("connectTracker(): %s\n", x.p.Name) b.connections.Lock() for _, item := range b.connections.items { if item.p.Identifier == x.p.Identifier { fmt.Printf("connectTracker(): already connecting to %s\n", x.p.Name) b.connections.Unlock() return true } } b.connections.items = append(b.connections.items, x) b.connections.Unlock() fmt.Printf("BLE.Connect(): calling cm.ConnectPeripheral(%p)\n", x.p.p.Ptr()) b.cm.ConnectPeripheral(x.p.p, nil) cancel := func() { b.cm.CancelPeripheralConnection(x.p.p) b.connections.Lock() for n, item := range b.connections.items { if item.p.Identifier == x.p.Identifier { b.connections.items = append(b.connections.items[:n], b.connections.items[n+1:]...) } } b.connections.Unlock() } go func() { tick := time.NewTicker(time.Second * 5) select { case <-b.connections.close: fmt.Printf("connectTracker(): Closing connection to %s\n", x.p.Name) b.cm.CancelPeripheralConnection(x.p.p) case <-tick.C: fmt.Printf("connectTracker(): Connection to %s timed out\n", x.p.Name) cancel() b.events <- ConnectTimeoutEvent{x.p} case state := <-x.state: if state == "cancel" { cancel() } fmt.Printf("connectTracker(): state %s\n", state) } }() return true } func (b *BLE) Connect(p Peripheral) bool { b.Lock() if !b.ready { b.Unlock() fmt.Printf("--BLE not ready\n") return false } b.Unlock() if p.p == nil { fmt.Printf("RetrievePeripheralsWithIdentifiers\n") ps := b.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() //NOTE: ns.ObjectAtIndex() calls SetFinalizer for us, which will be a problem //later when we call GC(), so we first clear the finalizer here. runtime.SetFinalizer(cbp, nil) p = newPeripheral(cbp) } else { fmt.Printf("--none found\n") return false } } item := ConnectionListItem{p, make(chan string)} fmt.Printf("BLE.Connect() calling connectTracker\n") return connectTracker(b, item) } func Disconnect(p Peripheral) { b := pdLookup[p.p.Ptr()] found := false b.connections.Lock() for i, item := range b.connections.items { if item.p.Identifier == p.Identifier { found = true b.connections.items = append(b.connections.items[:i], b.connections.items[i+1:]...) break } } b.connections.Unlock() if !found { return } b.cm.CancelPeripheralConnection(p.p) } func updateState(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.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: stringState(st)} } func discoverPeripheral(c *ns.CBCentralManager, p *ns.CBPeripheral, d *ns.NSDictionary, rssi *ns.NSNumber) { b := cdLookup[c.Ptr()] peripheral := newPeripheral(p) peripheral.RSSI = (int)(rssi.IntValue()) _discoverPeripheral(b, peripheral) } func _discoverPeripheral(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 connectPeripheral(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.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: connectPeripheral returning\n") } func (x Peripheral) DiscoverServices() { fmt.Printf("Discovering services on %s\n", x.Name) p := x.p // discover all services on this device p.DiscoverServices(nil) } func discoverServices(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: discoverServices returning\n") } func hr(d *ns.NSData) int { if l := int(d.Length()); l < 4 { return 0 } x := C.GoBytes(d.Bytes(), 4) flags := x[0] if flags&0x80 != 0 { // uint16 format return int(binary.BigEndian.Uint16(x[1:2])) } else { return int(x[1]) } } func (p Peripheral) DiscoverCharacteristics(serv *ns.CBService) { p.p.DiscoverCharacteristics(nil, serv) } func discoverCharacteristics(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: discoverCharacteristics returning\n") } func (p Peripheral) SetNotifyValue(c *ns.CBCharacteristic) { p.p.SetNotifyValue(1, c) } func updateValue(p *ns.CBPeripheral, chr *ns.CBCharacteristic, e *ns.NSError) { b := pdLookup[p.Ptr()] v := chr.Value() b.events <-UpdateValueEvent{ Data: C.GoBytes(v.Bytes(), (C.int)(v.Length())), } } var ( hrm_uuid *ns.CBUUID hrv_uuid *ns.CBUUID info_uuid *ns.CBUUID gble *BLE ) func NewBLE() *BLE { ps := Peripherals{items: make([]PeripheralListItem, 0)} ble := &BLE{events: make(chan interface{}), peripherals: ps} gble = ble queue := ns.DispatchQueueCreate(ns.CharWithGoString("go_hrm_queue"), nil) cd := ns.CBDelegateAlloc() cd.CentralManagerDidUpdateStateCallback(updateState) cd.CentralManagerDidDiscoverPeripheralCallback(discoverPeripheral) cd.CentralManagerDidConnectPeripheralCallback(connectPeripheral) cd.PeripheralDidDiscoverServicesCallback(discoverServices) cd.PeripheralDidDiscoverCharacteristicsForServiceCallback(discoverCharacteristics) cd.PeripheralDidUpdateValueForCharacteristicCallback(updateValue) hrm_uuid = ns.CBUUIDWithGoString("180D") hrv_uuid = ns.CBUUIDWithGoString("2A37") info_uuid = ns.CBUUIDWithGoString("180A") ble.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.cm = ns.CBCentralManagerAlloc().InitWithDelegateQueue(cd, queue) cdLookup[ble.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 }