Compare commits
28 commits
ninafw-cus
...
release
Author | SHA1 | Date | |
---|---|---|---|
eae5a517c8 | |||
|
a668e1b0a0 | ||
|
b1081a9db1 | ||
|
0d0c149a20 | ||
|
457af7571a | ||
|
9905abd00e | ||
|
926aeb43f6 | ||
|
d46f2cc206 | ||
|
c26c9d5630 | ||
|
abbb565ef0 | ||
|
348de057f8 | ||
|
314ca89209 | ||
|
6b08161955 | ||
|
12b6f0bc25 | ||
|
52c3c068e2 | ||
|
852fa4ab6a | ||
|
e0d5fd4c3a | ||
|
0087e0549b | ||
|
d82232b16d | ||
|
9a53d2a327 | ||
|
3e90718eb8 | ||
|
553633e56a | ||
|
b6fde65fd6 | ||
|
8e8dd34fc2 | ||
|
07a9e1d02e | ||
|
0a9bffe397 | ||
|
00a475adf1 | ||
|
4c90cf4ab6 |
48 changed files with 2156 additions and 495 deletions
85
CHANGELOG.md
85
CHANGELOG.md
|
@ -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
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
8
Makefile
8
Makefile
|
@ -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.
|
||||||
|
|
47
README.md
47
README.md
|
@ -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
116
adapter_cyw43439.go
Normal 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)
|
||||||
|
}
|
|
@ -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
205
adapter_hci.go
Normal 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
122
adapter_hci_uart.go
Normal 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
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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"
|
||||||
|
|
|
@ -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())
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -12,7 +12,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"tinygo.org/x/bluetooth"
|
"gitrepo.ru/neonxp/bluetooth"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"tinygo.org/x/bluetooth"
|
"gitrepo.ru/neonxp/bluetooth"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
48
examples/tinyscan/clue.go
Normal 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
55
examples/tinyscan/main.go
Normal 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)
|
||||||
|
}
|
39
examples/tinyscan/pybadge.go
Normal file
39
examples/tinyscan/pybadge.go
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
45
examples/tinyscan/pyportal.go
Normal file
45
examples/tinyscan/pyportal.go
Normal 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
204
gap.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
37
gap_linux.go
37
gap_linux.go
|
@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
63
gap_test.go
63
gap_test.go
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
141
gap_windows.go
141
gap_windows.go
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
4
gatts.go
4
gatts.go
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -1,4 +1,4 @@
|
||||||
//go:build !linux
|
//go:build !linux && !windows
|
||||||
|
|
||||||
package bluetooth
|
package bluetooth
|
||||||
|
|
||||||
|
|
|
@ -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
309
gatts_windows.go
Normal 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
10
go.mod
|
@ -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
12
go.sum
|
@ -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=
|
||||||
|
|
|
@ -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
156
l2cap_hci.go
Normal 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)
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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
22
uuid.go
|
@ -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{}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
//go:build ninafw
|
//go:build hci || ninafw || cyw43439
|
||||||
|
|
||||||
package bluetooth
|
package bluetooth
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue