//go:generate mkdir -p classes //go:generate javac -bootclasspath $ANDROID_HOME/platforms/android-29/android.jar -classpath blessed-full.jar -d classes BlessedConnect.java //go:generate jar cf Ble.jar -C classes . //go:generate rm -rf classes package ble import ( "log" "sync" "unsafe" "gioui.org/app" _ "gioui.org/app/permission/bluetooth" "git.wow.st/gmp/ble/gatt" ) /* #include "jni_android.h" */ import "C" // Types required for ble.go type bleHandle struct { BlessedConnect C.jobject state int } type bleState string type Peripheral struct { Name string RSSI int Identifier string peripheral C.jobject } type Service struct { UUID string service C.jobject } type Characteristic struct { UUID string characteristic C.jobject } // Internal global variables var ( gBLE *BLE // FIXME: move to lookup tables as in ble_darwin.go? // FIXME: lifecycle, this may need to fire again? Or can I depend // on my Fragment re-starting? Or call setRetained(true) from the // fragment so we don't have to re-discover our BluetoothGatt and // BluetoothDevice objects? installCompleteOnce sync.Once waitch chan struct{} ) const ( // https://developer.android.com/reference/android/bluetooth/BluetoothAdapter.html // FIXME: create a type for these? STATE_OFF int = 10 STATE_TURNING_ON = 11 STATE_ON = 12 STATE_TURNING_OFF = 13 ) func init() { waitch = make(chan struct{}) } func connect() { <-waitch } // Functions required by API //peripheralLookup returns a pointer to a BLE struct related to the given //Peripheral. func peripheralLookup(p Peripheral) *BLE { return gBLE } //newPeripheral creates a new Peripheral struct // FIXME: check what happens with "incomplete" Peripherals that have only // an Identifier set. Do the other functions on Peripherals do something // sensible? func newPeripheral(name, id string, rssi int, peripheral C.jobject) Peripheral { return Peripheral{ Name: name, RSSI: rssi, Identifier: id, peripheral: peripheral, } } func (p Peripheral) IsIncomplete() bool { if p.peripheral == 0 { return true } else { return false } } // FIXME: should I call NewGlobalRef() here? func (p Peripheral) Retain() { } //stringState returns a string version of the BLE state func (b *BLE) stringState() string { switch b.handle.state { case STATE_OFF: return "powered off" case STATE_TURNING_ON: return "turning on" case STATE_ON: return "powered on" case STATE_TURNING_OFF: return "turning off" default: return "no state" } } //readyToScan returns true if the hardware is ready to initiate a scan func (b *BLE) readyToScan() bool { connect() var ret bool runInJVM(func(env *C.JNIEnv) { ret = C.enabled(env, b.handle.BlessedConnect) == C.JNI_TRUE }) return ret } //scan puts the BLE hardware into scanning mode func (b *BLE) scan() { connect() runInJVM(func(env *C.JNIEnv) { C.scan(env, b.handle.BlessedConnect); }) } //stopScan stops a scan in progress func (b *BLE) stopScan() { connect() runInJVM(func(env *C.JNIEnv) { C.stopScan(env, b.handle.BlessedConnect); }) } //connectPeripheral attempts to connect to a Peripheral func (b *BLE) connectPeripheral(x Peripheral) { runInJVM(func(env *C.JNIEnv) { C.connect(env, b.handle.BlessedConnect, x.peripheral) }) } //cancelConnection cancels an in-progress connection attempt func (b *BLE) cancelConnection(p Peripheral) { connect() runInJVM(func(env *C.JNIEnv) { C.disconnect(env, b.handle.BlessedConnect, p.peripheral) }) } //knownPeripheral returns a Peripheral that is known to the system without //scanning //Not implemented for Android func (b *BLE) knownPeripheral(p Peripheral) (Peripheral, bool) { return Peripheral{}, false } //DiscoverServices asks a Peripheral for its Services func (p Peripheral) DiscoverServices() { //launch a goroutine because this function calls back directly //from the same thread (the underlying Java call is synchronous) go func() { connect() log.Printf("discovering services") runInJVM(func(env *C.JNIEnv) { C.discoverServices(env, gBLE.handle.BlessedConnect, p.peripheral) }) }() } //DiscoverCharacteristics asks a Peripheral for the Characteristics related //to a Service func (p Peripheral) DiscoverCharacteristics(serv Service) { //launch a goroutine because this function calls back directly //from the same thread (the underlying Java call is synchronous) go func() { connect() log.Printf("discovering characteristics") runInJVM(func(env *C.JNIEnv) { C.discoverCharacteristics(env, gBLE.handle.BlessedConnect, p.peripheral, serv.service) }) }() } //SetNotifyValue subscribes to a characteristic func (p Peripheral) SetNotifyValue(c Characteristic) { runInJVM(func(env *C.JNIEnv) { result := (C.setCharacteristicNotification(env, gBLE.handle.BlessedConnect, p.peripheral, c.characteristic) == C.JNI_TRUE) if (!result) { log.Printf("setCharacteristicNotification: %s failed", c.UUID) } }) } //NewBLE returns a pointer to a BLE struct after setting up the OS //Bluetooth API. func NewBLE() *BLE { ps := Peripherals{items: make([]PeripheralListItem, 0)} gBLE = &BLE{ events: make(chan interface{}), peripherals: ps, } return gBLE } //Enable func (b *BLE) Enable(view uintptr) { log.Printf("ble.Enable()") setJVM(app.JavaVM()) runInJVM(func(env *JNIEnv) { log.Printf("ble.Enable(): inside runInJVM()") C.registerFragment(env, (C.jobject)(unsafe.Pointer(view))) }) } // Go callbacks from Java //export Java_st_wow_git_ble_BlessedConnect_installComplete func Java_st_wow_git_ble_BlessedConnect_installComplete(env *C.JNIEnv, class C.jclass, b C.jobject) { log.Printf("installComplete()") if (b == 0) { log.Printf("BlessedConnect object is nil!") } gBLE.handle.BlessedConnect = (C.NewGlobalRef)(env,b) if C.enabled(env, gBLE.handle.BlessedConnect) == C.JNI_TRUE { gBLE.handle.state = STATE_ON gBLE.ready = true gBLE.events <- UpdateStateEvent{State: gBLE.stringState()} } installCompleteOnce.Do(func() { close(waitch) }) log.Printf("installComplete() returning") } //export Java_st_wow_git_ble_BlessedConnect_updateState func Java_st_wow_git_ble_BlessedConnect_updateState(env *C.JNIEnv, class C.jclass, s C.jint) { log.Printf("UpdateState: %d", s) gBLE.handle.state = (int)(s) if gBLE.handle.state == STATE_ON { gBLE.ready = true } gBLE.events <- UpdateStateEvent{State: gBLE.stringState()} } //export goOnScan func goOnScan(cname, cid *C.char, rssi C.int, dev C.jobject) { name := C.GoString(cname); id := C.GoString(cid); if name == "" { return } peripheral := newPeripheral(name, id, (int)(rssi), dev) if ok := gBLE.peripherals.Add(peripheral); ok { gBLE.events <- DiscoverPeripheralEvent{Peripheral: peripheral} } } //export goOnConnect func goOnConnect(p C.jobject, cid *C.char) { id := C.GoString(cid) peripheral := gBLE.retrievePeripheral(id, p) if peripheral.peripheral == 0 { log.Printf("goOnConnect(): peripheral == null") } gBLE.connections.UpdateState(peripheral, "connected") gBLE.events <- ConnectEvent{peripheral} log.Printf("Go: goOnConnect returning\n") } //export goOnDisconnect func goOnDisconnect(p C.jobject, cid *C.char) { id := C.GoString(cid) peripheral := gBLE.retrievePeripheral(id) if peripheral.peripheral == 0 { log.Printf("goOnDisconnect(): peripheral == null") } go func() { gBLE.connections.UpdateState(peripheral, "cancel") gBLE.events <- DisconnectEvent{peripheral} }() log.Printf("Go: goOnDisconnect returning\n") } //export goOnDiscoverService func goOnDiscoverService(cid, cuuid *C.char, serv C.jobject) { id := C.GoString(cid) uuid := C.GoString(cuuid) peripheral := gBLE.retrievePeripheral(id) service := Service{ UUID: uuid, service: serv, } gBLE.events <- DiscoverServiceEvent{ Peripheral: peripheral, Gatt: gatt.Service{uuid}, Service: service, } } //export goOnDiscoverCharacteristic func goOnDiscoverCharacteristic(cid, csuuid *C.char, serv C.jobject, ccuuid *C.char, characteristic C.jobject) { id := C.GoString(cid) suuid := C.GoString(csuuid) cuuid := C.GoString(ccuuid) log.Printf("goOnDiscoverCharacteristic: %s", cuuid) peripheral := gBLE.retrievePeripheral(id) service := Service{ UUID: suuid, service: serv, } ch := Characteristic{ UUID: cuuid, characteristic: characteristic, } gBLE.events <- DiscoverCharacteristicEvent{ Peripheral: peripheral, Service: service, Characteristic: ch, Gatt: gatt.Characteristic{cuuid}, } } //export goOnCharacteristicChanged func goOnCharacteristicChanged(cid, ccuuid *C.char, characteristic C.jobject, cvalue *C.char, length C.jint) { id := C.GoString(cid) cuuid := C.GoString(ccuuid) peripheral := gBLE.retrievePeripheral(id) ch := Characteristic{ UUID: cuuid, characteristic: characteristic, } gBLE.events <- UpdateValueEvent{ Peripheral: peripheral, Characteristic: ch, Data: C.GoBytes(unsafe.Pointer(cvalue), length), } } // internal convenience functions func (b *BLE) retrievePeripheral(id string, devs ...C.jobject) Peripheral { var dev C.jobject if len(devs) > 0 { dev = devs[0] } found := false var peripheral Peripheral gBLE.peripherals.Lock() for i, item := range gBLE.peripherals.items { if item.p.Identifier == id { peripheral = item.p if dev != 0 { peripheral.peripheral = dev } found = true gBLE.peripherals.items[i].p = peripheral break } } gBLE.peripherals.Unlock() if !found { log.Printf("Go: peripheral not found!") } return peripheral }