ble/ble_darwin.go

494 lines
11 KiB
Go

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
hr int
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()
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 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 (b *BLE) HR() int {
b.Lock()
defer b.Unlock()
return b.hr
}
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 {
go func() {
fmt.Printf("Go: Scanning\n")
b.cm.ScanForPeripheralsWithServices(nil, nil)
}()
}
}
func (b *BLE) Scan() {
b.Lock()
if b.state != (ns.CBManagerState)(ns.CBManagerStatePoweredOn) {
b.wantScan = true
b.Unlock()
} else {
b.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 connectTracker(b *BLE, x ConnectionListItem) {
tick := time.NewTicker(time.Second * 30)
select {
case <-b.connections.close:
fmt.Printf("Closing connection to %s\n", x.p.Name)
b.cm.CancelPeripheralConnection(x.p.p)
case <-tick.C:
fmt.Printf("Connection to %s timed out\n", x.p.Name)
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()
b.events <- ConnectTimeoutEvent{x.p}
case state := <-x.state:
fmt.Printf("connectTracker: state\n")
if state == "connected" {
fmt.Printf("--connected")
}
}
}
func (b *BLE) Connect(p Peripheral) {
b.Lock()
if !b.ready {
b.Unlock()
return
}
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)
p.p = ps.ObjectAtIndex(0).CBPeripheral()
} else {
fmt.Printf("--none found\n")
return
}
}
item := ConnectionListItem{p, make(chan string)}
b.connections.Add(item)
go connectTracker(b, item)
//time.Sleep(time.Second/10)
b.cm.ConnectPeripheral(p.p, nil)
fmt.Printf("cm.ConnectPeripheral() returned\n")
}
func updateState(c *ns.CBCentralManager) {
b := cdLookup[c.Ptr()]
fmt.Printf("Go: did update state\n")
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)
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.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())
if peripheral.Name == "" {
return
}
if ok := b.peripherals.Add(peripheral); ok {
pdLookup[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) {
//ble := pdLookup[p]
ble := gble
if chr.UUID().IsEqualTo(hrv_uuid) {
v := chr.Value()
ble.Lock()
ble.hr = hr(v)
ble.Unlock()
fmt.Printf("Heart rate: %d\n", ble.hr)
}
}
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
}