Compare commits

...

28 commits

Author SHA1 Message Date
eae5a517c8
rename mod 2024-07-31 22:38:08 +03:00
deadprogram
a668e1b0a0 all: release 0.10
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-06-18 13:23:46 +02:00
deadprogram
b1081a9db1 docs: add mention of support for rp2040-W to README
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-06-11 10:06:22 +02:00
deadprogram
0d0c149a20 modules: update for cyw43439 HCI functionality
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-06-11 10:06:22 +02:00
deadprogram
457af7571a cyw43439: HCI implementation
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-06-11 10:06:22 +02:00
deadprogram
9905abd00e test: add hci_uart based implementation to smoke tests
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-06-07 08:41:27 +02:00
deadprogram
926aeb43f6 hci: refactor to separate HCI transport implementation from interface to not always assume UART.
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-06-07 08:41:27 +02:00
Carter Melnychuk
d46f2cc206
fix: assign char handle write event (#274) 2024-05-29 22:01:47 +02:00
Ayke van Laethem
c26c9d5630 gap: fix ServiceDataElement.UUID comment
I can't find anything that says the service UUID can also be a company
UUID (though the UUID number ranges don't overlap, so it could be
possible). Maybe I'm wrong, considering the size of the Bluetooth
specification I could easily have missed something.
2024-05-25 17:18:48 +02:00
Lars Gohr
abbb565ef0
Improve documentation of RSSI
Fixes https://github.com/tinygo-org/bluetooth/issues/272
2024-05-21 21:04:24 +02:00
Jagoba Gascón
348de057f8 winrt-go: bump to latest
This version fixes an error that leaked Go pointers to the WinRT runtime
causing random access violation errors (0xc0000005) whenver these
pointers where freed or moved by Go's GC. For more info checkout
saltosystems/winrt-go#94.

Diff:
45c2d7a623...4f7860a3bd
2024-05-10 11:36:17 +02:00
Carter Melnychuk
314ca89209
Winrt full support (#266)
windows: full functionality
2024-05-09 18:34:24 +02:00
Elara
6b08161955 Add Address field to Windows Device struct 2024-04-24 19:53:59 +02:00
deadprogram
12b6f0bc25 Prepare for release 0.9.0
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-03-24 18:30:45 +01:00
Jagoba Gascón
52c3c068e2 winrt: bump to latest
The latest version of winrt-go fixes an error that could cause function
name collisions for static methods. Diff:
43a71786fb...a2e4fc03f5
2024-03-21 20:00:59 +01:00
Ayke van Laethem
852fa4ab6a windows: check for error when scanning
This was my attempt to figure out why scanning doesn't work on my
system. Sadly it still doesn't work, but at least I know there's no
error in this place.

(Note: I'm doing this on Windows ARM in a VM, so it's a rather special
setup).
2024-03-21 06:46:36 +01:00
dnlwgnd
e0d5fd4c3a
add ServiceData advertising element (#243)
* gap: fix comment

* gap: expose ServiceData() in AdvertisementFields

* macos: include ServiceData in AdvertisementFields

* gap/linux: include ServiceData in AdvertisementFields

* gap: add unimplemented ServiceData() to raw advertisement

* added ServiceData advertising element also to the sending pieces

* more explicitly use the ad element type ids

* added a test case for ServiceData

* linux: added ServiceData advertising element

* sd: fix: handle no servicedata present

* linux: bluez uses string uuids for service data

* linux: fix: correct datatype for advertise with ServiceData

* uuid: add 32-Bit functions

* ServiceData now also uses a slice instead of a map as in #244

* Revert unnessesary changes

* formatting

* remove extra check

---------

Co-authored-by: William Johansson <radar@radhuset.org>
2024-03-18 22:15:09 +01:00
Ayke van Laethem
0087e0549b all: change ManufacturerData from a map to a slice
This is a breaking change, but I believe it is necessary for
correctness. Because maps have an undefined iteration order, the actual
advertised packet could change each time which I think is a bad thing.
In addition to that, using a slice should be much more lightweight than
using a map.

I've also added some tests (that should have been there in the first
place) and added some manufacturer data to the advertisement example.

Furthermore, I've optimized the code that constructs manufacturer data
for raw advertisement payloads, it should now be entirely free of heap
allocations.
2024-02-21 12:39:24 +01:00
deadprogram
d82232b16d modules: update to latest winrt-go package
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-02-21 12:20:19 +01:00
deadprogram
9a53d2a327 hci: implement Characteristic WriteHandler
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-02-02 00:01:51 -05:00
deadprogram
3e90718eb8 hci: multiple connections
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-02-01 22:03:41 -05:00
deadprogram
553633e56a examples: tinyscan to replace clue-scanner, also works on pyportal and pybadge+airlift
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-02-01 08:58:59 +01:00
deadprogram
b6fde65fd6 hci: return service UUIDs with scan results
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-27 00:16:09 -05:00
deadprogram
8e8dd34fc2 hci: allow for both ninafw and pure hci uart adapter implementations
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-26 07:31:00 -05:00
deadprogram
07a9e1d02e hci: several improvements and fixes including:
- 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

Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-23 21:38:07 -05:00
deadprogram
0a9bffe397 ninafw: should support muliple connections as a central
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-23 21:38:07 -05:00
deadprogram
00a475adf1 hci: add check for poll buffer overflow
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-23 21:38:07 -05:00
deadprogram
4c90cf4ab6 sd: replace unsafe.SliceData call with expression that is still supported in older Go versions
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-22 18:24:47 +01:00
48 changed files with 2156 additions and 495 deletions

View file

@ -1,3 +1,88 @@
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 0.8.0
--- ---

View file

@ -42,6 +42,12 @@ smoketest-tinygo:
@md5sum test.hex @md5sum test.hex
$(TINYGO) build -o test.uf2 -size=short -target=nano-rp2040 ./examples/advertisement $(TINYGO) build -o test.uf2 -size=short -target=nano-rp2040 ./examples/advertisement
@md5sum test.hex @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: smoketest-linux:
# Test on Linux. # Test on Linux.
@ -58,6 +64,8 @@ smoketest-windows:
GOOS=windows go build -o /tmp/go-build-discard ./examples/scanner 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/discover
GOOS=windows go build -o /tmp/go-build-discard ./examples/heartrate-monitor 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: smoketest-macos:
# Test on macos. # Test on macos.

View file

@ -2,13 +2,13 @@
[![Go Bluetooth](./images/gobluetooth.png)](https://tinygo.org/bluetooth) [![Go Bluetooth](./images/gobluetooth.png)](https://tinygo.org/bluetooth)
[![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) [![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)
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. 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 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/) by using [TinyGo](https://tinygo.org/). 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/).
The Go Bluetooth package can be used to create both Bluetooth Low Energy Centrals as well as to create Bluetooth Low Energy Peripherals. 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 package main
import ( import (
"tinygo.org/x/bluetooth" "gitrepo.ru/neonxp/bluetooth"
) )
var adapter = bluetooth.DefaultAdapter var adapter = bluetooth.DefaultAdapter
@ -58,7 +58,7 @@ package main
import ( import (
"time" "time"
"tinygo.org/x/bluetooth" "gitrepo.ru/neonxp/bluetooth"
) )
var adapter = bluetooth.DefaultAdapter var adapter = bluetooth.DefaultAdapter
@ -92,17 +92,17 @@ func must(action string, err error) {
## Current support ## Current support
| | Linux | macOS | Windows | Nordic Semi | ESP32 (NINA-FW) | | | Linux | macOS | Windows | Nordic Semi | ESP32 (NINA-FW) | CYW43439 (RP2040-W) |
| -------------------------------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | | -------------------------------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------- |
| API used | BlueZ | CoreBluetooth | WinRT | SoftDevice | HCI | | API used | BlueZ | CoreBluetooth | WinRT | SoftDevice | HCI | HCI |
| Scanning | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | 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: | | 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: | | 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: | | 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: | | 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: | | 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: | | 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: | | Send notifications | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
## Linux ## Linux
@ -286,6 +286,23 @@ 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 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 ## 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. **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.

116
adapter_cyw43439.go Normal file
View file

@ -0,0 +1,116 @@
//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,11 +141,32 @@ func makeScanResult(prph cbgo.Peripheral, advFields cbgo.AdvFields, rssi int) Sc
serviceUUIDs = append(serviceUUIDs, parsedUUID) serviceUUIDs = append(serviceUUIDs, parsedUUID)
} }
manufacturerData := make(map[uint16][]byte) var manufacturerData []ManufacturerDataElement
if len(advFields.ManufacturerData) > 2 { 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[0])
manufacturerID += uint16(advFields.ManufacturerData[1]) << 8 manufacturerID += uint16(advFields.ManufacturerData[1]) << 8
manufacturerData[manufacturerID] = advFields.ManufacturerData[2:] 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,
})
} }
// Peripheral UUID is randomized on macOS, which means to // Peripheral UUID is randomized on macOS, which means to
@ -160,6 +181,7 @@ func makeScanResult(prph cbgo.Peripheral, advFields cbgo.AdvFields, rssi int) Sc
LocalName: advFields.LocalName, LocalName: advFields.LocalName,
ServiceUUIDs: serviceUUIDs, ServiceUUIDs: serviceUUIDs,
ManufacturerData: manufacturerData, ManufacturerData: manufacturerData,
ServiceData: serviceData,
}, },
}, },
} }

205
adapter_hci.go Normal file
View file

@ -0,0 +1,205 @@
//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
}

122
adapter_hci_uart.go Normal file
View file

@ -0,0 +1,122 @@
//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,36 +4,27 @@ package bluetooth
import ( import (
"machine" "machine"
"runtime"
"time" "time"
) )
const maxConnections = 1 const maxConnections = 1
// Adapter represents the UART connection to the NINA fw. // Adapter represents the HCI connection to the NINA fw using the hardware UART.
type Adapter struct { type Adapter struct {
hci *hci hciAdapter
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. // DefaultAdapter is the default adapter on the current system.
// //
// Make sure to call Enable() before using it to initialize the adapter. // Make sure to call Enable() before using it to initialize the adapter.
var DefaultAdapter = &Adapter{ var DefaultAdapter = &Adapter{
hciAdapter: hciAdapter{
isDefault: true, isDefault: true,
connectHandler: func(device Device, connected bool) { connectHandler: func(device Device, connected bool) {
return return
}, },
connectedDevices: make([]Device, 0, maxConnections), connectedDevices: make([]Device, 0, maxConnections),
},
} }
// Enable configures the BLE stack. It must be called before any // Enable configures the BLE stack. It must be called before any
@ -63,73 +54,16 @@ func (a *Adapter) Enable() error {
uart.Configure(cfg) uart.Configure(cfg)
a.hci, a.att = newBLEStack(uart) transport := &hciUART{uart: uart}
if machine.NINA_SOFT_FLOWCONTROL { if machine.NINA_SOFT_FLOWCONTROL {
a.hci.softRTS = machine.NINA_RTS machine.NINA_RTS.Configure(machine.PinConfig{Mode: machine.PinOutput})
a.hci.softRTS.Configure(machine.PinConfig{Mode: machine.PinOutput}) machine.NINA_RTS.High()
a.hci.softRTS.High()
a.hci.softCTS = machine.NINA_CTS
machine.NINA_CTS.Configure(machine.PinConfig{Mode: machine.PinInput}) machine.NINA_CTS.Configure(machine.PinConfig{Mode: machine.PinInput})
} }
a.hci.start() a.hci, a.att = newBLEStack(transport)
return a.enable()
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
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() { func resetNINA() {
@ -150,78 +84,51 @@ func resetNINAInverted() {
time.Sleep(1000 * time.Millisecond) time.Sleep(1000 * time.Millisecond)
} }
func (a *Adapter) startNotifications() { type hciUART struct {
if a.notificationsStarted { uart *machine.UART
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(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.findDevice(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) findDevice(handle uint16) Device { func (h *hciUART) startRead() {
for _, d := range a.connectedDevices { if machine.NINA_SOFT_FLOWCONTROL {
if d.handle == handle { machine.NINA_RTS.Low()
if debug {
println("found device", handle, d.Address.String(), "with notifications registered", len(d.notificationRegistrations))
} }
}
return d
} func (h *hciUART) endRead() {
} if machine.NINA_SOFT_FLOWCONTROL {
machine.NINA_RTS.High()
return Device{} }
}
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 machine.NINA_SOFT_FLOWCONTROL {
retries := writeAttempts
for machine.NINA_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

@ -1,6 +1,7 @@
package bluetooth package bluetooth
import ( import (
"errors"
"fmt" "fmt"
"github.com/go-ole/go-ole" "github.com/go-ole/go-ole"
@ -13,6 +14,8 @@ type Adapter struct {
watcher *advertisement.BluetoothLEAdvertisementWatcher watcher *advertisement.BluetoothLEAdvertisementWatcher
connectHandler func(device Device, connected bool) connectHandler func(device Device, connected bool)
defaultAdvertisement *Advertisement
} }
// DefaultAdapter is the default adapter on the system. // DefaultAdapter is the default adapter on the system.
@ -56,3 +59,8 @@ func awaitAsyncOperation(asyncOperation *foundation.IAsyncOperation, genericPara
} }
return nil 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 ninafw //go:build hci || ninafw || cyw43439
package bluetooth package bluetooth
@ -12,9 +12,6 @@ import (
) )
const ( const (
attCID = 0x0004
bleCTL = 0x0008
attOpError = 0x01 attOpError = 0x01
attOpMTUReq = 0x02 attOpMTUReq = 0x02
attOpMTUResponse = 0x03 attOpMTUResponse = 0x03
@ -74,8 +71,11 @@ var (
ErrATTUnknownEvent = errors.New("bluetooth: ATT unknown event") ErrATTUnknownEvent = errors.New("bluetooth: ATT unknown event")
ErrATTUnknown = errors.New("bluetooth: ATT unknown error") ErrATTUnknown = errors.New("bluetooth: ATT unknown error")
ErrATTOp = errors.New("bluetooth: ATT OP error") ErrATTOp = errors.New("bluetooth: ATT OP error")
ErrATTUnknownConnection = errors.New("bluetooth: ATT unknown connection")
) )
const defaultTimeoutSeconds = 10
type rawService struct { type rawService struct {
startHandle uint16 startHandle uint16
endHandle uint16 endHandle uint16
@ -252,38 +252,46 @@ func (a *rawAttribute) length() int {
} }
} }
type att struct { type connectData struct {
hci *hci
busy sync.Mutex
responded bool responded bool
errored bool errored bool
lastErrorOpcode uint8 lastErrorOpcode uint8
lastErrorHandle uint16 lastErrorHandle uint16
lastErrorCode uint8 lastErrorCode uint8
mtu uint16 mtu uint16
maxMTU uint16
services []rawService services []rawService
characteristics []rawCharacteristic characteristics []rawCharacteristic
descriptors []rawDescriptor descriptors []rawDescriptor
value []byte value []byte
}
type att struct {
hci *hci
busy sync.Mutex
mtu uint16
maxMTU uint16
notifications chan rawNotification notifications chan rawNotification
connections []uint16 connections []uint16
connectionsData map[uint16]*connectData
lastHandle uint16 lastHandle uint16
attributes []rawAttribute
localServices []rawService localServices []rawService
localCharacteristics []rawCharacteristic
attributes []rawAttribute
} }
func newATT(hci *hci) *att { func newATT(hci *hci) *att {
return &att{ return &att{
hci: hci, hci: hci,
services: []rawService{}, localCharacteristics: []rawCharacteristic{},
characteristics: []rawCharacteristic{},
value: []byte{},
notifications: make(chan rawNotification, 32), notifications: make(chan rawNotification, 32),
connections: []uint16{}, connections: []uint16{},
connectionsData: make(map[uint16]*connectData),
lastHandle: 0x0001, lastHandle: 0x0001,
attributes: []rawAttribute{}, attributes: []rawAttribute{},
localServices: []rawService{}, localServices: []rawService{},
maxMTU: 248,
} }
} }
@ -305,7 +313,7 @@ func (a *att) readByGroupReq(connectionHandle, startHandle, endHandle uint16, uu
return err return err
} }
return a.waitUntilResponse() return a.waitUntilResponse(connectionHandle)
} }
func (a *att) readByTypeReq(connectionHandle, startHandle, endHandle uint16, typ uint16) error { func (a *att) readByTypeReq(connectionHandle, startHandle, endHandle uint16, typ uint16) error {
@ -326,7 +334,7 @@ func (a *att) readByTypeReq(connectionHandle, startHandle, endHandle uint16, typ
return err return err
} }
return a.waitUntilResponse() return a.waitUntilResponse(connectionHandle)
} }
func (a *att) findInfoReq(connectionHandle, startHandle, endHandle uint16) error { func (a *att) findInfoReq(connectionHandle, startHandle, endHandle uint16) error {
@ -346,7 +354,7 @@ func (a *att) findInfoReq(connectionHandle, startHandle, endHandle uint16) error
return err return err
} }
return a.waitUntilResponse() return a.waitUntilResponse(connectionHandle)
} }
func (a *att) readReq(connectionHandle, valueHandle uint16) error { func (a *att) readReq(connectionHandle, valueHandle uint16) error {
@ -365,7 +373,7 @@ func (a *att) readReq(connectionHandle, valueHandle uint16) error {
return err return err
} }
return a.waitUntilResponse() return a.waitUntilResponse(connectionHandle)
} }
func (a *att) writeCmd(connectionHandle, valueHandle uint16, data []byte) error { func (a *att) writeCmd(connectionHandle, valueHandle uint16, data []byte) error {
@ -384,7 +392,7 @@ func (a *att) writeCmd(connectionHandle, valueHandle uint16, data []byte) error
return err return err
} }
return a.waitUntilResponse() return nil
} }
func (a *att) writeReq(connectionHandle, valueHandle uint16, data []byte) error { func (a *att) writeReq(connectionHandle, valueHandle uint16, data []byte) error {
@ -403,30 +411,43 @@ func (a *att) writeReq(connectionHandle, valueHandle uint16, data []byte) error
return err return err
} }
return a.waitUntilResponse() return a.waitUntilResponse(connectionHandle)
} }
func (a *att) mtuReq(connectionHandle, mtu uint16) error { func (a *att) mtuReq(connectionHandle uint16) error {
if debug { if debug {
println("att.mtuReq:", connectionHandle) println("att.mtuReq:", connectionHandle)
} }
cd, err := a.findConnectionData(connectionHandle)
if err != nil {
return err
}
a.busy.Lock() a.busy.Lock()
defer a.busy.Unlock() defer a.busy.Unlock()
var b [3]byte var b [3]byte
b[0] = attOpMTUReq b[0] = attOpMTUReq
binary.LittleEndian.PutUint16(b[1:], mtu) binary.LittleEndian.PutUint16(b[1:], cd.mtu)
if err := a.sendReq(connectionHandle, b[:]); err != nil { if err := a.sendReq(connectionHandle, b[:]); err != nil {
return err return err
} }
return a.waitUntilResponse() return a.waitUntilResponse(connectionHandle)
}
func (a *att) setMaxMTU(mtu uint16) error {
a.maxMTU = mtu
return nil
} }
func (a *att) sendReq(handle uint16, data []byte) error { func (a *att) sendReq(handle uint16, data []byte) error {
a.clearResponse() if err := a.clearResponse(handle); err != nil {
return err
}
if debug { if debug {
println("att.sendReq:", handle, "data:", hex.EncodeToString(data)) println("att.sendReq:", handle, "data:", hex.EncodeToString(data))
@ -465,7 +486,9 @@ func (a *att) sendNotification(handle uint16, data []byte) error {
} }
func (a *att) sendError(handle uint16, opcode uint8, hdl uint16, code uint8) error { func (a *att) sendError(handle uint16, opcode uint8, hdl uint16, code uint8) error {
a.clearResponse() if err := a.clearResponse(handle); err != nil {
return err
}
if debug { if debug {
println("att.sendError:", handle, "data:", opcode, hdl, code) println("att.sendError:", handle, "data:", opcode, hdl, code)
@ -489,26 +512,41 @@ func (a *att) handleData(handle uint16, buf []byte) error {
println("att.handleData:", handle, "data:", hex.EncodeToString(buf)) println("att.handleData:", handle, "data:", hex.EncodeToString(buf))
} }
cd, err := a.findConnectionData(handle)
if err != nil {
return err
}
switch buf[0] { switch buf[0] {
case attOpError: case attOpError:
a.errored = true cd.errored = true
a.lastErrorOpcode = buf[1] cd.lastErrorOpcode = buf[1]
a.lastErrorHandle = binary.LittleEndian.Uint16(buf[2:]) cd.lastErrorHandle = binary.LittleEndian.Uint16(buf[2:])
a.lastErrorCode = buf[4] cd.lastErrorCode = buf[4]
if debug { if debug {
println("att.handleData: attOpERROR", a.lastErrorOpcode, a.lastErrorCode) println("att.handleData: attOpERROR", handle, cd.lastErrorOpcode, cd.lastErrorCode)
} }
return ErrATTOp return ErrATTOp
case attOpMTUReq: case attOpMTUReq:
if debug { if debug {
println("att.handleData: attOpMTUReq") println("att.handleData: attOpMTUReq", hex.EncodeToString(buf))
} }
a.mtu = binary.LittleEndian.Uint16(buf[1:]) mtu := binary.LittleEndian.Uint16(buf[1:])
response := [3]byte{attOpMTUResponse, buf[1], buf[2]} if mtu > a.maxMTU {
if err := a.hci.sendAclPkt(handle, attCID, response[:]); err != nil { 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 {
return err return err
} }
@ -516,8 +554,8 @@ func (a *att) handleData(handle uint16, buf []byte) error {
if debug { if debug {
println("att.handleData: attOpMTUResponse") println("att.handleData: attOpMTUResponse")
} }
a.responded = true cd.responded = true
a.mtu = binary.LittleEndian.Uint16(buf[1:]) cd.mtu = binary.LittleEndian.Uint16(buf[1:])
case attOpFindInfoReq: case attOpFindInfoReq:
if debug { if debug {
@ -533,7 +571,7 @@ func (a *att) handleData(handle uint16, buf []byte) error {
if debug { if debug {
println("att.handleData: attOpFindInfoResponse") println("att.handleData: attOpFindInfoResponse")
} }
a.responded = true cd.responded = true
lengthPerDescriptor := int(buf[1]) lengthPerDescriptor := int(buf[1])
@ -545,7 +583,7 @@ func (a *att) handleData(handle uint16, buf []byte) error {
println("att.handleData: descriptor", d.handle, hex.EncodeToString(d.data)) println("att.handleData: descriptor", d.handle, hex.EncodeToString(d.data))
} }
a.descriptors = append(a.descriptors, d) cd.descriptors = append(cd.descriptors, d)
} }
case attOpFindByTypeReq: case attOpFindByTypeReq:
@ -568,7 +606,7 @@ func (a *att) handleData(handle uint16, buf []byte) error {
if debug { if debug {
println("att.handleData: attOpReadByTypeResponse") println("att.handleData: attOpReadByTypeResponse")
} }
a.responded = true cd.responded = true
lengthPerCharacteristic := int(buf[1]) lengthPerCharacteristic := int(buf[1])
@ -580,7 +618,7 @@ func (a *att) handleData(handle uint16, buf []byte) error {
println("att.handleData: characteristic", c.startHandle, c.properties, c.valueHandle, c.uuid.String()) println("att.handleData: characteristic", c.startHandle, c.properties, c.valueHandle, c.uuid.String())
} }
a.characteristics = append(a.characteristics, c) cd.characteristics = append(cd.characteristics, c)
} }
return nil return nil
@ -600,7 +638,7 @@ func (a *att) handleData(handle uint16, buf []byte) error {
if debug { if debug {
println("att.handleData: attOpReadByGroupResponse") println("att.handleData: attOpReadByGroupResponse")
} }
a.responded = true cd.responded = true
lengthPerService := int(buf[1]) lengthPerService := int(buf[1])
@ -612,7 +650,7 @@ func (a *att) handleData(handle uint16, buf []byte) error {
println("att.handleData: service", service.startHandle, service.endHandle, service.uuid.String()) println("att.handleData: service", service.startHandle, service.endHandle, service.uuid.String())
} }
a.services = append(a.services, service) cd.services = append(cd.services, service)
} }
return nil return nil
@ -634,8 +672,8 @@ func (a *att) handleData(handle uint16, buf []byte) error {
if debug { if debug {
println("att.handleData: attOpReadResponse") println("att.handleData: attOpReadResponse")
} }
a.responded = true cd.responded = true
a.value = append(a.value, buf[1:]...) cd.value = append(cd.value, buf[1:]...)
case attOpWriteReq: case attOpWriteReq:
if debug { if debug {
@ -654,7 +692,7 @@ func (a *att) handleData(handle uint16, buf []byte) error {
if debug { if debug {
println("att.handleData: attOpWriteResponse") println("att.handleData: attOpWriteResponse")
} }
a.responded = true cd.responded = true
case attOpPrepWriteReq: case attOpPrepWriteReq:
if debug { if debug {
@ -783,7 +821,7 @@ func (a *att) handleReadByTypeReq(handle, start, end uint16, uuid shortUUID) err
pos = 2 pos = 2
response[1] = 0 response[1] = 0
for _, c := range a.characteristics { for _, c := range a.localCharacteristics {
if debug { if debug {
println("handleReadByTypeReq: looking at characteristic", c.startHandle, c.uuid.String()) println("handleReadByTypeReq: looking at characteristic", c.startHandle, c.uuid.String())
} }
@ -1006,16 +1044,28 @@ func (a *att) handleWriteReq(handle, attrHandle uint16, data []byte) error {
return a.sendError(handle, attOpWriteReq, attrHandle, attErrorWriteNotPermitted) return a.sendError(handle, attOpWriteReq, attrHandle, attErrorWriteNotPermitted)
} }
func (a *att) clearResponse() { func (a *att) clearResponse(handle uint16) error {
a.responded = false cd, err := a.findConnectionData(handle)
a.errored = false if err != nil {
a.lastErrorOpcode = 0 return err
a.lastErrorHandle = 0 }
a.lastErrorCode = 0
a.value = []byte{} cd.responded = false
cd.errored = false
cd.lastErrorOpcode = 0
cd.lastErrorHandle = 0
cd.lastErrorCode = 0
cd.value = []byte{}
return nil
} }
func (a *att) waitUntilResponse() error { func (a *att) waitUntilResponse(handle uint16) error {
cd, err := a.findConnectionData(handle)
if err != nil {
return err
}
start := time.Now().UnixNano() start := time.Now().UnixNano()
for { for {
if err := a.hci.poll(); err != nil { if err := a.hci.poll(); err != nil {
@ -1023,16 +1073,15 @@ func (a *att) waitUntilResponse() error {
} }
switch { switch {
case a.responded: case cd.responded:
return nil return nil
case (time.Now().UnixNano()-start)/int64(time.Second) > defaultTimeoutSeconds:
return ErrATTTimeout
default: default:
// check for timeout // check for timeout
if (time.Now().UnixNano()-start)/int64(time.Second) > 3 { time.Sleep(5 * time.Millisecond)
break
}
time.Sleep(100 * time.Millisecond)
} }
} }
@ -1050,17 +1099,34 @@ func (a *att) poll() error {
return nil return nil
} }
func (a *att) addConnection(handle uint16) { func (a *att) addConnection(handle uint16) error {
if debug {
println("att.addConnection:", handle)
}
a.connections = append(a.connections, 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) { func (a *att) removeConnection(handle uint16) error {
if debug {
println("att.removeConnection:", handle)
}
for i := range a.connections { for i := range a.connections {
if a.connections[i] == handle { if a.connections[i] == handle {
a.connections = append(a.connections[:i], a.connections[i+1:]...) a.connections = append(a.connections[:i], a.connections[i+1:]...)
return delete(a.connectionsData, handle)
break
} }
} }
return nil
} }
func (a *att) addLocalAttribute(typ attributeType, parent uint16, uuid UUID, permissions CharacteristicPermissions, value []byte) uint16 { func (a *att) addLocalAttribute(typ attributeType, parent uint16, uuid UUID, permissions CharacteristicPermissions, value []byte) uint16 {
@ -1088,7 +1154,7 @@ func (a *att) addLocalService(start, end uint16, uuid UUID) {
} }
func (a *att) addLocalCharacteristic(startHandle uint16, properties CharacteristicPermissions, valueHandle uint16, uuid UUID, chr *Characteristic) { func (a *att) addLocalCharacteristic(startHandle uint16, properties CharacteristicPermissions, valueHandle uint16, uuid UUID, chr *Characteristic) {
a.characteristics = append(a.characteristics, a.localCharacteristics = append(a.localCharacteristics,
rawCharacteristic{ rawCharacteristic{
startHandle: startHandle, startHandle: startHandle,
properties: uint8(properties), properties: uint8(properties),
@ -1109,11 +1175,29 @@ func (a *att) findAttribute(hdl uint16) *rawAttribute {
} }
func (a *att) findCharacteristic(hdl uint16) *rawCharacteristic { func (a *att) findCharacteristic(hdl uint16) *rawCharacteristic {
for i := range a.characteristics { for i := range a.localCharacteristics {
if a.characteristics[i].startHandle == hdl { if a.localCharacteristics[i].startHandle == hdl {
return &a.characteristics[i] return &a.localCharacteristics[i]
} }
} }
return nil 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. // those produced by Nordic Semiconductor.
// //
// This package can be use to create Bluetooth Low Energy centrals as well as peripherals. // This package can be use to create Bluetooth Low Energy centrals as well as peripherals.
package bluetooth // import "tinygo.org/x/bluetooth" package bluetooth // import "gitrepo.ru/neonxp/bluetooth"

View file

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

View file

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

View file

@ -1,87 +0,0 @@
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 ( import (
"time" "time"
"tinygo.org/x/bluetooth" "gitrepo.ru/neonxp/bluetooth"
) )
var ( var (

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

48
examples/tinyscan/clue.go Normal file
View file

@ -0,0 +1,48 @@
//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,
})
}

55
examples/tinyscan/main.go Normal file
View file

@ -0,0 +1,55 @@
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

@ -0,0 +1,39 @@
//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

@ -0,0 +1,45 @@
//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,
})
}

204
gap.go
View file

@ -55,8 +55,34 @@ type AdvertisementOptions struct {
Interval Duration Interval Duration
// ManufacturerData stores Advertising Data. // ManufacturerData stores Advertising Data.
// Keys are the Manufacturer ID to associate with the data. ManufacturerData []ManufacturerDataElement
ManufacturerData map[uint16]interface{}
// 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
} }
// Duration is the unit of time used in BLE, in 0.625µs units. This unit of time // Duration is the unit of time used in BLE, in 0.625µs units. This unit of time
@ -79,7 +105,7 @@ type ScanResult struct {
// Bluetooth address of the scanned device. // Bluetooth address of the scanned device.
Address Address Address Address
// RSSI the last time a packet from this device has been received. // Signal strength of the advertisement packet.
RSSI int16 RSSI int16
// The data obtained from the advertisement data, which may contain many // The data obtained from the advertisement data, which may contain many
@ -110,9 +136,13 @@ type AdvertisementPayload interface {
// if this data is not available. // if this data is not available.
Bytes() []byte Bytes() []byte
// ManufacturerData returns a map with all the manufacturer data present in the // ManufacturerData returns a slice with all the manufacturer data present in the
//advertising. IT may be empty. // advertising. It may be empty.
ManufacturerData() map[uint16][]byte ManufacturerData() []ManufacturerDataElement
// ServiceData returns a slice with all the service data present in the
// advertising. It may be empty.
ServiceData() []ServiceDataElement
} }
// AdvertisementFields contains advertisement fields in structured form. // AdvertisementFields contains advertisement fields in structured form.
@ -127,7 +157,10 @@ type AdvertisementFields struct {
ServiceUUIDs []UUID ServiceUUIDs []UUID
// ManufacturerData is the manufacturer data of the advertisement. // ManufacturerData is the manufacturer data of the advertisement.
ManufacturerData map[uint16][]byte ManufacturerData []ManufacturerDataElement
// ServiceData is the service data of the advertisement.
ServiceData []ServiceDataElement
} }
// advertisementFields wraps AdvertisementFields to implement the // advertisementFields wraps AdvertisementFields to implement the
@ -161,10 +194,15 @@ func (p *advertisementFields) Bytes() []byte {
} }
// ManufacturerData returns the underlying ManufacturerData field. // ManufacturerData returns the underlying ManufacturerData field.
func (p *advertisementFields) ManufacturerData() map[uint16][]byte { func (p *advertisementFields) ManufacturerData() []ManufacturerDataElement {
return p.AdvertisementFields.ManufacturerData 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 // rawAdvertisementPayload encapsulates a raw advertisement packet. Methods to
// get the data (such as LocalName()) will parse just the needed field. Scanning // 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 // the data should be fast as most advertisement packets only have a very small
@ -254,22 +292,58 @@ func (buf *rawAdvertisementPayload) HasServiceUUID(uuid UUID) bool {
} }
// ManufacturerData returns the manufacturer data in the advertisement payload. // ManufacturerData returns the manufacturer data in the advertisement payload.
func (buf *rawAdvertisementPayload) ManufacturerData() map[uint16][]byte { func (buf *rawAdvertisementPayload) ManufacturerData() []ManufacturerDataElement {
mData := make(map[uint16][]byte) var manufacturerData []ManufacturerDataElement
data := buf.Bytes() for index := 0; index < int(buf.len)+4; index += int(buf.data[index]) + 1 {
for len(data) >= 2 { fieldLength := int(buf.data[index+0])
fieldLength := data[0] if fieldLength < 3 {
if int(fieldLength)+1 > len(data) { continue
// Invalid field length.
return nil
} }
// If this is the manufacturer data fieldType := buf.data[index+1]
if byte(0xFF) == data[1] { if fieldType != 0xff {
mData[uint16(data[2])+(uint16(data[3])<<8)] = data[4 : fieldLength+1] continue
} }
data = data[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],
})
} }
return mData 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
} }
// reset restores this buffer to the original state. // reset restores this buffer to the original state.
@ -300,36 +374,88 @@ func (buf *rawAdvertisementPayload) addFromOptions(options AdvertisementOptions)
} }
} }
if len(options.ManufacturerData) > 0 { for _, element := range options.ManufacturerData {
buf.addManufacturerData(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
}
} }
return true return true
} }
// addManufacturerData adds manufacturer data ([]byte) entries to the advertisement payload. // addManufacturerData adds manufacturer data ([]byte) entries to the advertisement payload.
func (buf *rawAdvertisementPayload) addManufacturerData(manufacturerData map[uint16]interface{}) (ok bool) { func (buf *rawAdvertisementPayload) addManufacturerData(key uint16, value []byte) (ok bool) {
payloadData := buf.Bytes() // Check whether the field can fit this manufacturer data.
for manufacturerID, rawData := range manufacturerData { fieldLength := len(value) + 4
data := rawData.([]byte) if int(buf.len)+fieldLength > len(buf.data) {
// Check if the manufacturer ID is within the range of 16 bits (0-65535).
if manufacturerID > 0xFFFF {
// Invalid manufacturer ID.
return false return false
} }
fieldLength := len(data) + 3 // 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)
// Build manufacturer ID parts return true
manufacturerDataBit := byte(0xFF) }
manufacturerIDPart1 := byte(manufacturerID & 0xFF)
manufacturerIDPart2 := byte((manufacturerID >> 8) & 0xFF) // 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)
payloadData = append(payloadData, byte(fieldLength), manufacturerDataBit, manufacturerIDPart1, manufacturerIDPart2)
payloadData = append(payloadData, data...)
} }
buf.len = uint8(len(payloadData))
copy(buf.data[:], payloadData)
return true return true
} }

View file

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

View file

@ -53,16 +53,29 @@ func (a *Advertisement) Configure(options AdvertisementOptions) error {
for _, uuid := range options.ServiceUUIDs { for _, uuid := range options.ServiceUUIDs {
serviceUUIDs = append(serviceUUIDs, uuid.String()) 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. // 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) id := atomic.AddUint64(&advertisementID, 1)
a.path = dbus.ObjectPath(fmt.Sprintf("/org/tinygo/bluetooth/advertisement%d", id)) a.path = dbus.ObjectPath(fmt.Sprintf("/org/tinygo/bluetooth/advertisement%d", id))
propsSpec := map[string]map[string]*prop.Prop{ propsSpec := map[string]map[string]*prop.Prop{
"org.bluez.LEAdvertisement1": { "org.bluez.LEAdvertisement1": {
"Type": {Value: "broadcast"}, "Type": {Value: "broadcast"},
"ServiceUUIDs": {Value: serviceUUIDs}, "ServiceUUIDs": {Value: serviceUUIDs},
"ManufacturerData": {Value: options.ManufacturerData}, "ManufacturerData": {Value: manufacturerData},
"LocalName": {Value: options.LocalName}, "LocalName": {Value: options.LocalName},
"ServiceData": {Value: serviceData},
// The documentation states: // The documentation states:
// > Timeout of the advertisement in seconds. This defines the // > Timeout of the advertisement in seconds. This defines the
// > lifetime of the advertisement. // > lifetime of the advertisement.
@ -266,10 +279,13 @@ func makeScanResult(props map[string]dbus.Variant) ScanResult {
a := Address{MACAddress{MAC: addr}} a := Address{MACAddress{MAC: addr}}
a.SetRandom(props["AddressType"].Value().(string) == "random") a.SetRandom(props["AddressType"].Value().(string) == "random")
manufacturerData := make(map[uint16][]byte) var manufacturerData []ManufacturerDataElement
if mdata, ok := props["ManufacturerData"].Value().(map[uint16]dbus.Variant); ok { if mdata, ok := props["ManufacturerData"].Value().(map[uint16]dbus.Variant); ok {
for k, v := range mdata { for k, v := range mdata {
manufacturerData[k] = v.Value().([]byte) manufacturerData = append(manufacturerData, ManufacturerDataElement{
CompanyID: k,
Data: v.Value().([]byte),
})
} }
} }
@ -277,6 +293,20 @@ func makeScanResult(props map[string]dbus.Variant) ScanResult {
localName, _ := props["Name"].Value().(string) localName, _ := props["Name"].Value().(string)
rssi, _ := props["RSSI"].Value().(int16) 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{ return ScanResult{
RSSI: rssi, RSSI: rssi,
Address: a, Address: a,
@ -285,6 +315,7 @@ func makeScanResult(props map[string]dbus.Variant) ScanResult {
LocalName: localName, LocalName: localName,
ServiceUUIDs: serviceUUIDs, ServiceUUIDs: serviceUUIDs,
ManufacturerData: manufacturerData, ManufacturerData: manufacturerData,
ServiceData: serviceData,
}, },
}, },
} }

View file

@ -1,6 +1,7 @@
package bluetooth package bluetooth
import ( import (
"reflect"
"testing" "testing"
"time" "time"
) )
@ -55,6 +56,64 @@ 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 { for _, tc := range tests {
var expectedRaw rawAdvertisementPayload var expectedRaw rawAdvertisementPayload
@ -66,5 +125,9 @@ func TestCreateAdvertisementPayload(t *testing.T) {
if raw != expectedRaw { if raw != expectedRaw {
t.Errorf("error when serializing options: %#v\nexpected: %#v\nactual: %#v\n", tc.parsed, tc.raw, string(raw.data[:raw.len])) 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,6 +18,108 @@ type Address struct {
MACAddress 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 // 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. // is to cancel the scan when a particular device has been found.
func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) { func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) {
@ -68,18 +170,25 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) {
// Wait for when advertisement has stopped by a call to StopScan(). // Wait for when advertisement has stopped by a call to StopScan().
// Advertisement doesn't seem to stop right away, there is an // Advertisement doesn't seem to stop right away, there is an
// intermediate Stopping state. // intermediate Stopping state.
stoppingChan := make(chan struct{}) stoppingChan := make(chan error)
// TypedEventHandler<BluetoothLEAdvertisementWatcher, BluetoothLEAdvertisementWatcherStoppedEventArgs> // TypedEventHandler<BluetoothLEAdvertisementWatcher, BluetoothLEAdvertisementWatcherStoppedEventArgs>
eventStoppedGuid := winrt.ParameterizedInstanceGUID( eventStoppedGuid := winrt.ParameterizedInstanceGUID(
foundation.GUIDTypedEventHandler, foundation.GUIDTypedEventHandler,
advertisement.SignatureBluetoothLEAdvertisementWatcher, advertisement.SignatureBluetoothLEAdvertisementWatcher,
advertisement.SignatureBluetoothLEAdvertisementWatcherStoppedEventArgs, advertisement.SignatureBluetoothLEAdvertisementWatcherStoppedEventArgs,
) )
stoppedHandler := foundation.NewTypedEventHandler(ole.NewGUID(eventStoppedGuid), func(_ *foundation.TypedEventHandler, _, _ unsafe.Pointer) { stoppedHandler := foundation.NewTypedEventHandler(ole.NewGUID(eventStoppedGuid), func(_ *foundation.TypedEventHandler, _, arg unsafe.Pointer) {
// Note: the args parameter has an Error property that should args := (*advertisement.BluetoothLEAdvertisementWatcherStoppedEventArgs)(arg)
// probably be checked, but I'm not sure when stopping the errCode, err := args.GetError()
// advertisement watcher could ever result in an error (except if err != nil {
// for bugs). // 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)
}
close(stoppingChan) close(stoppingChan)
}) })
defer stoppedHandler.Release() defer stoppedHandler.Release()
@ -96,8 +205,7 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) {
} }
// Wait until advertisement has stopped, and finish. // Wait until advertisement has stopped, and finish.
<-stoppingChan return <-stoppingChan
return nil
} }
func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedEventArgs) ScanResult { func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedEventArgs) ScanResult {
@ -114,7 +222,7 @@ func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedE
Address: adr, Address: adr,
} }
var manufacturerData map[uint16][]byte = make(map[uint16][]byte) var manufacturerData []ManufacturerDataElement
if winAdv, err := args.GetAdvertisement(); err == nil && winAdv != nil { if winAdv, err := args.GetAdvertisement(); err == nil && winAdv != nil {
vector, _ := winAdv.GetManufacturerData() vector, _ := winAdv.GetManufacturerData()
size, _ := vector.GetSize() size, _ := vector.GetSize()
@ -123,7 +231,10 @@ func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedE
manData := (*advertisement.BluetoothLEManufacturerData)(element) manData := (*advertisement.BluetoothLEManufacturerData)(element)
companyID, _ := manData.GetCompanyId() companyID, _ := manData.GetCompanyId()
buffer, _ := manData.GetData() buffer, _ := manData.GetData()
manufacturerData[companyID] = bufferToSlice(buffer) manufacturerData = append(manufacturerData, ManufacturerDataElement{
CompanyID: companyID,
Data: bufferToSlice(buffer),
})
} }
} }
@ -141,7 +252,7 @@ func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedE
} }
func bufferToSlice(buffer *streams.IBuffer) []byte { func bufferToSlice(buffer *streams.IBuffer) []byte {
dataReader, _ := streams.FromBuffer(buffer) dataReader, _ := streams.DataReaderFromBuffer(buffer)
defer dataReader.Release() defer dataReader.Release()
bufferSize, _ := buffer.GetLength() bufferSize, _ := buffer.GetLength()
if bufferSize == 0 { if bufferSize == 0 {
@ -163,6 +274,8 @@ func (a *Adapter) StopScan() error {
// Device is a connection to a remote peripheral. // Device is a connection to a remote peripheral.
type Device struct { type Device struct {
Address Address // the MAC address of the device
device *bluetooth.BluetoothLEDevice device *bluetooth.BluetoothLEDevice
session *genericattributeprofile.GattSession session *genericattributeprofile.GattSession
} }
@ -177,7 +290,7 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, err
} }
// IAsyncOperation<BluetoothLEDevice> // IAsyncOperation<BluetoothLEDevice>
bleDeviceOp, err := bluetooth.FromBluetoothAddressAsync(winAddr) bleDeviceOp, err := bluetooth.BluetoothLEDeviceFromBluetoothAddressAsync(winAddr)
if err != nil { if err != nil {
return Device{}, err return Device{}, err
} }
@ -210,7 +323,7 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, err
// Windows does not support explicitly connecting to a device. // Windows does not support explicitly connecting to a device.
// Instead it has the concept of a GATT session that is owned // Instead it has the concept of a GATT session that is owned
// by the calling program. // by the calling program.
gattSessionOp, err := genericattributeprofile.FromDeviceIdAsync(dID) // IAsyncOperation<GattSession> gattSessionOp, err := genericattributeprofile.GattSessionFromDeviceIdAsync(dID) // IAsyncOperation<GattSession>
if err != nil { if err != nil {
return Device{}, err return Device{}, err
} }
@ -229,7 +342,7 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, err
return Device{}, err return Device{}, err
} }
return Device{bleDevice, newSession}, nil return Device{address, bleDevice, newSession}, nil
} }
// Disconnect from the BLE device. This method is non-blocking and does not // Disconnect from the BLE device. This method is non-blocking and does not

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
//go:build !linux //go:build !linux && !windows
package bluetooth 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)), unsafe.SliceData(p)) 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])))
if errCode != 0 { if errCode != 0 {
return 0, Error(errCode) return 0, Error(errCode)
} }

309
gatts_windows.go Normal file
View file

@ -0,0 +1,309 @@
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,11 +1,12 @@
module tinygo.org/x/bluetooth module gitrepo.ru/neonxp/bluetooth
go 1.18 go 1.20
require ( require (
github.com/go-ole/go-ole v1.2.6 github.com/go-ole/go-ole v1.2.6
github.com/godbus/dbus/v5 v5.1.0 github.com/godbus/dbus/v5 v5.1.0
github.com/saltosystems/winrt-go v0.0.0-20230921082907-2ab5b7d431e1 github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b
github.com/soypat/cyw43439 v0.0.0-20240609122733-da9153086796
github.com/tinygo-org/cbgo v0.0.4 github.com/tinygo-org/cbgo v0.0.4
golang.org/x/crypto v0.12.0 golang.org/x/crypto v0.12.0
tinygo.org/x/drivers v0.26.1-0.20230922160320-ed51435c2ef6 tinygo.org/x/drivers v0.26.1-0.20230922160320-ed51435c2ef6
@ -16,6 +17,9 @@ require (
require ( require (
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/sirupsen/logrus v1.9.3 // 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/sys v0.11.0 // indirect
golang.org/x/term v0.11.0 // indirect golang.org/x/term v0.11.0 // indirect
) )

12
go.sum
View file

@ -10,19 +10,27 @@ 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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/saltosystems/winrt-go v0.0.0-20230921082907-2ab5b7d431e1 h1:L2YoWezgwpAZ2SEKjXk6yLnwOkM3u7mXq/mKuJeEpFM= github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b h1:du3zG5fd8snsFN6RBoLA7fpaYV9ZQIsyH9snlk2Zvik=
github.com/saltosystems/winrt-go v0.0.0-20230921082907-2ab5b7d431e1/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA= github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA=
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= 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 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 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/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.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.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q= 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 h1:3D76CRYbH03Rudi8sEgs/YO0x3JIMdyq8jlQtk/44fU=
github.com/tinygo-org/cbgo v0.0.4/go.mod h1:7+HgWIHd4nbAz0ESjGlJ1/v9LDU1Ox8MGzP9mah/fLk= 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 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= 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-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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View file

@ -1,4 +1,4 @@
//go:build ninafw //go:build ninafw || hci || cyw43439
package bluetooth package bluetooth
@ -6,7 +6,6 @@ import (
"encoding/binary" "encoding/binary"
"encoding/hex" "encoding/hex"
"errors" "errors"
"machine"
"time" "time"
) )
@ -69,6 +68,7 @@ const (
hciCommandPkt = 0x01 hciCommandPkt = 0x01
hciACLDataPkt = 0x02 hciACLDataPkt = 0x02
hciSynchronousDataPkt = 0x03
hciEventPkt = 0x04 hciEventPkt = 0x04
hciSecurityPkt = 0x06 hciSecurityPkt = 0x06
@ -87,6 +87,11 @@ const (
const ( const (
hciACLLenPos = 4 hciACLLenPos = 4
hciEvtLenPos = 2 hciEvtLenPos = 2
attCID = 0x0004
bleCTL = 0x0008
signalingCID = 0x0005
securityCID = 0x0006
) )
var ( var (
@ -113,13 +118,23 @@ type leConnectData struct {
role uint8 role uint8
peerBdaddrType uint8 peerBdaddrType uint8
peerBdaddr [6]uint8 peerBdaddr [6]uint8
interval uint16
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 { type hci struct {
uart *machine.UART transport hciTransport
softCTS machine.Pin
softRTS machine.Pin
att *att att *att
l2cap *l2cap
buf []byte buf []byte
address [6]byte address [6]byte
cmdCompleteOpcode uint16 cmdCompleteOpcode uint16
@ -128,26 +143,34 @@ type hci struct {
scanning bool scanning bool
advData leAdvertisingReport advData leAdvertisingReport
connectData leConnectData connectData leConnectData
maxPkt uint16
pendingPkt uint16
} }
func newHCI(uart *machine.UART) *hci { func newHCI(t hciTransport) *hci {
return &hci{ return &hci{
uart: uart, transport: t,
softCTS: machine.NoPin,
softRTS: machine.NoPin,
buf: make([]byte, 256), buf: make([]byte, 256),
} }
} }
func (h *hci) start() error { func (h *hci) start() error {
if h.softRTS != machine.NoPin { h.transport.startRead()
h.softRTS.Low() defer h.transport.endRead()
defer h.softRTS.High() 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
} }
for h.uart.Buffered() > 0 { continue
h.uart.ReadByte() }
return nil
} }
return nil return nil
@ -162,22 +185,24 @@ func (h *hci) reset() error {
} }
func (h *hci) poll() error { func (h *hci) poll() error {
if h.softRTS != machine.NoPin { h.transport.startRead()
h.softRTS.Low() defer h.transport.endRead()
defer h.softRTS.High()
}
i := 0 i := 0
for h.uart.Buffered() > 0 { for h.transport.Buffered() > 0 {
data, _ := h.uart.ReadByte() sz := h.transport.Buffered()
h.buf[i] = data c := sz + 4 - (sz % 4)
_, err := h.transport.Read(h.buf[i : i+c])
if err != nil {
return err
}
i += sz
done, err := h.processPacket(i) done, err := h.processPacket(i)
switch { switch {
case err == ErrHCIUnknown || err == ErrHCIInvalidPacket || err == ErrHCIUnknownEvent: case err == ErrHCIUnknown || err == ErrHCIInvalidPacket || err == ErrHCIUnknownEvent:
if debug { if debug {
println("hci error:", err.Error()) println("hci error:", err.Error(), hex.EncodeToString(h.buf[:i]))
} }
i = 0 i = 0
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
@ -185,8 +210,13 @@ func (h *hci) poll() error {
return err return err
case done: case done:
return nil return nil
case i+1 >= len(h.buf):
if debug {
println("hci error: buffer overflow")
}
i = 0
time.Sleep(5 * time.Millisecond)
default: default:
i++
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
} }
} }
@ -225,9 +255,19 @@ 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: default:
if debug { if debug {
println("unknown packet data:", h.buf[0]) println("unknown packet data:", hex.EncodeToString(h.buf[0:i]))
} }
return true, ErrHCIUnknown return true, ErrHCIUnknown
} }
@ -257,6 +297,26 @@ func (h *hci) setLeEventMask(eventMask uint64) error {
return h.sendCommandWithParams(ogfLECtrl<<ogfCommandPos|0x01, b[:]) return h.sendCommandWithParams(ogfLECtrl<<ogfCommandPos|0x01, b[:])
} }
func (h *hci) readLeBufferSize() error {
if err := h.sendCommand(ogfLECtrl<<ogfCommandPos | ocfLEReadBufferSize); err != nil {
return err
}
pktLen := binary.LittleEndian.Uint16(h.buf[0:])
h.maxPkt = uint16(h.buf[2])
// pkt len must be at least 27 bytes
if pktLen < 27 {
pktLen = 27
}
if err := h.att.setMaxMTU(pktLen); err != nil {
return err
}
return nil
}
func (h *hci) leSetScanEnable(enabled, duplicates bool) error { func (h *hci) leSetScanEnable(enabled, duplicates bool) error {
h.scanning = enabled h.scanning = enabled
@ -351,6 +411,21 @@ func (h *hci) leCancelConn() error {
return h.sendCommand(ogfLECtrl<<ogfCommandPos | ocfLECancelConn) return h.sendCommand(ogfLECtrl<<ogfCommandPos | ocfLECancelConn)
} }
func (h *hci) leConnUpdate(handle uint16, minInterval, maxInterval,
latency, supervisionTimeout uint16) error {
var b [14]byte
binary.LittleEndian.PutUint16(b[0:], handle)
binary.LittleEndian.PutUint16(b[2:], minInterval)
binary.LittleEndian.PutUint16(b[4:], maxInterval)
binary.LittleEndian.PutUint16(b[6:], latency)
binary.LittleEndian.PutUint16(b[8:], supervisionTimeout)
binary.LittleEndian.PutUint16(b[10:], 0x0004)
binary.LittleEndian.PutUint16(b[12:], 0x0006)
return h.sendCommandWithParams(ogfLECtrl<<ogfCommandPos|ocfLEConnUpdate, b[:])
}
func (h *hci) disconnect(handle uint16) error { func (h *hci) disconnect(handle uint16) error {
var b [3]byte var b [3]byte
binary.LittleEndian.PutUint16(b[0:], handle) binary.LittleEndian.PutUint16(b[0:], handle)
@ -431,28 +506,13 @@ func (h *hci) sendAclPkt(handle uint16, cid uint8, data []byte) error {
return err return err
} }
h.pendingPkt++
return nil return nil
} }
const writeAttempts = 200
func (h *hci) write(buf []byte) (int, error) { func (h *hci) write(buf []byte) (int, error) {
if h.softCTS != machine.NoPin { return h.transport.Write(buf)
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 { type aclDataHeader struct {
@ -486,6 +546,13 @@ func (h *hci) handleACLData(buf []byte) error {
} else { } else {
return h.att.handleData(aclHdr.handle&0x0fff, buf[8:aclHdr.len+8]) return h.att.handleData(aclHdr.handle&0x0fff, buf[8:aclHdr.len+8])
} }
case signalingCID:
if debug {
println("signaling cid", aclHdr.cid, hex.EncodeToString(buf))
}
return h.l2cap.handleData(aclHdr.handle&0x0fff, buf[8:aclHdr.len+8])
default: default:
if debug { if debug {
println("unknown acl data cid", aclHdr.cid) println("unknown acl data cid", aclHdr.cid)
@ -507,6 +574,7 @@ func (h *hci) handleEventData(buf []byte) error {
handle := binary.LittleEndian.Uint16(buf[3:]) handle := binary.LittleEndian.Uint16(buf[3:])
h.att.removeConnection(handle) h.att.removeConnection(handle)
h.l2cap.removeConnection(handle)
return h.leSetAdvertiseEnable(true) return h.leSetAdvertiseEnable(true)
@ -543,8 +611,28 @@ func (h *hci) handleEventData(buf []byte) error {
case evtNumCompPkts: case evtNumCompPkts:
if debug { if debug {
println("evtNumCompPkts") println("evtNumCompPkts", hex.EncodeToString(buf))
} }
// 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: case evtLEMetaEvent:
if debug { if debug {
println("evtLEMetaEvent") println("evtLEMetaEvent")
@ -563,7 +651,20 @@ func (h *hci) handleEventData(buf []byte) error {
h.connectData.peerBdaddrType = buf[7] h.connectData.peerBdaddrType = buf[7]
copy(h.connectData.peerBdaddr[0:], buf[8:]) copy(h.connectData.peerBdaddr[0:], buf[8:])
switch buf[2] {
case leMetaEventConnComplete:
h.connectData.interval = binary.LittleEndian.Uint16(buf[14:])
h.connectData.timeout = binary.LittleEndian.Uint16(buf[16:])
case leMetaEventEnhancedConnectionComplete:
h.connectData.interval = binary.LittleEndian.Uint16(buf[26:])
h.connectData.timeout = binary.LittleEndian.Uint16(buf[28:])
}
h.att.addConnection(h.connectData.handle) h.att.addConnection(h.connectData.handle)
if err := h.l2cap.addConnection(h.connectData.handle, h.connectData.role,
h.connectData.interval, h.connectData.timeout); err != nil {
return err
}
return h.leSetAdvertiseEnable(false) return h.leSetAdvertiseEnable(false)
@ -651,6 +752,10 @@ func (h *hci) handleEventData(buf []byte) error {
return ErrHCIUnknownEvent return ErrHCIUnknownEvent
} }
case evtHardwareError: case evtHardwareError:
if debug {
println("evtHardwareError", hex.EncodeToString(buf))
}
return ErrHCIUnknownEvent return ErrHCIUnknownEvent
} }

156
l2cap_hci.go Normal file
View file

@ -0,0 +1,156 @@
//go:build ninafw || hci || cyw43439
package bluetooth
import (
"encoding/binary"
"encoding/hex"
)
const (
connectionParamUpdateRequest = 0x12
connectionParamUpdateResponse = 0x13
)
type l2capConnectionParamReqPkt struct {
minInterval uint16
maxInterval uint16
latency uint16
timeout uint16
}
func (l *l2capConnectionParamReqPkt) Write(buf []byte) (int, error) {
l.minInterval = binary.LittleEndian.Uint16(buf[0:])
l.maxInterval = binary.LittleEndian.Uint16(buf[2:])
l.latency = binary.LittleEndian.Uint16(buf[4:])
l.timeout = binary.LittleEndian.Uint16(buf[6:])
return 8, nil
}
func (l *l2capConnectionParamReqPkt) Read(p []byte) (int, error) {
binary.LittleEndian.PutUint16(p[0:], l.minInterval)
binary.LittleEndian.PutUint16(p[2:], l.maxInterval)
binary.LittleEndian.PutUint16(p[4:], l.latency)
binary.LittleEndian.PutUint16(p[6:], l.timeout)
return 8, nil
}
type l2capConnectionParamResponsePkt struct {
code uint8
identifier uint8
length uint16
value uint16
}
func (l *l2capConnectionParamResponsePkt) Read(p []byte) (int, error) {
p[0] = l.code
p[1] = l.identifier
binary.LittleEndian.PutUint16(p[2:], l.length)
binary.LittleEndian.PutUint16(p[4:], l.value)
return 6, nil
}
type l2cap struct {
hci *hci
}
func newL2CAP(hci *hci) *l2cap {
return &l2cap{
hci: hci,
}
}
func (l *l2cap) addConnection(handle uint16, role uint8, interval, timeout uint16) error {
if role != 0x01 {
return nil
}
var b [12]byte
b[0] = connectionParamUpdateRequest
b[1] = 0x01
binary.LittleEndian.PutUint16(b[2:], 8)
binary.LittleEndian.PutUint16(b[4:], interval)
binary.LittleEndian.PutUint16(b[6:], interval)
binary.LittleEndian.PutUint16(b[8:], 0)
binary.LittleEndian.PutUint16(b[10:], timeout)
return l.sendReq(handle, b[:])
}
func (l *l2cap) removeConnection(handle uint16) error {
return nil
}
func (l *l2cap) handleData(handle uint16, buf []byte) error {
code := buf[0]
identifier := buf[1]
//length := binary.LittleEndian.Uint16(buf[2:4])
if debug {
println("l2cap.handleData:", handle, "data:", hex.EncodeToString(buf))
}
// TODO: check length
switch code {
case connectionParamUpdateRequest:
return l.handleParameterUpdateRequest(handle, identifier, buf[4:])
case connectionParamUpdateResponse:
return l.handleParameterUpdateResponse(handle, identifier, buf[4:])
}
return nil
}
func (l *l2cap) handleParameterUpdateRequest(connectionHandle uint16, identifier uint8, data []byte) error {
if debug {
println("l2cap.handleParameterUpdateRequest:", connectionHandle, "data:", hex.EncodeToString(data))
}
req := l2capConnectionParamReqPkt{}
req.Write(data)
// TODO: check against min/max
resp := l2capConnectionParamResponsePkt{
code: connectionParamUpdateResponse,
identifier: identifier,
length: 2,
value: 0,
}
var b [6]byte
resp.Read(b[:])
if err := l.sendReq(connectionHandle, b[:]); err != nil {
return err
}
// valid so update connection parameters
if resp.value == 0 {
return l.hci.leConnUpdate(connectionHandle, req.minInterval, req.maxInterval, req.latency, req.timeout)
}
return nil
}
func (l *l2cap) handleParameterUpdateResponse(connectionHandle uint16, identifier uint8, data []byte) error {
if debug {
println("l2cap.handleParameterUpdateResponse:", connectionHandle, "data:", hex.EncodeToString(data))
}
// for now do nothing
return nil
}
func (l *l2cap) sendReq(handle uint16, data []byte) error {
if debug {
println("l2cap.sendReq:", handle, "data:", hex.EncodeToString(data))
}
return l.hci.sendAclPkt(handle, signalingCID, data)
}

View file

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

View file

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

22
uuid.go
View file

@ -38,6 +38,20 @@ func New16BitUUID(shortUUID uint16) UUID {
return 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 // 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 // with the bits given in the argument. These bits are the same bits that vary
// in the 16-bit compressed UUID form. // in the 16-bit compressed UUID form.
@ -68,6 +82,14 @@ func (uuid UUID) Get16Bit() uint16 {
return uint16(uuid[3]) 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. // Bytes returns a 16-byte array containing the raw UUID.
func (uuid UUID) Bytes() [16]byte { func (uuid UUID) Bytes() [16]byte {
buf := [16]byte{} buf := [16]byte{}

View file

@ -1,4 +1,4 @@
//go:build ninafw //go:build hci || ninafw || cyw43439
package bluetooth 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. // 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. // Update this value before release of new version of software.
const Version = "0.8.0" const Version = "0.10.0"