Compare commits

..

4 commits

Author SHA1 Message Date
deadprogram
1e90928486 hci: add l2cap for signaling
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-21 13:29:48 +01:00
deadprogram
73acc91d67 examples: add examples that makes connections to multiple peripherals
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-19 16:36:45 +01:00
deadprogram
6c4166d882 ninafw: should support muliple connections as a central
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-19 00:33:07 +01:00
deadprogram
e8c6a28223 hci: add check for poll buffer overflow
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-19 00:32:03 +01:00
51 changed files with 764 additions and 1923 deletions

View file

@ -1,88 +1,3 @@
0.10.0
---
* **core**
- gap: fix ServiceDataElement.UUID comment
* **docs**
- add mention of support for rp2040-W to README
- Improve documentation of RSSI Fixes https://github.com/tinygo-org/bluetooth/issues/272
* **hci**
- cyw43439: HCI implementation
- refactor to separate HCI transport implementation from interface to not always assume UART.
- update for cyw43439 HCI functionality
* **windows**
- Add Address field to Windows Device struct
- Winrt full support (#266)
- winrt-go: bump to latest
- assign char handle write event (#274)
* **test**
- add hci_uart based implementation to smoke tests
0.9.0
---
* **build**
- add arduino-nano33 and pyportal to smoke tests
- add nina-fw smoketest as peripheral
- add some ninafw examples to smoketest
* **core**
- add ServiceData advertising element (#243)
- add RequestConnectionParams to request new connection parameters
- change ManufacturerData from a map to a slice
- don't use a pointer receiver for many method calls
- make Device a value instead of a pointer
- use 'debug' variable protected by build tags for debug logging
- use Device instead of Address in SetConnectHandler
* **docs**
- a small mention of the NINA BLE support
- complete README info about nina-fw support
* **linux**
- fix characteristic value
- rewrite everything to use DBus directly
* **macos**
- add Write command to the gattc implementation
* **examples**
- tinyscan to replace clue-scanner, also works on pyportal and pybadge+airlift
- update MCU central examples to use ldflags to pass the desired device to connect to
- discover: add MTU
* **hci**
- add check for poll buffer overflow
- allow for both ninafw and pure hci uart adapter implementations
- implement Characteristic WriteHandler
- multiple connections
- return service UUIDs with scan results
- add l2cap signaling support
- implement evtNumCompPkts to count in-flight packets
- correct implementation for WriteWithoutReponse
- speed up time waiting for hardware - corrections to MTU exchange
- add support for software RTS/CTS flow control for boards where hardware support is not available
- BLE central implementation on nina-fw co-processors
- fix connection timeout
- implement BLE peripheral support
- implement GetMTU()
- remove some pointer receivers from method calls
- should support muliple connections as a central
- correctly return from read requests instead of returning spurious error
- move some steps previously being done during Configure() into Start() where they more correctly belonged.
- use advertising display name as the correct default value for the generic access characteristic.
- speed up the polling for new notifications for Centrals
- use NINA settings from board file in main TinyGo repo
* **nordic semi**
- replace unsafe.SliceData call with expression that is still supported in older Go versions
- update to prepare for changes in the TinyGo CGo implementation
- add address of connecting device
- add support for connection timeout on connect
- don't send a notify/indicate without a CCCD
- fix connect timeout
- fix writing to a characteristic
- print connection parameters when debug is enabled
- return an error on a connection timeout
* **windows**
- Release AsyncOperationCompletedHandler (#208)
- check for error when scanning
- bump to latest winrt
0.8.0
---

View file

@ -42,12 +42,6 @@ smoketest-tinygo:
@md5sum test.hex
$(TINYGO) build -o test.uf2 -size=short -target=nano-rp2040 ./examples/advertisement
@md5sum test.hex
$(TINYGO) build -o test.uf2 -size=short -target=circuitplay-express -tags="hci hci_uart" ./examples/advertisement
@md5sum test.hex
$(TINYGO) build -o test.uf2 -size=short -target=pico-w ./examples/discover
@md5sum test.hex
$(TINYGO) build -o test.uf2 -size=short -target=badger2040-w ./examples/advertisement
@md5sum test.hex
smoketest-linux:
# Test on Linux.
@ -64,8 +58,6 @@ smoketest-windows:
GOOS=windows go build -o /tmp/go-build-discard ./examples/scanner
GOOS=windows go build -o /tmp/go-build-discard ./examples/discover
GOOS=windows go build -o /tmp/go-build-discard ./examples/heartrate-monitor
GOOS=windows go build -o /tmp/go-build-discard ./examples/advertisement
GOOS=windows go build -o /tmp/go-build-discard ./examples/heartrate
smoketest-macos:
# Test on macos.

View file

@ -2,13 +2,13 @@
[![Go Bluetooth](./images/gobluetooth.png)](https://tinygo.org/bluetooth)
[![PkgGoDev](https://pkg.go.dev/badge/pkg.go.dev/gitrepo.ru/neonxp/bluetooth)](https://pkg.go.dev/gitrepo.ru/neonxp/bluetooth) [![Linux](https://github.com/tinygo-org/bluetooth/actions/workflows/linux.yml/badge.svg?branch=dev)](https://github.com/tinygo-org/bluetooth/actions/workflows/linux.yml) [![macOS](https://github.com/tinygo-org/bluetooth/actions/workflows/macos.yml/badge.svg?branch=dev)](https://github.com/tinygo-org/bluetooth/actions/workflows/macos.yml)
[![PkgGoDev](https://pkg.go.dev/badge/pkg.go.dev/tinygo.org/x/bluetooth)](https://pkg.go.dev/tinygo.org/x/bluetooth) [![Linux](https://github.com/tinygo-org/bluetooth/actions/workflows/linux.yml/badge.svg?branch=dev)](https://github.com/tinygo-org/bluetooth/actions/workflows/linux.yml) [![macOS](https://github.com/tinygo-org/bluetooth/actions/workflows/macos.yml/badge.svg?branch=dev)](https://github.com/tinygo-org/bluetooth/actions/workflows/macos.yml)
Go Bluetooth is a cross-platform package for using [Bluetooth Low Energy](https://en.wikipedia.org/wiki/Bluetooth_Low_Energy) hardware from the Go programming language.
It works on typical operating systems such as [Linux](#linux), [macOS](#macos), and [Windows](#windows).
It can also be used running "bare metal" on microcontrollers produced by [Nordic Semiconductor](https://www.nordicsemi.com/) or using the Bluetooth Host Controller Interface (HCI) by using [TinyGo](https://tinygo.org/).
It can also be used running "bare metal" on microcontrollers produced by [Nordic Semiconductor](https://www.nordicsemi.com/) by using [TinyGo](https://tinygo.org/).
The Go Bluetooth package can be used to create both Bluetooth Low Energy Centrals as well as to create Bluetooth Low Energy Peripherals.
@ -22,7 +22,7 @@ This example shows a central that scans for peripheral devices and then displays
package main
import (
"gitrepo.ru/neonxp/bluetooth"
"tinygo.org/x/bluetooth"
)
var adapter = bluetooth.DefaultAdapter
@ -58,7 +58,7 @@ package main
import (
"time"
"gitrepo.ru/neonxp/bluetooth"
"tinygo.org/x/bluetooth"
)
var adapter = bluetooth.DefaultAdapter
@ -92,17 +92,17 @@ func must(action string, err error) {
## Current support
| | Linux | macOS | Windows | Nordic Semi | ESP32 (NINA-FW) | CYW43439 (RP2040-W) |
| -------------------------------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------- |
| API used | BlueZ | CoreBluetooth | WinRT | SoftDevice | HCI | HCI |
| Scanning | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Connect to peripheral | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Write peripheral characteristics | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Receive notifications | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Advertisement | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Local services | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Local characteristics | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Send notifications | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| | Linux | macOS | Windows | Nordic Semi | ESP32 (NINA-FW) |
| -------------------------------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ |
| API used | BlueZ | CoreBluetooth | WinRT | SoftDevice | HCI |
| Scanning | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Connect to peripheral | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Write peripheral characteristics | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Receive notifications | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Advertisement | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: |
| Local services | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: |
| Local characteristics | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: |
| Send notifications | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: |
## Linux
@ -286,23 +286,6 @@ For example, this command can be used to compile and flash an Arduino Nano RP204
If you want more information about the `nina-fw` firmware, or want to add support for other ESP32-equipped boards, please see https://github.com/arduino/nina-fw
## CYW43439 (RP2040-W)
Go Bluetooth has bare metal support for boards that include a separate CYW43439 Bluetooth Low Energy radio co-processor.
Currently supported boards include:
* [Raspberry Pi Pico RP2040-W](https://www.raspberrypi.com/documentation/microcontrollers/raspberry-pi-pico.html#raspberry-pi-pico-w)
* [Pimoroni Badger2040-W](https://shop.pimoroni.com/products/badger-2040-w)
After you have installed TinyGo and the Go Bluetooth package, you should be able to compile/run code for your device.
For example, this command can be used to compile and flash a Pico RP2040-W board with the example we provide that turns it into a BLE peripheral to act like a heart rate monitor:
tinygo flash -target pico-w ./examples/heartrate
If you want more information about the `cyw43439` support, please see https://github.com/soypat/cyw43439
## API stability
**The API is not stable!** Because many features are not yet implemented and some platforms (e.g. Windows and macOS) are not yet fully supported, it's hard to say what a good API will be. Therefore, if you want stability you should pick a particular git commit and use that. Go modules can be useful for this purpose.

View file

@ -1,116 +0,0 @@
//go:build cyw43439
package bluetooth
import (
"machine"
"log/slog"
"github.com/soypat/cyw43439"
)
const maxConnections = 1
// Adapter represents a SPI connection to the HCI controller on an attached CYW4349 module.
type Adapter struct {
hciAdapter
}
// DefaultAdapter is the default adapter on the current system.
//
// Make sure to call Enable() before using it to initialize the adapter.
var DefaultAdapter = &Adapter{
hciAdapter: hciAdapter{
isDefault: true,
connectHandler: func(device Device, connected bool) {
return
},
connectedDevices: make([]Device, 0, maxConnections),
},
}
// Enable configures the BLE stack. It must be called before any
// Bluetooth-related calls (unless otherwise indicated).
func (a *Adapter) Enable() error {
if debug {
println("Initializing CYW43439 device")
}
dev := cyw43439.NewPicoWDevice()
cfg := cyw43439.DefaultBluetoothConfig()
if debug {
cfg.Logger = slog.New(slog.NewTextHandler(machine.USBCDC, &slog.HandlerOptions{
Level: slog.LevelDebug - 2,
}))
}
err := dev.Init(cfg)
if err != nil {
if debug {
println("Error initializing CYW43439 device", err.Error())
}
return err
}
transport := &hciSPI{dev: dev}
a.hci, a.att = newBLEStack(transport)
if debug {
println("Enabling CYW43439 device")
}
a.enable()
if debug {
println("Enabled CYW43439 device")
}
return nil
}
type hciSPI struct {
dev *cyw43439.Device
}
func (h *hciSPI) startRead() {
}
func (h *hciSPI) endRead() {
}
func (h *hciSPI) Buffered() int {
return h.dev.BufferedHCI()
}
func (h *hciSPI) ReadByte() (byte, error) {
var buf [1]byte
r, err := h.dev.HCIReadWriter()
if err != nil {
return 0, err
}
if _, err := r.Read(buf[:]); err != nil {
return 0, err
}
return buf[0], nil
}
func (h *hciSPI) Read(buf []byte) (int, error) {
r, err := h.dev.HCIReadWriter()
if err != nil {
return 0, err
}
return r.Read(buf)
}
func (h *hciSPI) Write(buf []byte) (int, error) {
w, err := h.dev.HCIReadWriter()
if err != nil {
return 0, err
}
return w.Write(buf)
}

View file

@ -141,32 +141,11 @@ func makeScanResult(prph cbgo.Peripheral, advFields cbgo.AdvFields, rssi int) Sc
serviceUUIDs = append(serviceUUIDs, parsedUUID)
}
var manufacturerData []ManufacturerDataElement
manufacturerData := make(map[uint16][]byte)
if len(advFields.ManufacturerData) > 2 {
// Note: CoreBluetooth seems to assume there can be only one
// manufacturer data fields in an advertisement packet, while the
// specification allows multiple such fields. See the Bluetooth Core
// Specification Supplement, table 1.1:
// https://www.bluetooth.com/specifications/css-11/
manufacturerID := uint16(advFields.ManufacturerData[0])
manufacturerID += uint16(advFields.ManufacturerData[1]) << 8
manufacturerData = append(manufacturerData, ManufacturerDataElement{
CompanyID: manufacturerID,
Data: advFields.ManufacturerData[2:],
})
}
var serviceData []ServiceDataElement
for _, svcData := range advFields.ServiceData {
cbgoUUID := svcData.UUID
uuid, err := ParseUUID(cbgoUUID.String())
if err != nil {
continue
}
serviceData = append(serviceData, ServiceDataElement{
UUID: uuid,
Data: svcData.Data,
})
manufacturerData[manufacturerID] = advFields.ManufacturerData[2:]
}
// Peripheral UUID is randomized on macOS, which means to
@ -181,7 +160,6 @@ func makeScanResult(prph cbgo.Peripheral, advFields cbgo.AdvFields, rssi int) Sc
LocalName: advFields.LocalName,
ServiceUUIDs: serviceUUIDs,
ManufacturerData: manufacturerData,
ServiceData: serviceData,
},
},
}

View file

@ -1,205 +0,0 @@
//go:build hci || ninafw || cyw43439
package bluetooth
import (
"runtime"
"time"
)
// hciAdapter represents the implementation for the connection to the HCI controller.
type hciAdapter struct {
hciport hciTransport
hci *hci
att *att
isDefault bool
scanning bool
connectHandler func(device Device, connected bool)
connectedDevices []Device
notificationsStarted bool
charWriteHandlers []charWriteHandler
}
func (a *hciAdapter) enable() error {
if err := a.hci.start(); err != nil {
if debug {
println("error starting HCI:", err.Error())
}
return err
}
if err := a.hci.reset(); err != nil {
if debug {
println("error resetting HCI:", err.Error())
}
return err
}
time.Sleep(150 * time.Millisecond)
if err := a.hci.setEventMask(0x3FFFFFFFFFFFFFFF); err != nil {
return err
}
return a.hci.setLeEventMask(0x00000000000003FF)
}
func (a *hciAdapter) Address() (MACAddress, error) {
if err := a.hci.readBdAddr(); err != nil {
return MACAddress{}, err
}
return MACAddress{MAC: makeAddress(a.hci.address)}, nil
}
func newBLEStack(port hciTransport) (*hci, *att) {
h := newHCI(port)
a := newATT(h)
h.att = a
l := newL2CAP(h)
h.l2cap = l
return h, a
}
// Convert a NINA MAC address into a Go MAC address.
func makeAddress(mac [6]uint8) MAC {
return MAC{
uint8(mac[0]),
uint8(mac[1]),
uint8(mac[2]),
uint8(mac[3]),
uint8(mac[4]),
uint8(mac[5]),
}
}
// Convert a Go MAC address into a NINA MAC Address.
func makeNINAAddress(mac MAC) [6]uint8 {
return [6]uint8{
uint8(mac[0]),
uint8(mac[1]),
uint8(mac[2]),
uint8(mac[3]),
uint8(mac[4]),
uint8(mac[5]),
}
}
func (a *hciAdapter) startNotifications() {
if a.notificationsStarted {
return
}
if debug {
println("starting notifications...")
}
a.notificationsStarted = true
// go routine to poll for HCI events for ATT notifications
go func() {
for {
if err := a.att.poll(); err != nil {
// TODO: handle error
if debug {
println("error polling for notifications:", err.Error())
}
}
time.Sleep(5 * time.Millisecond)
}
}()
// go routine to handle characteristic notifications
go func() {
for {
select {
case not := <-a.att.notifications:
if debug {
println("notification received", not.connectionHandle, not.handle, not.data)
}
d := a.findConnection(not.connectionHandle)
if d.deviceInternal == nil {
if debug {
println("no device found for handle", not.connectionHandle)
}
continue
}
n := d.findNotificationRegistration(not.handle)
if n == nil {
if debug {
println("no notification registered for handle", not.handle)
}
continue
}
if n.callback != nil {
n.callback(not.data)
}
default:
}
runtime.Gosched()
}
}()
}
func (a *hciAdapter) addConnection(d Device) {
a.connectedDevices = append(a.connectedDevices, d)
}
func (a *hciAdapter) removeConnection(d Device) {
for i := range a.connectedDevices {
if d.handle == a.connectedDevices[i].handle {
a.connectedDevices[i] = a.connectedDevices[len(a.connectedDevices)-1]
a.connectedDevices[len(a.connectedDevices)-1] = Device{}
a.connectedDevices = a.connectedDevices[:len(a.connectedDevices)-1]
return
}
}
}
func (a *hciAdapter) findConnection(handle uint16) Device {
for _, d := range a.connectedDevices {
if d.handle == handle {
if debug {
println("found device", handle, d.Address.String(), "with notifications registered", len(d.notificationRegistrations))
}
return d
}
}
return Device{}
}
// charWriteHandler contains a handler->callback mapping for characteristic
// writes.
type charWriteHandler struct {
handle uint16
callback func(connection Connection, offset int, value []byte)
}
// getCharWriteHandler returns a characteristic write handler if one matches the
// handle, or nil otherwise.
func (a *Adapter) getCharWriteHandler(handle uint16) *charWriteHandler {
for i := range a.charWriteHandlers {
h := &a.charWriteHandlers[i]
if h.handle == handle {
return h
}
}
return nil
}

View file

@ -1,122 +0,0 @@
//go:build hci && hci_uart
package bluetooth
import (
"machine"
)
const maxConnections = 1
// Adapter represents a "plain" UART connection to the HCI controller.
type Adapter struct {
hciAdapter
uart *machine.UART
// used for software flow control
cts, rts machine.Pin
}
// DefaultAdapter is the default adapter on the current system.
//
// Make sure to call Enable() before using it to initialize the adapter.
var DefaultAdapter = &Adapter{
hciAdapter: hciAdapter{
isDefault: true,
connectHandler: func(device Device, connected bool) {
return
},
connectedDevices: make([]Device, 0, maxConnections),
},
}
// SetUART sets the UART to use for the HCI connection.
// It must be called before calling Enable().
// Note that the UART must be configured with hardware flow control, or
// SetSoftwareFlowControl() must be called.
func (a *Adapter) SetUART(uart *machine.UART) error {
a.uart = uart
return nil
}
// SetSoftwareFlowControl sets the pins to use for software flow control,
// if hardware flow control is not available.
func (a *Adapter) SetSoftwareFlowControl(cts, rts machine.Pin) error {
a.cts = cts
a.rts = rts
return nil
}
// Enable configures the BLE stack. It must be called before any
// Bluetooth-related calls (unless otherwise indicated).
func (a *Adapter) Enable() error {
transport := &hciUART{uart: a.uart}
if a.cts != 0 && a.rts != 0 {
transport.rts = a.rts
a.rts.Configure(machine.PinConfig{Mode: machine.PinOutput})
a.rts.High()
transport.cts = a.cts
a.cts.Configure(machine.PinConfig{Mode: machine.PinInput})
}
a.hci, a.att = newBLEStack(transport)
a.enable()
return nil
}
type hciUART struct {
uart *machine.UART
// used for software flow control
cts, rts machine.Pin
}
func (h *hciUART) startRead() {
if h.rts != machine.NoPin {
h.rts.Low()
}
}
func (h *hciUART) endRead() {
if h.rts != machine.NoPin {
h.rts.High()
}
}
func (h *hciUART) Buffered() int {
return h.uart.Buffered()
}
func (h *hciUART) ReadByte() (byte, error) {
return h.uart.ReadByte()
}
func (h *hciUART) Read(buf []byte) (int, error) {
return h.uart.Read(buf)
}
const writeAttempts = 200
func (h *hciUART) Write(buf []byte) (int, error) {
if h.cts != machine.NoPin {
retries := writeAttempts
for h.cts.Get() {
retries--
if retries == 0 {
return 0, ErrHCITimeout
}
}
}
n, err := h.uart.Write(buf)
if err != nil {
return 0, err
}
return n, nil
}

View file

@ -4,27 +4,36 @@ package bluetooth
import (
"machine"
"runtime"
"time"
)
const maxConnections = 1
// Adapter represents the HCI connection to the NINA fw using the hardware UART.
// Adapter represents the UART connection to the NINA fw.
type Adapter struct {
hciAdapter
hci *hci
att *att
isDefault bool
scanning bool
connectHandler func(device Device, connected bool)
connectedDevices []Device
notificationsStarted bool
}
// DefaultAdapter is the default adapter on the current system.
//
// Make sure to call Enable() before using it to initialize the adapter.
var DefaultAdapter = &Adapter{
hciAdapter: hciAdapter{
isDefault: true,
connectHandler: func(device Device, connected bool) {
return
},
connectedDevices: make([]Device, 0, maxConnections),
isDefault: true,
connectHandler: func(device Device, connected bool) {
return
},
connectedDevices: make([]Device, 0, maxConnections),
}
// Enable configures the BLE stack. It must be called before any
@ -54,16 +63,76 @@ func (a *Adapter) Enable() error {
uart.Configure(cfg)
transport := &hciUART{uart: uart}
a.hci, a.att = newBLEStack(uart)
if machine.NINA_SOFT_FLOWCONTROL {
machine.NINA_RTS.Configure(machine.PinConfig{Mode: machine.PinOutput})
machine.NINA_RTS.High()
a.hci.softRTS = machine.NINA_RTS
a.hci.softRTS.Configure(machine.PinConfig{Mode: machine.PinOutput})
a.hci.softRTS.High()
a.hci.softCTS = machine.NINA_CTS
machine.NINA_CTS.Configure(machine.PinConfig{Mode: machine.PinInput})
}
a.hci, a.att = newBLEStack(transport)
return a.enable()
a.hci.start()
if err := a.hci.reset(); err != nil {
return err
}
time.Sleep(150 * time.Millisecond)
if err := a.hci.setEventMask(0x3FFFFFFFFFFFFFFF); err != nil {
return err
}
if err := a.hci.setLeEventMask(0x00000000000003FF); err != nil {
return err
}
return nil
}
func (a *Adapter) Address() (MACAddress, error) {
if err := a.hci.readBdAddr(); err != nil {
return MACAddress{}, err
}
return MACAddress{MAC: makeAddress(a.hci.address)}, nil
}
func newBLEStack(uart *machine.UART) (*hci, *att) {
h := newHCI(uart)
a := newATT(h)
h.att = a
l := newL2CAP(h)
h.l2cap = l
return h, a
}
// Convert a NINA MAC address into a Go MAC address.
func makeAddress(mac [6]uint8) MAC {
return MAC{
uint8(mac[0]),
uint8(mac[1]),
uint8(mac[2]),
uint8(mac[3]),
uint8(mac[4]),
uint8(mac[5]),
}
}
// Convert a Go MAC address into a NINA MAC Address.
func makeNINAAddress(mac MAC) [6]uint8 {
return [6]uint8{
uint8(mac[0]),
uint8(mac[1]),
uint8(mac[2]),
uint8(mac[3]),
uint8(mac[4]),
uint8(mac[5]),
}
}
func resetNINA() {
@ -84,51 +153,94 @@ func resetNINAInverted() {
time.Sleep(1000 * time.Millisecond)
}
type hciUART struct {
uart *machine.UART
}
func (h *hciUART) startRead() {
if machine.NINA_SOFT_FLOWCONTROL {
machine.NINA_RTS.Low()
func (a *Adapter) startNotifications() {
if a.notificationsStarted {
return
}
}
func (h *hciUART) endRead() {
if machine.NINA_SOFT_FLOWCONTROL {
machine.NINA_RTS.High()
if debug {
println("starting notifications...")
}
}
func (h *hciUART) Buffered() int {
return h.uart.Buffered()
}
a.notificationsStarted = true
func (h *hciUART) ReadByte() (byte, error) {
return h.uart.ReadByte()
}
func (h *hciUART) Read(buf []byte) (int, error) {
return h.uart.Read(buf)
}
const writeAttempts = 200
func (h *hciUART) Write(buf []byte) (int, error) {
if machine.NINA_SOFT_FLOWCONTROL {
retries := writeAttempts
for machine.NINA_CTS.Get() {
retries--
if retries == 0 {
return 0, ErrHCITimeout
// go routine to poll for HCI events for ATT notifications
go func() {
for {
if err := a.att.poll(); err != nil {
// TODO: handle error
if debug {
println("error polling for notifications:", err.Error())
}
}
time.Sleep(10 * time.Millisecond)
}
}()
// go routine to handle characteristic notifications
go func() {
for {
select {
case not := <-a.att.notifications:
if debug {
println("notification received", not.connectionHandle, not.handle, not.data)
}
d := a.findConnectedDevice(not.connectionHandle)
if d.deviceInternal == nil {
if debug {
println("no device found for handle", not.connectionHandle)
}
continue
}
n := d.findNotificationRegistration(not.handle)
if n == nil {
if debug {
println("no notification registered for handle", not.handle)
}
continue
}
if n.callback != nil {
n.callback(not.data)
}
default:
}
runtime.Gosched()
}
}()
}
func (a *Adapter) addDevice(d Device) {
a.connectedDevices = append(a.connectedDevices, d)
}
func (a *Adapter) removeDevice(d Device) {
for i := range a.connectedDevices {
if d.handle == a.connectedDevices[i].handle {
copy(a.connectedDevices[i:], a.connectedDevices[i+1:])
a.connectedDevices[len(a.connectedDevices)-1] = Device{} // the zero value of T
a.connectedDevices = a.connectedDevices[:len(a.connectedDevices)-1]
return
}
}
}
func (a *Adapter) findConnectedDevice(handle uint16) Device {
for _, d := range a.connectedDevices {
if d.handle == handle {
if debug {
println("found device", handle, d.Address.String(), "with notifications registered", len(d.notificationRegistrations))
}
return d
}
}
n, err := h.uart.Write(buf)
if err != nil {
return 0, err
}
return n, nil
return Device{}
}

View file

@ -1,7 +1,6 @@
package bluetooth
import (
"errors"
"fmt"
"github.com/go-ole/go-ole"
@ -14,8 +13,6 @@ type Adapter struct {
watcher *advertisement.BluetoothLEAdvertisementWatcher
connectHandler func(device Device, connected bool)
defaultAdvertisement *Advertisement
}
// DefaultAdapter is the default adapter on the system.
@ -59,8 +56,3 @@ func awaitAsyncOperation(asyncOperation *foundation.IAsyncOperation, genericPara
}
return nil
}
func (a *Adapter) Address() (MACAddress, error) {
// TODO: get mac address
return MACAddress{}, errors.New("not implemented")
}

View file

@ -1,4 +1,4 @@
//go:build hci || ninafw || cyw43439
//go:build ninafw
package bluetooth
@ -67,15 +67,12 @@ const (
)
var (
ErrATTTimeout = errors.New("bluetooth: ATT timeout")
ErrATTUnknownEvent = errors.New("bluetooth: ATT unknown event")
ErrATTUnknown = errors.New("bluetooth: ATT unknown error")
ErrATTOp = errors.New("bluetooth: ATT OP error")
ErrATTUnknownConnection = errors.New("bluetooth: ATT unknown connection")
ErrATTTimeout = errors.New("bluetooth: ATT timeout")
ErrATTUnknownEvent = errors.New("bluetooth: ATT unknown event")
ErrATTUnknown = errors.New("bluetooth: ATT unknown error")
ErrATTOp = errors.New("bluetooth: ATT OP error")
)
const defaultTimeoutSeconds = 10
type rawService struct {
startHandle uint16
endHandle uint16
@ -252,7 +249,9 @@ func (a *rawAttribute) length() int {
}
}
type connectData struct {
type att struct {
hci *hci
busy sync.Mutex
responded bool
errored bool
lastErrorOpcode uint8
@ -264,34 +263,26 @@ type connectData struct {
characteristics []rawCharacteristic
descriptors []rawDescriptor
value []byte
}
notifications chan rawNotification
type att struct {
hci *hci
busy sync.Mutex
mtu uint16
maxMTU uint16
notifications chan rawNotification
connections []uint16
connectionsData map[uint16]*connectData
lastHandle uint16
localServices []rawService
localCharacteristics []rawCharacteristic
attributes []rawAttribute
connections []uint16
lastHandle uint16
attributes []rawAttribute
localServices []rawService
}
func newATT(hci *hci) *att {
return &att{
hci: hci,
localCharacteristics: []rawCharacteristic{},
notifications: make(chan rawNotification, 32),
connections: []uint16{},
connectionsData: make(map[uint16]*connectData),
lastHandle: 0x0001,
attributes: []rawAttribute{},
localServices: []rawService{},
maxMTU: 248,
hci: hci,
services: []rawService{},
characteristics: []rawCharacteristic{},
value: []byte{},
notifications: make(chan rawNotification, 32),
connections: []uint16{},
lastHandle: 0x0001,
attributes: []rawAttribute{},
localServices: []rawService{},
maxMTU: 23,
}
}
@ -313,7 +304,7 @@ func (a *att) readByGroupReq(connectionHandle, startHandle, endHandle uint16, uu
return err
}
return a.waitUntilResponse(connectionHandle)
return a.waitUntilResponse()
}
func (a *att) readByTypeReq(connectionHandle, startHandle, endHandle uint16, typ uint16) error {
@ -334,7 +325,7 @@ func (a *att) readByTypeReq(connectionHandle, startHandle, endHandle uint16, typ
return err
}
return a.waitUntilResponse(connectionHandle)
return a.waitUntilResponse()
}
func (a *att) findInfoReq(connectionHandle, startHandle, endHandle uint16) error {
@ -354,7 +345,7 @@ func (a *att) findInfoReq(connectionHandle, startHandle, endHandle uint16) error
return err
}
return a.waitUntilResponse(connectionHandle)
return a.waitUntilResponse()
}
func (a *att) readReq(connectionHandle, valueHandle uint16) error {
@ -373,7 +364,7 @@ func (a *att) readReq(connectionHandle, valueHandle uint16) error {
return err
}
return a.waitUntilResponse(connectionHandle)
return a.waitUntilResponse()
}
func (a *att) writeCmd(connectionHandle, valueHandle uint16, data []byte) error {
@ -392,7 +383,7 @@ func (a *att) writeCmd(connectionHandle, valueHandle uint16, data []byte) error
return err
}
return nil
return a.waitUntilResponse()
}
func (a *att) writeReq(connectionHandle, valueHandle uint16, data []byte) error {
@ -411,31 +402,33 @@ func (a *att) writeReq(connectionHandle, valueHandle uint16, data []byte) error
return err
}
return a.waitUntilResponse(connectionHandle)
return a.waitUntilResponse()
}
func (a *att) mtuReq(connectionHandle uint16) error {
func (a *att) mtuReq(connectionHandle, mtu uint16) error {
if debug {
println("att.mtuReq:", connectionHandle)
}
cd, err := a.findConnectionData(connectionHandle)
if err != nil {
return err
println("att.mtuReq:", connectionHandle, mtu)
}
a.busy.Lock()
defer a.busy.Unlock()
if mtu > a.maxMTU {
mtu = a.maxMTU
}
// save mtu for connection
a.mtu = mtu
var b [3]byte
b[0] = attOpMTUReq
binary.LittleEndian.PutUint16(b[1:], cd.mtu)
binary.LittleEndian.PutUint16(b[1:], mtu)
if err := a.sendReq(connectionHandle, b[:]); err != nil {
return err
}
return a.waitUntilResponse(connectionHandle)
return a.waitUntilResponse()
}
func (a *att) setMaxMTU(mtu uint16) error {
@ -445,9 +438,7 @@ func (a *att) setMaxMTU(mtu uint16) error {
}
func (a *att) sendReq(handle uint16, data []byte) error {
if err := a.clearResponse(handle); err != nil {
return err
}
a.clearResponse()
if debug {
println("att.sendReq:", handle, "data:", hex.EncodeToString(data))
@ -486,9 +477,7 @@ func (a *att) sendNotification(handle uint16, data []byte) error {
}
func (a *att) sendError(handle uint16, opcode uint8, hdl uint16, code uint8) error {
if err := a.clearResponse(handle); err != nil {
return err
}
a.clearResponse()
if debug {
println("att.sendError:", handle, "data:", opcode, hdl, code)
@ -512,41 +501,26 @@ func (a *att) handleData(handle uint16, buf []byte) error {
println("att.handleData:", handle, "data:", hex.EncodeToString(buf))
}
cd, err := a.findConnectionData(handle)
if err != nil {
return err
}
switch buf[0] {
case attOpError:
cd.errored = true
cd.lastErrorOpcode = buf[1]
cd.lastErrorHandle = binary.LittleEndian.Uint16(buf[2:])
cd.lastErrorCode = buf[4]
a.errored = true
a.lastErrorOpcode = buf[1]
a.lastErrorHandle = binary.LittleEndian.Uint16(buf[2:])
a.lastErrorCode = buf[4]
if debug {
println("att.handleData: attOpERROR", handle, cd.lastErrorOpcode, cd.lastErrorCode)
println("att.handleData: attOpERROR", a.lastErrorOpcode, a.lastErrorCode)
}
return ErrATTOp
case attOpMTUReq:
if debug {
println("att.handleData: attOpMTUReq", hex.EncodeToString(buf))
println("att.handleData: attOpMTUReq")
}
mtu := binary.LittleEndian.Uint16(buf[1:])
if mtu > a.maxMTU {
mtu = a.maxMTU
}
// save mtu for connection
cd.mtu = mtu
var b [3]byte
b[0] = attOpMTUResponse
binary.LittleEndian.PutUint16(b[1:], mtu)
if err := a.hci.sendAclPkt(handle, attCID, b[:]); err != nil {
a.mtu = binary.LittleEndian.Uint16(buf[1:])
response := [3]byte{attOpMTUResponse, buf[1], buf[2]}
if err := a.hci.sendAclPkt(handle, attCID, response[:]); err != nil {
return err
}
@ -554,8 +528,8 @@ func (a *att) handleData(handle uint16, buf []byte) error {
if debug {
println("att.handleData: attOpMTUResponse")
}
cd.responded = true
cd.mtu = binary.LittleEndian.Uint16(buf[1:])
a.responded = true
a.mtu = binary.LittleEndian.Uint16(buf[1:])
case attOpFindInfoReq:
if debug {
@ -571,7 +545,7 @@ func (a *att) handleData(handle uint16, buf []byte) error {
if debug {
println("att.handleData: attOpFindInfoResponse")
}
cd.responded = true
a.responded = true
lengthPerDescriptor := int(buf[1])
@ -583,7 +557,7 @@ func (a *att) handleData(handle uint16, buf []byte) error {
println("att.handleData: descriptor", d.handle, hex.EncodeToString(d.data))
}
cd.descriptors = append(cd.descriptors, d)
a.descriptors = append(a.descriptors, d)
}
case attOpFindByTypeReq:
@ -606,7 +580,7 @@ func (a *att) handleData(handle uint16, buf []byte) error {
if debug {
println("att.handleData: attOpReadByTypeResponse")
}
cd.responded = true
a.responded = true
lengthPerCharacteristic := int(buf[1])
@ -618,7 +592,7 @@ func (a *att) handleData(handle uint16, buf []byte) error {
println("att.handleData: characteristic", c.startHandle, c.properties, c.valueHandle, c.uuid.String())
}
cd.characteristics = append(cd.characteristics, c)
a.characteristics = append(a.characteristics, c)
}
return nil
@ -638,7 +612,7 @@ func (a *att) handleData(handle uint16, buf []byte) error {
if debug {
println("att.handleData: attOpReadByGroupResponse")
}
cd.responded = true
a.responded = true
lengthPerService := int(buf[1])
@ -650,7 +624,7 @@ func (a *att) handleData(handle uint16, buf []byte) error {
println("att.handleData: service", service.startHandle, service.endHandle, service.uuid.String())
}
cd.services = append(cd.services, service)
a.services = append(a.services, service)
}
return nil
@ -672,8 +646,8 @@ func (a *att) handleData(handle uint16, buf []byte) error {
if debug {
println("att.handleData: attOpReadResponse")
}
cd.responded = true
cd.value = append(cd.value, buf[1:]...)
a.responded = true
a.value = append(a.value, buf[1:]...)
case attOpWriteReq:
if debug {
@ -692,7 +666,7 @@ func (a *att) handleData(handle uint16, buf []byte) error {
if debug {
println("att.handleData: attOpWriteResponse")
}
cd.responded = true
a.responded = true
case attOpPrepWriteReq:
if debug {
@ -821,7 +795,7 @@ func (a *att) handleReadByTypeReq(handle, start, end uint16, uuid shortUUID) err
pos = 2
response[1] = 0
for _, c := range a.localCharacteristics {
for _, c := range a.characteristics {
if debug {
println("handleReadByTypeReq: looking at characteristic", c.startHandle, c.uuid.String())
}
@ -1044,28 +1018,16 @@ func (a *att) handleWriteReq(handle, attrHandle uint16, data []byte) error {
return a.sendError(handle, attOpWriteReq, attrHandle, attErrorWriteNotPermitted)
}
func (a *att) clearResponse(handle uint16) error {
cd, err := a.findConnectionData(handle)
if err != nil {
return err
}
cd.responded = false
cd.errored = false
cd.lastErrorOpcode = 0
cd.lastErrorHandle = 0
cd.lastErrorCode = 0
cd.value = []byte{}
return nil
func (a *att) clearResponse() {
a.responded = false
a.errored = false
a.lastErrorOpcode = 0
a.lastErrorHandle = 0
a.lastErrorCode = 0
a.value = []byte{}
}
func (a *att) waitUntilResponse(handle uint16) error {
cd, err := a.findConnectionData(handle)
if err != nil {
return err
}
func (a *att) waitUntilResponse() error {
start := time.Now().UnixNano()
for {
if err := a.hci.poll(); err != nil {
@ -1073,15 +1035,16 @@ func (a *att) waitUntilResponse(handle uint16) error {
}
switch {
case cd.responded:
case a.responded:
return nil
case (time.Now().UnixNano()-start)/int64(time.Second) > defaultTimeoutSeconds:
return ErrATTTimeout
default:
// check for timeout
time.Sleep(5 * time.Millisecond)
if (time.Now().UnixNano()-start)/int64(time.Second) > 3 {
break
}
time.Sleep(100 * time.Millisecond)
}
}
@ -1100,28 +1063,15 @@ func (a *att) poll() error {
}
func (a *att) addConnection(handle uint16) error {
if debug {
println("att.addConnection:", handle)
}
a.connections = append(a.connections, handle)
a.connectionsData[handle] = &connectData{
services: []rawService{},
characteristics: []rawCharacteristic{},
value: []byte{},
}
return nil
}
func (a *att) removeConnection(handle uint16) error {
if debug {
println("att.removeConnection:", handle)
}
for i := range a.connections {
if a.connections[i] == handle {
a.connections = append(a.connections[:i], a.connections[i+1:]...)
delete(a.connectionsData, handle)
break
}
}
@ -1154,7 +1104,7 @@ func (a *att) addLocalService(start, end uint16, uuid UUID) {
}
func (a *att) addLocalCharacteristic(startHandle uint16, properties CharacteristicPermissions, valueHandle uint16, uuid UUID, chr *Characteristic) {
a.localCharacteristics = append(a.localCharacteristics,
a.characteristics = append(a.characteristics,
rawCharacteristic{
startHandle: startHandle,
properties: uint8(properties),
@ -1175,29 +1125,11 @@ func (a *att) findAttribute(hdl uint16) *rawAttribute {
}
func (a *att) findCharacteristic(hdl uint16) *rawCharacteristic {
for i := range a.localCharacteristics {
if a.localCharacteristics[i].startHandle == hdl {
return &a.localCharacteristics[i]
for i := range a.characteristics {
if a.characteristics[i].startHandle == hdl {
return &a.characteristics[i]
}
}
return nil
}
func (a *att) findConnectionData(handle uint16) (*connectData, error) {
cd, ok := a.connectionsData[handle]
if !ok {
return nil, ErrATTUnknownConnection
}
return cd, nil
}
func (a *att) lastError(handle uint16) (uint8, uint16, uint8) {
cd, err := a.findConnectionData(handle)
if err != nil {
return 0, 0, 0
}
return cd.lastErrorOpcode, cd.lastErrorHandle, cd.lastErrorCode
}

View file

@ -5,4 +5,4 @@
// those produced by Nordic Semiconductor.
//
// This package can be use to create Bluetooth Low Energy centrals as well as peripherals.
package bluetooth // import "gitrepo.ru/neonxp/bluetooth"
package bluetooth // import "tinygo.org/x/bluetooth"

View file

@ -3,7 +3,7 @@ package main
import (
"time"
"gitrepo.ru/neonxp/bluetooth"
"tinygo.org/x/bluetooth"
)
var adapter = bluetooth.DefaultAdapter
@ -13,9 +13,6 @@ func main() {
adv := adapter.DefaultAdvertisement()
must("config adv", adv.Configure(bluetooth.AdvertisementOptions{
LocalName: "Go Bluetooth",
ManufacturerData: []bluetooth.ManufacturerDataElement{
{CompanyID: 0xffff, Data: []byte{0x01, 0x02}},
},
}))
must("start adv", adv.Start())

View file

@ -8,7 +8,7 @@ import (
"machine"
"time"
"gitrepo.ru/neonxp/bluetooth"
"tinygo.org/x/bluetooth"
"tinygo.org/x/drivers/ws2812"
)

View file

@ -0,0 +1,87 @@
package main
import (
"fmt"
"image/color"
"machine"
"time"
"tinygo.org/x/bluetooth"
"tinygo.org/x/drivers/st7789"
"tinygo.org/x/tinyfont/proggy"
"tinygo.org/x/tinyterm"
)
var (
display st7789.Device
terminal = tinyterm.NewTerminal(&display)
black = color.RGBA{0, 0, 0, 255}
font = &proggy.TinySZ8pt7b
adapter = bluetooth.DefaultAdapter
)
func main() {
initDisplay()
time.Sleep(time.Second)
fmt.Fprintf(terminal, "\nenable interface...")
println("enable interface...")
must("enable BLE interface", adapter.Enable())
time.Sleep(time.Second)
println("start scan...")
fmt.Fprintf(terminal, "\nstart scan...")
must("start scan", adapter.Scan(scanHandler))
for {
time.Sleep(time.Minute)
println("scanning...")
fmt.Fprintf(terminal, "\nscanning...")
}
}
func scanHandler(adapter *bluetooth.Adapter, device bluetooth.ScanResult) {
println("device:", device.Address.String(), device.RSSI, device.LocalName())
fmt.Fprintf(terminal, "\n%s %d %s", device.Address.String(), device.RSSI, device.LocalName())
}
func must(action string, err error) {
if err != nil {
for {
println("failed to " + action + ": " + err.Error())
time.Sleep(time.Second)
}
}
}
func initDisplay() {
machine.SPI1.Configure(machine.SPIConfig{
Frequency: 8000000,
SCK: machine.TFT_SCK,
SDO: machine.TFT_SDO,
SDI: machine.TFT_SDO,
Mode: 0,
})
display = st7789.New(machine.SPI1,
machine.TFT_RESET,
machine.TFT_DC,
machine.TFT_CS,
machine.TFT_LITE)
display.Configure(st7789.Config{
Rotation: st7789.ROTATION_180,
Height: 320,
FrameRate: st7789.FRAMERATE_111,
VSyncLines: st7789.MAX_VSYNC_SCANLINES,
})
display.FillScreen(black)
terminal.Configure(&tinyterm.Config{
Font: font,
FontHeight: 10,
FontOffset: 6,
})
}

View file

@ -12,7 +12,7 @@ package main
import (
"time"
"gitrepo.ru/neonxp/bluetooth"
"tinygo.org/x/bluetooth"
)
var (

View file

@ -18,7 +18,7 @@ package main
import (
"strconv"
"gitrepo.ru/neonxp/bluetooth"
"tinygo.org/x/bluetooth"
)
var adapter = bluetooth.DefaultAdapter

View file

@ -24,7 +24,7 @@
package main
import (
"gitrepo.ru/neonxp/bluetooth"
"tinygo.org/x/bluetooth"
)
var (

View file

@ -4,7 +4,7 @@ import (
"math/rand"
"time"
"gitrepo.ru/neonxp/bluetooth"
"tinygo.org/x/bluetooth"
)
var adapter = bluetooth.DefaultAdapter

View file

@ -4,7 +4,7 @@ import (
"machine"
"time"
"gitrepo.ru/neonxp/bluetooth"
"tinygo.org/x/bluetooth"
)
var adapter = bluetooth.DefaultAdapter

146
examples/multiples/main.go Normal file
View file

@ -0,0 +1,146 @@
// This example scans and then connects to multiple Bluetooth peripherals
// that provide the Heart Rate Service (HRS).
//
// Once connected to all the desired devices, it subscribes to notifications.
//
// To run on bare metal microcontroller:
// tinygo flash -target metro-m4-airlift -ldflags="-X main.wanted=D9:2A:A1:5C:ED:56,4D:A1:3C:24:F0:46" -monitor ./examples/multiples/
//
// To run on OS:
// go run ./examples/multiples/ D9:2A:A1:5C:ED:56,64:0B:1D:46:D8:1D
package main
import (
"context"
"os"
"slices"
"time"
"tinygo.org/x/bluetooth"
)
var (
adapter = bluetooth.DefaultAdapter
heartRateServiceUUID = bluetooth.ServiceUUIDHeartRate
heartRateCharacteristicUUID = bluetooth.CharacteristicUUIDHeartRateMeasurement
exitCtx context.Context
)
func main() {
exitCtx = initExitHandler()
println("enabling")
// Enable BLE interface.
must("enable BLE stack", adapter.Enable())
scanResults := make(map[string]bluetooth.ScanResult)
finished := make(chan bool, 1)
searchList, _ := connectAddresses()
// Start scanning.
println("scanning...")
err := adapter.Scan(func(adapter *bluetooth.Adapter, result bluetooth.ScanResult) {
print(".")
// is the scanned device one of the ones we want?
if slices.Contains(searchList, result.Address.String()) {
if _, ok := scanResults[result.Address.String()]; !ok {
println(".")
println("found device:", result.Address.String(), result.RSSI, result.LocalName())
scanResults[result.Address.String()] = result
}
if len(scanResults) == len(searchList) {
println(".")
adapter.StopScan()
finished <- true
}
}
select {
case <-exitCtx.Done():
println("exiting.")
os.Exit(0)
default:
}
})
must("scan", err)
devices := []bluetooth.Device{}
select {
case <-time.After(5 * time.Second):
failMessage("timed out")
return
case <-exitCtx.Done():
println("exiting.")
return
case <-finished:
}
defer func() {
for _, device := range devices {
device.Disconnect()
}
}()
// now connect to all devices
for _, result := range scanResults {
device, err := adapter.Connect(result.Address, bluetooth.ConnectionParams{})
if err != nil {
failMessage(err.Error())
return
}
println("connected to", result.Address.String())
devices = append(devices, device)
}
// get services
println("discovering services/characteristics")
for _, device := range devices {
srvcs, err := device.DiscoverServices([]bluetooth.UUID{heartRateServiceUUID})
must("discover services", err)
if len(srvcs) == 0 {
failMessage("could not find heart rate service")
return
}
srvc := srvcs[0]
println("found service", srvc.UUID().String(), "for device", device.Address.String())
chars, err := srvc.DiscoverCharacteristics([]bluetooth.UUID{heartRateCharacteristicUUID})
if err != nil {
failMessage(err.Error())
return
}
if len(chars) == 0 {
failMessage("could not find heart rate characteristic")
return
}
char := chars[0]
addr := device.Address.String()
println("found characteristic", char.UUID().String(), "for device", addr)
char.EnableNotifications(func(buf []byte) {
println(addr, "data:", uint8(buf[1]))
})
}
// wait for exit
<-exitCtx.Done()
println("exiting.")
}
func must(action string, err error) {
if err != nil {
failMessage("failed to " + action + ": " + err.Error())
return
}
}

37
examples/multiples/mcu.go Normal file
View file

@ -0,0 +1,37 @@
//go:build baremetal
package main
import (
"context"
"errors"
"strings"
"time"
)
// Devices are the MAC addresses of the Bluetooth peripherals you want to connect to.
// Replace this by using -ldflags="-X main.Devices='[MAC ADDRESS],[MAC ADDRESS]'"
// where [MAC ADDRESS] is the actual MAC address of the peripheral.
// For example:
// tinygo flash -target nano-rp2040 -ldflags="-X main.Devices='7B:36:98:8C:41:1C,7B:36:98:8C:41:1D" ./examples/heartrate-monitor/
var Devices string
func initExitHandler() context.Context {
return context.Background()
}
func connectAddresses() ([]string, error) {
addrs := strings.Split(Devices, ",")
if len(addrs) == 0 {
return nil, errors.New("no devices specified")
}
return addrs, nil
}
func failMessage(msg string) {
for {
println(msg)
time.Sleep(1 * time.Second)
}
}

49
examples/multiples/os.go Normal file
View file

@ -0,0 +1,49 @@
//go:build !baremetal
package main
import (
"context"
"errors"
"os"
"os/signal"
"strings"
"syscall"
)
func initExitHandler() context.Context {
return contextWithSignal(context.Background())
}
// ContextWithSignal creates a context canceled when SIGINT or SIGTERM are notified
func contextWithSignal(ctx context.Context) context.Context {
newCtx, cancel := context.WithCancel(ctx)
signals := make(chan os.Signal)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
go func() {
select {
case <-signals:
cancel()
}
}()
return newCtx
}
func connectAddresses() ([]string, error) {
if len(os.Args) < 2 {
println("usage: multiples [address],[address]")
os.Exit(1)
}
addrs := strings.Split(os.Args[1], ",")
if len(addrs) == 0 {
return nil, errors.New("no devices specified")
}
return addrs, nil
}
func failMessage(msg string) {
println(msg)
exitCtx.Done()
}

View file

@ -4,8 +4,8 @@ package main
// details.
import (
"gitrepo.ru/neonxp/bluetooth"
"gitrepo.ru/neonxp/bluetooth/rawterm"
"tinygo.org/x/bluetooth"
"tinygo.org/x/bluetooth/rawterm"
)
var (

View file

@ -8,8 +8,8 @@ package main
// Code to interact with a raw terminal is in separate files with build tags.
import (
"gitrepo.ru/neonxp/bluetooth"
"gitrepo.ru/neonxp/bluetooth/rawterm"
"tinygo.org/x/bluetooth"
"tinygo.org/x/bluetooth/rawterm"
)
var (

View file

@ -1,7 +1,7 @@
package main
import (
"gitrepo.ru/neonxp/bluetooth"
"tinygo.org/x/bluetooth"
)
var adapter = bluetooth.DefaultAdapter

View file

@ -7,7 +7,7 @@ package main
import (
"time"
"gitrepo.ru/neonxp/bluetooth"
"tinygo.org/x/bluetooth"
)
var adapter = bluetooth.DefaultAdapter

View file

@ -1,48 +0,0 @@
//go:build clue
package main
import (
"machine"
"tinygo.org/x/drivers/st7789"
"tinygo.org/x/tinyfont/proggy"
"tinygo.org/x/tinyterm"
)
var (
font = &proggy.TinySZ8pt7b
)
func initTerminal() {
machine.SPI1.Configure(machine.SPIConfig{
Frequency: 8000000,
SCK: machine.TFT_SCK,
SDO: machine.TFT_SDO,
SDI: machine.TFT_SDO,
Mode: 0,
})
display := st7789.New(machine.SPI1,
machine.TFT_RESET,
machine.TFT_DC,
machine.TFT_CS,
machine.TFT_LITE)
display.Configure(st7789.Config{
Rotation: st7789.ROTATION_90,
//Height: 320,
FrameRate: st7789.FRAMERATE_111,
VSyncLines: st7789.MAX_VSYNC_SCANLINES,
})
display.FillScreen(black)
terminal = tinyterm.NewTerminal(&display)
terminal.Configure(&tinyterm.Config{
Font: font,
FontHeight: 10,
FontOffset: 6,
UseSoftwareScroll: true,
})
}

View file

@ -1,55 +0,0 @@
package main
import (
"fmt"
"image/color"
"time"
"gitrepo.ru/neonxp/bluetooth"
"tinygo.org/x/tinyterm"
)
var (
terminal *tinyterm.Terminal
black = color.RGBA{0, 0, 0, 255}
adapter = bluetooth.DefaultAdapter
)
func main() {
initTerminal()
terminalOutput("enable interface...")
must("enable BLE interface", adapter.Enable())
time.Sleep(time.Second)
terminalOutput("start scan...")
must("start scan", adapter.Scan(scanHandler))
for {
time.Sleep(time.Minute)
terminalOutput("scanning...")
}
}
func scanHandler(adapter *bluetooth.Adapter, device bluetooth.ScanResult) {
msg := fmt.Sprintf("%s %d %s", device.Address.String(), device.RSSI, device.LocalName())
terminalOutput(msg)
}
func must(action string, err error) {
if err != nil {
for {
terminalOutput("failed to " + action + ": " + err.Error())
time.Sleep(time.Second)
}
}
}
func terminalOutput(s string) {
println(s)
fmt.Fprintf(terminal, "\n%s", s)
}

View file

@ -1,39 +0,0 @@
//go:build pybadge
package main
import (
"machine"
"tinygo.org/x/drivers/st7735"
"tinygo.org/x/tinyfont"
"tinygo.org/x/tinyterm"
)
var (
font = &tinyfont.Picopixel
)
func initTerminal() {
machine.SPI1.Configure(machine.SPIConfig{
SCK: machine.SPI1_SCK_PIN,
SDO: machine.SPI1_SDO_PIN,
SDI: machine.SPI1_SDI_PIN,
Frequency: 8000000,
})
display := st7735.New(machine.SPI1, machine.TFT_RST, machine.TFT_DC, machine.TFT_CS, machine.TFT_LITE)
display.Configure(st7735.Config{
Rotation: st7735.ROTATION_90,
})
display.FillScreen(black)
terminal = tinyterm.NewTerminal(&display)
terminal.Configure(&tinyterm.Config{
Font: font,
FontHeight: 8,
FontOffset: 4,
UseSoftwareScroll: true,
})
}

View file

@ -1,45 +0,0 @@
//go:build pyportal
package main
import (
"machine"
"tinygo.org/x/drivers/ili9341"
"tinygo.org/x/tinyfont/proggy"
"tinygo.org/x/tinyterm"
)
var (
font = &proggy.TinySZ8pt7b
)
func initTerminal() {
display := ili9341.NewParallel(
machine.LCD_DATA0,
machine.TFT_WR,
machine.TFT_DC,
machine.TFT_CS,
machine.TFT_RESET,
machine.TFT_RD,
)
// configure backlight
backlight := machine.TFT_BACKLIGHT
backlight.Configure(machine.PinConfig{machine.PinOutput})
// configure display
display.Configure(ili9341.Config{})
display.SetRotation(ili9341.Rotation270)
display.FillScreen(black)
backlight.High()
terminal = tinyterm.NewTerminal(display)
terminal.Configure(&tinyterm.Config{
Font: font,
FontHeight: 10,
FontOffset: 6,
UseSoftwareScroll: true,
})
}

214
gap.go
View file

@ -55,34 +55,8 @@ type AdvertisementOptions struct {
Interval Duration
// ManufacturerData stores Advertising Data.
ManufacturerData []ManufacturerDataElement
// ServiceData stores Advertising Data.
ServiceData []ServiceDataElement
}
// Manufacturer data that's part of an advertisement packet.
type ManufacturerDataElement struct {
// The company ID, which must be one of the assigned company IDs.
// The full list is in here:
// https://www.bluetooth.com/specifications/assigned-numbers/
// The list can also be viewed here:
// https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/company_identifiers/company_identifiers.yaml
// The value 0xffff can also be used for testing.
CompanyID uint16
// The value, which can be any value but can't be very large.
Data []byte
}
// ServiceDataElement strores a uuid/byte-array pair used as ServiceData advertisment elements
type ServiceDataElement struct {
// Service UUID.
// The list can also be viewed here:
// https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/uuids/service_uuids.yaml
UUID UUID
// the data byte array
Data []byte
// Keys are the Manufacturer ID to associate with the data.
ManufacturerData map[uint16]interface{}
}
// Duration is the unit of time used in BLE, in 0.625µs units. This unit of time
@ -105,7 +79,7 @@ type ScanResult struct {
// Bluetooth address of the scanned device.
Address Address
// Signal strength of the advertisement packet.
// RSSI the last time a packet from this device has been received.
RSSI int16
// The data obtained from the advertisement data, which may contain many
@ -136,13 +110,9 @@ type AdvertisementPayload interface {
// if this data is not available.
Bytes() []byte
// ManufacturerData returns a slice with all the manufacturer data present in the
// advertising. It may be empty.
ManufacturerData() []ManufacturerDataElement
// ServiceData returns a slice with all the service data present in the
// advertising. It may be empty.
ServiceData() []ServiceDataElement
// ManufacturerData returns a map with all the manufacturer data present in the
//advertising. IT may be empty.
ManufacturerData() map[uint16][]byte
}
// AdvertisementFields contains advertisement fields in structured form.
@ -157,10 +127,7 @@ type AdvertisementFields struct {
ServiceUUIDs []UUID
// ManufacturerData is the manufacturer data of the advertisement.
ManufacturerData []ManufacturerDataElement
// ServiceData is the service data of the advertisement.
ServiceData []ServiceDataElement
ManufacturerData map[uint16][]byte
}
// advertisementFields wraps AdvertisementFields to implement the
@ -194,15 +161,10 @@ func (p *advertisementFields) Bytes() []byte {
}
// ManufacturerData returns the underlying ManufacturerData field.
func (p *advertisementFields) ManufacturerData() []ManufacturerDataElement {
func (p *advertisementFields) ManufacturerData() map[uint16][]byte {
return p.AdvertisementFields.ManufacturerData
}
// ServiceData returns the underlying ServiceData field.
func (p *advertisementFields) ServiceData() []ServiceDataElement {
return p.AdvertisementFields.ServiceData
}
// rawAdvertisementPayload encapsulates a raw advertisement packet. Methods to
// get the data (such as LocalName()) will parse just the needed field. Scanning
// the data should be fast as most advertisement packets only have a very small
@ -292,58 +254,22 @@ func (buf *rawAdvertisementPayload) HasServiceUUID(uuid UUID) bool {
}
// ManufacturerData returns the manufacturer data in the advertisement payload.
func (buf *rawAdvertisementPayload) ManufacturerData() []ManufacturerDataElement {
var manufacturerData []ManufacturerDataElement
for index := 0; index < int(buf.len)+4; index += int(buf.data[index]) + 1 {
fieldLength := int(buf.data[index+0])
if fieldLength < 3 {
continue
func (buf *rawAdvertisementPayload) ManufacturerData() map[uint16][]byte {
mData := make(map[uint16][]byte)
data := buf.Bytes()
for len(data) >= 2 {
fieldLength := data[0]
if int(fieldLength)+1 > len(data) {
// Invalid field length.
return nil
}
fieldType := buf.data[index+1]
if fieldType != 0xff {
continue
// If this is the manufacturer data
if byte(0xFF) == data[1] {
mData[uint16(data[2])+(uint16(data[3])<<8)] = data[4 : fieldLength+1]
}
key := uint16(buf.data[index+2]) | uint16(buf.data[index+3])<<8
manufacturerData = append(manufacturerData, ManufacturerDataElement{
CompanyID: key,
Data: buf.data[index+4 : index+fieldLength+1],
})
data = data[fieldLength+1:]
}
return manufacturerData
}
// ServiceData returns the service data in the advertisment payload
func (buf *rawAdvertisementPayload) ServiceData() []ServiceDataElement {
var serviceData []ServiceDataElement
for index := 0; index < int(buf.len)+4; index += int(buf.data[index]) + 1 {
fieldLength := int(buf.data[index+0])
if fieldLength < 3 { // field has only length and type and no data
continue
}
fieldType := buf.data[index+1]
switch fieldType {
case 0x16: // 16-bit uuid
serviceData = append(serviceData, ServiceDataElement{
UUID: New16BitUUID(uint16(buf.data[index+2]) + (uint16(buf.data[index+3]) << 8)),
Data: buf.data[index+4 : index+fieldLength+1],
})
case 0x20: // 32-bit uuid
serviceData = append(serviceData, ServiceDataElement{
UUID: New32BitUUID(uint32(buf.data[index+2]) + (uint32(buf.data[index+3]) << 8) + (uint32(buf.data[index+4]) << 16) + (uint32(buf.data[index+5]) << 24)),
Data: buf.data[index+6 : index+fieldLength+1],
})
case 0x21: // 128-bit uuid
var uuidArray [16]byte
copy(uuidArray[:], buf.data[index+2:index+18])
serviceData = append(serviceData, ServiceDataElement{
UUID: NewUUID(uuidArray),
Data: buf.data[index+18 : index+fieldLength+1],
})
default:
continue
}
}
return serviceData
return mData
}
// reset restores this buffer to the original state.
@ -374,88 +300,36 @@ func (buf *rawAdvertisementPayload) addFromOptions(options AdvertisementOptions)
}
}
for _, element := range options.ManufacturerData {
if !buf.addManufacturerData(element.CompanyID, element.Data) {
return false
}
}
for _, element := range options.ServiceData {
if !buf.addServiceData(element.UUID, element.Data) {
return false
}
if len(options.ManufacturerData) > 0 {
buf.addManufacturerData(options.ManufacturerData)
}
return true
}
// addManufacturerData adds manufacturer data ([]byte) entries to the advertisement payload.
func (buf *rawAdvertisementPayload) addManufacturerData(key uint16, value []byte) (ok bool) {
// Check whether the field can fit this manufacturer data.
fieldLength := len(value) + 4
if int(buf.len)+fieldLength > len(buf.data) {
return false
}
// Add the data.
buf.data[buf.len+0] = uint8(fieldLength - 1)
buf.data[buf.len+1] = 0xff
buf.data[buf.len+2] = uint8(key)
buf.data[buf.len+3] = uint8(key >> 8)
copy(buf.data[buf.len+4:], value)
buf.len += uint8(fieldLength)
return true
}
// addServiceData adds service data ([]byte) entries to the advertisement payload.
func (buf *rawAdvertisementPayload) addServiceData(uuid UUID, data []byte) (ok bool) {
switch {
case uuid.Is16Bit():
// check if it fits
fieldLength := 1 + 1 + 2 + len(data) // 1 byte length, 1 byte ad type, 2 bytes uuid, actual service data
if int(buf.len)+fieldLength > len(buf.data) {
return false
}
// Add the data.
buf.data[buf.len+0] = byte(fieldLength - 1)
buf.data[buf.len+1] = 0x16
buf.data[buf.len+2] = byte(uuid.Get16Bit())
buf.data[buf.len+3] = byte(uuid.Get16Bit() >> 8)
copy(buf.data[buf.len+4:], data)
buf.len += uint8(fieldLength)
case uuid.Is32Bit():
// check if it fits
fieldLength := 1 + 1 + 4 + len(data) // 1 byte length, 1 byte ad type, 4 bytes uuid, actual service data
if int(buf.len)+fieldLength > len(buf.data) {
return false
}
// Add the data.
buf.data[buf.len+0] = byte(fieldLength - 1)
buf.data[buf.len+1] = 0x20
buf.data[buf.len+2] = byte(uuid.Get32Bit())
buf.data[buf.len+3] = byte(uuid.Get32Bit() >> 8)
buf.data[buf.len+4] = byte(uuid.Get32Bit() >> 16)
buf.data[buf.len+5] = byte(uuid.Get32Bit() >> 24)
copy(buf.data[buf.len+6:], data)
buf.len += uint8(fieldLength)
default: // must be 128-bit uuid
// check if it fits
fieldLength := 1 + 1 + 16 + len(data) // 1 byte length, 1 byte ad type, 16 bytes uuid, actual service data
if int(buf.len)+fieldLength > len(buf.data) {
return false
}
// Add the data.
buf.data[buf.len+0] = byte(fieldLength - 1)
buf.data[buf.len+1] = 0x21
uuid_bytes := uuid.Bytes()
copy(buf.data[buf.len+2:], uuid_bytes[:])
copy(buf.data[buf.len+2+16:], data)
buf.len += uint8(fieldLength)
func (buf *rawAdvertisementPayload) addManufacturerData(manufacturerData map[uint16]interface{}) (ok bool) {
payloadData := buf.Bytes()
for manufacturerID, rawData := range manufacturerData {
data := rawData.([]byte)
// Check if the manufacturer ID is within the range of 16 bits (0-65535).
if manufacturerID > 0xFFFF {
// Invalid manufacturer ID.
return false
}
fieldLength := len(data) + 3
// Build manufacturer ID parts
manufacturerDataBit := byte(0xFF)
manufacturerIDPart1 := byte(manufacturerID & 0xFF)
manufacturerIDPart2 := byte((manufacturerID >> 8) & 0xFF)
payloadData = append(payloadData, byte(fieldLength), manufacturerDataBit, manufacturerIDPart1, manufacturerIDPart2)
payloadData = append(payloadData, data...)
}
buf.len = uint8(len(payloadData))
copy(buf.data[:], payloadData)
return true
}

View file

@ -53,29 +53,16 @@ func (a *Advertisement) Configure(options AdvertisementOptions) error {
for _, uuid := range options.ServiceUUIDs {
serviceUUIDs = append(serviceUUIDs, uuid.String())
}
var serviceData = make(map[string]interface{})
for _, element := range options.ServiceData {
serviceData[element.UUID.String()] = element.Data
}
// Convert map[uint16][]byte to map[uint16]any because that's what BlueZ needs.
manufacturerData := map[uint16]any{}
for _, element := range options.ManufacturerData {
manufacturerData[element.CompanyID] = element.Data
}
// Build an org.bluez.LEAdvertisement1 object, to be exported over DBus.
// See:
// https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/org.bluez.LEAdvertisement.rst
id := atomic.AddUint64(&advertisementID, 1)
a.path = dbus.ObjectPath(fmt.Sprintf("/org/tinygo/bluetooth/advertisement%d", id))
propsSpec := map[string]map[string]*prop.Prop{
"org.bluez.LEAdvertisement1": {
"Type": {Value: "broadcast"},
"ServiceUUIDs": {Value: serviceUUIDs},
"ManufacturerData": {Value: manufacturerData},
"ManufacturerData": {Value: options.ManufacturerData},
"LocalName": {Value: options.LocalName},
"ServiceData": {Value: serviceData},
// The documentation states:
// > Timeout of the advertisement in seconds. This defines the
// > lifetime of the advertisement.
@ -279,13 +266,10 @@ func makeScanResult(props map[string]dbus.Variant) ScanResult {
a := Address{MACAddress{MAC: addr}}
a.SetRandom(props["AddressType"].Value().(string) == "random")
var manufacturerData []ManufacturerDataElement
manufacturerData := make(map[uint16][]byte)
if mdata, ok := props["ManufacturerData"].Value().(map[uint16]dbus.Variant); ok {
for k, v := range mdata {
manufacturerData = append(manufacturerData, ManufacturerDataElement{
CompanyID: k,
Data: v.Value().([]byte),
})
manufacturerData[k] = v.Value().([]byte)
}
}
@ -293,20 +277,6 @@ func makeScanResult(props map[string]dbus.Variant) ScanResult {
localName, _ := props["Name"].Value().(string)
rssi, _ := props["RSSI"].Value().(int16)
var serviceData []ServiceDataElement
if sdata, ok := props["ServiceData"].Value().(map[string]dbus.Variant); ok {
for k, v := range sdata {
uuid, err := ParseUUID(k)
if err != nil {
continue
}
serviceData = append(serviceData, ServiceDataElement{
UUID: uuid,
Data: v.Value().([]byte),
})
}
}
return ScanResult{
RSSI: rssi,
Address: a,
@ -315,7 +285,6 @@ func makeScanResult(props map[string]dbus.Variant) ScanResult {
LocalName: localName,
ServiceUUIDs: serviceUUIDs,
ManufacturerData: manufacturerData,
ServiceData: serviceData,
},
},
}

View file

@ -1,4 +1,4 @@
//go:build hci || ninafw || cyw43439
//go:build ninafw
package bluetooth
@ -65,12 +65,8 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
switch t {
case 0x02, 0x03:
// 16-bit Service Class UUID
adf.ServiceUUIDs = append(adf.ServiceUUIDs, New16BitUUID(binary.LittleEndian.Uint16(a.hci.advData.eirData[i+2:i+4])))
case 0x06, 0x07:
// 128-bit Service Class UUID
var uuid [16]byte
copy(uuid[:], a.hci.advData.eirData[i+2:i+18])
adf.ServiceUUIDs = append(adf.ServiceUUIDs, NewUUID(uuid))
case 0x08, 0x09:
if debug {
println("local name", string(a.hci.advData.eirData[i+2:i+1+l]))
@ -100,7 +96,7 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
})
a.hci.clearAdvData()
time.Sleep(5 * time.Millisecond)
time.Sleep(10 * time.Millisecond)
default:
if !a.scanning {
@ -112,7 +108,7 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
lastUpdate = time.Now().UnixNano()
}
time.Sleep(5 * time.Millisecond)
time.Sleep(10 * time.Millisecond)
}
}
@ -182,7 +178,7 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, err
notificationRegistrations: make([]notificationRegistration, 0),
},
}
a.addConnection(d)
a.addDevice(d)
return d, nil
@ -192,7 +188,7 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, err
break
}
time.Sleep(5 * time.Millisecond)
time.Sleep(10 * time.Millisecond)
}
}
@ -232,7 +228,7 @@ func (d Device) Disconnect() error {
return err
}
d.adapter.removeConnection(d)
d.adapter.removeDevice(d)
return nil
}
@ -409,7 +405,7 @@ func (a *Advertisement) Start() error {
}
}
time.Sleep(5 * time.Millisecond)
time.Sleep(10 * time.Millisecond)
}
}()

View file

@ -1,7 +1,6 @@
package bluetooth
import (
"reflect"
"testing"
"time"
)
@ -56,64 +55,6 @@ func TestCreateAdvertisementPayload(t *testing.T) {
},
},
},
{
raw: "\x02\x01\x06" + // flags
"\a\xff\x34\x12asdf", // manufacturer data
parsed: AdvertisementOptions{
ManufacturerData: []ManufacturerDataElement{
{0x1234, []byte("asdf")},
},
},
},
{
raw: "\x02\x01\x06" + // flags
"\x04\xff\x34\x12\x05" + // manufacturer data 1
"\x05\xff\xff\xff\x03\x07" + // manufacturer data 2
"\x03\xff\x11\x00", // manufacturer data 3
parsed: AdvertisementOptions{
ManufacturerData: []ManufacturerDataElement{
{0x1234, []byte{5}},
{0xffff, []byte{3, 7}},
{0x0011, []byte{}},
},
},
},
{
raw: "\x02\x01\x06" + // flags
"\x05\x16\xD2\xFC\x40\x02" + // service data 16-Bit UUID
"\x06\x20\xD2\xFC\x40\x02\xC4", // service data 32-Bit UUID
parsed: AdvertisementOptions{
ServiceData: []ServiceDataElement{
{UUID: New16BitUUID(0xFCD2), Data: []byte{0x40, 0x02}},
{UUID: New32BitUUID(0x0240FCD2), Data: []byte{0xC4}},
},
},
},
{
raw: "\x02\x01\x06" + // flags
"\x05\x16\xD2\xFC\x40\x02" + // service data 16-Bit UUID
"\x05\x16\xD3\xFC\x40\x02", // service data 16-Bit UUID
parsed: AdvertisementOptions{
ServiceData: []ServiceDataElement{
{UUID: New16BitUUID(0xFCD2), Data: []byte{0x40, 0x02}},
{UUID: New16BitUUID(0xFCD3), Data: []byte{0x40, 0x02}},
},
},
},
{
raw: "\x02\x01\x06" + // flags
"\x04\x16\xD2\xFC\x40" + // service data 16-Bit UUID
"\x12\x21\xB8\x6C\x75\x05\xE9\x25\xBD\x93\xA8\x42\x32\xC3\x00\x01\xAF\xAD\x09", // service data 128-Bit UUID
parsed: AdvertisementOptions{
ServiceData: []ServiceDataElement{
{UUID: New16BitUUID(0xFCD2), Data: []byte{0x40}},
{
UUID: NewUUID([16]byte{0xad, 0xaf, 0x01, 0x00, 0xc3, 0x32, 0x42, 0xa8, 0x93, 0xbd, 0x25, 0xe9, 0x05, 0x75, 0x6c, 0xb8}),
Data: []byte{0x09},
},
},
},
},
}
for _, tc := range tests {
var expectedRaw rawAdvertisementPayload
@ -125,9 +66,5 @@ func TestCreateAdvertisementPayload(t *testing.T) {
if raw != expectedRaw {
t.Errorf("error when serializing options: %#v\nexpected: %#v\nactual: %#v\n", tc.parsed, tc.raw, string(raw.data[:raw.len]))
}
mdata := raw.ManufacturerData()
if !reflect.DeepEqual(mdata, tc.parsed.ManufacturerData) {
t.Errorf("ManufacturerData was not parsed as expected:\nexpected: %#v\nactual: %#v", tc.parsed.ManufacturerData, mdata)
}
}
}

View file

@ -18,108 +18,6 @@ type Address struct {
MACAddress
}
type Advertisement struct {
advertisement *advertisement.BluetoothLEAdvertisement
publisher *advertisement.BluetoothLEAdvertisementPublisher
}
// DefaultAdvertisement returns the default advertisement instance but does not
// configure it.
func (a *Adapter) DefaultAdvertisement() *Advertisement {
if a.defaultAdvertisement == nil {
a.defaultAdvertisement = &Advertisement{}
}
return a.defaultAdvertisement
}
// Configure this advertisement.
// on Windows we're only able to set "Manufacturer Data" for advertisements.
// https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothleadvertisementpublisher?view=winrt-22621#remarks
// following this c# source for this implementation: https://github.com/microsoft/Windows-universal-samples/blob/main/Samples/BluetoothAdvertisement/cs/Scenario2_Publisher.xaml.cs
// adding service data / localname leads to errors when starting the advertisement.
func (a *Advertisement) Configure(options AdvertisementOptions) error {
// we can only advertise manufacturer / company data on windows, so no need to continue if we have none
if len(options.ManufacturerData) == 0 {
return nil
}
if a.publisher != nil {
a.publisher.Release()
}
if a.advertisement != nil {
a.advertisement.Release()
}
pub, err := advertisement.NewBluetoothLEAdvertisementPublisher()
if err != nil {
return err
}
a.publisher = pub
ad, err := a.publisher.GetAdvertisement()
if err != nil {
return err
}
a.advertisement = ad
vec, err := ad.GetManufacturerData()
if err != nil {
return err
}
for _, optManData := range options.ManufacturerData {
writer, err := streams.NewDataWriter()
if err != nil {
return err
}
defer writer.Release()
err = writer.WriteBytes(uint32(len(optManData.Data)), optManData.Data)
if err != nil {
return err
}
buf, err := writer.DetachBuffer()
if err != nil {
return err
}
manData, err := advertisement.BluetoothLEManufacturerDataCreate(optManData.CompanyID, buf)
if err != nil {
return err
}
if err = vec.Append(unsafe.Pointer(&manData.IUnknown.RawVTable)); err != nil {
return err
}
}
return nil
}
// Start advertisement. May only be called after it has been configured.
func (a *Advertisement) Start() error {
// publisher will be present if we actually have manufacturer data to advertise.
if a.publisher != nil {
return a.publisher.Start()
}
return nil
}
// Stop advertisement. May only be called after it has been started.
func (a *Advertisement) Stop() error {
if a.publisher != nil {
return a.publisher.Stop()
}
return nil
}
// Scan starts a BLE scan. It is stopped by a call to StopScan. A common pattern
// is to cancel the scan when a particular device has been found.
func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) {
@ -170,25 +68,18 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) {
// Wait for when advertisement has stopped by a call to StopScan().
// Advertisement doesn't seem to stop right away, there is an
// intermediate Stopping state.
stoppingChan := make(chan error)
stoppingChan := make(chan struct{})
// TypedEventHandler<BluetoothLEAdvertisementWatcher, BluetoothLEAdvertisementWatcherStoppedEventArgs>
eventStoppedGuid := winrt.ParameterizedInstanceGUID(
foundation.GUIDTypedEventHandler,
advertisement.SignatureBluetoothLEAdvertisementWatcher,
advertisement.SignatureBluetoothLEAdvertisementWatcherStoppedEventArgs,
)
stoppedHandler := foundation.NewTypedEventHandler(ole.NewGUID(eventStoppedGuid), func(_ *foundation.TypedEventHandler, _, arg unsafe.Pointer) {
args := (*advertisement.BluetoothLEAdvertisementWatcherStoppedEventArgs)(arg)
errCode, err := args.GetError()
if err != nil {
// Got an error while getting the error value, that shouldn't
// happen.
stoppingChan <- fmt.Errorf("failed to get stopping error value: %w", err)
} else if errCode != bluetooth.BluetoothErrorSuccess {
// Could not stop the scan? I'm not sure when this would actually
// happen.
stoppingChan <- fmt.Errorf("failed to stop scanning (error code %d)", errCode)
}
stoppedHandler := foundation.NewTypedEventHandler(ole.NewGUID(eventStoppedGuid), func(_ *foundation.TypedEventHandler, _, _ unsafe.Pointer) {
// Note: the args parameter has an Error property that should
// probably be checked, but I'm not sure when stopping the
// advertisement watcher could ever result in an error (except
// for bugs).
close(stoppingChan)
})
defer stoppedHandler.Release()
@ -205,7 +96,8 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) {
}
// Wait until advertisement has stopped, and finish.
return <-stoppingChan
<-stoppingChan
return nil
}
func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedEventArgs) ScanResult {
@ -222,7 +114,7 @@ func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedE
Address: adr,
}
var manufacturerData []ManufacturerDataElement
var manufacturerData map[uint16][]byte = make(map[uint16][]byte)
if winAdv, err := args.GetAdvertisement(); err == nil && winAdv != nil {
vector, _ := winAdv.GetManufacturerData()
size, _ := vector.GetSize()
@ -231,10 +123,7 @@ func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedE
manData := (*advertisement.BluetoothLEManufacturerData)(element)
companyID, _ := manData.GetCompanyId()
buffer, _ := manData.GetData()
manufacturerData = append(manufacturerData, ManufacturerDataElement{
CompanyID: companyID,
Data: bufferToSlice(buffer),
})
manufacturerData[companyID] = bufferToSlice(buffer)
}
}
@ -252,7 +141,7 @@ func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedE
}
func bufferToSlice(buffer *streams.IBuffer) []byte {
dataReader, _ := streams.DataReaderFromBuffer(buffer)
dataReader, _ := streams.FromBuffer(buffer)
defer dataReader.Release()
bufferSize, _ := buffer.GetLength()
if bufferSize == 0 {
@ -274,8 +163,6 @@ func (a *Adapter) StopScan() error {
// Device is a connection to a remote peripheral.
type Device struct {
Address Address // the MAC address of the device
device *bluetooth.BluetoothLEDevice
session *genericattributeprofile.GattSession
}
@ -290,7 +177,7 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, err
}
// IAsyncOperation<BluetoothLEDevice>
bleDeviceOp, err := bluetooth.BluetoothLEDeviceFromBluetoothAddressAsync(winAddr)
bleDeviceOp, err := bluetooth.FromBluetoothAddressAsync(winAddr)
if err != nil {
return Device{}, err
}
@ -323,7 +210,7 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, err
// Windows does not support explicitly connecting to a device.
// Instead it has the concept of a GATT session that is owned
// by the calling program.
gattSessionOp, err := genericattributeprofile.GattSessionFromDeviceIdAsync(dID) // IAsyncOperation<GattSession>
gattSessionOp, err := genericattributeprofile.FromDeviceIdAsync(dID) // IAsyncOperation<GattSession>
if err != nil {
return Device{}, err
}
@ -342,7 +229,7 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, err
return Device{}, err
}
return Device{address, bleDevice, newSession}, nil
return Device{bleDevice, newSession}, nil
}
// Disconnect from the BLE device. This method is non-blocking and does not

View file

@ -1,4 +1,4 @@
//go:build hci || ninafw || cyw43439
//go:build ninafw
package bluetooth
@ -18,8 +18,8 @@ var (
)
const (
maxDefaultServicesToDiscover = 8
maxDefaultCharacteristicsToDiscover = 16
maxDefaultServicesToDiscover = 6
maxDefaultCharacteristicsToDiscover = 8
)
const (
@ -59,11 +59,6 @@ func (d Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
services := make([]DeviceService, 0, maxDefaultServicesToDiscover)
foundServices := make(map[UUID]DeviceService)
cd, err := d.adapter.att.findConnectionData(d.handle)
if err != nil {
return nil, err
}
startHandle := uint16(0x0001)
endHandle := uint16(0xffff)
for endHandle == uint16(0xffff) {
@ -73,14 +68,14 @@ func (d Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
}
if debug {
println("found services", len(cd.services))
println("found d.adapter.att.services", len(d.adapter.att.services))
}
if len(cd.services) == 0 {
if len(d.adapter.att.services) == 0 {
break
}
for _, rawService := range cd.services {
for _, rawService := range d.adapter.att.services {
if len(uuids) == 0 || rawService.uuid.isIn(uuids) {
foundServices[rawService.uuid] =
DeviceService{
@ -98,12 +93,7 @@ func (d Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
}
// reset raw services
cd.services = []rawService{}
// did we find them all?
if len(foundServices) == len(uuids) {
break
}
d.adapter.att.services = []rawService{}
}
switch {
@ -160,35 +150,30 @@ func (s DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteri
characteristics := make([]DeviceCharacteristic, 0, maxDefaultCharacteristicsToDiscover)
foundCharacteristics := make(map[UUID]DeviceCharacteristic)
cd, err := s.device.adapter.att.findConnectionData(s.device.handle)
if err != nil {
return nil, err
}
startHandle := s.startHandle
endHandle := s.endHandle
for startHandle < endHandle {
err := s.device.adapter.att.readByTypeReq(s.device.handle, startHandle, endHandle, gattCharacteristicUUID)
switch {
case err == ErrATTOp:
opcode, _, errcode := s.device.adapter.att.lastError(s.device.handle)
if opcode == attOpReadByTypeReq && errcode == attErrorAttrNotFound {
// no characteristics found
break
}
case err == ErrATTOp &&
s.device.adapter.att.lastErrorOpcode == attOpReadByTypeReq &&
s.device.adapter.att.lastErrorCode == attErrorAttrNotFound:
// no characteristics found
break
case err != nil:
return nil, err
}
if debug {
println("found characteristics", len(cd.characteristics))
println("found s.device.adapter.att.characteristics", len(s.device.adapter.att.characteristics))
}
if len(cd.characteristics) == 0 {
if len(s.device.adapter.att.characteristics) == 0 {
break
}
for _, rawCharacteristic := range cd.characteristics {
for _, rawCharacteristic := range s.device.adapter.att.characteristics {
if len(uuids) == 0 || rawCharacteristic.uuid.isIn(uuids) {
dc := DeviceCharacteristic{
service: &s,
@ -205,12 +190,7 @@ func (s DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteri
}
// reset raw characteristics
cd.characteristics = []rawCharacteristic{}
// did we find them all?
if len(foundCharacteristics) == len(uuids) {
break
}
s.device.adapter.att.characteristics = []rawCharacteristic{}
}
switch {
@ -294,7 +274,7 @@ func (c DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) err
// GetMTU returns the MTU for the characteristic.
func (c DeviceCharacteristic) GetMTU() (uint16, error) {
err := c.service.device.adapter.att.mtuReq(c.service.device.handle)
err := c.service.device.adapter.att.mtuReq(c.service.device.handle, c.service.device.mtu)
if err != nil {
return 0, err
}
@ -315,16 +295,11 @@ func (c DeviceCharacteristic) Read(data []byte) (int, error) {
return 0, err
}
cd, err := c.service.device.adapter.att.findConnectionData(c.service.device.handle)
if err != nil {
return 0, err
}
if len(cd.value) == 0 {
if len(c.service.device.adapter.att.value) == 0 {
return 0, errReadFailed
}
copy(data, cd.value)
copy(data, c.service.device.adapter.att.value)
return len(cd.value), nil
return len(c.service.device.adapter.att.value), nil
}

View file

@ -341,7 +341,7 @@ func (c DeviceCharacteristic) Read(data []byte) (int, error) {
return 0, err
}
datareader, err := streams.DataReaderFromBuffer(buffer)
datareader, err := streams.FromBuffer(buffer)
if err != nil {
return 0, err
}
@ -381,7 +381,7 @@ func (c DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) err
return
}
reader, err := streams.DataReaderFromBuffer(buf)
reader, err := streams.FromBuffer(buf)
if err != nil {
return
}

View file

@ -7,8 +7,6 @@ type Service struct {
Characteristics []CharacteristicConfig
}
type WriteEvent = func(client Connection, offset int, value []byte)
// CharacteristicConfig contains some parameters for the configuration of a
// single characteristic.
//
@ -19,7 +17,7 @@ type CharacteristicConfig struct {
UUID
Value []byte
Flags CharacteristicPermissions
WriteEvent WriteEvent
WriteEvent func(client Connection, offset int, value []byte)
}
// CharacteristicPermissions lists a number of basic permissions/capabilities

View file

@ -1,4 +1,4 @@
//go:build hci || ninafw || cyw43439
//go:build ninafw
package bluetooth
@ -53,16 +53,6 @@ func (a *Adapter) AddService(service *Service) error {
service.Characteristics[i].Handle.value = service.Characteristics[i].Value
}
if (service.Characteristics[i].Flags.Write() ||
service.Characteristics[i].Flags.WriteWithoutResponse()) &&
service.Characteristics[i].WriteEvent != nil {
handlers := append(a.charWriteHandlers, charWriteHandler{
handle: valueHandle,
callback: service.Characteristics[i].WriteEvent,
})
a.charWriteHandlers = handlers
}
if debug {
println("added characteristic", charHandle, valueHandle, service.Characteristics[i].UUID.String())
}
@ -81,17 +71,11 @@ func (a *Adapter) AddService(service *Service) error {
// Write replaces the characteristic value with a new value.
func (c *Characteristic) Write(p []byte) (n int, err error) {
if !(c.permissions.Write() || c.permissions.WriteWithoutResponse() ||
c.permissions.Notify() || c.permissions.Indicate()) {
return 0, errNoWrite
if !c.permissions.Notify() {
return 0, errNoNotify
}
hdl := c.adapter.getCharWriteHandler(c.handle)
if hdl != nil {
hdl.callback(Connection(c.handle), 0, p)
}
copy(c.value, p)
c.value = append([]byte{}, p...)
if c.cccd&0x01 != 0 {
// send notification

View file

@ -1,4 +1,4 @@
//go:build !linux && !windows
//go:build !linux
package bluetooth

View file

@ -150,7 +150,7 @@ func (c *Characteristic) Write(p []byte) (n int, err error) {
}
}
errCode := C.sd_ble_gatts_value_set_noescape(C.BLE_CONN_HANDLE_INVALID, c.handle, C.uint16_t(len(p)), (*C.uint8_t)(unsafe.Pointer(&p[0])))
errCode := C.sd_ble_gatts_value_set_noescape(C.BLE_CONN_HANDLE_INVALID, c.handle, C.uint16_t(len(p)), unsafe.SliceData(p))
if errCode != 0 {
return 0, Error(errCode)
}

View file

@ -1,309 +0,0 @@
package bluetooth
import (
"fmt"
"sync"
"syscall"
"unsafe"
"github.com/go-ole/go-ole"
"github.com/saltosystems/winrt-go"
"github.com/saltosystems/winrt-go/windows/devices/bluetooth/genericattributeprofile"
"github.com/saltosystems/winrt-go/windows/foundation"
"github.com/saltosystems/winrt-go/windows/foundation/collections"
"github.com/saltosystems/winrt-go/windows/storage/streams"
)
// Characteristic is a single characteristic in a service. It has an UUID and a
// value.
type Characteristic struct {
wintCharacteristic *genericattributeprofile.GattLocalCharacteristic
writeEvent WriteEvent
flags CharacteristicPermissions
valueMtx *sync.Mutex
value []byte
}
// AddService creates a new service with the characteristics listed in the
// Service struct.
func (a *Adapter) AddService(s *Service) error {
gattServiceOp, err := genericattributeprofile.GattServiceProviderCreateAsync(syscallUUIDFromUUID(s.UUID))
if err != nil {
return err
}
if err = awaitAsyncOperation(gattServiceOp, genericattributeprofile.SignatureGattServiceProviderResult); err != nil {
return err
}
res, err := gattServiceOp.GetResults()
if err != nil {
return err
}
serviceProviderResult := (*genericattributeprofile.GattServiceProviderResult)(res)
serviceProvider, err := serviceProviderResult.GetServiceProvider()
if err != nil {
return err
}
localService, err := serviceProvider.GetService()
if err != nil {
return err
}
// TODO: "ParameterizedInstanceGUID" + "foundation.NewTypedEventHandler"
// seems to always return the same instance, need to figure out how to get different instances each time...
// was following c# source for this flow: https://github.com/microsoft/Windows-universal-samples/blob/main/Samples/BluetoothLE/cs/Scenario3_ServerForeground.xaml.cs
// which relies on instanced event handlers. for now we'll manually setup our handlers with a map of golang characteristics
//
// TypedEventHandler<GattLocalCharacteristic,GattWriteRequestedEventArgs>
guid := winrt.ParameterizedInstanceGUID(
foundation.GUIDTypedEventHandler,
genericattributeprofile.SignatureGattLocalCharacteristic,
genericattributeprofile.SignatureGattWriteRequestedEventArgs)
goChars := map[syscall.GUID]*Characteristic{}
writeRequestedHandler := foundation.NewTypedEventHandler(ole.NewGUID(guid), func(instance *foundation.TypedEventHandler, sender, args unsafe.Pointer) {
writeReqArgs := (*genericattributeprofile.GattWriteRequestedEventArgs)(args)
reqAsyncOp, err := writeReqArgs.GetRequestAsync()
if err != nil {
return
}
if err = awaitAsyncOperation(reqAsyncOp, genericattributeprofile.SignatureGattWriteRequest); err != nil {
return
}
res, err := reqAsyncOp.GetResults()
if err != nil {
return
}
gattWriteRequest := (*genericattributeprofile.GattWriteRequest)(res)
buf, err := gattWriteRequest.GetValue()
if err != nil {
return
}
offset, err := gattWriteRequest.GetOffset()
if err != nil {
return
}
characteristic := (*genericattributeprofile.GattLocalCharacteristic)(sender)
uuid, err := characteristic.GetUuid()
if err != nil {
return
}
goChar, ok := goChars[uuid]
if !ok {
return
}
if goChar.writeEvent != nil {
// TODO: connection?
goChar.writeEvent(0, int(offset), bufferToSlice(buf))
}
})
guid = winrt.ParameterizedInstanceGUID(
foundation.GUIDTypedEventHandler,
genericattributeprofile.SignatureGattLocalCharacteristic,
genericattributeprofile.SignatureGattReadRequestedEventArgs)
readRequestedHandler := foundation.NewTypedEventHandler(ole.NewGUID(guid), func(instance *foundation.TypedEventHandler, sender, args unsafe.Pointer) {
readReqArgs := (*genericattributeprofile.GattReadRequestedEventArgs)(args)
reqAsyncOp, err := readReqArgs.GetRequestAsync()
if err != nil {
return
}
if err = awaitAsyncOperation(reqAsyncOp, genericattributeprofile.SignatureGattReadRequest); err != nil {
return
}
res, err := reqAsyncOp.GetResults()
if err != nil {
return
}
gattReadRequest := (*genericattributeprofile.GattReadRequest)(res)
characteristic := (*genericattributeprofile.GattLocalCharacteristic)(sender)
uuid, err := characteristic.GetUuid()
if err != nil {
return
}
goChar, ok := goChars[uuid]
if !ok {
return
}
writer, err := streams.NewDataWriter()
if err != nil {
return
}
defer writer.Release()
goChar.valueMtx.Lock()
defer goChar.valueMtx.Unlock()
if len(goChar.value) > 0 {
if err = writer.WriteBytes(uint32(len(goChar.value)), goChar.value); err != nil {
return
}
}
buf, err := writer.DetachBuffer()
if err != nil {
return
}
gattReadRequest.RespondWithValue(buf)
buf.Release()
})
for _, char := range s.Characteristics {
params, err := genericattributeprofile.NewGattLocalCharacteristicParameters()
if err != nil {
return err
}
if err = params.SetCharacteristicProperties(genericattributeprofile.GattCharacteristicProperties(char.Flags)); err != nil {
return err
}
uuid := syscallUUIDFromUUID(char.UUID)
createCharOp, err := localService.CreateCharacteristicAsync(uuid, params)
if err != nil {
return err
}
if err = awaitAsyncOperation(createCharOp, genericattributeprofile.SignatureGattLocalCharacteristicResult); err != nil {
return err
}
res, err := createCharOp.GetResults()
if err != nil {
return err
}
characteristicResults := (*genericattributeprofile.GattLocalCharacteristicResult)(res)
characteristic, err := characteristicResults.GetCharacteristic()
if err != nil {
return err
}
_, err = characteristic.AddWriteRequested(writeRequestedHandler)
if err != nil {
return err
}
_, err = characteristic.AddReadRequested(readRequestedHandler)
if err != nil {
return err
}
// Keep the object around for Characteristic.Write.
if char.Handle != nil {
char.Handle.wintCharacteristic = characteristic
char.Handle.value = char.Value
char.Handle.valueMtx = &sync.Mutex{}
char.Handle.flags = char.Flags
char.Handle.writeEvent = char.WriteEvent
goChars[uuid] = char.Handle
}
}
params, err := genericattributeprofile.NewGattServiceProviderAdvertisingParameters()
if err != nil {
return err
}
if err = params.SetIsConnectable(true); err != nil {
return err
}
if err = params.SetIsDiscoverable(true); err != nil {
return err
}
return serviceProvider.StartAdvertisingWithParameters(params)
}
// Write replaces the characteristic value with a new value.
func (c *Characteristic) Write(p []byte) (n int, err error) {
length := len(p)
if length == 0 {
return 0, nil // nothing to do
}
if c.writeEvent != nil {
c.writeEvent(0, 0, p)
}
// writes are only actually processed on read events from clients, we just set a variable here.
c.valueMtx.Lock()
defer c.valueMtx.Unlock()
c.value = p
// only notify if it's enabled, otherwise the below leads to an error
if c.flags&CharacteristicNotifyPermission != 0 {
writer, err := streams.NewDataWriter()
if err != nil {
return length, err
}
defer writer.Release()
err = writer.WriteBytes(uint32(len(p)), p)
if err != nil {
return length, err
}
buf, err := writer.DetachBuffer()
if err != nil {
return length, err
}
defer buf.Release()
op, err := c.wintCharacteristic.NotifyValueAsync(buf)
if err != nil {
return length, err
}
// IVectorView<GattClientNotificationResult>
signature := fmt.Sprintf("pinterface({%s};%s)", collections.GUIDIVectorView, genericattributeprofile.SignatureGattClientNotificationResult)
if err = awaitAsyncOperation(op, signature); err != nil {
return length, err
}
defer op.Release()
res, err := op.GetResults()
if err != nil {
return length, err
}
// TODO: process notification results, just getting this to release
vec := (*collections.IVectorView)(res)
vec.Release()
}
return length, nil
}
func syscallUUIDFromUUID(uuid UUID) syscall.GUID {
guid := ole.NewGUID(uuid.String())
return syscall.GUID{
Data1: guid.Data1,
Data2: guid.Data2,
Data3: guid.Data3,
Data4: guid.Data4,
}
}

10
go.mod
View file

@ -1,12 +1,11 @@
module gitrepo.ru/neonxp/bluetooth
module tinygo.org/x/bluetooth
go 1.20
go 1.18
require (
github.com/go-ole/go-ole v1.2.6
github.com/godbus/dbus/v5 v5.1.0
github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b
github.com/soypat/cyw43439 v0.0.0-20240609122733-da9153086796
github.com/saltosystems/winrt-go v0.0.0-20230921082907-2ab5b7d431e1
github.com/tinygo-org/cbgo v0.0.4
golang.org/x/crypto v0.12.0
tinygo.org/x/drivers v0.26.1-0.20230922160320-ed51435c2ef6
@ -17,9 +16,6 @@ require (
require (
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef // indirect
github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899 // indirect
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/term v0.11.0 // indirect
)

12
go.sum
View file

@ -10,27 +10,19 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b h1:du3zG5fd8snsFN6RBoLA7fpaYV9ZQIsyH9snlk2Zvik=
github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA=
github.com/saltosystems/winrt-go v0.0.0-20230921082907-2ab5b7d431e1 h1:L2YoWezgwpAZ2SEKjXk6yLnwOkM3u7mXq/mKuJeEpFM=
github.com/saltosystems/winrt-go v0.0.0-20230921082907-2ab5b7d431e1/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA=
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/soypat/cyw43439 v0.0.0-20240609122733-da9153086796 h1:1/r2URInjjFtWqT61gU7YGVCq3BRyXt/C7z4oLRF9Lo=
github.com/soypat/cyw43439 v0.0.0-20240609122733-da9153086796/go.mod h1:1Otjk6PRhfzfcVHeWMEeku/VntFqWghUwuSQyivb2vE=
github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef h1:phH95I9wANjTYw6bSYLZDQfNvao+HqYDom8owbNa0P4=
github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef/go.mod h1:oCVCNGCHMKoBj97Zp9znLbQ1nHxpkmOY9X+UAGzOxc8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q=
github.com/tinygo-org/cbgo v0.0.4 h1:3D76CRYbH03Rudi8sEgs/YO0x3JIMdyq8jlQtk/44fU=
github.com/tinygo-org/cbgo v0.0.4/go.mod h1:7+HgWIHd4nbAz0ESjGlJ1/v9LDU1Ox8MGzP9mah/fLk=
github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899 h1:/DyaXDEWMqoVUVEJVJIlNk1bXTbFs8s3Q4GdPInSKTQ=
github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899/go.mod h1:LU7Dw00NJ+N86QkeTGjMLNkYcEYMor6wTDpTCu0EaH8=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 h1:/yRP+0AN7mf5DkD3BAI6TOFnd51gEoDEb8o35jIFtgw=
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View file

@ -1,4 +1,4 @@
//go:build ninafw || hci || cyw43439
//go:build ninafw
package bluetooth
@ -6,6 +6,7 @@ import (
"encoding/binary"
"encoding/hex"
"errors"
"machine"
"time"
)
@ -66,11 +67,10 @@ const (
leMetaEventEnhancedConnectionComplete = 0x0A
leMetaEventDirectAdvertisingReport = 0x0B
hciCommandPkt = 0x01
hciACLDataPkt = 0x02
hciSynchronousDataPkt = 0x03
hciEventPkt = 0x04
hciSecurityPkt = 0x06
hciCommandPkt = 0x01
hciACLDataPkt = 0x02
hciEventPkt = 0x04
hciSecurityPkt = 0x06
evtDisconnComplete = 0x05
evtEncryptionChange = 0x08
@ -122,17 +122,10 @@ type leConnectData struct {
timeout uint16
}
type hciTransport interface {
startRead()
endRead()
Buffered() int
ReadByte() (byte, error)
Read(buf []byte) (int, error)
Write(buf []byte) (int, error)
}
type hci struct {
transport hciTransport
uart *machine.UART
softCTS machine.Pin
softRTS machine.Pin
att *att
l2cap *l2cap
buf []byte
@ -143,34 +136,27 @@ type hci struct {
scanning bool
advData leAdvertisingReport
connectData leConnectData
maxPkt uint16
pendingPkt uint16
maxPkt uint8
}
func newHCI(t hciTransport) *hci {
func newHCI(uart *machine.UART) *hci {
return &hci{
transport: t,
buf: make([]byte, 256),
uart: uart,
softCTS: machine.NoPin,
softRTS: machine.NoPin,
buf: make([]byte, 256),
}
}
func (h *hci) start() error {
h.transport.startRead()
defer h.transport.endRead()
if h.softRTS != machine.NoPin {
h.softRTS.Low()
var data [32]byte
for {
if i := h.transport.Buffered(); i > 0 {
if i > len(data) {
i = len(data)
}
if _, err := h.transport.Read(data[:i]); err != nil {
return err
}
defer h.softRTS.High()
}
continue
}
return nil
for h.uart.Buffered() > 0 {
h.uart.ReadByte()
}
return nil
@ -185,24 +171,22 @@ func (h *hci) reset() error {
}
func (h *hci) poll() error {
h.transport.startRead()
defer h.transport.endRead()
if h.softRTS != machine.NoPin {
h.softRTS.Low()
defer h.softRTS.High()
}
i := 0
for h.transport.Buffered() > 0 {
sz := h.transport.Buffered()
c := sz + 4 - (sz % 4)
_, err := h.transport.Read(h.buf[i : i+c])
if err != nil {
return err
}
i += sz
for h.uart.Buffered() > 0 {
data, _ := h.uart.ReadByte()
h.buf[i] = data
done, err := h.processPacket(i)
switch {
case err == ErrHCIUnknown || err == ErrHCIInvalidPacket || err == ErrHCIUnknownEvent:
if debug {
println("hci error:", err.Error(), hex.EncodeToString(h.buf[:i]))
println("hci error:", err.Error())
}
i = 0
time.Sleep(5 * time.Millisecond)
@ -217,6 +201,7 @@ func (h *hci) poll() error {
i = 0
time.Sleep(5 * time.Millisecond)
default:
i++
time.Sleep(1 * time.Millisecond)
}
}
@ -255,19 +240,9 @@ func (h *hci) processPacket(i int) (bool, error) {
}
}
case hciSynchronousDataPkt:
// not supported by BLE, so ignore
if i > 3 {
pktlen := int(h.buf[3])
if debug {
println("hci synchronous data:", i, pktlen, hex.EncodeToString(h.buf[:1+3+pktlen]))
}
return true, nil
}
default:
if debug {
println("unknown packet data:", hex.EncodeToString(h.buf[0:i]))
println("unknown packet data:", h.buf[0])
}
return true, ErrHCIUnknown
}
@ -303,7 +278,7 @@ func (h *hci) readLeBufferSize() error {
}
pktLen := binary.LittleEndian.Uint16(h.buf[0:])
h.maxPkt = uint16(h.buf[2])
h.maxPkt = h.buf[2]
// pkt len must be at least 27 bytes
if pktLen < 27 {
@ -506,13 +481,28 @@ func (h *hci) sendAclPkt(handle uint16, cid uint8, data []byte) error {
return err
}
h.pendingPkt++
return nil
}
const writeAttempts = 200
func (h *hci) write(buf []byte) (int, error) {
return h.transport.Write(buf)
if h.softCTS != machine.NoPin {
retries := writeAttempts
for h.softCTS.Get() {
retries--
if retries == 0 {
return 0, ErrHCITimeout
}
}
}
n, err := h.uart.Write(buf)
if err != nil {
return 0, err
}
return n, nil
}
type aclDataHeader struct {
@ -611,28 +601,8 @@ func (h *hci) handleEventData(buf []byte) error {
case evtNumCompPkts:
if debug {
println("evtNumCompPkts", hex.EncodeToString(buf))
println("evtNumCompPkts")
}
// count of handles
c := buf[2]
pkts := uint16(0)
for i := byte(0); i < c; i++ {
pkts += binary.LittleEndian.Uint16(buf[5+i*4:])
}
if pkts > 0 && h.pendingPkt > pkts {
h.pendingPkt -= pkts
} else {
h.pendingPkt = 0
}
if debug {
println("evtNumCompPkts", pkts, h.pendingPkt)
}
return nil
case evtLEMetaEvent:
if debug {
println("evtLEMetaEvent")
@ -752,10 +722,6 @@ func (h *hci) handleEventData(buf []byte) error {
return ErrHCIUnknownEvent
}
case evtHardwareError:
if debug {
println("evtHardwareError", hex.EncodeToString(buf))
}
return ErrHCIUnknownEvent
}

View file

@ -1,4 +1,4 @@
//go:build ninafw || hci || cyw43439
//go:build ninafw
package bluetooth
@ -77,7 +77,11 @@ func (l *l2cap) addConnection(handle uint16, role uint8, interval, timeout uint1
binary.LittleEndian.PutUint16(b[8:], 0)
binary.LittleEndian.PutUint16(b[10:], timeout)
return l.sendReq(handle, b[:])
if err := l.sendReq(handle, b[:]); err != nil {
return err
}
return nil
}
func (l *l2cap) removeConnection(handle uint16) error {
@ -152,5 +156,9 @@ func (l *l2cap) sendReq(handle uint16, data []byte) error {
println("l2cap.sendReq:", handle, "data:", hex.EncodeToString(data))
}
return l.hci.sendAclPkt(handle, signalingCID, data)
if err := l.hci.sendAclPkt(handle, signalingCID, data); err != nil {
return err
}
return nil
}

View file

@ -16,7 +16,7 @@ import (
"golang.org/x/text/cases"
"golang.org/x/text/language"
"gitrepo.ru/neonxp/bluetooth"
"tinygo.org/x/bluetooth"
)
type Characteristic struct {

View file

@ -16,7 +16,7 @@ import (
"golang.org/x/text/cases"
"golang.org/x/text/language"
"gitrepo.ru/neonxp/bluetooth"
"tinygo.org/x/bluetooth"
)
type Service struct {

22
uuid.go
View file

@ -38,20 +38,6 @@ func New16BitUUID(shortUUID uint16) UUID {
return uuid
}
// New32BitUUID returns a new 128-bit UUID based on a 32-bit UUID.
//
// Note: only use registered UUIDs. See
// https://www.bluetooth.com/specifications/gatt/services/ for a list.
func New32BitUUID(shortUUID uint32) UUID {
// https://stackoverflow.com/questions/36212020/how-can-i-convert-a-bluetooth-16-bit-service-uuid-into-a-128-bit-uuid
var uuid UUID
uuid[0] = 0x5F9B34FB
uuid[1] = 0x80000080
uuid[2] = 0x00001000
uuid[3] = shortUUID
return uuid
}
// Replace16BitComponent returns a new UUID where bits 16..32 have been replaced
// with the bits given in the argument. These bits are the same bits that vary
// in the 16-bit compressed UUID form.
@ -82,14 +68,6 @@ func (uuid UUID) Get16Bit() uint16 {
return uint16(uuid[3])
}
// Get32Bit returns the 32-bit version of this UUID. This is only valid if it
// actually is a 32-bit UUID, see Is32Bit.
func (uuid UUID) Get32Bit() uint32 {
// Note: using a Get* function as a getter because method names can't start
// with a number.
return uuid[3]
}
// Bytes returns a 16-byte array containing the raw UUID.
func (uuid UUID) Bytes() [16]byte {
buf := [16]byte{}

View file

@ -1,4 +1,4 @@
//go:build hci || ninafw || cyw43439
//go:build ninafw
package bluetooth

View file

@ -2,4 +2,4 @@ package bluetooth
// Version returns a user-readable string showing the version of the bluetooth package for support purposes.
// Update this value before release of new version of software.
const Version = "0.10.0"
const Version = "0.8.0"