Compare commits

..

1 commit

Author SHA1 Message Date
BCG
f100ba6eb2 ninafw: Adding init options to enable customizing hardware 2024-01-17 20:17:00 -05:00
50 changed files with 588 additions and 2174 deletions

View file

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

View file

@ -42,11 +42,9 @@ 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 $(TINYGO) build -o test.uf2 -size=short -target=feather-m4 -tags="ninafw ninafw_featherwing_init" ./examples/advertisement
@md5sum test.hex @md5sum test.hex
$(TINYGO) build -o test.uf2 -size=short -target=pico-w ./examples/discover $(TINYGO) build -o test.uf2 -size=short -target=pybadge ./examples/advertisement
@md5sum test.hex
$(TINYGO) build -o test.uf2 -size=short -target=badger2040-w ./examples/advertisement
@md5sum test.hex @md5sum test.hex
smoketest-linux: smoketest-linux:
@ -64,8 +62,6 @@ smoketest-windows:
GOOS=windows go build -o /tmp/go-build-discard ./examples/scanner GOOS=windows go build -o /tmp/go-build-discard ./examples/scanner
GOOS=windows go build -o /tmp/go-build-discard ./examples/discover GOOS=windows go build -o /tmp/go-build-discard ./examples/discover
GOOS=windows go build -o /tmp/go-build-discard ./examples/heartrate-monitor GOOS=windows go build -o /tmp/go-build-discard ./examples/heartrate-monitor
GOOS=windows go build -o /tmp/go-build-discard ./examples/advertisement
GOOS=windows go build -o /tmp/go-build-discard ./examples/heartrate
smoketest-macos: smoketest-macos:
# Test on macos. # Test on macos.

View file

@ -2,13 +2,13 @@
[![Go Bluetooth](./images/gobluetooth.png)](https://tinygo.org/bluetooth) [![Go Bluetooth](./images/gobluetooth.png)](https://tinygo.org/bluetooth)
[![PkgGoDev](https://pkg.go.dev/badge/pkg.go.dev/gitrepo.ru/neonxp/bluetooth)](https://pkg.go.dev/gitrepo.ru/neonxp/bluetooth) [![Linux](https://github.com/tinygo-org/bluetooth/actions/workflows/linux.yml/badge.svg?branch=dev)](https://github.com/tinygo-org/bluetooth/actions/workflows/linux.yml) [![macOS](https://github.com/tinygo-org/bluetooth/actions/workflows/macos.yml/badge.svg?branch=dev)](https://github.com/tinygo-org/bluetooth/actions/workflows/macos.yml) [![PkgGoDev](https://pkg.go.dev/badge/pkg.go.dev/tinygo.org/x/bluetooth)](https://pkg.go.dev/tinygo.org/x/bluetooth) [![Linux](https://github.com/tinygo-org/bluetooth/actions/workflows/linux.yml/badge.svg?branch=dev)](https://github.com/tinygo-org/bluetooth/actions/workflows/linux.yml) [![macOS](https://github.com/tinygo-org/bluetooth/actions/workflows/macos.yml/badge.svg?branch=dev)](https://github.com/tinygo-org/bluetooth/actions/workflows/macos.yml)
Go Bluetooth is a cross-platform package for using [Bluetooth Low Energy](https://en.wikipedia.org/wiki/Bluetooth_Low_Energy) hardware from the Go programming language. 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/) or using the Bluetooth Host Controller Interface (HCI) by using [TinyGo](https://tinygo.org/). It can also be used running "bare metal" on microcontrollers produced by [Nordic Semiconductor](https://www.nordicsemi.com/) by using [TinyGo](https://tinygo.org/).
The Go Bluetooth package can be used to create both Bluetooth Low Energy Centrals as well as to create Bluetooth Low Energy Peripherals. 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 (
"gitrepo.ru/neonxp/bluetooth" "tinygo.org/x/bluetooth"
) )
var adapter = bluetooth.DefaultAdapter var adapter = bluetooth.DefaultAdapter
@ -58,7 +58,7 @@ package main
import ( import (
"time" "time"
"gitrepo.ru/neonxp/bluetooth" "tinygo.org/x/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) | CYW43439 (RP2040-W) | | | Linux | macOS | Windows | Nordic Semi | ESP32 (NINA-FW) |
| -------------------------------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------- | | -------------------------------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ |
| API used | BlueZ | CoreBluetooth | WinRT | SoftDevice | HCI | HCI | | API used | BlueZ | CoreBluetooth | WinRT | SoftDevice | HCI |
| Scanning | :heavy_check_mark: | :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: |
| Connect to peripheral | :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: |
| Write peripheral characteristics | :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: |
| Receive notifications | :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: |
| Advertisement | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | Advertisement | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: |
| Local services | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | Local services | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: |
| Local characteristics | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | Local characteristics | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: |
| Send notifications | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | Send notifications | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: |
## Linux ## Linux
@ -284,25 +284,12 @@ For example, this command can be used to compile and flash an Arduino Nano RP204
tinygo flash -target nano-rp2040 ./examples/heartrate tinygo flash -target nano-rp2040 ./examples/heartrate
When using the AirLift WiFi Featherwing with one of Adafruit's feather boards, you can use `ninafw ninafw_featherwing_init` build tags to set up the hardware using the default pins. Make sure to [connect the solder pads on the underside of the board](https://learn.adafruit.com/adafruit-airlift-featherwing-esp32-wifi-co-processor-featherwing/pinouts#spi-and-control-pins-3029450) in order to enable BLE support (see "Optional Control Pins" section).
To use ninafw with other boards, you will need to use the `ninafw` build tag as well as configure the pins and UART for communicating with the ESP32 module by configuring the `AdapterConfig` package variable before calling `DefaultAdapter.Enable()`. See [`adapter_ninafw-featherwing.go`](adapter_ninafw-featherwing.go) for an example of setting the hardware configuration options.
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.

View file

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

View file

@ -141,32 +141,11 @@ func makeScanResult(prph cbgo.Peripheral, advFields cbgo.AdvFields, rssi int) Sc
serviceUUIDs = append(serviceUUIDs, parsedUUID) serviceUUIDs = append(serviceUUIDs, parsedUUID)
} }
var manufacturerData []ManufacturerDataElement manufacturerData := make(map[uint16][]byte)
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 = append(manufacturerData, ManufacturerDataElement{ manufacturerData[manufacturerID] = advFields.ManufacturerData[2:]
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
@ -181,7 +160,6 @@ 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,
}, },
}, },
} }

View file

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

View file

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

View file

@ -0,0 +1,22 @@
//go:build ninafw && ninafw_featherwing_init
package bluetooth
import (
"machine"
)
func init() {
AdapterConfig = NINAConfig{
UART: machine.DefaultUART,
CS: machine.D13,
ACK: machine.D11,
GPIO0: machine.D10,
RESETN: machine.D12,
CTS: machine.D11, // same as ACK
RTS: machine.D10, // same as GPIO0
BaudRate: 115200,
ResetInverted: true,
SoftFlowControl: true,
}
}

22
adapter_ninafw-machine.go Normal file
View file

@ -0,0 +1,22 @@
//go:build ninafw && ninafw_machine_init
package bluetooth
import (
"machine"
)
func init() {
AdapterConfig = NINAConfig{
UART: machine.UART_NINA,
CS: machine.NINA_CS,
ACK: machine.NINA_ACK,
GPIO0: machine.NINA_GPIO0,
RESETN: machine.NINA_RESETN,
CTS: machine.NINA_CTS,
RTS: machine.NINA_RTS,
BaudRate: machine.NINA_BAUDRATE,
ResetInverted: machine.NINA_RESET_INVERTED,
SoftFlowControl: machine.NINA_SOFT_FLOWCONTROL,
}
}

View file

@ -4,131 +4,247 @@ package bluetooth
import ( import (
"machine" "machine"
"runtime"
"time" "time"
) )
const maxConnections = 1 const maxConnections = 1
// Adapter represents the HCI connection to the NINA fw using the hardware UART. // NINAConfig encapsulates the hardware options for the NINA firmware
type NINAConfig struct {
UART *machine.UART
CS machine.Pin
ACK machine.Pin
GPIO0 machine.Pin
RESETN machine.Pin
TX machine.Pin
RX machine.Pin
CTS machine.Pin
RTS machine.Pin
BaudRate uint32
ResetInverted bool
SoftFlowControl bool
}
// AdapterConfig is used to set the hardware options for the NINA adapter prior
// to calling DefaultAdapter.Enable()
var AdapterConfig NINAConfig
// Adapter represents the UART connection to the NINA fw.
type Adapter struct { type Adapter struct {
hciAdapter hci *hci
att *att
isDefault bool
scanning bool
connectHandler func(device Device, connected bool)
connectedDevices []Device
notificationsStarted bool
} }
// DefaultAdapter is the default adapter on the current system. // 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
// Bluetooth-related calls (unless otherwise indicated). // Bluetooth-related calls (unless otherwise indicated).
func (a *Adapter) Enable() error { func (a *Adapter) Enable() error {
// reset the NINA in BLE mode // reset the NINA in BLE mode
machine.NINA_CS.Configure(machine.PinConfig{Mode: machine.PinOutput}) AdapterConfig.CS.Configure(machine.PinConfig{Mode: machine.PinOutput})
machine.NINA_CS.Low() AdapterConfig.CS.Low()
if machine.NINA_RESET_INVERTED { if AdapterConfig.ResetInverted {
resetNINAInverted() resetNINAInverted()
} else { } else {
resetNINA() resetNINA()
} }
// serial port for nina chip // serial port for nina chip
uart := machine.UART_NINA uart := AdapterConfig.UART
cfg := machine.UARTConfig{ cfg := machine.UARTConfig{
TX: machine.NINA_TX, TX: AdapterConfig.TX,
RX: machine.NINA_RX, RX: AdapterConfig.RX,
BaudRate: machine.NINA_BAUDRATE, BaudRate: AdapterConfig.BaudRate,
} }
if !machine.NINA_SOFT_FLOWCONTROL { if !AdapterConfig.SoftFlowControl {
cfg.CTS = machine.NINA_CTS cfg.CTS = AdapterConfig.CTS
cfg.RTS = machine.NINA_RTS cfg.RTS = AdapterConfig.RTS
} }
uart.Configure(cfg) uart.Configure(cfg)
transport := &hciUART{uart: uart} a.hci, a.att = newBLEStack(uart)
if machine.NINA_SOFT_FLOWCONTROL { if AdapterConfig.SoftFlowControl {
machine.NINA_RTS.Configure(machine.PinConfig{Mode: machine.PinOutput}) a.hci.softRTS = AdapterConfig.RTS
machine.NINA_RTS.High() a.hci.softRTS.Configure(machine.PinConfig{Mode: machine.PinOutput})
a.hci.softRTS.High()
machine.NINA_CTS.Configure(machine.PinConfig{Mode: machine.PinInput}) a.hci.softCTS = AdapterConfig.CTS
AdapterConfig.CTS.Configure(machine.PinConfig{Mode: machine.PinInput})
} }
a.hci, a.att = newBLEStack(transport) a.hci.start()
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() {
machine.NINA_RESETN.Configure(machine.PinConfig{Mode: machine.PinOutput}) AdapterConfig.RESETN.Configure(machine.PinConfig{Mode: machine.PinOutput})
machine.NINA_RESETN.High() AdapterConfig.RESETN.High()
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
machine.NINA_RESETN.Low() AdapterConfig.RESETN.Low()
time.Sleep(1000 * time.Millisecond) time.Sleep(1000 * time.Millisecond)
} }
func resetNINAInverted() { func resetNINAInverted() {
machine.NINA_RESETN.Configure(machine.PinConfig{Mode: machine.PinOutput}) AdapterConfig.RESETN.Configure(machine.PinConfig{Mode: machine.PinOutput})
machine.NINA_RESETN.Low() AdapterConfig.RESETN.Low()
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
machine.NINA_RESETN.High() AdapterConfig.RESETN.High()
time.Sleep(1000 * time.Millisecond) time.Sleep(1000 * time.Millisecond)
} }
type hciUART struct { func (a *Adapter) startNotifications() {
uart *machine.UART if a.notificationsStarted {
} return
func (h *hciUART) startRead() {
if machine.NINA_SOFT_FLOWCONTROL {
machine.NINA_RTS.Low()
} }
}
func (h *hciUART) endRead() { if debug {
if machine.NINA_SOFT_FLOWCONTROL { println("starting notifications...")
machine.NINA_RTS.High()
} }
}
func (h *hciUART) Buffered() int { a.notificationsStarted = true
return h.uart.Buffered()
}
func (h *hciUART) ReadByte() (byte, error) { // go routine to poll for HCI events for ATT notifications
return h.uart.ReadByte() go func() {
} for {
if err := a.att.poll(); err != nil {
func (h *hciUART) Read(buf []byte) (int, error) { // TODO: handle error
return h.uart.Read(buf) if debug {
} println("error polling for notifications:", err.Error())
}
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
} }
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 {
for _, d := range a.connectedDevices {
if d.handle == handle {
if debug {
println("found device", handle, d.Address.String(), "with notifications registered", len(d.notificationRegistrations))
}
return d
} }
} }
n, err := h.uart.Write(buf) return Device{}
if err != nil {
return 0, err
}
return n, nil
} }

View file

@ -1,7 +1,6 @@
package bluetooth package bluetooth
import ( import (
"errors"
"fmt" "fmt"
"github.com/go-ole/go-ole" "github.com/go-ole/go-ole"
@ -14,8 +13,6 @@ 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.
@ -59,8 +56,3 @@ func awaitAsyncOperation(asyncOperation *foundation.IAsyncOperation, genericPara
} }
return nil return nil
} }
func (a *Adapter) Address() (MACAddress, error) {
// TODO: get mac address
return MACAddress{}, errors.New("not implemented")
}

View file

@ -1,4 +1,4 @@
//go:build hci || ninafw || cyw43439 //go:build ninafw
package bluetooth package bluetooth
@ -12,6 +12,9 @@ import (
) )
const ( const (
attCID = 0x0004
bleCTL = 0x0008
attOpError = 0x01 attOpError = 0x01
attOpMTUReq = 0x02 attOpMTUReq = 0x02
attOpMTUResponse = 0x03 attOpMTUResponse = 0x03
@ -67,15 +70,12 @@ const (
) )
var ( var (
ErrATTTimeout = errors.New("bluetooth: ATT timeout") ErrATTTimeout = errors.New("bluetooth: ATT timeout")
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,46 +252,38 @@ func (a *rawAttribute) length() int {
} }
} }
type connectData struct { type att 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
} notifications chan rawNotification
type att struct { connections []uint16
hci *hci lastHandle uint16
busy sync.Mutex attributes []rawAttribute
mtu uint16 localServices []rawService
maxMTU uint16
notifications chan rawNotification
connections []uint16
connectionsData map[uint16]*connectData
lastHandle uint16
localServices []rawService
localCharacteristics []rawCharacteristic
attributes []rawAttribute
} }
func newATT(hci *hci) *att { func newATT(hci *hci) *att {
return &att{ return &att{
hci: hci, hci: hci,
localCharacteristics: []rawCharacteristic{}, services: []rawService{},
notifications: make(chan rawNotification, 32), characteristics: []rawCharacteristic{},
connections: []uint16{}, value: []byte{},
connectionsData: make(map[uint16]*connectData), notifications: make(chan rawNotification, 32),
lastHandle: 0x0001, connections: []uint16{},
attributes: []rawAttribute{}, lastHandle: 0x0001,
localServices: []rawService{}, attributes: []rawAttribute{},
maxMTU: 248, localServices: []rawService{},
} }
} }
@ -313,7 +305,7 @@ func (a *att) readByGroupReq(connectionHandle, startHandle, endHandle uint16, uu
return err return err
} }
return a.waitUntilResponse(connectionHandle) return a.waitUntilResponse()
} }
func (a *att) readByTypeReq(connectionHandle, startHandle, endHandle uint16, typ uint16) error { func (a *att) readByTypeReq(connectionHandle, startHandle, endHandle uint16, typ uint16) error {
@ -334,7 +326,7 @@ func (a *att) readByTypeReq(connectionHandle, startHandle, endHandle uint16, typ
return err return err
} }
return a.waitUntilResponse(connectionHandle) return a.waitUntilResponse()
} }
func (a *att) findInfoReq(connectionHandle, startHandle, endHandle uint16) error { func (a *att) findInfoReq(connectionHandle, startHandle, endHandle uint16) error {
@ -354,7 +346,7 @@ func (a *att) findInfoReq(connectionHandle, startHandle, endHandle uint16) error
return err return err
} }
return a.waitUntilResponse(connectionHandle) return a.waitUntilResponse()
} }
func (a *att) readReq(connectionHandle, valueHandle uint16) error { func (a *att) readReq(connectionHandle, valueHandle uint16) error {
@ -373,7 +365,7 @@ func (a *att) readReq(connectionHandle, valueHandle uint16) error {
return err return err
} }
return a.waitUntilResponse(connectionHandle) return a.waitUntilResponse()
} }
func (a *att) writeCmd(connectionHandle, valueHandle uint16, data []byte) error { func (a *att) writeCmd(connectionHandle, valueHandle uint16, data []byte) error {
@ -392,7 +384,7 @@ func (a *att) writeCmd(connectionHandle, valueHandle uint16, data []byte) error
return err return err
} }
return nil return a.waitUntilResponse()
} }
func (a *att) writeReq(connectionHandle, valueHandle uint16, data []byte) error { func (a *att) writeReq(connectionHandle, valueHandle uint16, data []byte) error {
@ -411,43 +403,30 @@ func (a *att) writeReq(connectionHandle, valueHandle uint16, data []byte) error
return err return err
} }
return a.waitUntilResponse(connectionHandle) return a.waitUntilResponse()
} }
func (a *att) mtuReq(connectionHandle uint16) error { func (a *att) mtuReq(connectionHandle, mtu uint16) error {
if debug { 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:], cd.mtu) binary.LittleEndian.PutUint16(b[1:], mtu)
if err := a.sendReq(connectionHandle, b[:]); err != nil { if err := a.sendReq(connectionHandle, b[:]); err != nil {
return err return err
} }
return a.waitUntilResponse(connectionHandle) return a.waitUntilResponse()
}
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 {
if err := a.clearResponse(handle); err != nil { a.clearResponse()
return err
}
if debug { if debug {
println("att.sendReq:", handle, "data:", hex.EncodeToString(data)) println("att.sendReq:", handle, "data:", hex.EncodeToString(data))
@ -486,9 +465,7 @@ 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 {
if err := a.clearResponse(handle); err != nil { a.clearResponse()
return err
}
if debug { if debug {
println("att.sendError:", handle, "data:", opcode, hdl, code) println("att.sendError:", handle, "data:", opcode, hdl, code)
@ -512,41 +489,26 @@ 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:
cd.errored = true a.errored = true
cd.lastErrorOpcode = buf[1] a.lastErrorOpcode = buf[1]
cd.lastErrorHandle = binary.LittleEndian.Uint16(buf[2:]) a.lastErrorHandle = binary.LittleEndian.Uint16(buf[2:])
cd.lastErrorCode = buf[4] a.lastErrorCode = buf[4]
if debug { if debug {
println("att.handleData: attOpERROR", handle, cd.lastErrorOpcode, cd.lastErrorCode) println("att.handleData: attOpERROR", a.lastErrorOpcode, a.lastErrorCode)
} }
return ErrATTOp return ErrATTOp
case attOpMTUReq: case attOpMTUReq:
if debug { if debug {
println("att.handleData: attOpMTUReq", hex.EncodeToString(buf)) println("att.handleData: attOpMTUReq")
} }
mtu := binary.LittleEndian.Uint16(buf[1:]) a.mtu = binary.LittleEndian.Uint16(buf[1:])
if mtu > a.maxMTU { response := [3]byte{attOpMTUResponse, buf[1], buf[2]}
mtu = a.maxMTU if err := a.hci.sendAclPkt(handle, attCID, response[:]); err != nil {
}
// 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
} }
@ -554,8 +516,8 @@ func (a *att) handleData(handle uint16, buf []byte) error {
if debug { if debug {
println("att.handleData: attOpMTUResponse") println("att.handleData: attOpMTUResponse")
} }
cd.responded = true a.responded = true
cd.mtu = binary.LittleEndian.Uint16(buf[1:]) a.mtu = binary.LittleEndian.Uint16(buf[1:])
case attOpFindInfoReq: case attOpFindInfoReq:
if debug { if debug {
@ -571,7 +533,7 @@ func (a *att) handleData(handle uint16, buf []byte) error {
if debug { if debug {
println("att.handleData: attOpFindInfoResponse") println("att.handleData: attOpFindInfoResponse")
} }
cd.responded = true a.responded = true
lengthPerDescriptor := int(buf[1]) lengthPerDescriptor := int(buf[1])
@ -583,7 +545,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))
} }
cd.descriptors = append(cd.descriptors, d) a.descriptors = append(a.descriptors, d)
} }
case attOpFindByTypeReq: case attOpFindByTypeReq:
@ -606,7 +568,7 @@ func (a *att) handleData(handle uint16, buf []byte) error {
if debug { if debug {
println("att.handleData: attOpReadByTypeResponse") println("att.handleData: attOpReadByTypeResponse")
} }
cd.responded = true a.responded = true
lengthPerCharacteristic := int(buf[1]) lengthPerCharacteristic := int(buf[1])
@ -618,7 +580,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())
} }
cd.characteristics = append(cd.characteristics, c) a.characteristics = append(a.characteristics, c)
} }
return nil return nil
@ -638,7 +600,7 @@ func (a *att) handleData(handle uint16, buf []byte) error {
if debug { if debug {
println("att.handleData: attOpReadByGroupResponse") println("att.handleData: attOpReadByGroupResponse")
} }
cd.responded = true a.responded = true
lengthPerService := int(buf[1]) lengthPerService := int(buf[1])
@ -650,7 +612,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())
} }
cd.services = append(cd.services, service) a.services = append(a.services, service)
} }
return nil return nil
@ -672,8 +634,8 @@ func (a *att) handleData(handle uint16, buf []byte) error {
if debug { if debug {
println("att.handleData: attOpReadResponse") println("att.handleData: attOpReadResponse")
} }
cd.responded = true a.responded = true
cd.value = append(cd.value, buf[1:]...) a.value = append(a.value, buf[1:]...)
case attOpWriteReq: case attOpWriteReq:
if debug { if debug {
@ -692,7 +654,7 @@ func (a *att) handleData(handle uint16, buf []byte) error {
if debug { if debug {
println("att.handleData: attOpWriteResponse") println("att.handleData: attOpWriteResponse")
} }
cd.responded = true a.responded = true
case attOpPrepWriteReq: case attOpPrepWriteReq:
if debug { if debug {
@ -821,7 +783,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.localCharacteristics { for _, c := range a.characteristics {
if debug { if debug {
println("handleReadByTypeReq: looking at characteristic", c.startHandle, c.uuid.String()) println("handleReadByTypeReq: looking at characteristic", c.startHandle, c.uuid.String())
} }
@ -1044,28 +1006,16 @@ 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(handle uint16) error { func (a *att) clearResponse() {
cd, err := a.findConnectionData(handle) a.responded = false
if err != nil { a.errored = false
return err a.lastErrorOpcode = 0
} a.lastErrorHandle = 0
a.lastErrorCode = 0
cd.responded = false a.value = []byte{}
cd.errored = false
cd.lastErrorOpcode = 0
cd.lastErrorHandle = 0
cd.lastErrorCode = 0
cd.value = []byte{}
return nil
} }
func (a *att) waitUntilResponse(handle uint16) error { func (a *att) waitUntilResponse() 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 {
@ -1073,15 +1023,16 @@ func (a *att) waitUntilResponse(handle uint16) error {
} }
switch { switch {
case cd.responded: case a.responded:
return nil return nil
case (time.Now().UnixNano()-start)/int64(time.Second) > defaultTimeoutSeconds:
return ErrATTTimeout
default: default:
// check for timeout // check for timeout
time.Sleep(5 * time.Millisecond) if (time.Now().UnixNano()-start)/int64(time.Second) > 3 {
break
}
time.Sleep(100 * time.Millisecond)
} }
} }
@ -1099,34 +1050,17 @@ func (a *att) poll() error {
return nil return nil
} }
func (a *att) addConnection(handle uint16) error { func (a *att) addConnection(handle uint16) {
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) error { func (a *att) removeConnection(handle uint16) {
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:]...)
delete(a.connectionsData, handle) return
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 {
@ -1154,7 +1088,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.localCharacteristics = append(a.localCharacteristics, a.characteristics = append(a.characteristics,
rawCharacteristic{ rawCharacteristic{
startHandle: startHandle, startHandle: startHandle,
properties: uint8(properties), properties: uint8(properties),
@ -1175,29 +1109,11 @@ 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.localCharacteristics { for i := range a.characteristics {
if a.localCharacteristics[i].startHandle == hdl { if a.characteristics[i].startHandle == hdl {
return &a.localCharacteristics[i] return &a.characteristics[i]
} }
} }
return nil return nil
} }
func (a *att) findConnectionData(handle uint16) (*connectData, error) {
cd, ok := a.connectionsData[handle]
if !ok {
return nil, ErrATTUnknownConnection
}
return cd, nil
}
func (a *att) lastError(handle uint16) (uint8, uint16, uint8) {
cd, err := a.findConnectionData(handle)
if err != nil {
return 0, 0, 0
}
return cd.lastErrorOpcode, cd.lastErrorHandle, cd.lastErrorCode
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

214
gap.go
View file

@ -55,34 +55,8 @@ type AdvertisementOptions struct {
Interval Duration Interval Duration
// ManufacturerData stores Advertising Data. // ManufacturerData stores Advertising Data.
ManufacturerData []ManufacturerDataElement // Keys are the Manufacturer ID to associate with the data.
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
@ -105,7 +79,7 @@ type ScanResult struct {
// Bluetooth address of the scanned device. // Bluetooth address of the scanned device.
Address Address Address Address
// Signal strength of the advertisement packet. // RSSI the last time a packet from this device has been received.
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
@ -136,13 +110,9 @@ type AdvertisementPayload interface {
// if this data is not available. // if this data is not available.
Bytes() []byte Bytes() []byte
// ManufacturerData returns a slice with all the manufacturer data present in the // ManufacturerData returns a map with all the manufacturer data present in the
// advertising. It may be empty. //advertising. IT may be empty.
ManufacturerData() []ManufacturerDataElement ManufacturerData() map[uint16][]byte
// 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.
@ -157,10 +127,7 @@ type AdvertisementFields struct {
ServiceUUIDs []UUID ServiceUUIDs []UUID
// ManufacturerData is the manufacturer data of the advertisement. // ManufacturerData is the manufacturer data of the advertisement.
ManufacturerData []ManufacturerDataElement ManufacturerData map[uint16][]byte
// ServiceData is the service data of the advertisement.
ServiceData []ServiceDataElement
} }
// advertisementFields wraps AdvertisementFields to implement the // advertisementFields wraps AdvertisementFields to implement the
@ -194,15 +161,10 @@ func (p *advertisementFields) Bytes() []byte {
} }
// ManufacturerData returns the underlying ManufacturerData field. // ManufacturerData returns the underlying ManufacturerData field.
func (p *advertisementFields) ManufacturerData() []ManufacturerDataElement { func (p *advertisementFields) ManufacturerData() map[uint16][]byte {
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
@ -292,58 +254,22 @@ 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() []ManufacturerDataElement { func (buf *rawAdvertisementPayload) ManufacturerData() map[uint16][]byte {
var manufacturerData []ManufacturerDataElement mData := make(map[uint16][]byte)
for index := 0; index < int(buf.len)+4; index += int(buf.data[index]) + 1 { data := buf.Bytes()
fieldLength := int(buf.data[index+0]) for len(data) >= 2 {
if fieldLength < 3 { fieldLength := data[0]
continue if int(fieldLength)+1 > len(data) {
// Invalid field length.
return nil
} }
fieldType := buf.data[index+1] // If this is the manufacturer data
if fieldType != 0xff { if byte(0xFF) == data[1] {
continue mData[uint16(data[2])+(uint16(data[3])<<8)] = data[4 : fieldLength+1]
} }
key := uint16(buf.data[index+2]) | uint16(buf.data[index+3])<<8 data = data[fieldLength+1:]
manufacturerData = append(manufacturerData, ManufacturerDataElement{
CompanyID: key,
Data: buf.data[index+4 : index+fieldLength+1],
})
} }
return manufacturerData return mData
}
// 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.
@ -374,88 +300,36 @@ func (buf *rawAdvertisementPayload) addFromOptions(options AdvertisementOptions)
} }
} }
for _, element := range options.ManufacturerData { if len(options.ManufacturerData) > 0 {
if !buf.addManufacturerData(element.CompanyID, element.Data) { buf.addManufacturerData(options.ManufacturerData)
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(key uint16, value []byte) (ok bool) { func (buf *rawAdvertisementPayload) addManufacturerData(manufacturerData map[uint16]interface{}) (ok bool) {
// Check whether the field can fit this manufacturer data. payloadData := buf.Bytes()
fieldLength := len(value) + 4 for manufacturerID, rawData := range manufacturerData {
if int(buf.len)+fieldLength > len(buf.data) { data := rawData.([]byte)
return false // Check if the manufacturer ID is within the range of 16 bits (0-65535).
} if manufacturerID > 0xFFFF {
// Invalid manufacturer ID.
// Add the data. return false
buf.data[buf.len+0] = uint8(fieldLength - 1) }
buf.data[buf.len+1] = 0xff
buf.data[buf.len+2] = uint8(key) fieldLength := len(data) + 3
buf.data[buf.len+3] = uint8(key >> 8)
copy(buf.data[buf.len+4:], value) // Build manufacturer ID parts
buf.len += uint8(fieldLength) manufacturerDataBit := byte(0xFF)
manufacturerIDPart1 := byte(manufacturerID & 0xFF)
return true manufacturerIDPart2 := byte((manufacturerID >> 8) & 0xFF)
}
payloadData = append(payloadData, byte(fieldLength), manufacturerDataBit, manufacturerIDPart1, manufacturerIDPart2)
// addServiceData adds service data ([]byte) entries to the advertisement payload. payloadData = append(payloadData, data...)
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)
} }
buf.len = uint8(len(payloadData))
copy(buf.data[:], payloadData)
return true return true
} }

View file

@ -53,29 +53,16 @@ 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: manufacturerData}, "ManufacturerData": {Value: options.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.
@ -279,13 +266,10 @@ 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")
var manufacturerData []ManufacturerDataElement manufacturerData := make(map[uint16][]byte)
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 = append(manufacturerData, ManufacturerDataElement{ manufacturerData[k] = v.Value().([]byte)
CompanyID: k,
Data: v.Value().([]byte),
})
} }
} }
@ -293,20 +277,6 @@ 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,
@ -315,7 +285,6 @@ func makeScanResult(props map[string]dbus.Variant) ScanResult {
LocalName: localName, LocalName: localName,
ServiceUUIDs: serviceUUIDs, ServiceUUIDs: serviceUUIDs,
ManufacturerData: manufacturerData, ManufacturerData: manufacturerData,
ServiceData: serviceData,
}, },
}, },
} }

View file

@ -1,4 +1,4 @@
//go:build hci || ninafw || cyw43439 //go:build ninafw
package bluetooth package bluetooth
@ -65,12 +65,8 @@ 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]))
@ -100,7 +96,7 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
}) })
a.hci.clearAdvData() a.hci.clearAdvData()
time.Sleep(5 * time.Millisecond) time.Sleep(10 * time.Millisecond)
default: default:
if !a.scanning { if !a.scanning {
@ -112,7 +108,7 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
lastUpdate = time.Now().UnixNano() lastUpdate = time.Now().UnixNano()
} }
time.Sleep(5 * time.Millisecond) time.Sleep(10 * time.Millisecond)
} }
} }
@ -182,7 +178,7 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, err
notificationRegistrations: make([]notificationRegistration, 0), notificationRegistrations: make([]notificationRegistration, 0),
}, },
} }
a.addConnection(d) a.connectedDevices = append(a.connectedDevices, d)
return d, nil return d, nil
@ -192,7 +188,7 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, err
break break
} }
time.Sleep(5 * time.Millisecond) time.Sleep(10 * time.Millisecond)
} }
} }
@ -232,7 +228,7 @@ func (d Device) Disconnect() error {
return err return err
} }
d.adapter.removeConnection(d) d.adapter.connectedDevices = []Device{}
return nil return nil
} }
@ -409,7 +405,7 @@ func (a *Advertisement) Start() error {
} }
} }
time.Sleep(5 * time.Millisecond) time.Sleep(10 * time.Millisecond)
} }
}() }()

View file

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

View file

@ -18,108 +18,6 @@ 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) {
@ -170,25 +68,18 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) {
// Wait for when advertisement has stopped by a call to StopScan(). // 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 error) stoppingChan := make(chan struct{})
// 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, _, arg unsafe.Pointer) { stoppedHandler := foundation.NewTypedEventHandler(ole.NewGUID(eventStoppedGuid), func(_ *foundation.TypedEventHandler, _, _ unsafe.Pointer) {
args := (*advertisement.BluetoothLEAdvertisementWatcherStoppedEventArgs)(arg) // Note: the args parameter has an Error property that should
errCode, err := args.GetError() // probably be checked, but I'm not sure when stopping the
if err != nil { // advertisement watcher could ever result in an error (except
// Got an error while getting the error value, that shouldn't // for bugs).
// 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()
@ -205,7 +96,8 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) {
} }
// Wait until advertisement has stopped, and finish. // Wait until advertisement has stopped, and finish.
return <-stoppingChan <-stoppingChan
return nil
} }
func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedEventArgs) ScanResult { func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedEventArgs) ScanResult {
@ -222,7 +114,7 @@ func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedE
Address: adr, Address: adr,
} }
var manufacturerData []ManufacturerDataElement var manufacturerData map[uint16][]byte = make(map[uint16][]byte)
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()
@ -231,10 +123,7 @@ 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 = append(manufacturerData, ManufacturerDataElement{ manufacturerData[companyID] = bufferToSlice(buffer)
CompanyID: companyID,
Data: bufferToSlice(buffer),
})
} }
} }
@ -252,7 +141,7 @@ func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedE
} }
func bufferToSlice(buffer *streams.IBuffer) []byte { func bufferToSlice(buffer *streams.IBuffer) []byte {
dataReader, _ := streams.DataReaderFromBuffer(buffer) dataReader, _ := streams.FromBuffer(buffer)
defer dataReader.Release() defer dataReader.Release()
bufferSize, _ := buffer.GetLength() bufferSize, _ := buffer.GetLength()
if bufferSize == 0 { if bufferSize == 0 {
@ -274,8 +163,6 @@ 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
} }
@ -290,7 +177,7 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, err
} }
// IAsyncOperation<BluetoothLEDevice> // IAsyncOperation<BluetoothLEDevice>
bleDeviceOp, err := bluetooth.BluetoothLEDeviceFromBluetoothAddressAsync(winAddr) bleDeviceOp, err := bluetooth.FromBluetoothAddressAsync(winAddr)
if err != nil { if err != nil {
return Device{}, err return Device{}, err
} }
@ -323,7 +210,7 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, err
// Windows does not support explicitly connecting to a device. // 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.GattSessionFromDeviceIdAsync(dID) // IAsyncOperation<GattSession> gattSessionOp, err := genericattributeprofile.FromDeviceIdAsync(dID) // IAsyncOperation<GattSession>
if err != nil { if err != nil {
return Device{}, err return Device{}, err
} }
@ -342,7 +229,7 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, err
return Device{}, err return Device{}, err
} }
return Device{address, bleDevice, newSession}, nil return Device{bleDevice, newSession}, nil
} }
// Disconnect from the BLE device. This method is non-blocking and does not // Disconnect from the BLE device. This method is non-blocking and does not

View file

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

View file

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

View file

@ -7,8 +7,6 @@ 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.
// //
@ -19,7 +17,7 @@ type CharacteristicConfig struct {
UUID UUID
Value []byte Value []byte
Flags CharacteristicPermissions Flags CharacteristicPermissions
WriteEvent WriteEvent WriteEvent func(client Connection, offset int, value []byte)
} }
// CharacteristicPermissions lists a number of basic permissions/capabilities // CharacteristicPermissions lists a number of basic permissions/capabilities

View file

@ -1,4 +1,4 @@
//go:build hci || ninafw || cyw43439 //go:build ninafw
package bluetooth package bluetooth
@ -53,16 +53,6 @@ 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())
} }
@ -81,17 +71,11 @@ 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.Write() || c.permissions.WriteWithoutResponse() || if !c.permissions.Notify() {
c.permissions.Notify() || c.permissions.Indicate()) { return 0, errNoNotify
return 0, errNoWrite
} }
hdl := c.adapter.getCharWriteHandler(c.handle) c.value = append([]byte{}, p...)
if hdl != nil {
hdl.callback(Connection(c.handle), 0, p)
}
copy(c.value, p)
if c.cccd&0x01 != 0 { if c.cccd&0x01 != 0 {
// send notification // send notification

View file

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

View file

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

View file

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

10
go.mod
View file

@ -1,12 +1,11 @@
module gitrepo.ru/neonxp/bluetooth module tinygo.org/x/bluetooth
go 1.20 go 1.18
require ( 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-20240509164145-4f7860a3bd2b github.com/saltosystems/winrt-go v0.0.0-20230921082907-2ab5b7d431e1
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
@ -17,9 +16,6 @@ require (
require ( require (
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect github.com/sirupsen/logrus v1.9.3 // indirect
github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef // indirect
github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899 // indirect
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 // indirect
golang.org/x/sys v0.11.0 // indirect golang.org/x/sys v0.11.0 // indirect
golang.org/x/term v0.11.0 // indirect golang.org/x/term v0.11.0 // indirect
) )

12
go.sum
View file

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

View file

@ -1,4 +1,4 @@
//go:build ninafw || hci || cyw43439 //go:build ninafw
package bluetooth package bluetooth
@ -6,6 +6,7 @@ import (
"encoding/binary" "encoding/binary"
"encoding/hex" "encoding/hex"
"errors" "errors"
"machine"
"time" "time"
) )
@ -66,11 +67,10 @@ const (
leMetaEventEnhancedConnectionComplete = 0x0A leMetaEventEnhancedConnectionComplete = 0x0A
leMetaEventDirectAdvertisingReport = 0x0B leMetaEventDirectAdvertisingReport = 0x0B
hciCommandPkt = 0x01 hciCommandPkt = 0x01
hciACLDataPkt = 0x02 hciACLDataPkt = 0x02
hciSynchronousDataPkt = 0x03 hciEventPkt = 0x04
hciEventPkt = 0x04 hciSecurityPkt = 0x06
hciSecurityPkt = 0x06
evtDisconnComplete = 0x05 evtDisconnComplete = 0x05
evtEncryptionChange = 0x08 evtEncryptionChange = 0x08
@ -87,11 +87,6 @@ const (
const ( const (
hciACLLenPos = 4 hciACLLenPos = 4
hciEvtLenPos = 2 hciEvtLenPos = 2
attCID = 0x0004
bleCTL = 0x0008
signalingCID = 0x0005
securityCID = 0x0006
) )
var ( var (
@ -118,23 +113,13 @@ 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 {
transport hciTransport uart *machine.UART
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
@ -143,34 +128,26 @@ type hci struct {
scanning bool scanning bool
advData leAdvertisingReport advData leAdvertisingReport
connectData leConnectData connectData leConnectData
maxPkt uint16
pendingPkt uint16
} }
func newHCI(t hciTransport) *hci { func newHCI(uart *machine.UART) *hci {
return &hci{ return &hci{
transport: t, uart: uart,
buf: make([]byte, 256), softCTS: machine.NoPin,
softRTS: machine.NoPin,
buf: make([]byte, 256),
} }
} }
func (h *hci) start() error { func (h *hci) start() error {
h.transport.startRead() if h.softRTS != machine.NoPin {
defer h.transport.endRead() h.softRTS.Low()
var data [32]byte defer h.softRTS.High()
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
}
continue for h.uart.Buffered() > 0 {
} h.uart.ReadByte()
return nil
} }
return nil return nil
@ -185,24 +162,22 @@ func (h *hci) reset() error {
} }
func (h *hci) poll() error { func (h *hci) poll() error {
h.transport.startRead() if h.softRTS != machine.NoPin {
defer h.transport.endRead() h.softRTS.Low()
defer h.softRTS.High()
}
i := 0 i := 0
for h.transport.Buffered() > 0 { for h.uart.Buffered() > 0 {
sz := h.transport.Buffered() data, _ := h.uart.ReadByte()
c := sz + 4 - (sz % 4) h.buf[i] = data
_, 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(), hex.EncodeToString(h.buf[:i])) println("hci error:", err.Error())
} }
i = 0 i = 0
time.Sleep(5 * time.Millisecond) time.Sleep(5 * time.Millisecond)
@ -210,13 +185,8 @@ 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)
} }
} }
@ -255,19 +225,9 @@ func (h *hci) processPacket(i int) (bool, error) {
} }
} }
case hciSynchronousDataPkt:
// not supported by BLE, so ignore
if i > 3 {
pktlen := int(h.buf[3])
if debug {
println("hci synchronous data:", i, pktlen, hex.EncodeToString(h.buf[:1+3+pktlen]))
}
return true, nil
}
default: default:
if debug { if debug {
println("unknown packet data:", hex.EncodeToString(h.buf[0:i])) println("unknown packet data:", h.buf[0])
} }
return true, ErrHCIUnknown return true, ErrHCIUnknown
} }
@ -297,26 +257,6 @@ 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
@ -411,21 +351,6 @@ 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)
@ -506,13 +431,28 @@ 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) {
return h.transport.Write(buf) if h.softCTS != machine.NoPin {
retries := writeAttempts
for h.softCTS.Get() {
retries--
if retries == 0 {
return 0, ErrHCITimeout
}
}
}
n, err := h.uart.Write(buf)
if err != nil {
return 0, err
}
return n, nil
} }
type aclDataHeader struct { type aclDataHeader struct {
@ -546,13 +486,6 @@ 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)
@ -574,7 +507,6 @@ 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)
@ -611,28 +543,8 @@ func (h *hci) handleEventData(buf []byte) error {
case evtNumCompPkts: case evtNumCompPkts:
if debug { if debug {
println("evtNumCompPkts", hex.EncodeToString(buf)) println("evtNumCompPkts")
} }
// count of handles
c := buf[2]
pkts := uint16(0)
for i := byte(0); i < c; i++ {
pkts += binary.LittleEndian.Uint16(buf[5+i*4:])
}
if pkts > 0 && h.pendingPkt > pkts {
h.pendingPkt -= pkts
} else {
h.pendingPkt = 0
}
if debug {
println("evtNumCompPkts", pkts, h.pendingPkt)
}
return nil
case evtLEMetaEvent: case evtLEMetaEvent:
if debug { if debug {
println("evtLEMetaEvent") println("evtLEMetaEvent")
@ -651,20 +563,7 @@ 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)
@ -752,10 +651,6 @@ 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
} }

View file

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

View file

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

View file

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

22
uuid.go
View file

@ -38,20 +38,6 @@ 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.
@ -82,14 +68,6 @@ func (uuid UUID) Get16Bit() uint16 {
return uint16(uuid[3]) return uint16(uuid[3])
} }
// Get32Bit returns the 32-bit version of this UUID. This is only valid if it
// actually is a 32-bit UUID, see Is32Bit.
func (uuid UUID) Get32Bit() uint32 {
// Note: using a Get* function as a getter because method names can't start
// with a number.
return uuid[3]
}
// Bytes returns a 16-byte array containing the raw UUID. // Bytes returns a 16-byte array containing the raw UUID.
func (uuid UUID) Bytes() [16]byte { func (uuid UUID) Bytes() [16]byte {
buf := [16]byte{} buf := [16]byte{}

View file

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

View file

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