Compare commits

..

1 commit

Author SHA1 Message Date
deadprogram
da24b75b33 linux: update go-bluetooth package to latest version with Bluez 5.55 compatible wrappers
Signed-off-by: deadprogram <ron@hybridgroup.com>
2020-12-10 07:13:34 +01:00
140 changed files with 11448 additions and 35335 deletions

48
.circleci/config.yml Normal file
View file

@ -0,0 +1,48 @@
version: 2.1
jobs:
build:
docker:
- image: tinygo/tinygo-dev
working_directory: /usr/local/go/src/github.com/tinygo-org/bluetooth
steps:
- checkout
- run: tinygo version
- run:
name: "Run unit tests"
command: go test
- run:
name: "Run TinyGo smoke tests"
command: make smoketest-tinygo
- run:
name: "Run Linux smoke tests"
command: make smoketest-linux
- run:
name: "Install Windows cross compiler"
command: |
# Install the tools themselves.
apt-get install -y gcc-mingw-w64-x86-64
- run:
name: "Run Windows smoke tests"
command: make smoketest-windows
build-macos:
macos:
xcode: "10.1.0"
steps:
- checkout
- run:
name: "Install dependencies"
command: |
curl https://dl.google.com/go/go1.14.darwin-amd64.tar.gz -o go1.14.darwin-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.14.darwin-amd64.tar.gz
ln -s /usr/local/go/bin/go /usr/local/bin/go
- run: go version
- run:
name: "Run macOS smoke tests"
command: make smoketest-macos
workflows:
test-all:
jobs:
- build
- build-macos

View file

@ -1,30 +0,0 @@
name: Linux
on:
pull_request:
push:
branches:
- dev
- release
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
container: ghcr.io/tinygo-org/tinygo-dev
steps:
- name: Work around CVE-2022-24765
# We're not on a multi-user machine, so this is safe.
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Checkout
uses: actions/checkout@v3
- name: TinyGo version check
run: tinygo version
- name: Run unit tests
run: go test
- name: Run TinyGo smoke tests
run: make smoketest-tinygo
- name: Run Linux smoke tests
run: make smoketest-linux
- name: "Run Windows cross-compiled smoke tests"
run: make smoketest-windows

View file

@ -1,58 +0,0 @@
name: macOS
on:
pull_request:
push:
branches:
- dev
- release
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
macos-11:
name: macos-11
runs-on: macos-11
steps:
- name: Install Go
uses: actions/setup-go@v4.1.0
with:
go-version: '1.18.3'
- name: Checkout
uses: actions/checkout@v3.6.0
- name: Run unit tests
run: go test
- name: "Run macOS smoke tests"
run: make smoketest-macos
macos-12:
name: macos-12
runs-on: macos-12
steps:
- name: Install Go
uses: actions/setup-go@v4.1.0
with:
go-version: '1.21.0'
- name: Checkout
uses: actions/checkout@v3.6.0
- name: Run unit tests
run: go test
- name: "Run macOS smoke tests"
run: make smoketest-macos
macos-13:
name: macos-13
runs-on: macos-13
steps:
- name: Install Go
uses: actions/setup-go@v4.1.0
with:
go-version: '1.21.0'
- name: Checkout
uses: actions/checkout@v3.6.0
- name: Run unit tests
run: go test
- name: "Run macOS smoke tests"
run: make smoketest-macos

View file

@ -1,24 +0,0 @@
name: Windows
on:
pull_request:
push:
branches:
- dev
- release
jobs:
build-windows:
name: build-windows
runs-on: windows-2022
steps:
- name: Install Go
uses: actions/setup-go@v4.1.0
with:
go-version: '1.21.0'
- name: Checkout
uses: actions/checkout@v3.6.0
- name: Run unit tests
run: go test
- name: "Run Windows smoke tests"
run: make smoketest-windows

3
.gitmodules vendored
View file

@ -1,3 +0,0 @@
[submodule "bluetooth-numbers-database"]
path = bluetooth-numbers-database
url = https://github.com/NordicSemiconductor/bluetooth-numbers-database.git

View file

@ -1,218 +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
---
* **build**
- remove CGo dependencies for Windows cross-compiler tests
- add Windows to GH actions build jobs
- add macOS 12 to GH actions build jobs
* **core**
- go 1.18 and remove old-style build tags
- Noescape workaround
* **docs**
- update README to remove CGo requirement for Windows
- add documentation to heartrate-monitor
* **linux**
- Added option to add ManufacturerData to Advertisement
* **macos**
- enable support for duplicate chars by moving from a map to a slice
* **examples**
- Include WriteWithoutResponse permission, for examples, where Write exists
* **nordic semi**
- softdevice: added manufacturer data support
- softdevice: test creation of raw BLE advertisement packets
* **windows**
- update github.com/saltosystems/winrt-go to no longer require CGo
0.7.0
---
* **build**
- switch to ghcr.io for docker container
- update to actions/checkout@v3
- work around for CVE-2022-24765
* **core**
- gap: Set and SetRandom methods should have a pointer receiver
- mtu-{darwin,linux,windows,sd}: add get mtu function
- remove Addresser
- update uuid generation
* **docs**
- CONTRIBUTING: add note on new APIs
- correct badge link for GH actions
- README: add note on macOS Big Sur and iTerm2
* **linux**
- do not randomize order of returned discovered services/chars
- fix characteristic scan order
- implement disconnect handling
* **macos**
- implement disconnect handling
- fix characteristic scan order
* **examples**
- add examples/stop-advertisement
* **nordic semi**
- nrf528xx: handle BLE_GAP_EVT_PHY_UPDATE_REQUEST and explicitly ignore some other events
- softdevice: avoid a heap allocation in the SoftDevice event handler
* **windows**
- Added Indicate support to Windows driver
- gap/windows: Scan should set scanning mode to active to match other platforms
- support empty manufacturer data
- winrt-go: bump to latest
0.6.0
---
* **core**
- unify UUID16 creation for all platforms
- Improve UUID (#107)
- gap: stop advertising
- advertising: add manufacturer data field to advertisement payload
* **linux**
- gap: workaround for https://github.com/muka/go-bluetooth/issues/163
- update to latest muka/go-bluetooth
* **windows**
- add characteristic read, write and notify operations
- add characteristic discovery
- add service discovery
- add device connection and disconnection
- add winrt-go dependency and remove manually generated code
- disable cache when reading characteristics
* **macos**
- update to tinygo-org fork of cbgo v0.0.4
- use the same UUID format as expected by other standard
* **docs**
- update README with info on Windows support
* **build**
- add Github Action based CI build (#108)
0.5.0
---
* **core**
- update to drivers 0.20.0
- Fix ParseMAC bug
- Add //go:build lines for Go 1.18
* **nordic semi**
- nrf: fix CGo errors after TinyGo update
0.4.0
---
* **core**
- adapter: add host address function
* **linux**
- fixes bluez 0.55 service registration
- update muka/go-bluetooth to latest version
- gattc/linux: DiscoverServices times out in 10s
* **macos**
- make Adapter.Connect thread-safe
* **nordic semi**
- nrf51: fix assertHandler function signature
- nrf: add support for S113 SoftDevice
- nrf: update s140v7 SoftDevice version to latest, 7.3.0
* **examples**
- add scanner for Adafruit Clue
* **build**
- circleci: update xcode in use to 10.3.0
- modules: add tinyterm package for clue example
0.3.0
---
* **core**
- generate standard service and characteristic UUIDs from Nordic Semiconductor bluetooth numbers database
* **linux**
- downgrade to older version of go-bluetooth that appears to work correctly with BlueZ 5.50
* **macos**
- properly handle 16-bit UUIDs for service and characteristics in the unique format used by macOS
* **docs**
- add a few details on some newly supported boards
* **examples**
- use standard service and characteristic UUIDs
- correct heart rate monitor data format
0.2.0
---
* **core**

View file

@ -18,11 +18,6 @@ We probably have not implemented it yet. Your pull request adding the functional
Please open a Github issue. We want to help, and also make sure that there is no duplications of efforts. Sometimes what you need is already being worked on by someone else.
If your contribution includes a new API (one that does not exist yet for any BLE stack), please make sure it is portable:
* Ideally, add support for it to two different Bluetooth APIs at the same time, for example CoreBluetooth and BlueZ.
* If you are unable to do so, please explain (with links to documentation!) why this feature also fits a different API.
## How to use our Github repository
The `release` branch of this repo will always have the latest released version of the Go Bluetooth module. All of the active development work for the next release will take place in the `dev` branch. The Go Bluetooth module will use semantic versioning and will create a tag/release for each release.

View file

@ -1,4 +1,4 @@
Copyright (c) 2019-2023 TinyGo Authors. All rights reserved.
Copyright (c) 2019 Ayke van Laethem. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
@ -25,7 +25,3 @@ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
TinyGo Bluetooth includes data files from the Nordic Semiconductor Bluetooth
Numbers Database (https://github.com/NordicSemiconductor/bluetooth-numbers-database).
Copyright (c) 2019 - 2020, Nordic Semiconductor ASA. All rights reserved.

View file

@ -11,8 +11,6 @@ smoketest-tinygo:
@md5sum test.hex
$(TINYGO) build -o test.uf2 -size=short -target=circuitplay-bluefruit ./examples/circuitplay
@md5sum test.hex
$(TINYGO) build -o test.hex -size=short -target=circuitplay-bluefruit ./examples/connparams
@md5sum test.hex
$(TINYGO) build -o test.uf2 -size=short -target=circuitplay-bluefruit ./examples/discover
@md5sum test.hex
$(TINYGO) build -o test.hex -size=short -target=pca10040-s132v6 ./examples/heartrate
@ -25,56 +23,26 @@ smoketest-tinygo:
@md5sum test.hex
$(TINYGO) build -o test.hex -size=short -target=pca10040-s132v6 ./examples/scanner
@md5sum test.hex
$(TINYGO) build -o test.hex -size=short -target=pca10040-s132v6 ./examples/stop-advertisement
@md5sum test.hex
# Test some more boards that are not tested above.
$(TINYGO) build -o test.hex -size=short -target=pca10056-s140v7 ./examples/advertisement
@md5sum test.hex
$(TINYGO) build -o test.hex -size=short -target=microbit-s110v8 ./examples/nusserver
@md5sum test.hex
$(TINYGO) build -o test.hex -size=short -target=microbit-v2-s113v7 ./examples/nusserver
@md5sum test.hex
$(TINYGO) build -o test.uf2 -size=short -target=nano-rp2040 ./examples/discover
@md5sum test.hex
$(TINYGO) build -o test.uf2 -size=short -target=arduino-nano33 ./examples/discover
@md5sum test.hex
$(TINYGO) build -o test.uf2 -size=short -target=pyportal ./examples/discover
@md5sum test.hex
$(TINYGO) build -o test.uf2 -size=short -target=nano-rp2040 ./examples/advertisement
@md5sum test.hex
$(TINYGO) build -o test.uf2 -size=short -target=circuitplay-express -tags="hci hci_uart" ./examples/advertisement
@md5sum test.hex
$(TINYGO) build -o test.uf2 -size=short -target=pico-w ./examples/discover
@md5sum test.hex
$(TINYGO) build -o test.uf2 -size=short -target=badger2040-w ./examples/advertisement
@md5sum test.hex
smoketest-linux:
# Test on Linux.
GOOS=linux go build -o /tmp/go-build-discard ./examples/advertisement
GOOS=linux go build -o /tmp/go-build-discard ./examples/connparams
GOOS=linux go build -o /tmp/go-build-discard ./examples/heartrate
GOOS=linux go build -o /tmp/go-build-discard ./examples/heartrate-monitor
GOOS=linux go build -o /tmp/go-build-discard ./examples/nusserver
GOOS=linux go build -o /tmp/go-build-discard ./examples/scanner
GOOS=linux go build -o /tmp/go-build-discard ./examples/discover
smoketest-windows:
# Test on Windows.
GOOS=windows go build -o /tmp/go-build-discard ./examples/scanner
GOOS=windows go build -o /tmp/go-build-discard ./examples/discover
GOOS=windows go build -o /tmp/go-build-discard ./examples/heartrate-monitor
GOOS=windows go build -o /tmp/go-build-discard ./examples/advertisement
GOOS=windows go build -o /tmp/go-build-discard ./examples/heartrate
GOOS=windows CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc go build -o /tmp/go-build-discard ./examples/scanner
smoketest-macos:
# Test on macos.
GOOS=darwin CGO_ENABLED=1 go build -o /tmp/go-build-discard ./examples/scanner
GOOS=darwin CGO_ENABLED=1 go build -o /tmp/go-build-discard ./examples/discover
GOOS=darwin CGO_ENABLED=1 go build -o /tmp/go-build-discard ./examples/nusclient
GOOS=darwin CGO_ENABLED=1 go build -o /tmp/go-build-discard ./examples/heartrate-monitor
gen-uuids:
# generate the standard service and characteristic UUIDs
go run ./tools/gen-service-uuids/main.go
go run ./tools/gen-characteristic-uuids/main.go

101
README.md
View file

@ -2,13 +2,14 @@
[![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)
[![CircleCI](https://circleci.com/gh/tinygo-org/bluetooth/tree/dev.svg?style=svg)](https://circleci.com/gh/tinygo-org/bluetooth/tree/dev)
Go Bluetooth is a cross-platform package for using [Bluetooth Low Energy](https://en.wikipedia.org/wiki/Bluetooth_Low_Energy) hardware from the Go programming language.
It works on typical operating systems such as [Linux](#linux), [macOS](#macos), and [Windows](#windows).
It can also be used running "bare metal" on microcontrollers produced by [Nordic Semiconductor](https://www.nordicsemi.com/) or using the Bluetooth Host Controller Interface (HCI) by using [TinyGo](https://tinygo.org/).
It can also be used running "bare metal" on microcontrollers produced by [Nordic Semiconductor](https://www.nordicsemi.com/) by using [TinyGo](https://tinygo.org/).
The Go Bluetooth package can be used to create both Bluetooth Low Energy Centrals as well as to create Bluetooth Low Energy Peripherals.
@ -22,7 +23,7 @@ This example shows a central that scans for peripheral devices and then displays
package main
import (
"gitrepo.ru/neonxp/bluetooth"
"tinygo.org/x/bluetooth"
)
var adapter = bluetooth.DefaultAdapter
@ -58,7 +59,7 @@ package main
import (
"time"
"gitrepo.ru/neonxp/bluetooth"
"tinygo.org/x/bluetooth"
)
var adapter = bluetooth.DefaultAdapter
@ -92,21 +93,21 @@ func must(action string, err error) {
## Current support
| | Linux | macOS | Windows | Nordic Semi | ESP32 (NINA-FW) | CYW43439 (RP2040-W) |
| -------------------------------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------- |
| API used | BlueZ | CoreBluetooth | WinRT | SoftDevice | HCI | HCI |
| Scanning | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Connect to peripheral | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Write peripheral characteristics | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Receive notifications | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Advertisement | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Local services | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Local characteristics | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Send notifications | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| | Linux | macOS | Windows | Nordic Semi |
| -------------------------------- | ------------------ | ------------------ | ------------------ | ------------------ |
| API used | BlueZ | CoreBluetooth | WinRT | SoftDevice |
| Scanning | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Connect to peripheral | :heavy_check_mark: | :heavy_check_mark: | :x: | :heavy_check_mark: |
| Write peripheral characteristics | :heavy_check_mark: | :heavy_check_mark: | :x: | :heavy_check_mark: |
| Receive notifications | :heavy_check_mark: | :heavy_check_mark: | :x: | :heavy_check_mark: |
| Advertisement | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: |
| Local services | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: |
| Local characteristics | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: |
| Send notifications | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: |
## Linux
Go Bluetooth support for Linux uses [BlueZ](http://www.bluez.org/) via the [D-Bus](https://en.wikipedia.org/wiki/D-Bus) interface. This should work with most distros that support BlueZ such as Ubuntu, Debian, Fedora, and Arch Linux, among others.
Go Bluetooth support for Linux uses [BlueZ](http://www.bluez.org/) via the [D-Bus](https://en.wikipedia.org/wiki/D-Bus) interface thanks to the https://github.com/muka/go-bluetooth package. This should work with most distros that support BlueZ such as Ubuntu, Debian, Fedora, and Arch Linux, among others.
Linux can be used both as a BLE Central or as a BLE Peripheral.
@ -130,7 +131,7 @@ After you have followed the installation, you should be able to compile/run the
## macOS
Go Bluetooth support for macOS uses the [CoreBluetooth](https://developer.apple.com/documentation/corebluetooth?language=objc) libraries thanks to the https://github.com/tinygo-org/cbgo fork of the `cbgo` package.
Go Bluetooth support for macOS uses the [CoreBluetooth](https://developer.apple.com/documentation/corebluetooth?language=objc) libraries thanks to the https://github.com/JuulLabs-OSS/cbgo package.
As a result, it should work with most versions of macOS, although it will require compiling using whatever specific version of XCode is required by your version of the operating system.
@ -153,21 +154,17 @@ After you have followed the installation, you should be able to compile/run the
cd bluetooth
go run ./examples/scanner
### Troubleshooting
There is a known issue with iTerm2 and the Bluetooth package. If you are getting a message like `abort: trap`, try whitelisting iTerm2 manually through System Settings -> Privacy & Security -> Bluetooth.
## Windows
Go Bluetooth support for Windows uses the [WinRT Bluetooth](https://docs.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothadapter?view=winrt-19041) interfaces by way of the https://github.com/saltosystems/winrt-go package.
Go Bluetooth support for Windows uses the [WinRT Bluetooth](https://docs.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothadapter?view=winrt-19041) interfaces by way of the https://github.com/tinygo-org/bluetooth/winbt package that is part of this package.
The Windows support only can only act as a BLE Central at this time, with some additional development work needed for full functionality.
The Windows support is still experimental, and needs additional development to be useful. At this time, it can only be used to perform scanning operations as a BLE Central.
For specifics please see https://github.com/tinygo-org/bluetooth/issues/13
### Installation
Only the Go compiler itself is needed to compile Go Bluetooth code targeting Windows.
You can obtain the Go Bluetooth package using Git:
Once you have done this, you can obtain the Go Bluetooth package using Git:
git clone https://github.com/tinygo-org/bluetooth.git
@ -213,15 +210,8 @@ For example, this command can be used to compile and flash an Adafruit Circuit P
tinygo flash -target circuitplay-bluefruit ./examples/circuitplay
There are other boards with TinyGo support that also use the same UF2 bootloader with pre-loaded SoftDevice. They include:
* [Nice Keyboards nice!nano](https://nicekeyboards.com/products/nice-nano-v1-0)
* [Makerdiary nRF52840 MDK USB Dongle](https://wiki.makerdiary.com/nrf52840-mdk-usb-dongle/)
### BBC micro:bit
#### Version 1
The [BBC micro:bit](https://microbit.org/) uses an nRF51 chip with a CMSIS-DAP interface.
You will need to install OpenOCD (http://openocd.org/) to flash the board.
@ -235,12 +225,6 @@ Once you have copied the SoftDevice firmware to the BBC micro:bit, you can then
tinygo flash -target=microbit-s110v8 ./examples/heartrate
#### Version 2
The [BBC micro:bit v2](https://microbit.org/new-microbit/) uses an nRF52833 chip with a CMSIS-DAP interface.
Support for the v2 will be available soon.
### Supported Chips
The following Nordic Semiconductor chips are currently supported:
@ -264,45 +248,6 @@ After that, don't reset the board but instead flash a new program to it. For exa
Flashing will normally reset the board.
## ESP32 (NINA)
Go Bluetooth has bare metal support for boards that include a separate ESP32 Bluetooth Low Energy radio co-processor. The ESP32 must be running the Arduino or Adafruit `nina_fw` firmware.
Several boards created by Adafruit and Arduino already have the `nina-fw` firmware pre-loaded. This means you can use TinyGo and the Go Bluetooth package without any additional steps required.
Currently supported boards include:
* [Adafruit Metro M4 AirLift](https://www.adafruit.com/product/4000)
* [Adafruit PyBadge](https://www.adafruit.com/product/4200) with [AirLift WiFi FeatherWing](https://www.adafruit.com/product/4264)
* [Adafruit PyPortal](https://www.adafruit.com/product/4116)
* [Arduino Nano 33 IoT](https://docs.arduino.cc/hardware/nano-33-iot)
* [Arduino Nano RP2040 Connect](https://docs.arduino.cc/hardware/nano-rp2040-connect)
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 an Arduino Nano RP2040 Connect board with the example we provide that turns it into a BLE peripheral to act like a heart rate monitor:
tinygo flash -target nano-rp2040 ./examples/heartrate
If you want more information about the `nina-fw` firmware, or want to add support for other ESP32-equipped boards, please see https://github.com/arduino/nina-fw
## CYW43439 (RP2040-W)
Go Bluetooth has bare metal support for boards that include a separate CYW43439 Bluetooth Low Energy radio co-processor.
Currently supported boards include:
* [Raspberry Pi Pico RP2040-W](https://www.raspberrypi.com/documentation/microcontrollers/raspberry-pi-pico.html#raspberry-pi-pico-w)
* [Pimoroni Badger2040-W](https://shop.pimoroni.com/products/badger-2040-w)
After you have installed TinyGo and the Go Bluetooth package, you should be able to compile/run code for your device.
For example, this command can be used to compile and flash a Pico RP2040-W board with the example we provide that turns it into a BLE peripheral to act like a heart rate monitor:
tinygo flash -target pico-w ./examples/heartrate
If you want more information about the `cyw43439` support, please see https://github.com/soypat/cyw43439
## API stability
**The API is not stable!** Because many features are not yet implemented and some platforms (e.g. Windows and macOS) are not yet fully supported, it's hard to say what a good API will be. Therefore, if you want stability you should pick a particular git commit and use that. Go modules can be useful for this purpose.

View file

@ -1,8 +1,11 @@
package bluetooth
// Set this to true to print debug messages, for example for unknown events.
const debug = false
// SetConnectHandler sets a handler function to be called whenever the adaptor connects
// or disconnects. You must call this before you call adaptor.Connect() for centrals
// or adaptor.Start() for peripherals in order for it to work.
func (a *Adapter) SetConnectHandler(c func(device Device, connected bool)) {
func (a *Adapter) SetConnectHandler(c func(device Addresser, connected bool)) {
a.connectHandler = c
}

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

@ -2,10 +2,9 @@ package bluetooth
import (
"errors"
"sync"
"time"
"github.com/tinygo-org/cbgo"
"github.com/JuulLabs-OSS/cbgo"
)
// Adapter is a connection to BLE devices.
@ -19,23 +18,19 @@ type Adapter struct {
peripheralFoundHandler func(*Adapter, ScanResult)
scanChan chan error
poweredChan chan error
connectChan chan cbgo.Peripheral
// connectMap is a mapping of peripheralId -> chan cbgo.Peripheral,
// used to allow multiple callers to call Connect concurrently.
connectMap sync.Map
connectHandler func(device Device, connected bool)
connectHandler func(device Addresser, connected bool)
}
// DefaultAdapter is the default adapter on the system.
//
// Make sure to call Enable() before using it to initialize the adapter.
var DefaultAdapter = &Adapter{
cm: cbgo.NewCentralManager(nil),
pm: cbgo.NewPeripheralManager(nil),
connectMap: sync.Map{},
connectHandler: func(device Device, connected bool) {
cm: cbgo.NewCentralManager(nil),
pm: cbgo.NewPeripheralManager(nil),
connectChan: make(chan cbgo.Peripheral),
connectHandler: func(device Addresser, connected bool) {
return
},
}
@ -100,35 +95,10 @@ func (cmd *centralManagerDelegate) DidDiscoverPeripheral(cmgr cbgo.CentralManage
}
}
// DidDisconnectPeripheral when peripheral is disconnected.
func (cmd *centralManagerDelegate) DidDisconnectPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peripheral, err error) {
id := prph.Identifier().String()
addr := Address{}
uuid, _ := ParseUUID(id)
addr.UUID = uuid
cmd.a.connectHandler(Device{Address: addr}, false)
// like with DidConnectPeripheral, check if we have a chan allocated for this and send through the peripheral
// this will only be true if the receiving side is still waiting for a connection to complete
if ch, ok := cmd.a.connectMap.LoadAndDelete(id); ok {
ch.(chan cbgo.Peripheral) <- prph
}
}
// DidConnectPeripheral when peripheral is connected.
func (cmd *centralManagerDelegate) DidConnectPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peripheral) {
id := prph.Identifier().String()
// Check if we have a chan allocated for this peripheral, and remove it
// from the map if so (it's single-use, will be garbage collected after
// receiver receives the peripheral).
//
// If we don't have a chan allocated, the receiving side timed out, so
// ignore this connection.
if ch, ok := cmd.a.connectMap.LoadAndDelete(id); ok {
// Unblock now that we're connected.
ch.(chan cbgo.Peripheral) <- prph
}
// Unblock now that we're connected.
cmd.a.connectChan <- prph
}
// makeScanResult creates a ScanResult when peripheral is found.
@ -141,34 +111,6 @@ func makeScanResult(prph cbgo.Peripheral, advFields cbgo.AdvFields, rssi int) Sc
serviceUUIDs = append(serviceUUIDs, parsedUUID)
}
var manufacturerData []ManufacturerDataElement
if len(advFields.ManufacturerData) > 2 {
// Note: CoreBluetooth seems to assume there can be only one
// manufacturer data fields in an advertisement packet, while the
// specification allows multiple such fields. See the Bluetooth Core
// Specification Supplement, table 1.1:
// https://www.bluetooth.com/specifications/css-11/
manufacturerID := uint16(advFields.ManufacturerData[0])
manufacturerID += uint16(advFields.ManufacturerData[1]) << 8
manufacturerData = append(manufacturerData, ManufacturerDataElement{
CompanyID: manufacturerID,
Data: advFields.ManufacturerData[2:],
})
}
var serviceData []ServiceDataElement
for _, svcData := range advFields.ServiceData {
cbgoUUID := svcData.UUID
uuid, err := ParseUUID(cbgoUUID.String())
if err != nil {
continue
}
serviceData = append(serviceData, ServiceDataElement{
UUID: uuid,
Data: svcData.Data,
})
}
// Peripheral UUID is randomized on macOS, which means to
// different centrals it will appear to have a different UUID.
return ScanResult{
@ -178,10 +120,8 @@ func makeScanResult(prph cbgo.Peripheral, advFields cbgo.AdvFields, rssi int) Sc
},
AdvertisementPayload: &advertisementFields{
AdvertisementFields{
LocalName: advFields.LocalName,
ServiceUUIDs: serviceUUIDs,
ManufacturerData: manufacturerData,
ServiceData: serviceData,
LocalName: advFields.LocalName,
ServiceUUIDs: serviceUUIDs,
},
},
}

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

@ -1,4 +1,4 @@
//go:build !baremetal
// +build !baremetal
// Some documentation for the BlueZ D-Bus interface:
// https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc
@ -6,24 +6,17 @@
package bluetooth
import (
"errors"
"fmt"
"github.com/godbus/dbus/v5"
"github.com/muka/go-bluetooth/api"
"github.com/muka/go-bluetooth/bluez/profile/adapter"
)
const defaultAdapter = "hci0"
type Adapter struct {
adapter *adapter.Adapter1
id string
scanCancelChan chan struct{}
bus *dbus.Conn
bluez dbus.BusObject // object at /
adapter dbus.BusObject // object at /org/bluez/hciX
address string
cancelChan chan struct{}
defaultAdvertisement *Advertisement
connectHandler func(device Device, connected bool)
connectHandler func(device Addresser, connected bool)
}
// DefaultAdapter is the default adapter on the system. On Linux, it is the
@ -31,40 +24,20 @@ type Adapter struct {
//
// Make sure to call Enable() before using it to initialize the adapter.
var DefaultAdapter = &Adapter{
id: defaultAdapter,
connectHandler: func(device Device, connected bool) {
connectHandler: func(device Addresser, connected bool) {
return
},
}
// Enable configures the BLE stack. It must be called before any
// Bluetooth-related calls (unless otherwise indicated).
func (a *Adapter) Enable() (err error) {
bus, err := dbus.SystemBus()
if err != nil {
return err
}
a.bus = bus
a.bluez = a.bus.Object("org.bluez", dbus.ObjectPath("/"))
a.adapter = a.bus.Object("org.bluez", dbus.ObjectPath("/org/bluez/"+a.id))
addr, err := a.adapter.GetProperty("org.bluez.Adapter1.Address")
if err != nil {
if err, ok := err.(dbus.Error); ok && err.Name == "org.freedesktop.DBus.Error.UnknownObject" {
return fmt.Errorf("bluetooth: adapter %s does not exist", a.adapter.Path())
if a.id == "" {
a.adapter, err = api.GetDefaultAdapter()
if err != nil {
return
}
return fmt.Errorf("could not activate BlueZ adapter: %w", err)
a.id, err = a.adapter.GetAdapterID()
}
addr.Store(&a.address)
return nil
}
func (a *Adapter) Address() (MACAddress, error) {
if a.address == "" {
return MACAddress{}, errors.New("adapter not enabled")
}
mac, err := ParseMAC(a.address)
if err != nil {
return MACAddress{}, err
}
return MACAddress{MAC: mac}, nil
}

View file

@ -1,134 +0,0 @@
//go:build ninafw
package bluetooth
import (
"machine"
"time"
)
const maxConnections = 1
// Adapter represents the HCI connection to the NINA fw using the hardware UART.
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 {
// reset the NINA in BLE mode
machine.NINA_CS.Configure(machine.PinConfig{Mode: machine.PinOutput})
machine.NINA_CS.Low()
if machine.NINA_RESET_INVERTED {
resetNINAInverted()
} else {
resetNINA()
}
// serial port for nina chip
uart := machine.UART_NINA
cfg := machine.UARTConfig{
TX: machine.NINA_TX,
RX: machine.NINA_RX,
BaudRate: machine.NINA_BAUDRATE,
}
if !machine.NINA_SOFT_FLOWCONTROL {
cfg.CTS = machine.NINA_CTS
cfg.RTS = machine.NINA_RTS
}
uart.Configure(cfg)
transport := &hciUART{uart: uart}
if machine.NINA_SOFT_FLOWCONTROL {
machine.NINA_RTS.Configure(machine.PinConfig{Mode: machine.PinOutput})
machine.NINA_RTS.High()
machine.NINA_CTS.Configure(machine.PinConfig{Mode: machine.PinInput})
}
a.hci, a.att = newBLEStack(transport)
return a.enable()
}
func resetNINA() {
machine.NINA_RESETN.Configure(machine.PinConfig{Mode: machine.PinOutput})
machine.NINA_RESETN.High()
time.Sleep(100 * time.Millisecond)
machine.NINA_RESETN.Low()
time.Sleep(1000 * time.Millisecond)
}
func resetNINAInverted() {
machine.NINA_RESETN.Configure(machine.PinConfig{Mode: machine.PinOutput})
machine.NINA_RESETN.Low()
time.Sleep(100 * time.Millisecond)
machine.NINA_RESETN.High()
time.Sleep(1000 * time.Millisecond)
}
type hciUART struct {
uart *machine.UART
}
func (h *hciUART) startRead() {
if machine.NINA_SOFT_FLOWCONTROL {
machine.NINA_RTS.Low()
}
}
func (h *hciUART) endRead() {
if machine.NINA_SOFT_FLOWCONTROL {
machine.NINA_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 machine.NINA_SOFT_FLOWCONTROL {
retries := writeAttempts
for machine.NINA_CTS.Get() {
retries--
if retries == 0 {
return 0, ErrHCITimeout
}
}
}
n, err := h.uart.Write(buf)
if err != nil {
return 0, err
}
return n, nil
}

View file

@ -1,13 +1,17 @@
//go:build softdevice && s110v8
// +build softdevice,s110v8
package bluetooth
/*
// Define SoftDevice functions as regular function declarations (not inline
// static functions).
#define SVCALL_AS_NORMAL_FUNCTION
#include "nrf_sdm.h"
#include "ble.h"
#include "ble_gap.h"
void assertHandler(uint32_t pc, uint16_t line_number, const uint8_t * p_file_name);
void assertHandler(void);
*/
import "C"
@ -42,13 +46,8 @@ func handleEvent() {
gapEvent := eventBuf.evt.unionfield_gap_evt()
switch id {
case C.BLE_GAP_EVT_CONNECTED:
currentConnection.handle.Reg = uint16(gapEvent.conn_handle)
connectEvent := gapEvent.params.unionfield_connected()
device := Device{
Address: Address{makeMACAddress(connectEvent.peer_addr)},
connectionHandle: gapEvent.conn_handle,
}
DefaultAdapter.connectHandler(device, true)
currentConnection.Reg = gapEvent.conn_handle
DefaultAdapter.connectHandler(nil, true)
case C.BLE_GAP_EVT_DISCONNECTED:
if defaultAdvertisement.isAdvertising.Get() != 0 {
// The advertisement was running but was automatically stopped
@ -59,11 +58,8 @@ func handleEvent() {
// necessary.
defaultAdvertisement.start()
}
currentConnection.handle.Reg = C.BLE_CONN_HANDLE_INVALID
device := Device{
connectionHandle: gapEvent.conn_handle,
}
DefaultAdapter.connectHandler(device, false)
currentConnection.Reg = C.BLE_CONN_HANDLE_INVALID
DefaultAdapter.connectHandler(nil, false)
case C.BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST:
// Respond with the default PPCP connection parameters by passing
// nil:
@ -110,20 +106,3 @@ func handleEvent() {
}
}
}
func (a *Adapter) Address() (MACAddress, error) {
var addr C.ble_gap_addr_t
errCode := C.sd_ble_gap_address_get(&addr)
if errCode != 0 {
return MACAddress{}, Error(errCode)
}
return MACAddress{MAC: makeAddress(addr.addr)}, nil
}
// Convert a C.ble_gap_addr_t to a MACAddress struct.
func makeMACAddress(addr C.ble_gap_addr_t) MACAddress {
return MACAddress{
MAC: makeAddress(addr.addr),
isRandom: addr.addr_type != 0,
}
}

View file

@ -1,268 +0,0 @@
//go:build (softdevice && s132v6) || (softdevice && s140v6) || (softdevice && s140v7)
package bluetooth
// This file implements the event handler for SoftDevices with full support:
// both central and peripheral mode. This includes S132 and S140.
/*
#include "nrf_sdm.h"
#include "nrf_nvic.h"
#include "ble.h"
#include "ble_gap.h"
*/
import "C"
import (
"unsafe"
)
func handleEvent() {
id := eventBuf.header.evt_id
switch {
case id >= C.BLE_GAP_EVT_BASE && id <= C.BLE_GAP_EVT_LAST:
gapEvent := eventBuf.evt.unionfield_gap_evt()
switch id {
case C.BLE_GAP_EVT_CONNECTED:
connectEvent := gapEvent.params.unionfield_connected()
device := Device{
Address: Address{makeMACAddress(connectEvent.peer_addr)},
connectionHandle: gapEvent.conn_handle,
}
switch connectEvent.role {
case C.BLE_GAP_ROLE_PERIPH:
if debug {
println("evt: connected in peripheral role")
}
currentConnection.handle.Reg = uint16(gapEvent.conn_handle)
DefaultAdapter.connectHandler(device, true)
case C.BLE_GAP_ROLE_CENTRAL:
if debug {
println("evt: connected in central role")
}
connectionAttempt.connectionHandle = gapEvent.conn_handle
connectionAttempt.state.Set(2) // connection was successful
DefaultAdapter.connectHandler(device, true)
}
case C.BLE_GAP_EVT_DISCONNECTED:
if debug {
println("evt: disconnected")
}
// Clean up state for this connection.
for i, cb := range gattcNotificationCallbacks {
if uint16(cb.connectionHandle) == currentConnection.handle.Reg {
gattcNotificationCallbacks[i].valueHandle = 0 // 0 means invalid
}
}
currentConnection.handle.Reg = C.BLE_CONN_HANDLE_INVALID
// Auto-restart advertisement if needed.
if defaultAdvertisement.isAdvertising.Get() != 0 {
// The advertisement was running but was automatically stopped
// by the connection event.
// Note that it cannot be restarted during connect like this,
// because it would need to be reconfigured as a non-connectable
// advertisement. That's left as a future addition, if
// necessary.
C.sd_ble_gap_adv_start(defaultAdvertisement.handle, C.BLE_CONN_CFG_TAG_DEFAULT)
}
device := Device{
connectionHandle: gapEvent.conn_handle,
}
DefaultAdapter.connectHandler(device, false)
case C.BLE_GAP_EVT_CONN_PARAM_UPDATE:
if debug {
// Print connection parameters for easy debugging.
params := gapEvent.params.unionfield_conn_param_update().conn_params
interval_ms := params.min_conn_interval * 125 / 100 // min and max are the same here
print("conn param update interval=", interval_ms, "ms latency=", params.slave_latency, " timeout=", params.conn_sup_timeout*10, "ms")
println()
}
case C.BLE_GAP_EVT_ADV_REPORT:
advReport := gapEvent.params.unionfield_adv_report()
if debug && &scanReportBuffer.data[0] != (*byte)(unsafe.Pointer(advReport.data.p_data)) {
// Sanity check.
panic("scanReportBuffer != advReport.p_data")
}
// Prepare the globalScanResult, which will be passed to the
// callback.
scanReportBuffer.len = byte(advReport.data.len)
globalScanResult.RSSI = int16(advReport.rssi)
globalScanResult.Address = Address{
makeMACAddress(advReport.peer_addr),
}
globalScanResult.AdvertisementPayload = &scanReportBuffer
// Signal to the main thread that there was a scan report.
// Scanning will be resumed (from the main thread) once the scan
// report has been processed.
gotScanReport.Set(1)
case C.BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST:
// Respond with the default PPCP connection parameters by passing
// nil:
// > If NULL is provided on a peripheral role, the parameters in the
// > PPCP characteristic of the GAP service will be used instead. If
// > NULL is provided on a central role and in response to a
// > BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST, the peripheral request
// > will be rejected
C.sd_ble_gap_conn_param_update(gapEvent.conn_handle, nil)
case C.BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST:
// We need to respond with sd_ble_gap_data_length_update. Setting
// both parameters to nil will make sure we send the default values.
C.sd_ble_gap_data_length_update(gapEvent.conn_handle, nil, nil)
case C.BLE_GAP_EVT_DATA_LENGTH_UPDATE:
// ignore confirmation of data length successfully updated
case C.BLE_GAP_EVT_PHY_UPDATE_REQUEST:
phyUpdateRequest := gapEvent.params.unionfield_phy_update_request()
C.sd_ble_gap_phy_update(gapEvent.conn_handle, &phyUpdateRequest.peer_preferred_phys)
case C.BLE_GAP_EVT_PHY_UPDATE:
// ignore confirmation of phy successfully updated
case C.BLE_GAP_EVT_TIMEOUT:
timeoutEvt := gapEvent.params.unionfield_timeout()
switch timeoutEvt.src {
case C.BLE_GAP_TIMEOUT_SRC_CONN:
// Failed to connect to a peripheral.
if debug {
println("gap timeout: conn")
}
connectionAttempt.state.Set(3) // connection timed out
default:
// For example a scan timeout.
if debug {
println("gap timeout: other")
}
}
default:
if debug {
println("unknown GAP event:", id)
}
}
case id >= C.BLE_GATTS_EVT_BASE && id <= C.BLE_GATTS_EVT_LAST:
gattsEvent := eventBuf.evt.unionfield_gatts_evt()
switch id {
case C.BLE_GATTS_EVT_WRITE:
writeEvent := gattsEvent.params.unionfield_write()
len := writeEvent.len - writeEvent.offset
data := (*[255]byte)(unsafe.Pointer(&writeEvent.data[0]))[:len:len]
handler := DefaultAdapter.getCharWriteHandler(writeEvent.handle)
if handler != nil {
handler.callback(Connection(gattsEvent.conn_handle), int(writeEvent.offset), data)
}
case C.BLE_GATTS_EVT_SYS_ATTR_MISSING:
// This event is generated when reading the Generic Attribute
// service. It appears to be necessary for bonded devices.
// From the docs:
// > If the pointer is NULL, the system attribute info is
// > initialized, assuming that the application does not have any
// > previously saved system attribute data for this device.
// Maybe we should look at the error, but as there's not really a
// way to handle it, ignore it.
C.sd_ble_gatts_sys_attr_set(gattsEvent.conn_handle, nil, 0, 0)
case C.BLE_GATTS_EVT_EXCHANGE_MTU_REQUEST:
// This event is generated by some devices. While we could support
// larger MTUs, this default MTU is supported everywhere.
C.sd_ble_gatts_exchange_mtu_reply(gattsEvent.conn_handle, C.BLE_GATT_ATT_MTU_DEFAULT)
case C.BLE_GATTS_EVT_HVN_TX_COMPLETE:
// ignore confirmation of a notification successfully sent
default:
if debug {
println("unknown GATTS event:", id, id-C.BLE_GATTS_EVT_BASE)
}
}
case id >= C.BLE_GATTC_EVT_BASE && id <= C.BLE_GATTC_EVT_LAST:
gattcEvent := eventBuf.evt.unionfield_gattc_evt()
switch id {
case C.BLE_GATTC_EVT_PRIM_SRVC_DISC_RSP:
discoveryEvent := gattcEvent.params.unionfield_prim_srvc_disc_rsp()
if debug {
println("evt: discovered primary service", discoveryEvent.count)
}
discoveringService.state.Set(2) // signal there is a result
if discoveryEvent.count >= 1 {
// Theoretically there may be more, but as we're only using
// sd_ble_gattc_primary_services_discover, there should only be
// one discovered service. Use the first as a sensible fallback.
discoveringService.startHandle.Set(discoveryEvent.services[0].handle_range.start_handle)
discoveringService.endHandle.Set(discoveryEvent.services[0].handle_range.end_handle)
discoveringService.uuid = discoveryEvent.services[0].uuid
} else {
// No service found.
discoveringService.startHandle.Set(0)
}
case C.BLE_GATTC_EVT_CHAR_DISC_RSP:
discoveryEvent := gattcEvent.params.unionfield_char_disc_rsp()
if debug {
println("evt: discovered characteristics", discoveryEvent.count)
}
if discoveryEvent.count >= 1 {
// There may be more, but for ease of implementing we only
// handle the first.
discoveringCharacteristic.handle_value.Set(discoveryEvent.chars[0].handle_value)
discoveringCharacteristic.char_props = discoveryEvent.chars[0].char_props
discoveringCharacteristic.uuid = discoveryEvent.chars[0].uuid
} else {
// zero indicates we received no characteristic, set handle_value to last
discoveringCharacteristic.handle_value.Set(0xffff)
}
case C.BLE_GATTC_EVT_DESC_DISC_RSP:
discoveryEvent := gattcEvent.params.unionfield_desc_disc_rsp()
if debug {
println("evt: discovered descriptors", discoveryEvent.count)
}
if discoveryEvent.count >= 1 {
// There may be more, but for ease of implementing we only
// handle the first.
uuid := discoveryEvent.descs[0].uuid
if uuid._type == C.BLE_UUID_TYPE_BLE && uuid.uuid == 0x2902 {
// Found a CCCD (Client Characteristic Configuration
// Descriptor), which has a 16-bit UUID with value 0x2902).
discoveringCharacteristic.handle_value.Set(discoveryEvent.descs[0].handle)
} else {
// Found something else?
// TODO: handle this properly by continuing the scan. For
// now, give up if we found something other than a CCCD.
if debug {
println(" found some other descriptor (unimplemented)")
}
}
}
case C.BLE_GATTC_EVT_READ_RSP:
readEvent := gattcEvent.params.unionfield_read_rsp()
if debug {
println("evt: read response, data length", readEvent.len)
}
readingCharacteristic.handle_value.Set(readEvent.handle)
readingCharacteristic.offset = readEvent.offset
readingCharacteristic.length = readEvent.len
// copy read event data into Go slice
copy(readingCharacteristic.value, (*[255]byte)(unsafe.Pointer(&readEvent.data[0]))[:readEvent.len:readEvent.len])
case C.BLE_GATTC_EVT_HVX:
hvxEvent := gattcEvent.params.unionfield_hvx()
switch hvxEvent._type {
case C.BLE_GATT_HVX_NOTIFICATION:
if debug {
println("evt: notification", hvxEvent.handle)
}
// Find the callback and call it (if there is any).
for _, callbackInfo := range gattcNotificationCallbacks {
if callbackInfo.valueHandle == hvxEvent.handle && callbackInfo.connectionHandle == gattcEvent.conn_handle {
// Create a Go slice from the data, to pass to the
// callback.
data := (*[255]byte)(unsafe.Pointer(&hvxEvent.data[0]))[:hvxEvent.len:hvxEvent.len]
if callbackInfo.callback != nil {
callbackInfo.callback(data)
}
break
}
}
}
default:
if debug {
println("unknown GATTC event:", id, id-C.BLE_GATTC_EVT_BASE)
}
}
default:
if debug {
println("unknown event:", id)
}
}
}

View file

@ -1,109 +0,0 @@
//go:build softdevice && s113v7
package bluetooth
// This file implements the event handler for SoftDevices with only peripheral
// mode support. This includes the S113.
/*
#include "nrf_sdm.h"
#include "nrf_nvic.h"
#include "ble.h"
#include "ble_gap.h"
*/
import "C"
import (
"unsafe"
)
func handleEvent() {
id := eventBuf.header.evt_id
switch {
case id >= C.BLE_GAP_EVT_BASE && id <= C.BLE_GAP_EVT_LAST:
gapEvent := eventBuf.evt.unionfield_gap_evt()
switch id {
case C.BLE_GAP_EVT_CONNECTED:
if debug {
println("evt: connected in peripheral role")
}
currentConnection.handle.Reg = uint16(gapEvent.conn_handle)
connectEvent := gapEvent.params.unionfield_connected()
device := Device{
Address: Address{makeMACAddress(connectEvent.peer_addr)},
connectionHandle: gapEvent.conn_handle,
}
DefaultAdapter.connectHandler(device, true)
case C.BLE_GAP_EVT_DISCONNECTED:
if debug {
println("evt: disconnected")
}
currentConnection.handle.Reg = C.BLE_CONN_HANDLE_INVALID
// Auto-restart advertisement if needed.
if defaultAdvertisement.isAdvertising.Get() != 0 {
// The advertisement was running but was automatically stopped
// by the connection event.
// Note that it cannot be restarted during connect like this,
// because it would need to be reconfigured as a non-connectable
// advertisement. That's left as a future addition, if
// necessary.
C.sd_ble_gap_adv_start(defaultAdvertisement.handle, C.BLE_CONN_CFG_TAG_DEFAULT)
}
device := Device{
connectionHandle: gapEvent.conn_handle,
}
DefaultAdapter.connectHandler(device, false)
case C.BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST:
// We need to respond with sd_ble_gap_data_length_update. Setting
// both parameters to nil will make sure we send the default values.
C.sd_ble_gap_data_length_update(gapEvent.conn_handle, nil, nil)
case C.BLE_GAP_EVT_DATA_LENGTH_UPDATE:
// ignore confirmation of data length successfully updated
case C.BLE_GAP_EVT_PHY_UPDATE_REQUEST:
phyUpdateRequest := gapEvent.params.unionfield_phy_update_request()
C.sd_ble_gap_phy_update(gapEvent.conn_handle, &phyUpdateRequest.peer_preferred_phys)
case C.BLE_GAP_EVT_PHY_UPDATE:
// ignore confirmation of phy successfully updated
default:
if debug {
println("unknown GAP event:", id)
}
}
case id >= C.BLE_GATTS_EVT_BASE && id <= C.BLE_GATTS_EVT_LAST:
gattsEvent := eventBuf.evt.unionfield_gatts_evt()
switch id {
case C.BLE_GATTS_EVT_WRITE:
writeEvent := gattsEvent.params.unionfield_write()
len := writeEvent.len - writeEvent.offset
data := (*[255]byte)(unsafe.Pointer(&writeEvent.data[0]))[:len:len]
handler := DefaultAdapter.getCharWriteHandler(writeEvent.handle)
if handler != nil {
handler.callback(Connection(gattsEvent.conn_handle), int(writeEvent.offset), data)
}
case C.BLE_GATTS_EVT_SYS_ATTR_MISSING:
// This event is generated when reading the Generic Attribute
// service. It appears to be necessary for bonded devices.
// From the docs:
// > If the pointer is NULL, the system attribute info is
// > initialized, assuming that the application does not have any
// > previously saved system attribute data for this device.
// Maybe we should look at the error, but as there's not really a
// way to handle it, ignore it.
C.sd_ble_gatts_sys_attr_set(gattsEvent.conn_handle, nil, 0, 0)
case C.BLE_GATTS_EVT_EXCHANGE_MTU_REQUEST:
// This event is generated by some devices. While we could support
// larger MTUs, this default MTU is supported everywhere.
C.sd_ble_gatts_exchange_mtu_reply(gattsEvent.conn_handle, C.BLE_GATT_ATT_MTU_DEFAULT)
case C.BLE_GATTS_EVT_HVN_TX_COMPLETE:
// ignore confirmation of a notification successfully sent
default:
if debug {
println("unknown GATTS event:", id, id-C.BLE_GATTS_EVT_BASE)
}
}
default:
if debug {
println("unknown event:", id)
}
}
}

View file

@ -1,10 +1,12 @@
//go:build (softdevice && s113v7) || (softdevice && s132v6) || (softdevice && s140v6) || (softdevice && s140v7)
// +build softdevice,!s110v8
package bluetooth
// This file defines the SoftDevice adapter for all nrf52-series chips.
/*
// Define SoftDevice functions as regular function declarations (not inline
// static functions).
#define SVCALL_AS_NORMAL_FUNCTION
#include "nrf_sdm.h"
#include "nrf_nvic.h"
#include "ble.h"
@ -31,9 +33,6 @@ var clockConfigXtal C.nrf_clock_lf_cfg_t = C.nrf_clock_lf_cfg_t{
accuracy: C.NRF_CLOCK_LF_ACCURACY_250_PPM,
}
//go:extern __app_ram_base
var appRAMBase [0]uint32
func (a *Adapter) enable() error {
// Enable the SoftDevice.
var clockConfig *C.nrf_clock_lf_cfg_t
@ -46,24 +45,219 @@ func (a *Adapter) enable() error {
}
// Enable the BLE stack.
appRAMBase := C.uint32_t(uintptr(unsafe.Pointer(&appRAMBase)))
appRAMBase := uint32(0x200039c0)
errCode = C.sd_ble_enable(&appRAMBase)
return makeError(errCode)
}
func (a *Adapter) Address() (MACAddress, error) {
var addr C.ble_gap_addr_t
errCode := C.sd_ble_gap_addr_get(&addr)
if errCode != 0 {
return MACAddress{}, Error(errCode)
}
return MACAddress{MAC: makeAddress(addr.addr)}, nil
}
func handleEvent() {
id := eventBuf.header.evt_id
switch {
case id >= C.BLE_GAP_EVT_BASE && id <= C.BLE_GAP_EVT_LAST:
gapEvent := eventBuf.evt.unionfield_gap_evt()
switch id {
case C.BLE_GAP_EVT_CONNECTED:
connectEvent := gapEvent.params.unionfield_connected()
switch connectEvent.role {
case C.BLE_GAP_ROLE_PERIPH:
if debug {
println("evt: connected in peripheral role")
}
currentConnection.Reg = gapEvent.conn_handle
DefaultAdapter.connectHandler(nil, true)
case C.BLE_GAP_ROLE_CENTRAL:
if debug {
println("evt: connected in central role")
}
connectionAttempt.connectionHandle = gapEvent.conn_handle
connectionAttempt.state.Set(2) // connection was successful
DefaultAdapter.connectHandler(nil, true)
}
case C.BLE_GAP_EVT_DISCONNECTED:
if debug {
println("evt: disconnected")
}
// Clean up state for this connection.
for i, cb := range gattcNotificationCallbacks {
if cb.connectionHandle == currentConnection.Reg {
gattcNotificationCallbacks[i].valueHandle = 0 // 0 means invalid
}
}
currentConnection.Reg = C.BLE_CONN_HANDLE_INVALID
// Auto-restart advertisement if needed.
if defaultAdvertisement.isAdvertising.Get() != 0 {
// The advertisement was running but was automatically stopped
// by the connection event.
// Note that it cannot be restarted during connect like this,
// because it would need to be reconfigured as a non-connectable
// advertisement. That's left as a future addition, if
// necessary.
C.sd_ble_gap_adv_start(defaultAdvertisement.handle, C.BLE_CONN_CFG_TAG_DEFAULT)
}
DefaultAdapter.connectHandler(nil, false)
case C.BLE_GAP_EVT_ADV_REPORT:
advReport := gapEvent.params.unionfield_adv_report()
if debug && &scanReportBuffer.data[0] != advReport.data.p_data {
// Sanity check.
panic("scanReportBuffer != advReport.p_data")
}
// Prepare the globalScanResult, which will be passed to the
// callback.
scanReportBuffer.len = byte(advReport.data.len)
globalScanResult.RSSI = int16(advReport.rssi)
globalScanResult.Address = Address{
MACAddress{MAC: advReport.peer_addr.addr,
isRandom: advReport.peer_addr.bitfield_addr_type() != 0},
}
globalScanResult.AdvertisementPayload = &scanReportBuffer
// Signal to the main thread that there was a scan report.
// Scanning will be resumed (from the main thread) once the scan
// report has been processed.
gotScanReport.Set(1)
case C.BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST:
// Respond with the default PPCP connection parameters by passing
// nil:
// > If NULL is provided on a peripheral role, the parameters in the
// > PPCP characteristic of the GAP service will be used instead. If
// > NULL is provided on a central role and in response to a
// > BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST, the peripheral request
// > will be rejected
C.sd_ble_gap_conn_param_update(gapEvent.conn_handle, nil)
case C.BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST:
// We need to respond with sd_ble_gap_data_length_update. Setting
// both parameters to nil will make sure we send the default values.
C.sd_ble_gap_data_length_update(gapEvent.conn_handle, nil, nil)
default:
if debug {
println("unknown GAP event:", id)
}
}
case id >= C.BLE_GATTS_EVT_BASE && id <= C.BLE_GATTS_EVT_LAST:
gattsEvent := eventBuf.evt.unionfield_gatts_evt()
switch id {
case C.BLE_GATTS_EVT_WRITE:
writeEvent := gattsEvent.params.unionfield_write()
len := writeEvent.len - writeEvent.offset
data := (*[255]byte)(unsafe.Pointer(&writeEvent.data[0]))[:len:len]
handler := DefaultAdapter.getCharWriteHandler(writeEvent.handle)
if handler != nil {
handler.callback(Connection(gattsEvent.conn_handle), int(writeEvent.offset), data)
}
case C.BLE_GATTS_EVT_SYS_ATTR_MISSING:
// This event is generated when reading the Generic Attribute
// service. It appears to be necessary for bonded devices.
// From the docs:
// > If the pointer is NULL, the system attribute info is
// > initialized, assuming that the application does not have any
// > previously saved system attribute data for this device.
// Maybe we should look at the error, but as there's not really a
// way to handle it, ignore it.
C.sd_ble_gatts_sys_attr_set(gattsEvent.conn_handle, nil, 0, 0)
case C.BLE_GATTS_EVT_EXCHANGE_MTU_REQUEST:
// This event is generated by some devices. While we could support
// larger MTUs, this default MTU is supported everywhere.
C.sd_ble_gatts_exchange_mtu_reply(gattsEvent.conn_handle, C.BLE_GATT_ATT_MTU_DEFAULT)
default:
if debug {
println("unknown GATTS event:", id, id-C.BLE_GATTS_EVT_BASE)
}
}
case id >= C.BLE_GATTC_EVT_BASE && id <= C.BLE_GATTC_EVT_LAST:
gattcEvent := eventBuf.evt.unionfield_gattc_evt()
switch id {
case C.BLE_GATTC_EVT_PRIM_SRVC_DISC_RSP:
discoveryEvent := gattcEvent.params.unionfield_prim_srvc_disc_rsp()
if debug {
println("evt: discovered primary service", discoveryEvent.count)
}
discoveringService.state.Set(2) // signal there is a result
if discoveryEvent.count >= 1 {
// Theoretically there may be more, but as we're only using
// sd_ble_gattc_primary_services_discover, there should only be
// one discovered service. Use the first as a sensible fallback.
discoveringService.startHandle.Set(discoveryEvent.services[0].handle_range.start_handle)
discoveringService.endHandle.Set(discoveryEvent.services[0].handle_range.end_handle)
discoveringService.uuid = discoveryEvent.services[0].uuid
} else {
// No service found.
discoveringService.startHandle.Set(0)
}
case C.BLE_GATTC_EVT_CHAR_DISC_RSP:
discoveryEvent := gattcEvent.params.unionfield_char_disc_rsp()
if debug {
println("evt: discovered characteristics", discoveryEvent.count)
}
if discoveryEvent.count >= 1 {
// There may be more, but for ease of implementing we only
// handle the first.
discoveringCharacteristic.handle_value.Set(discoveryEvent.chars[0].handle_value)
discoveringCharacteristic.char_props = discoveryEvent.chars[0].char_props
discoveringCharacteristic.uuid = discoveryEvent.chars[0].uuid
} else {
// zero indicates we received no characteristic, set handle_value to last
discoveringCharacteristic.handle_value.Set(0xffff)
}
case C.BLE_GATTC_EVT_DESC_DISC_RSP:
discoveryEvent := gattcEvent.params.unionfield_desc_disc_rsp()
if debug {
println("evt: discovered descriptors", discoveryEvent.count)
}
if discoveryEvent.count >= 1 {
// There may be more, but for ease of implementing we only
// handle the first.
uuid := discoveryEvent.descs[0].uuid
if uuid._type == C.BLE_UUID_TYPE_BLE && uuid.uuid == 0x2902 {
// Found a CCCD (Client Characteristic Configuration
// Descriptor), which has a 16-bit UUID with value 0x2902).
discoveringCharacteristic.handle_value.Set(discoveryEvent.descs[0].handle)
} else {
// Found something else?
// TODO: handle this properly by continuing the scan. For
// now, give up if we found something other than a CCCD.
if debug {
println(" found some other descriptor (unimplemented)")
}
}
}
case C.BLE_GATTC_EVT_READ_RSP:
readEvent := gattcEvent.params.unionfield_read_rsp()
if debug {
println("evt: read response, data length", readEvent.len)
}
readingCharacteristic.handle_value.Set(readEvent.handle)
readingCharacteristic.offset = readEvent.offset
readingCharacteristic.length = readEvent.len
// Convert a C.ble_gap_addr_t to a MACAddress struct.
func makeMACAddress(addr C.ble_gap_addr_t) MACAddress {
return MACAddress{
MAC: makeAddress(addr.addr),
isRandom: addr.bitfield_addr_type() != 0,
// copy read event data into Go slice
copy(readingCharacteristic.value, (*[255]byte)(unsafe.Pointer(&readEvent.data[0]))[:readEvent.len:readEvent.len])
case C.BLE_GATTC_EVT_HVX:
hvxEvent := gattcEvent.params.unionfield_hvx()
switch hvxEvent._type {
case C.BLE_GATT_HVX_NOTIFICATION:
if debug {
println("evt: notification", hvxEvent.handle)
}
// Find the callback and call it (if there is any).
for _, callbackInfo := range gattcNotificationCallbacks {
if callbackInfo.valueHandle == hvxEvent.handle && callbackInfo.connectionHandle == gattcEvent.conn_handle {
// Create a Go slice from the data, to pass to the
// callback.
data := (*[255]byte)(unsafe.Pointer(&hvxEvent.data[0]))[:hvxEvent.len:hvxEvent.len]
if callbackInfo.callback != nil {
callbackInfo.callback(data)
}
break
}
}
}
default:
if debug {
println("unknown GATTC event:", id, id-C.BLE_GATTC_EVT_BASE)
}
}
default:
if debug {
println("unknown event:", id)
}
}
}

11
adapter_s110.c Normal file
View file

@ -0,0 +1,11 @@
// +build softdevice,s110v8
// This file is necessary to define SVCall wrappers, because TinyGo does not yet
// support static functions in the preamble.
// Discard all 'static' attributes to define functions normally.
#define static
#include "s110_nrf51_8.0.0/s110_nrf51_8.0.0_API/include/nrf_sdm.h"
#include "s110_nrf51_8.0.0/s110_nrf51_8.0.0_API/include/nrf_soc.h"
#include "s110_nrf51_8.0.0/s110_nrf51_8.0.0_API/include/ble.h"

View file

@ -1,4 +1,4 @@
//go:build softdevice && s110v8
// +build softdevice,s110v8
package bluetooth

View file

@ -1,13 +0,0 @@
//go:build softdevice && s113v7
package bluetooth
/*
// Add the correct SoftDevice include path to CFLAGS, so #include will work as
// expected.
#cgo CFLAGS: -Is113_nrf52_7.0.1/s113_nrf52_7.0.1_API/include
#include "nrf_nvic.h"
nrf_nvic_state_t nrf_nvic_state = {0};
*/
import "C"

22
adapter_s132.c Normal file
View file

@ -0,0 +1,22 @@
// +build softdevice,s132v6
// This file is necessary to define SVCall wrappers, because TinyGo does not yet
// support static functions in the preamble.
// Discard all 'static' attributes to define functions normally.
#define static
// Get rid of all __STATIC_INLINE symbols.
// This is a bit less straightforward: we first need to include the header that
// defines it, and then redefine it.
#include "nrf.h"
#undef __STATIC_INLINE
#define __STATIC_INLINE
#include "s132_nrf52_6.1.1/s132_nrf52_6.1.1_API/include/nrf_sdm.h"
#include "s132_nrf52_6.1.1/s132_nrf52_6.1.1_API/include/nrf_nvic.h"
#include "s132_nrf52_6.1.1/s132_nrf52_6.1.1_API/include/ble.h"
// Define nrf_nvic_state, which is used by sd_nvic_critical_region_enter and
// sd_nvic_critical_region_exit.
nrf_nvic_state_t nrf_nvic_state = {0};

View file

@ -1,4 +1,4 @@
//go:build softdevice && s132v6
// +build softdevice,s132v6
package bluetooth
@ -6,8 +6,5 @@ package bluetooth
// Add the correct SoftDevice include path to CFLAGS, so #include will work as
// expected.
#cgo CFLAGS: -Is132_nrf52_6.1.1/s132_nrf52_6.1.1_API/include
#include "nrf_nvic.h"
nrf_nvic_state_t nrf_nvic_state = {0};
*/
import "C"

22
adapter_s140v6.c Normal file
View file

@ -0,0 +1,22 @@
// +build softdevice,s140v6
// This file is necessary to define SVCall wrappers, because TinyGo does not yet
// support static functions in the preamble.
// Discard all 'static' attributes to define functions normally.
#define static
// Get rid of all __STATIC_INLINE symbols.
// This is a bit less straightforward: we first need to include the header that
// defines it, and then redefine it.
#include "nrf.h"
#undef __STATIC_INLINE
#define __STATIC_INLINE
#include "s140_nrf52_6.1.1/s140_nrf52_6.1.1_API/include/nrf_sdm.h"
#include "s140_nrf52_6.1.1/s140_nrf52_6.1.1_API/include/nrf_nvic.h"
#include "s140_nrf52_6.1.1/s140_nrf52_6.1.1_API/include/ble.h"
// Define nrf_nvic_state, which is used by sd_nvic_critical_region_enter and
// sd_nvic_critical_region_exit.
nrf_nvic_state_t nrf_nvic_state = {0};

View file

@ -1,4 +1,4 @@
//go:build softdevice && s140v6
// +build softdevice,s140v6
package bluetooth
@ -6,8 +6,5 @@ package bluetooth
// Add the correct SoftDevice include path to CFLAGS, so #include will work as
// expected.
#cgo CFLAGS: -Is140_nrf52_6.1.1/s140_nrf52_6.1.1_API/include
#include "nrf_nvic.h"
nrf_nvic_state_t nrf_nvic_state = {0};
*/
import "C"

22
adapter_s140v7.c Normal file
View file

@ -0,0 +1,22 @@
// +build softdevice,s140v7
// This file is necessary to define SVCall wrappers, because TinyGo does not yet
// support static functions in the preamble.
// Discard all 'static' attributes to define functions normally.
#define static
// Get rid of all __STATIC_INLINE symbols.
// This is a bit less straightforward: we first need to include the header that
// defines it, and then redefine it.
#include "nrf.h"
#undef __STATIC_INLINE
#define __STATIC_INLINE
#include "s140_nrf52_7.0.1/s140_nrf52_7.0.1_API/include/nrf_sdm.h"
#include "s140_nrf52_7.0.1/s140_nrf52_7.0.1_API/include/nrf_nvic.h"
#include "s140_nrf52_7.0.1/s140_nrf52_7.0.1_API/include/ble.h"
// Define nrf_nvic_state, which is used by sd_nvic_critical_region_enter and
// sd_nvic_critical_region_exit.
nrf_nvic_state_t nrf_nvic_state = {0};

View file

@ -1,13 +1,10 @@
//go:build softdevice && s140v7
// +build softdevice,s140v7
package bluetooth
/*
// Add the correct SoftDevice include path to CFLAGS, so #include will work as
// expected.
#cgo CFLAGS: -Is140_nrf52_7.3.0/s140_nrf52_7.3.0_API/include
#include "nrf_nvic.h"
nrf_nvic_state_t nrf_nvic_state = {0};
#cgo CFLAGS: -Is140_nrf52_7.0.1/s140_nrf52_7.0.1_API/include
*/
import "C"

View file

@ -1,4 +1,4 @@
//go:build softdevice
// +build softdevice
package bluetooth
@ -10,14 +10,6 @@ import (
"unsafe"
)
// #include "ble.h"
// #ifdef NRF51
// #include "nrf_soc.h"
// #else
// #include "nrf_nvic.h"
// #endif
import "C"
var (
ErrNotDefaultAdapter = errors.New("bluetooth: not the default adapter")
)
@ -28,7 +20,7 @@ var (
)
// There can only be one connection at a time in the default configuration.
var currentConnection = volatileHandle{handle: volatile.Register16{C.BLE_CONN_HANDLE_INVALID}}
var currentConnection = volatile.Register16{C.BLE_CONN_HANDLE_INVALID}
// Globally allocated buffer for incoming SoftDevice events.
var eventBuf struct {
@ -48,7 +40,7 @@ type Adapter struct {
scanning bool
charWriteHandlers []charWriteHandler
connectHandler func(device Device, connected bool)
connectHandler func(device Addresser, connected bool)
}
// DefaultAdapter is the default adapter on the current system. On Nordic chips,
@ -56,12 +48,10 @@ type Adapter struct {
//
// Make sure to call Enable() before using it to initialize the adapter.
var DefaultAdapter = &Adapter{isDefault: true,
connectHandler: func(device Device, connected bool) {
connectHandler: func(device Addresser, connected bool) {
return
}}
var eventBufLen C.uint16_t
// Enable configures the BLE stack. It must be called before any
// Bluetooth-related calls (unless otherwise indicated).
func (a *Adapter) Enable() error {
@ -72,8 +62,8 @@ func (a *Adapter) Enable() error {
// Enable the IRQ that handles all events.
intr := interrupt.New(nrf.IRQ_SWI2, func(interrupt.Interrupt) {
for {
eventBufLen = C.uint16_t(unsafe.Sizeof(eventBuf))
errCode := C.sd_ble_evt_get((*C.uint8_t)(unsafe.Pointer(&eventBuf)), &eventBufLen)
eventBufLen := uint16(unsafe.Sizeof(eventBuf))
errCode := C.sd_ble_evt_get((*uint8)(unsafe.Pointer(&eventBuf)), &eventBufLen)
if errCode != 0 {
// Possible error conditions:
// * NRF_ERROR_NOT_FOUND: no events left, break
@ -97,7 +87,7 @@ func (a *Adapter) Enable() error {
return err
}
errCode := C.sd_ble_gap_device_name_set(&secModeOpen, (*C.uint8_t)(unsafe.Pointer(&defaultDeviceName[0])), C.uint16_t(len(defaultDeviceName)))
errCode := C.sd_ble_gap_device_name_set(&secModeOpen, &defaultDeviceName[0], uint16(len(defaultDeviceName)))
if errCode != 0 {
return Error(errCode)
}
@ -116,7 +106,7 @@ func (a *Adapter) Enable() error {
// play well with the SoftDevice. Restore interrupts to the previous state with
// RestoreInterrupts.
func DisableInterrupts() uintptr {
var is_nested_critical_region C.uint8_t
var is_nested_critical_region uint8
C.sd_nvic_critical_region_enter(&is_nested_critical_region)
return uintptr(is_nested_critical_region)
}
@ -125,43 +115,5 @@ func DisableInterrupts() uintptr {
// DisableInterrupts. The mask parameter must be the value returned by
// DisableInterrupts.
func RestoreInterrupts(mask uintptr) {
C.sd_nvic_critical_region_exit(C.uint8_t(mask))
}
// Wrapper for volatile.Register16 that uses C.uint16_t instead of uint16, for
// easier interoperability with C.
type volatileHandle struct {
handle volatile.Register16
}
func (a *volatileHandle) Set(handle C.uint16_t) {
a.handle.Set(uint16(handle))
}
func (a *volatileHandle) Get() C.uint16_t {
return C.uint16_t(a.handle.Get())
}
// Convert a SoftDevice MAC address into a Go MAC address.
func makeAddress(mac [6]C.uint8_t) 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 SoftDevice MAC Address.
func makeSDAddress(mac MAC) [6]C.uint8_t {
return [6]C.uint8_t{
C.uint8_t(mac[0]),
C.uint8_t(mac[1]),
C.uint8_t(mac[2]),
C.uint8_t(mac[3]),
C.uint8_t(mac[4]),
C.uint8_t(mac[5]),
}
C.sd_nvic_critical_region_exit(uint8(mask))
}

View file

@ -1,28 +1,21 @@
package bluetooth
import (
"errors"
"fmt"
"github.com/go-ole/go-ole"
"github.com/saltosystems/winrt-go"
"github.com/saltosystems/winrt-go/windows/devices/bluetooth/advertisement"
"github.com/saltosystems/winrt-go/windows/foundation"
"tinygo.org/x/bluetooth/winbt"
)
type Adapter struct {
watcher *advertisement.BluetoothLEAdvertisementWatcher
watcher *winbt.IBluetoothLEAdvertisementWatcher
connectHandler func(device Device, connected bool)
defaultAdvertisement *Advertisement
connectHandler func(device Addresser, connected bool)
}
// DefaultAdapter is the default adapter on the system.
//
// Make sure to call Enable() before using it to initialize the adapter.
var DefaultAdapter = &Adapter{
connectHandler: func(device Device, connected bool) {
connectHandler: func(device Addresser, connected bool) {
return
},
}
@ -32,35 +25,3 @@ var DefaultAdapter = &Adapter{
func (a *Adapter) Enable() error {
return ole.RoInitialize(1) // initialize with multithreading enabled
}
func awaitAsyncOperation(asyncOperation *foundation.IAsyncOperation, genericParamSignature string) error {
var status foundation.AsyncStatus
// We need to obtain the GUID of the AsyncOperationCompletedHandler, but its a generic delegate
// so we also need the generic parameter type's signature:
// AsyncOperationCompletedHandler<genericParamSignature>
iid := winrt.ParameterizedInstanceGUID(foundation.GUIDAsyncOperationCompletedHandler, genericParamSignature)
// Wait until the async operation completes.
waitChan := make(chan struct{})
handler := foundation.NewAsyncOperationCompletedHandler(ole.NewGUID(iid), func(instance *foundation.AsyncOperationCompletedHandler, asyncInfo *foundation.IAsyncOperation, asyncStatus foundation.AsyncStatus) {
status = asyncStatus
close(waitChan)
})
defer handler.Release()
asyncOperation.SetCompleted(handler)
// Wait until async operation has stopped, and finish.
<-waitChan
if status != foundation.AsyncStatusCompleted {
return fmt.Errorf("async operation failed with status %d", status)
}
return nil
}
func (a *Adapter) Address() (MACAddress, error) {
// TODO: get mac address
return MACAddress{}, errors.New("not implemented")
}

1203
att_hci.go

File diff suppressed because it is too large Load diff

@ -1 +0,0 @@
Subproject commit 3d0f452460237f76d7e11d8cd0de8c1cba46b62a

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,5 +0,0 @@
//go:build bledebug
package bluetooth
var debug = true

View file

@ -1,79 +1,74 @@
//go:build softdevice
// +build softdevice
package bluetooth
// #include <stdint.h>
// #include "nrf_error.h"
// #include "nrf_error_sdm.h"
import "C"
// Error is an error from within the SoftDevice.
type Error uint32
func (e Error) Error() string {
switch {
case e >= C.NRF_ERROR_BASE_NUM && e < C.NRF_ERROR_SDM_BASE_NUM:
case e < 0x1000:
// Global errors.
switch e {
case C.NRF_SUCCESS:
case 0:
return "no error"
case C.NRF_ERROR_SVC_HANDLER_MISSING:
case 1:
return "SVC handler is missing"
case C.NRF_ERROR_SOFTDEVICE_NOT_ENABLED:
case 2:
return "SoftDevice has not been enabled"
case C.NRF_ERROR_INTERNAL:
case 3:
return "internal error"
case C.NRF_ERROR_NO_MEM:
case 4:
return "no memory for operation"
case C.NRF_ERROR_NOT_FOUND:
case 5:
return "not found"
case C.NRF_ERROR_NOT_SUPPORTED:
case 6:
return "not supported"
case C.NRF_ERROR_INVALID_PARAM:
case 7:
return "invalid parameter"
case C.NRF_ERROR_INVALID_STATE:
case 8:
return "invalid state, operation disallowed in this state"
case C.NRF_ERROR_INVALID_LENGTH:
case 9:
return "invalid Length"
case C.NRF_ERROR_INVALID_FLAGS:
case 10:
return "invalid flags"
case C.NRF_ERROR_INVALID_DATA:
case 11:
return "invalid data"
case C.NRF_ERROR_DATA_SIZE:
case 12:
return "invalid data size"
case C.NRF_ERROR_TIMEOUT:
case 13:
return "operation timed out"
case C.NRF_ERROR_NULL:
case 14:
return "null pointer"
case C.NRF_ERROR_FORBIDDEN:
case 15:
return "forbidden operation"
case C.NRF_ERROR_INVALID_ADDR:
case 16:
return "bad memory address"
case C.NRF_ERROR_BUSY:
case 17:
return "busy"
case 18: // C.NRF_ERROR_CONN_COUNT, not available on nrf51
case 18:
return "maximum connection count exceeded"
case 19: // C.NRF_ERROR_RESOURCES, not available on nrf51
case 19:
return "not enough resources for operation"
default:
return "other global error"
}
case e >= C.NRF_ERROR_SDM_BASE_NUM && e < C.NRF_ERROR_SOC_BASE_NUM:
case e < 0x2000:
// SDM errors.
switch e {
case C.NRF_ERROR_SDM_LFCLK_SOURCE_UNKNOWN:
case 0x1000:
return "unknown LFCLK source"
case C.NRF_ERROR_SDM_INCORRECT_INTERRUPT_CONFIGURATION:
case 0x1001:
return "incorrect interrupt configuration"
case C.NRF_ERROR_SDM_INCORRECT_CLENR0:
case 0x1002:
return "incorrect CLENR0"
default:
return "other SDM error"
}
case e >= C.NRF_ERROR_SOC_BASE_NUM && e < C.NRF_ERROR_STK_BASE_NUM:
case e < 0x3000:
// SoC errors.
return "other SoC error"
case e >= C.NRF_ERROR_STK_BASE_NUM && e < 0x4000:
case e < 0x4000:
// STK errors.
return "other STK error"
default:
@ -84,7 +79,7 @@ func (e Error) Error() string {
// makeError returns an error (using the Error type) if the error code is
// non-zero, otherwise it returns nil. It is used with internal API calls.
func makeError(code C.uint32_t) error {
func makeError(code uint32) error {
if code != 0 {
return Error(code)
}

View file

@ -3,7 +3,7 @@ package main
import (
"time"
"gitrepo.ru/neonxp/bluetooth"
"tinygo.org/x/bluetooth"
)
var adapter = bluetooth.DefaultAdapter
@ -13,17 +13,13 @@ func main() {
adv := adapter.DefaultAdvertisement()
must("config adv", adv.Configure(bluetooth.AdvertisementOptions{
LocalName: "Go Bluetooth",
ManufacturerData: []bluetooth.ManufacturerDataElement{
{CompanyID: 0xffff, Data: []byte{0x01, 0x02}},
},
}))
must("start adv", adv.Start())
println("advertising...")
address, _ := adapter.Address()
for {
println("Go Bluetooth /", address.MAC.String())
time.Sleep(time.Second)
// Sleep forever.
time.Sleep(time.Hour)
}
}

View file

@ -1,6 +1,7 @@
// This example is intended to be used with the Adafruit Circuitplay Bluefruit board.
// It allows you to control the color of the built-in NeoPixel LEDS while they animate
// in a circular pattern.
//
package main
import (
@ -8,7 +9,7 @@ import (
"machine"
"time"
"gitrepo.ru/neonxp/bluetooth"
"tinygo.org/x/bluetooth"
"tinygo.org/x/drivers/ws2812"
)
@ -38,7 +39,7 @@ func main() {
neo.Configure(machine.PinConfig{Mode: machine.PinOutput})
ws = ws2812.New(neo)
adapter.SetConnectHandler(func(d bluetooth.Device, c bool) {
adapter.SetConnectHandler(func(d bluetooth.Addresser, c bool) {
connected = c
if !connected && !disconnected {
@ -66,7 +67,7 @@ func main() {
Handle: &ledColorCharacteristic,
UUID: bluetooth.NewUUID(charUUID),
Value: ledColor[:],
Flags: bluetooth.CharacteristicReadPermission | bluetooth.CharacteristicWritePermission | bluetooth.CharacteristicWriteWithoutResponsePermission,
Flags: bluetooth.CharacteristicReadPermission | bluetooth.CharacteristicWritePermission,
WriteEvent: func(client bluetooth.Connection, offset int, value []byte) {
if offset != 0 || len(value) != 3 {
return

View file

@ -1,84 +0,0 @@
// Test for setting connection parameters.
//
// To test this feature, run this either on a desktop OS or by flashing it to a
// device with TinyGo. Then connect to it from a BLE connection debugger, for
// example nRF Connect on Android. After a second, you should see in the log of
// the BLE app that the connection latency has been updated. It might look
// something like this:
//
// Connection parameters updated (interval: 510.0ms, latency: 0, timeout: 10000ms)
package main
import (
"time"
"gitrepo.ru/neonxp/bluetooth"
)
var (
adapter = bluetooth.DefaultAdapter
newDevice chan bluetooth.Device
)
func main() {
must("enable BLE stack", adapter.Enable())
newDevice = make(chan bluetooth.Device, 1)
adapter.SetConnectHandler(func(device bluetooth.Device, connected bool) {
// If this is a new device, signal it to the separate goroutine.
if connected {
select {
case newDevice <- device:
default:
}
}
})
// Start advertising, so we can be found.
const name = "Go BLE test"
adv := adapter.DefaultAdvertisement()
adv.Configure(bluetooth.AdvertisementOptions{
LocalName: name,
})
adv.Start()
println("advertising:", name)
for device := range newDevice {
println("connection from device:", device.Address.String())
// Discover services and characteristics.
svcs, err := device.DiscoverServices(nil)
if err != nil {
println(" failed to resolve services:", err)
}
for _, svc := range svcs {
println(" service:", svc.UUID().String())
chars, err := svc.DiscoverCharacteristics(nil)
if err != nil {
println(" failed to resolve characteristics:", err)
}
for _, char := range chars {
println(" characteristic:", char.UUID().String())
}
}
// Update connection parameters (as a test).
time.Sleep(time.Second)
err = device.RequestConnectionParams(bluetooth.ConnectionParams{
MinInterval: bluetooth.NewDuration(495 * time.Millisecond),
MaxInterval: bluetooth.NewDuration(510 * time.Millisecond),
Timeout: bluetooth.NewDuration(10 * time.Second),
})
if err != nil {
println(" failed to update connection parameters:", err)
continue
}
println(" updated connection parameters")
}
}
func must(action string, err error) {
if err != nil {
panic("failed to " + action + ": " + err.Error())
}
}

View file

@ -3,22 +3,23 @@
//
// To run this on a desktop system:
//
// go run ./examples/discover EE:74:7D:C9:2A:68
// go run ./examples/discover EE:74:7D:C9:2A:68
//
// To run this on a microcontroller, change the constant value in the file
// "mcu.go" to set the MAC address of the device you want to discover.
// Then, flash to the microcontroller board like this:
//
// tinygo flash -o circuitplay-bluefruit ./examples/discover
// tinygo flash -o circuitplay-bluefruit ./examples/discover
//
// Once the program is flashed to the board, connect to the USB port
// via serial to view the output.
//
package main
import (
"strconv"
"gitrepo.ru/neonxp/bluetooth"
"tinygo.org/x/bluetooth"
)
var adapter = bluetooth.DefaultAdapter
@ -43,7 +44,7 @@ func main() {
}
})
var device bluetooth.Device
var device *bluetooth.Device
select {
case result := <-ch:
device, err = adapter.Connect(result.Address, bluetooth.ConnectionParams{})
@ -72,12 +73,6 @@ func main() {
}
for _, char := range chars {
println("-- characteristic", char.UUID().String())
mtu, err := char.GetMTU()
if err != nil {
println(" mtu: error:", err.Error())
} else {
println(" mtu:", mtu)
}
n, err := char.Read(buf)
if err != nil {
println(" ", err.Error())

View file

@ -1,4 +1,4 @@
//go:build baremetal
// +build baremetal
package main
@ -6,15 +6,11 @@ import (
"time"
)
// DeviceAddress is the MAC address of the Bluetooth peripheral you want to connect to.
// Replace this by using -ldflags="-X main.DeviceAddress=[MAC ADDRESS]"
// where [MAC ADDRESS] is the actual MAC address of the peripheral.
// For example:
// tinygo flash -target circuitplay-bluefruit -ldflags="-X main.DeviceAddress=7B:36:98:8C:41:1C" ./examples/discover/
var DeviceAddress string
// replace this with the MAC address of the Bluetooth peripheral you want to connect to.
const deviceAddress = "E4:B7:F4:11:8D:33"
func connectAddress() string {
return DeviceAddress
return deviceAddress
}
// wait on baremetal, proceed immediately on desktop OS.

View file

@ -1,4 +1,4 @@
//go:build !baremetal
// +build !baremetal
package main

View file

@ -3,35 +3,31 @@
//
// Once connected, it subscribes to notifications for the data value, and
// displays it.
// The Heart Rate Measurement characteristic is a variable-length structure (array) containing a Flags field, a Heart
// Rate Measurement Value field and, based on the contents of the Flags field, may contain additional fields
// such as Energy Expended or RR-Interval.
// More info can be found here: https://www.bluetooth.com/specifications/specs/gatt-specification-supplement-6/
// In this example only the heart rate is used, this is the second element in the array of bytes.
//
// To run this on a desktop system:
//
// go run ./examples/heartrate-monitor EE:74:7D:C9:2A:68
// go run ./examples/heartrate-monitor EE:74:7D:C9:2A:68
//
// To run this on a microcontroller, change the constant value in the file
// "mcu.go" to set the MAC address of the device you want to discover.
// Then, flash to the microcontroller board like this:
//
// tinygo flash -o circuitplay-bluefruit ./examples/heartrate-monitor
// tinygo flash -o circuitplay-bluefruit ./examples/heartrate-monitor
//
// Once the program is flashed to the board, connect to the USB port
// via serial to view the output.
//
package main
import (
"gitrepo.ru/neonxp/bluetooth"
"tinygo.org/x/bluetooth"
)
var (
adapter = bluetooth.DefaultAdapter
heartRateServiceUUID = bluetooth.ServiceUUIDHeartRate
heartRateCharacteristicUUID = bluetooth.CharacteristicUUIDHeartRateMeasurement
heartRateServiceUUID = bluetooth.New16BitUUID(0x180D)
heartRateCharacteristicUUID = bluetooth.New16BitUUID(0x2A37)
)
func main() {
@ -52,7 +48,7 @@ func main() {
}
})
var device bluetooth.Device
var device *bluetooth.Device
select {
case result := <-ch:
device, err = adapter.Connect(result.Address, bluetooth.ConnectionParams{})
@ -90,7 +86,7 @@ func main() {
println("found characteristic", char.UUID().String())
char.EnableNotifications(func(buf []byte) {
println("data:", uint8(buf[1]))
println("data:", uint8(buf[0]))
})
select {}

View file

@ -1,4 +1,4 @@
//go:build baremetal
// +build baremetal
package main
@ -6,15 +6,11 @@ import (
"time"
)
// DeviceAddress is the MAC address of the Bluetooth peripheral you want to connect to.
// Replace this by using -ldflags="-X main.DeviceAddress=[MAC ADDRESS]"
// where [MAC ADDRESS] is the actual MAC address of the peripheral.
// For example:
// tinygo flash -target circuitplay-bluefruit -ldflags="-X main.DeviceAddress=7B:36:98:8C:41:1C" ./examples/heartrate-monitor/
var DeviceAddress string
// replace this with the MAC address of the Bluetooth peripheral you want to connect to.
const deviceAddress = "E4:B7:F4:11:8D:33"
func connectAddress() string {
return DeviceAddress
return deviceAddress
}
// done just blocks forever, allows USB CDC reset for flashing new software.

View file

@ -1,4 +1,4 @@
//go:build !baremetal
// +build !baremetal
package main

View file

@ -4,7 +4,7 @@ import (
"math/rand"
"time"
"gitrepo.ru/neonxp/bluetooth"
"tinygo.org/x/bluetooth"
)
var adapter = bluetooth.DefaultAdapter
@ -18,19 +18,29 @@ func main() {
adv := adapter.DefaultAdvertisement()
must("config adv", adv.Configure(bluetooth.AdvertisementOptions{
LocalName: "Go HRS",
ServiceUUIDs: []bluetooth.UUID{bluetooth.ServiceUUIDHeartRate},
ServiceUUIDs: []bluetooth.UUID{bluetooth.New16BitUUID(0x2A37)},
}))
must("start adv", adv.Start())
var heartRateMeasurement bluetooth.Characteristic
must("add service", adapter.AddService(&bluetooth.Service{
UUID: bluetooth.ServiceUUIDHeartRate,
UUID: bluetooth.New16BitUUID(0x180D), // Heart Rate
Characteristics: []bluetooth.CharacteristicConfig{
{
Handle: &heartRateMeasurement,
UUID: bluetooth.CharacteristicUUIDHeartRateMeasurement,
UUID: bluetooth.New16BitUUID(0x2A37), // Heart Rate Measurement
Value: []byte{0, heartRate},
Flags: bluetooth.CharacteristicNotifyPermission,
Flags: bluetooth.CharacteristicReadPermission | bluetooth.CharacteristicWritePermission |
bluetooth.CharacteristicNotifyPermission,
WriteEvent: func(client bluetooth.Connection, offset int, value []byte) {
if offset != 0 || len(value) < 2 {
return
}
if value[1] != 0 { // avoid divide by zero
heartRate = value[1]
println("heart rate is now:", heartRate)
}
},
},
},
}))
@ -45,7 +55,7 @@ func main() {
heartRate = randomInt(65, 85)
// and push the next notification
heartRateMeasurement.Write([]byte{0, heartRate})
heartRateMeasurement.Write([]byte{byte(heartRate)})
}
}

View file

@ -4,7 +4,7 @@ import (
"machine"
"time"
"gitrepo.ru/neonxp/bluetooth"
"tinygo.org/x/bluetooth"
)
var adapter = bluetooth.DefaultAdapter
@ -36,7 +36,7 @@ func main() {
Handle: &ledColorCharacteristic,
UUID: bluetooth.NewUUID(charUUID),
Value: ledColor[:],
Flags: bluetooth.CharacteristicReadPermission | bluetooth.CharacteristicWritePermission | bluetooth.CharacteristicWriteWithoutResponsePermission,
Flags: bluetooth.CharacteristicReadPermission | bluetooth.CharacteristicWritePermission,
WriteEvent: func(client bluetooth.Connection, offset int, value []byte) {
if offset != 0 || len(value) != 3 {
return

View file

@ -4,14 +4,14 @@ package main
// details.
import (
"gitrepo.ru/neonxp/bluetooth"
"gitrepo.ru/neonxp/bluetooth/rawterm"
"tinygo.org/x/bluetooth"
"tinygo.org/x/bluetooth/rawterm"
)
var (
serviceUUID = bluetooth.ServiceUUIDNordicUART
rxUUID = bluetooth.CharacteristicUUIDUARTRX
txUUID = bluetooth.CharacteristicUUIDUARTTX
serviceUUID = bluetooth.NewUUID([16]byte{0x6E, 0x40, 0x00, 0x01, 0xB5, 0xA3, 0xF3, 0x93, 0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E})
rxUUID = bluetooth.NewUUID([16]byte{0x6E, 0x40, 0x00, 0x02, 0xB5, 0xA3, 0xF3, 0x93, 0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E})
txUUID = bluetooth.NewUUID([16]byte{0x6E, 0x40, 0x00, 0x03, 0xB5, 0xA3, 0xF3, 0x93, 0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E})
)
var adapter = bluetooth.DefaultAdapter

View file

@ -8,14 +8,14 @@ package main
// Code to interact with a raw terminal is in separate files with build tags.
import (
"gitrepo.ru/neonxp/bluetooth"
"gitrepo.ru/neonxp/bluetooth/rawterm"
"tinygo.org/x/bluetooth"
"tinygo.org/x/bluetooth/rawterm"
)
var (
serviceUUID = bluetooth.ServiceUUIDNordicUART
rxUUID = bluetooth.CharacteristicUUIDUARTRX
txUUID = bluetooth.CharacteristicUUIDUARTTX
serviceUUID = bluetooth.NewUUID([16]byte{0x6E, 0x40, 0x00, 0x01, 0xB5, 0xA3, 0xF3, 0x93, 0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E})
rxUUID = bluetooth.NewUUID([16]byte{0x6E, 0x40, 0x00, 0x02, 0xB5, 0xA3, 0xF3, 0x93, 0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E})
txUUID = bluetooth.NewUUID([16]byte{0x6E, 0x40, 0x00, 0x03, 0xB5, 0xA3, 0xF3, 0x93, 0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E})
)
func main() {

View file

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

View file

@ -1,53 +0,0 @@
// This example advertises for 5 minutes after
// - boot
// - disconnect
// and then stops advertising.
package main
import (
"time"
"gitrepo.ru/neonxp/bluetooth"
)
var adapter = bluetooth.DefaultAdapter
var advUntil = time.Now().Add(5 * time.Minute)
var advState = true
func main() {
must("enable BLE stack", adapter.Enable())
adv := adapter.DefaultAdvertisement()
must("config adv", adv.Configure(bluetooth.AdvertisementOptions{
LocalName: "Go Bluetooth",
}))
adapter.SetConnectHandler(func(device bluetooth.Device, connected bool) {
if connected {
println("connected, not advertising...")
advState = false
} else {
println("disconnected, advertising...")
advState = true
advUntil = time.Now().Add(5 * time.Minute)
}
})
must("start adv", adv.Start())
println("advertising...")
address, _ := adapter.Address()
for {
if advState && time.Now().After(advUntil) {
println("timeout, not advertising...")
advState = false
must("stop adv", adv.Stop())
}
println("Go Bluetooth /", address.MAC.String())
time.Sleep(time.Second)
}
}
func must(action string, err error) {
if err != nil {
panic("failed to " + action + ": " + err.Error())
}
}

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,
})
}

229
gap.go
View file

@ -25,12 +25,12 @@ func (mac MACAddress) IsRandom() bool {
}
// SetRandom if is a random address.
func (mac *MACAddress) SetRandom(val bool) {
func (mac MACAddress) SetRandom(val bool) {
mac.isRandom = val
}
// Set the address
func (mac *MACAddress) Set(val string) {
func (mac MACAddress) Set(val string) {
m, err := ParseMAC(val)
if err != nil {
return
@ -53,36 +53,6 @@ type AdvertisementOptions struct {
// Interval in BLE-specific units. Create an interval by using NewDuration.
Interval Duration
// ManufacturerData stores Advertising Data.
ManufacturerData []ManufacturerDataElement
// ServiceData stores Advertising Data.
ServiceData []ServiceDataElement
}
// Manufacturer data that's part of an advertisement packet.
type ManufacturerDataElement struct {
// The company ID, which must be one of the assigned company IDs.
// The full list is in here:
// https://www.bluetooth.com/specifications/assigned-numbers/
// The list can also be viewed here:
// https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/company_identifiers/company_identifiers.yaml
// The value 0xffff can also be used for testing.
CompanyID uint16
// The value, which can be any value but can't be very large.
Data []byte
}
// ServiceDataElement strores a uuid/byte-array pair used as ServiceData advertisment elements
type ServiceDataElement struct {
// Service UUID.
// The list can also be viewed here:
// https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/uuids/service_uuids.yaml
UUID UUID
// the data byte array
Data []byte
}
// Duration is the unit of time used in BLE, in 0.625µs units. This unit of time
@ -99,13 +69,34 @@ func NewDuration(interval time.Duration) Duration {
// Connection is a numeric identifier that indicates a connection handle.
type Connection uint16
// Addresser contains a Bluetooth address, which is a MAC address plus some extra
// information.
type Addresser interface {
// String of the address
String() string
// Set the address
Set(val string)
// Is this address a random address?
// Bluetooth addresses are roughly split in two kinds: public
// (IEEE-assigned) addresses and random (not IEEE assigned) addresses.
// "Random" here doesn't mean it is exactly random but at least it looks
// random. Sometimes, it contains a hash.
// For more information:
// https://www.novelbits.io/bluetooth-address-privacy-ble/
// Set the address
SetRandom(bool)
IsRandom() bool
}
// ScanResult contains information from when an advertisement packet was
// received. It is passed as a parameter to the callback of the Scan method.
type ScanResult struct {
// Bluetooth address of the scanned device.
Address Address
Address Addresser
// Signal strength of the advertisement packet.
// RSSI the last time a packet from this device has been received.
RSSI int16
// The data obtained from the advertisement data, which may contain many
@ -135,14 +126,6 @@ type AdvertisementPayload interface {
// Bytes returns the raw advertisement packet, if available. It returns nil
// if this data is not available.
Bytes() []byte
// ManufacturerData returns a slice with all the manufacturer data present in the
// advertising. It may be empty.
ManufacturerData() []ManufacturerDataElement
// ServiceData returns a slice with all the service data present in the
// advertising. It may be empty.
ServiceData() []ServiceDataElement
}
// AdvertisementFields contains advertisement fields in structured form.
@ -155,12 +138,6 @@ type AdvertisementFields struct {
// part of the advertisement packet, in data types such as "complete list of
// 128-bit UUIDs".
ServiceUUIDs []UUID
// ManufacturerData is the manufacturer data of the advertisement.
ManufacturerData []ManufacturerDataElement
// ServiceData is the service data of the advertisement.
ServiceData []ServiceDataElement
}
// advertisementFields wraps AdvertisementFields to implement the
@ -193,16 +170,6 @@ func (p *advertisementFields) Bytes() []byte {
return nil
}
// ManufacturerData returns the underlying ManufacturerData field.
func (p *advertisementFields) ManufacturerData() []ManufacturerDataElement {
return p.AdvertisementFields.ManufacturerData
}
// ServiceData returns the underlying ServiceData field.
func (p *advertisementFields) ServiceData() []ServiceDataElement {
return p.AdvertisementFields.ServiceData
}
// rawAdvertisementPayload encapsulates a raw advertisement packet. Methods to
// get the data (such as LocalName()) will parse just the needed field. Scanning
// the data should be fast as most advertisement packets only have a very small
@ -291,61 +258,6 @@ func (buf *rawAdvertisementPayload) HasServiceUUID(uuid UUID) bool {
}
}
// ManufacturerData returns the manufacturer data in the advertisement payload.
func (buf *rawAdvertisementPayload) ManufacturerData() []ManufacturerDataElement {
var manufacturerData []ManufacturerDataElement
for index := 0; index < int(buf.len)+4; index += int(buf.data[index]) + 1 {
fieldLength := int(buf.data[index+0])
if fieldLength < 3 {
continue
}
fieldType := buf.data[index+1]
if fieldType != 0xff {
continue
}
key := uint16(buf.data[index+2]) | uint16(buf.data[index+3])<<8
manufacturerData = append(manufacturerData, ManufacturerDataElement{
CompanyID: key,
Data: buf.data[index+4 : index+fieldLength+1],
})
}
return manufacturerData
}
// ServiceData returns the service data in the advertisment payload
func (buf *rawAdvertisementPayload) ServiceData() []ServiceDataElement {
var serviceData []ServiceDataElement
for index := 0; index < int(buf.len)+4; index += int(buf.data[index]) + 1 {
fieldLength := int(buf.data[index+0])
if fieldLength < 3 { // field has only length and type and no data
continue
}
fieldType := buf.data[index+1]
switch fieldType {
case 0x16: // 16-bit uuid
serviceData = append(serviceData, ServiceDataElement{
UUID: New16BitUUID(uint16(buf.data[index+2]) + (uint16(buf.data[index+3]) << 8)),
Data: buf.data[index+4 : index+fieldLength+1],
})
case 0x20: // 32-bit uuid
serviceData = append(serviceData, ServiceDataElement{
UUID: New32BitUUID(uint32(buf.data[index+2]) + (uint32(buf.data[index+3]) << 8) + (uint32(buf.data[index+4]) << 16) + (uint32(buf.data[index+5]) << 24)),
Data: buf.data[index+6 : index+fieldLength+1],
})
case 0x21: // 128-bit uuid
var uuidArray [16]byte
copy(uuidArray[:], buf.data[index+2:index+18])
serviceData = append(serviceData, ServiceDataElement{
UUID: NewUUID(uuidArray),
Data: buf.data[index+18 : index+fieldLength+1],
})
default:
continue
}
}
return serviceData
}
// reset restores this buffer to the original state.
func (buf *rawAdvertisementPayload) reset() {
// The data is not reset (only the length), because with a zero length the
@ -373,89 +285,6 @@ func (buf *rawAdvertisementPayload) addFromOptions(options AdvertisementOptions)
return false
}
}
for _, element := range options.ManufacturerData {
if !buf.addManufacturerData(element.CompanyID, element.Data) {
return false
}
}
for _, element := range options.ServiceData {
if !buf.addServiceData(element.UUID, element.Data) {
return false
}
}
return true
}
// addManufacturerData adds manufacturer data ([]byte) entries to the advertisement payload.
func (buf *rawAdvertisementPayload) addManufacturerData(key uint16, value []byte) (ok bool) {
// Check whether the field can fit this manufacturer data.
fieldLength := len(value) + 4
if int(buf.len)+fieldLength > len(buf.data) {
return false
}
// Add the data.
buf.data[buf.len+0] = uint8(fieldLength - 1)
buf.data[buf.len+1] = 0xff
buf.data[buf.len+2] = uint8(key)
buf.data[buf.len+3] = uint8(key >> 8)
copy(buf.data[buf.len+4:], value)
buf.len += uint8(fieldLength)
return true
}
// addServiceData adds service data ([]byte) entries to the advertisement payload.
func (buf *rawAdvertisementPayload) addServiceData(uuid UUID, data []byte) (ok bool) {
switch {
case uuid.Is16Bit():
// check if it fits
fieldLength := 1 + 1 + 2 + len(data) // 1 byte length, 1 byte ad type, 2 bytes uuid, actual service data
if int(buf.len)+fieldLength > len(buf.data) {
return false
}
// Add the data.
buf.data[buf.len+0] = byte(fieldLength - 1)
buf.data[buf.len+1] = 0x16
buf.data[buf.len+2] = byte(uuid.Get16Bit())
buf.data[buf.len+3] = byte(uuid.Get16Bit() >> 8)
copy(buf.data[buf.len+4:], data)
buf.len += uint8(fieldLength)
case uuid.Is32Bit():
// check if it fits
fieldLength := 1 + 1 + 4 + len(data) // 1 byte length, 1 byte ad type, 4 bytes uuid, actual service data
if int(buf.len)+fieldLength > len(buf.data) {
return false
}
// Add the data.
buf.data[buf.len+0] = byte(fieldLength - 1)
buf.data[buf.len+1] = 0x20
buf.data[buf.len+2] = byte(uuid.Get32Bit())
buf.data[buf.len+3] = byte(uuid.Get32Bit() >> 8)
buf.data[buf.len+4] = byte(uuid.Get32Bit() >> 16)
buf.data[buf.len+5] = byte(uuid.Get32Bit() >> 24)
copy(buf.data[buf.len+6:], data)
buf.len += uint8(fieldLength)
default: // must be 128-bit uuid
// check if it fits
fieldLength := 1 + 1 + 16 + len(data) // 1 byte length, 1 byte ad type, 16 bytes uuid, actual service data
if int(buf.len)+fieldLength > len(buf.data) {
return false
}
// Add the data.
buf.data[buf.len+0] = byte(fieldLength - 1)
buf.data[buf.len+1] = 0x21
uuid_bytes := uuid.Bytes()
copy(buf.data[buf.len+2:], uuid_bytes[:])
copy(buf.data[buf.len+2+16:], data)
buf.len += uint8(fieldLength)
}
return true
}
@ -517,8 +346,7 @@ func (buf *rawAdvertisementPayload) addServiceUUID(uuid UUID) (ok bool) {
}
}
// ConnectionParams are used when connecting to a peripherals or when changing
// the parameters of an active connection.
// ConnectionParams are used when connecting to a peripherals.
type ConnectionParams struct {
// The timeout for the connection attempt. Not used during the rest of the
// connection. If no duration is specified, a default timeout will be used.
@ -530,9 +358,4 @@ type ConnectionParams struct {
// will be used.
MinInterval Duration
MaxInterval Duration
// Connection Supervision Timeout. After this time has passed with no
// communication, the connection is considered lost. If no timeout is
// specified, the timeout will be unchanged.
Timeout Duration
}

View file

@ -5,12 +5,9 @@ import (
"fmt"
"time"
"github.com/tinygo-org/cbgo"
"github.com/JuulLabs-OSS/cbgo"
)
// default connection timeout
const defaultConnectionTimeout time.Duration = 10 * time.Second
// Address contains a Bluetooth address which on macOS is a UUID.
type Address struct {
// UUID since this is macOS.
@ -23,11 +20,11 @@ func (ad Address) IsRandom() bool {
}
// SetRandom ignored on macOS.
func (ad *Address) SetRandom(val bool) {
func (ad Address) SetRandom(val bool) {
}
// Set the address
func (ad *Address) Set(val string) {
func (ad Address) Set(val string) {
uuid, err := ParseUUID(val)
if err != nil {
return
@ -85,12 +82,6 @@ func (a *Adapter) StopScan() error {
// Device is a connection to a remote peripheral.
type Device struct {
Address Address
*deviceInternal
}
type deviceInternal struct {
delegate *peripheralDelegate
cm cbgo.CentralManager
@ -99,101 +90,57 @@ type deviceInternal struct {
servicesChan chan error
charsChan chan error
services map[UUID]DeviceService
services map[UUID]*DeviceService
characteristics map[UUID]*DeviceCharacteristic
}
// Connect starts a connection attempt to the given peripheral device address.
func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, error) {
uuid, err := cbgo.ParseUUID(address.UUID.String())
func (a *Adapter) Connect(address Addresser, params ConnectionParams) (*Device, error) {
adr := address.(Address)
uuid, err := cbgo.ParseUUID(adr.UUID.String())
if err != nil {
return Device{}, err
return nil, err
}
prphs := a.cm.RetrievePeripheralsWithIdentifiers([]cbgo.UUID{uuid})
if len(prphs) == 0 {
return Device{}, fmt.Errorf("Connect failed: no peer with address: %s", address.UUID.String())
return nil, fmt.Errorf("Connect failed: no peer with address: %s", adr.UUID.String())
}
timeout := defaultConnectionTimeout
if params.ConnectionTimeout != 0 {
timeout = time.Duration(int64(params.ConnectionTimeout)*625) * time.Microsecond
}
id := prphs[0].Identifier().String()
prphCh := make(chan cbgo.Peripheral)
a.connectMap.Store(id, prphCh)
defer a.connectMap.Delete(id)
a.cm.Connect(prphs[0], nil)
timeoutTimer := time.NewTimer(timeout)
var connectionError error
for {
// wait on channel for connect
select {
case p := <-prphCh:
// check if we have received a disconnected peripheral
if p.State() == cbgo.PeripheralStateDisconnected {
return Device{}, connectionError
}
d := Device{
Address: address,
deviceInternal: &deviceInternal{
cm: a.cm,
prph: p,
servicesChan: make(chan error),
charsChan: make(chan error),
},
}
d.delegate = &peripheralDelegate{d: d}
p.SetDelegate(d.delegate)
a.connectHandler(d, true)
return d, nil
case <-timeoutTimer.C:
// we need to cancel the connection if we have timed out ourselves
a.cm.CancelConnect(prphs[0])
// record an error to use when the disconnect comes through later.
connectionError = errors.New("timeout on Connect")
// we are not ready to return yet, we need to wait for the disconnect event to come through
// so continue on from this case and wait for something to show up on prphCh
continue
// wait on channel for connect
select {
case p := <-a.connectChan:
d := &Device{
cm: a.cm,
prph: p,
servicesChan: make(chan error),
charsChan: make(chan error),
}
d.delegate = &peripheralDelegate{d: d}
p.SetDelegate(d.delegate)
a.connectHandler(nil, true)
return d, nil
case <-time.NewTimer(10 * time.Second).C:
return nil, errors.New("timeout on Connect")
}
}
// Disconnect from the BLE device. This method is non-blocking and does not
// wait until the connection is fully gone.
func (d Device) Disconnect() error {
func (d *Device) Disconnect() error {
d.cm.CancelConnect(d.prph)
return nil
}
// RequestConnectionParams requests a different connection latency and timeout
// of the given device connection. Fields that are unset will be left alone.
// Whether or not the device will actually honor this, depends on the device and
// on the specific parameters.
//
// This call has not yet been implemented on macOS.
func (d Device) RequestConnectionParams(params ConnectionParams) error {
// TODO: implement this using setDesiredConnectionLatency, see:
// https://developer.apple.com/documentation/corebluetooth/cbperipheralmanager/1393277-setdesiredconnectionlatency
return nil
}
// Peripheral delegate functions
type peripheralDelegate struct {
cbgo.PeripheralDelegateBase
d Device
d *Device
}
// DidDiscoverServices is called when the services for a Peripheral
@ -213,39 +160,13 @@ func (pd *peripheralDelegate) DidDiscoverCharacteristics(prph cbgo.Peripheral, s
// or receives a value for a read request.
func (pd *peripheralDelegate) DidUpdateValueForCharacteristic(prph cbgo.Peripheral, chr cbgo.Characteristic, err error) {
uuid, _ := ParseUUID(chr.UUID().String())
svcuuid, _ := ParseUUID(chr.Service().UUID().String())
if svc, ok := pd.d.services[svcuuid]; ok {
for _, char := range svc.characteristics {
if char.characteristic == chr && uuid == char.UUID() { // compare pointers
if err == nil && char.callback != nil {
go char.callback(chr.Value())
}
if char.readChan != nil {
char.readChan <- err
}
}
if char, ok := pd.d.characteristics[uuid]; ok {
if err == nil && char.callback != nil {
go char.callback(chr.Value())
}
}
}
// DidWriteValueForCharacteristic is called after the characteristic for a Service
// for a Peripheral trigger a write with response. It contains the returned error or nil.
func (pd *peripheralDelegate) DidWriteValueForCharacteristic(_ cbgo.Peripheral, chr cbgo.Characteristic, err error) {
uuid, _ := ParseUUID(chr.UUID().String())
svcuuid, _ := ParseUUID(chr.Service().UUID().String())
if svc, ok := pd.d.services[svcuuid]; ok {
for _, char := range svc.characteristics {
if char.characteristic == chr && uuid == char.UUID() { // compare pointers
if char.writeChan != nil {
char.writeChan <- err
}
}
if char.readChan != nil {
char.readChan <- err
}
}
}

View file

@ -1,422 +0,0 @@
//go:build hci || ninafw || cyw43439
package bluetooth
import (
"encoding/binary"
"errors"
"slices"
"time"
)
const defaultMTU = 23
var (
ErrConnect = errors.New("bluetooth: could not connect")
)
// Scan starts a BLE scan.
func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
if a.scanning {
return errScanning
}
if err := a.hci.leSetScanEnable(false, true); err != nil {
return err
}
// passive scanning, every 40ms, for 30ms
if err := a.hci.leSetScanParameters(0x00, 0x0080, 0x0030, 0x00, 0x00); err != nil {
return err
}
a.scanning = true
// scan with duplicates
if err := a.hci.leSetScanEnable(true, false); err != nil {
return err
}
lastUpdate := time.Now().UnixNano()
for {
if err := a.hci.poll(); err != nil {
return err
}
switch {
case a.hci.advData.reported:
adf := AdvertisementFields{}
if a.hci.advData.eirLength > 31 {
if debug {
println("eirLength too long")
}
a.hci.clearAdvData()
continue
}
for i := 0; i < int(a.hci.advData.eirLength); {
l, t := int(a.hci.advData.eirData[i]), a.hci.advData.eirData[i+1]
if l < 1 {
break
}
switch t {
case 0x02, 0x03:
// 16-bit Service Class UUID
adf.ServiceUUIDs = append(adf.ServiceUUIDs, New16BitUUID(binary.LittleEndian.Uint16(a.hci.advData.eirData[i+2:i+4])))
case 0x06, 0x07:
// 128-bit Service Class UUID
var uuid [16]byte
copy(uuid[:], a.hci.advData.eirData[i+2:i+18])
adf.ServiceUUIDs = append(adf.ServiceUUIDs, NewUUID(uuid))
case 0x08, 0x09:
if debug {
println("local name", string(a.hci.advData.eirData[i+2:i+1+l]))
}
adf.LocalName = string(a.hci.advData.eirData[i+2 : i+1+l])
case 0xFF:
// Manufacturer Specific Data
}
i += l + 1
}
random := a.hci.advData.peerBdaddrType == 0x01
callback(a, ScanResult{
Address: Address{
MACAddress{
MAC: makeAddress(a.hci.advData.peerBdaddr),
isRandom: random,
},
},
RSSI: int16(a.hci.advData.rssi),
AdvertisementPayload: &advertisementFields{
AdvertisementFields: adf,
},
})
a.hci.clearAdvData()
time.Sleep(5 * time.Millisecond)
default:
if !a.scanning {
return nil
}
if debug && (time.Now().UnixNano()-lastUpdate)/int64(time.Second) > 1 {
println("still scanning...")
lastUpdate = time.Now().UnixNano()
}
time.Sleep(5 * time.Millisecond)
}
}
return nil
}
func (a *Adapter) StopScan() error {
if !a.scanning {
return errNotScanning
}
if err := a.hci.leSetScanEnable(false, false); err != nil {
return err
}
a.scanning = false
return nil
}
// Address contains a Bluetooth MAC address.
type Address struct {
MACAddress
}
// Connect starts a connection attempt to the given peripheral device address.
func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, error) {
if debug {
println("Connect")
}
random := uint8(0)
if address.isRandom {
random = 1
}
if err := a.hci.leCreateConn(0x0060, 0x0030, 0x00,
random, makeNINAAddress(address.MAC),
0x00, 0x0006, 0x000c, 0x0000, 0x00c8, 0x0004, 0x0006); err != nil {
return Device{}, err
}
// are we connected?
start := time.Now().UnixNano()
for {
if err := a.hci.poll(); err != nil {
return Device{}, err
}
if a.hci.connectData.connected {
defer a.hci.clearConnectData()
random := false
if address.isRandom {
random = true
}
d := Device{
Address: Address{
MACAddress{
MAC: makeAddress(a.hci.connectData.peerBdaddr),
isRandom: random},
},
deviceInternal: &deviceInternal{
adapter: a,
handle: a.hci.connectData.handle,
mtu: defaultMTU,
notificationRegistrations: make([]notificationRegistration, 0),
},
}
a.addConnection(d)
return d, nil
} else {
// check for timeout
if (time.Now().UnixNano()-start)/int64(time.Second) > 5 {
break
}
time.Sleep(5 * time.Millisecond)
}
}
// cancel connection attempt that failed
if err := a.hci.leCancelConn(); err != nil {
return Device{}, err
}
return Device{}, ErrConnect
}
type notificationRegistration struct {
handle uint16
callback func([]byte)
}
// Device is a connection to a remote peripheral.
type Device struct {
Address Address
*deviceInternal
}
type deviceInternal struct {
adapter *Adapter
handle uint16
mtu uint16
notificationRegistrations []notificationRegistration
}
// Disconnect from the BLE device.
func (d Device) Disconnect() error {
if debug {
println("Disconnect")
}
if err := d.adapter.hci.disconnect(d.handle); err != nil {
return err
}
d.adapter.removeConnection(d)
return nil
}
// RequestConnectionParams requests a different connection latency and timeout
// of the given device connection. Fields that are unset will be left alone.
// Whether or not the device will actually honor this, depends on the device and
// on the specific parameters.
//
// On NINA, this call hasn't been implemented yet.
func (d Device) RequestConnectionParams(params ConnectionParams) error {
return nil
}
func (d Device) findNotificationRegistration(handle uint16) *notificationRegistration {
for _, n := range d.notificationRegistrations {
if n.handle == handle {
return &n
}
}
return nil
}
func (d Device) addNotificationRegistration(handle uint16, callback func([]byte)) {
d.notificationRegistrations = append(d.notificationRegistrations,
notificationRegistration{
handle: handle,
callback: callback,
})
}
func (d Device) startNotifications() {
d.adapter.startNotifications()
}
var defaultAdvertisement Advertisement
// Advertisement encapsulates a single advertisement instance.
type Advertisement struct {
adapter *Adapter
localName []byte
serviceUUIDs []UUID
interval uint16
}
// DefaultAdvertisement returns the default advertisement instance but does not
// configure it.
func (a *Adapter) DefaultAdvertisement() *Advertisement {
if defaultAdvertisement.adapter == nil {
defaultAdvertisement.adapter = a
}
return &defaultAdvertisement
}
// Configure this advertisement.
func (a *Advertisement) Configure(options AdvertisementOptions) error {
switch {
case options.LocalName != "":
a.localName = []byte(options.LocalName)
default:
a.localName = []byte("TinyGo")
}
a.serviceUUIDs = append([]UUID{}, options.ServiceUUIDs...)
a.interval = uint16(options.Interval)
a.adapter.AddService(
&Service{
UUID: ServiceUUIDGenericAccess,
Characteristics: []CharacteristicConfig{
{
UUID: CharacteristicUUIDDeviceName,
Flags: CharacteristicReadPermission,
Value: a.localName,
},
{
UUID: CharacteristicUUIDAppearance,
Flags: CharacteristicReadPermission,
},
},
})
a.adapter.AddService(
&Service{
UUID: ServiceUUIDGenericAttribute,
Characteristics: []CharacteristicConfig{
{
UUID: CharacteristicUUIDServiceChanged,
Flags: CharacteristicIndicatePermission,
},
},
})
return nil
}
// Start advertisement. May only be called after it has been configured.
func (a *Advertisement) Start() error {
// uint8_t type = (_connectable) ? 0x00 : (_localName ? 0x02 : 0x03);
typ := uint8(0x00)
if err := a.adapter.hci.leSetAdvertisingParameters(a.interval, a.interval,
typ, 0x00, 0x00, [6]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0x07, 0); err != nil {
return err
}
var advertisingData [31]byte
advertisingDataLen := uint8(0)
advertisingData[0] = 0x02
advertisingData[1] = 0x01
advertisingData[2] = 0x06
advertisingDataLen += 3
// TODO: handle multiple service UUIDs
if len(a.serviceUUIDs) > 0 {
uuid := a.serviceUUIDs[0]
var sz uint8
switch {
case uuid.Is16Bit():
sz = 2
binary.LittleEndian.PutUint16(advertisingData[5:], uuid.Get16Bit())
case uuid.Is32Bit():
sz = 6
data := uuid.Bytes()
slices.Reverse(data[:])
copy(advertisingData[5:], data[:])
}
advertisingData[3] = sz + 1
advertisingData[4] = sz
advertisingDataLen += sz + 2
}
// TODO: handle manufacturer data
if err := a.adapter.hci.leSetAdvertisingData(advertisingData[:advertisingDataLen]); err != nil {
return err
}
var scanResponseData [31]byte
scanResponseDataLen := uint8(0)
switch {
case len(a.localName) > 29:
scanResponseData[1] = 0x08
scanResponseData[0] = 1 + 29
copy(scanResponseData[2:], a.localName[:29])
scanResponseDataLen = 31
case len(a.localName) > 0:
scanResponseData[1] = 0x09
scanResponseData[0] = uint8(1 + len(a.localName))
copy(scanResponseData[2:], a.localName)
scanResponseDataLen = uint8(2 + len(a.localName))
}
if err := a.adapter.hci.leSetScanResponseData(scanResponseData[:scanResponseDataLen]); err != nil {
return err
}
if err := a.adapter.hci.leSetAdvertiseEnable(true); err != nil {
return err
}
// go routine to poll for HCI events while advertising
go func() {
for {
if err := a.adapter.att.poll(); err != nil {
// TODO: handle error
if debug {
println("error polling while advertising:", err.Error())
}
}
time.Sleep(5 * time.Millisecond)
}
}()
return nil
}
// Stop advertisement. May only be called after it has been started.
func (a *Advertisement) Stop() error {
return a.adapter.hci.leSetAdvertiseEnable(false)
}

View file

@ -1,23 +1,16 @@
//go:build !baremetal
// +build !baremetal
package bluetooth
import (
"errors"
"fmt"
"strings"
"sync/atomic"
"github.com/godbus/dbus/v5"
"github.com/godbus/dbus/v5/prop"
"github.com/muka/go-bluetooth/api"
"github.com/muka/go-bluetooth/bluez/profile/advertising"
"github.com/muka/go-bluetooth/bluez/profile/device"
)
var errAdvertisementNotStarted = errors.New("bluetooth: stop advertisement that was not started")
var errAdvertisementAlreadyStarted = errors.New("bluetooth: start advertisement that was already started")
// Unique ID per advertisement (to generate a unique object path).
var advertisementID uint64
// Address contains a Bluetooth MAC address.
type Address struct {
MACAddress
@ -25,9 +18,9 @@ type Address struct {
// Advertisement encapsulates a single advertisement instance.
type Advertisement struct {
adapter *Adapter
properties *prop.Properties
path dbus.ObjectPath
adapter *Adapter
advertisement *api.Advertisement
properties *advertising.LEAdvertisement1Properties
}
// DefaultAdvertisement returns the default advertisement instance but does not
@ -45,82 +38,30 @@ func (a *Adapter) DefaultAdvertisement() *Advertisement {
//
// On Linux with BlueZ, it is not possible to set the advertisement interval.
func (a *Advertisement) Configure(options AdvertisementOptions) error {
if a.properties != nil {
if a.advertisement != nil {
panic("todo: configure advertisement a second time")
}
var serviceUUIDs []string
a.properties = &advertising.LEAdvertisement1Properties{
Type: advertising.AdvertisementTypeBroadcast,
Timeout: 1<<16 - 1,
LocalName: options.LocalName,
}
for _, uuid := range options.ServiceUUIDs {
serviceUUIDs = append(serviceUUIDs, uuid.String())
a.properties.ServiceUUIDs = append(a.properties.ServiceUUIDs, uuid.String())
}
var serviceData = make(map[string]interface{})
for _, element := range options.ServiceData {
serviceData[element.UUID.String()] = element.Data
}
// Convert map[uint16][]byte to map[uint16]any because that's what BlueZ needs.
manufacturerData := map[uint16]any{}
for _, element := range options.ManufacturerData {
manufacturerData[element.CompanyID] = element.Data
}
// Build an org.bluez.LEAdvertisement1 object, to be exported over DBus.
// See:
// https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/org.bluez.LEAdvertisement.rst
id := atomic.AddUint64(&advertisementID, 1)
a.path = dbus.ObjectPath(fmt.Sprintf("/org/tinygo/bluetooth/advertisement%d", id))
propsSpec := map[string]map[string]*prop.Prop{
"org.bluez.LEAdvertisement1": {
"Type": {Value: "broadcast"},
"ServiceUUIDs": {Value: serviceUUIDs},
"ManufacturerData": {Value: manufacturerData},
"LocalName": {Value: options.LocalName},
"ServiceData": {Value: serviceData},
// The documentation states:
// > Timeout of the advertisement in seconds. This defines the
// > lifetime of the advertisement.
// however, the value 0 also works, and presumably means "no
// timeout".
"Timeout": {Value: uint16(0)},
// TODO: MinInterval and MaxInterval (experimental as of BlueZ 5.71)
},
}
props, err := prop.Export(a.adapter.bus, a.path, propsSpec)
if err != nil {
return err
}
a.properties = props
return nil
}
// Start advertisement. May only be called after it has been configured.
func (a *Advertisement) Start() error {
// Register our advertisement object to start advertising.
err := a.adapter.adapter.Call("org.bluez.LEAdvertisingManager1.RegisterAdvertisement", 0, a.path, map[string]interface{}{}).Err
if err != nil {
if err, ok := err.(dbus.Error); ok && err.Name == "org.bluez.Error.AlreadyExists" {
return errAdvertisementAlreadyStarted
}
return fmt.Errorf("bluetooth: could not start advertisement: %w", err)
if a.advertisement != nil {
panic("todo: start advertisement a second time")
}
// Make us discoverable.
err = a.adapter.adapter.SetProperty("org.bluez.Adapter1.Discoverable", dbus.MakeVariant(true))
_, err := api.ExposeAdvertisement(a.adapter.id, a.properties, uint32(a.properties.Timeout))
if err != nil {
return fmt.Errorf("bluetooth: could not start advertisement: %w", err)
}
return nil
}
// Stop advertisement. May only be called after it has been started.
func (a *Advertisement) Stop() error {
err := a.adapter.adapter.Call("org.bluez.LEAdvertisingManager1.UnregisterAdvertisement", 0, a.path).Err
if err != nil {
if err, ok := err.(dbus.Error); ok && err.Name == "org.bluez.Error.DoesNotExist" {
return errAdvertisementNotStarted
}
return fmt.Errorf("bluetooth: could not stop advertisement: %w", err)
return err
}
return nil
}
@ -134,7 +75,7 @@ func (a *Advertisement) Stop() error {
// possible some events are missed and perhaps even possible that some events
// are duplicated.
func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
if a.scanCancelChan != nil {
if a.cancelChan != nil {
return errScanning
}
@ -142,61 +83,58 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
// Detecting whether the scan is stopped can be done by doing a non-blocking
// read from it. If it succeeds, the scan is stopped.
cancelChan := make(chan struct{})
a.scanCancelChan = cancelChan
a.cancelChan = cancelChan
// This appears to be necessary to receive any BLE discovery results at all.
defer a.adapter.Call("org.bluez.Adapter1.SetDiscoveryFilter", 0)
err := a.adapter.Call("org.bluez.Adapter1.SetDiscoveryFilter", 0, map[string]interface{}{
defer a.adapter.SetDiscoveryFilter(nil)
err := a.adapter.SetDiscoveryFilter(map[string]interface{}{
"Transport": "le",
}).Err
})
if err != nil {
return err
}
bus, err := dbus.SystemBus()
if err != nil {
return err
}
signal := make(chan *dbus.Signal)
a.bus.Signal(signal)
defer a.bus.RemoveSignal(signal)
bus.Signal(signal)
defer bus.RemoveSignal(signal)
propertiesChangedMatchOptions := []dbus.MatchOption{dbus.WithMatchInterface("org.freedesktop.DBus.Properties")}
a.bus.AddMatchSignal(propertiesChangedMatchOptions...)
defer a.bus.RemoveMatchSignal(propertiesChangedMatchOptions...)
bus.AddMatchSignal(propertiesChangedMatchOptions...)
defer bus.RemoveMatchSignal(propertiesChangedMatchOptions...)
newObjectMatchOptions := []dbus.MatchOption{dbus.WithMatchInterface("org.freedesktop.DBus.ObjectManager")}
a.bus.AddMatchSignal(newObjectMatchOptions...)
defer a.bus.RemoveMatchSignal(newObjectMatchOptions...)
bus.AddMatchSignal(newObjectMatchOptions...)
defer bus.RemoveMatchSignal(newObjectMatchOptions...)
// Go through all connected devices and present the connected devices as
// scan results. Also save the properties so that the full list of
// properties is known on a PropertiesChanged signal. We can't present the
// list of cached devices as scan results as devices may be cached for a
// long time, long after they have moved out of range.
var deviceList map[dbus.ObjectPath]map[string]map[string]dbus.Variant
err = a.bluez.Call("org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0).Store(&deviceList)
deviceList, err := a.adapter.GetDevices()
if err != nil {
return err
}
devices := make(map[dbus.ObjectPath]map[string]dbus.Variant)
for path, v := range deviceList {
device, ok := v["org.bluez.Device1"]
if !ok {
continue // not a device
}
if !strings.HasPrefix(string(path), string(a.adapter.Path())) {
continue // not part of our adapter
}
if device["Connected"].Value().(bool) {
callback(a, makeScanResult(device))
devices := make(map[dbus.ObjectPath]*device.Device1Properties)
for _, dev := range deviceList {
if dev.Properties.Connected {
callback(a, makeScanResult(dev.Properties))
select {
case <-cancelChan:
return nil
default:
}
}
devices[path] = device
devices[dev.Path()] = dev.Properties
}
// Instruct BlueZ to start discovering.
err = a.adapter.Call("org.bluez.Adapter1.StartDiscovery", 0).Err
err = a.adapter.StartDiscovery()
if err != nil {
return err
}
@ -208,7 +146,8 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
// StopScan is called).
select {
case <-cancelChan:
return a.adapter.Call("org.bluez.Adapter1.StopDiscovery", 0).Err
a.adapter.StopDiscovery()
return nil
default:
}
@ -224,24 +163,28 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
if !ok {
continue
}
devices[objectPath] = rawprops
callback(a, makeScanResult(rawprops))
var props *device.Device1Properties
props, _ = props.FromDBusMap(rawprops)
devices[objectPath] = props
callback(a, makeScanResult(props))
case "org.freedesktop.DBus.Properties.PropertiesChanged":
interfaceName := sig.Body[0].(string)
if interfaceName != "org.bluez.Device1" {
continue
}
changes := sig.Body[1].(map[string]dbus.Variant)
device, ok := devices[sig.Path]
if !ok {
// This shouldn't happen, but protect against it just in
// case.
continue
props := devices[sig.Path]
for field, val := range changes {
switch field {
case "RSSI":
props.RSSI = val.Value().(int16)
case "Name":
props.Name = val.Value().(string)
case "UUIDs":
props.UUIDs = val.Value().([]string)
}
}
for k, v := range changes {
device[k] = v
}
callback(a, makeScanResult(device))
callback(a, makeScanResult(props))
}
case <-cancelChan:
continue
@ -255,67 +198,37 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
// callback to stop the current scan. If no scan is in progress, an error will
// be returned.
func (a *Adapter) StopScan() error {
if a.scanCancelChan == nil {
if a.cancelChan == nil {
return errNotScanning
}
close(a.scanCancelChan)
a.scanCancelChan = nil
close(a.cancelChan)
a.cancelChan = nil
return nil
}
// makeScanResult creates a ScanResult from a raw DBus device.
func makeScanResult(props map[string]dbus.Variant) ScanResult {
// makeScanResult creates a ScanResult from a Device1 object.
func makeScanResult(props *device.Device1Properties) ScanResult {
// Assume the Address property is well-formed.
addr, _ := ParseMAC(props["Address"].Value().(string))
addr, _ := ParseMAC(props.Address)
// Create a list of UUIDs.
var serviceUUIDs []UUID
for _, uuid := range props["UUIDs"].Value().([]string) {
for _, uuid := range props.UUIDs {
// Assume the UUID is well-formed.
parsedUUID, _ := ParseUUID(uuid)
serviceUUIDs = append(serviceUUIDs, parsedUUID)
}
a := Address{MACAddress{MAC: addr}}
a.SetRandom(props["AddressType"].Value().(string) == "random")
var manufacturerData []ManufacturerDataElement
if mdata, ok := props["ManufacturerData"].Value().(map[uint16]dbus.Variant); ok {
for k, v := range mdata {
manufacturerData = append(manufacturerData, ManufacturerDataElement{
CompanyID: k,
Data: v.Value().([]byte),
})
}
}
// Get optional properties.
localName, _ := props["Name"].Value().(string)
rssi, _ := props["RSSI"].Value().(int16)
var serviceData []ServiceDataElement
if sdata, ok := props["ServiceData"].Value().(map[string]dbus.Variant); ok {
for k, v := range sdata {
uuid, err := ParseUUID(k)
if err != nil {
continue
}
serviceData = append(serviceData, ServiceDataElement{
UUID: uuid,
Data: v.Value().([]byte),
})
}
}
a.SetRandom(props.AddressType == "random")
return ScanResult{
RSSI: rssi,
RSSI: props.RSSI,
Address: a,
AdvertisementPayload: &advertisementFields{
AdvertisementFields{
LocalName: localName,
ServiceUUIDs: serviceUUIDs,
ManufacturerData: manufacturerData,
ServiceData: serviceData,
LocalName: props.Name,
ServiceUUIDs: serviceUUIDs,
},
},
}
@ -323,88 +236,39 @@ func makeScanResult(props map[string]dbus.Variant) ScanResult {
// Device is a connection to a remote peripheral.
type Device struct {
Address Address // the MAC address of the device
device dbus.BusObject // bluez device interface
adapter *Adapter // the adapter that was used to form this device connection
device *device.Device1
}
// Connect starts a connection attempt to the given peripheral device address.
//
// On Linux and Windows, the IsRandom part of the address is ignored.
func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, error) {
devicePath := dbus.ObjectPath(string(a.adapter.Path()) + "/dev_" + strings.Replace(address.MAC.String(), ":", "_", -1))
device := Device{
Address: address,
device: a.bus.Object("org.bluez", devicePath),
adapter: a,
}
// Already start watching for property changes. We do this before reading
// the Connected property below to avoid a race condition: if the device
// were connected between the two calls the signal wouldn't be picked up.
signal := make(chan *dbus.Signal)
a.bus.Signal(signal)
defer a.bus.RemoveSignal(signal)
propertiesChangedMatchOptions := []dbus.MatchOption{dbus.WithMatchInterface("org.freedesktop.DBus.Properties")}
a.bus.AddMatchSignal(propertiesChangedMatchOptions...)
defer a.bus.RemoveMatchSignal(propertiesChangedMatchOptions...)
// Read whether this device is already connected.
connected, err := device.device.GetProperty("org.bluez.Device1.Connected")
func (a *Adapter) Connect(address Addresser, params ConnectionParams) (*Device, error) {
adr := address.(Address)
devicePath := dbus.ObjectPath(string(a.adapter.Path()) + "/dev_" + strings.Replace(adr.MAC.String(), ":", "_", -1))
dev, err := device.NewDevice1(devicePath)
if err != nil {
return Device{}, err
return nil, err
}
// Connect to the device, if not already connected.
if !connected.Value().(bool) {
// Start connecting (async).
err := device.device.Call("org.bluez.Device1.Connect", 0).Err
if !dev.Properties.Connected {
// Not yet connected, so do it now.
// The properties have just been read so this is fresh data.
err := dev.Connect()
if err != nil {
return Device{}, fmt.Errorf("bluetooth: failed to connect: %w", err)
return nil, err
}
// Wait until the device has connected.
connectChan := make(chan struct{})
go func() {
for sig := range signal {
switch sig.Name {
case "org.freedesktop.DBus.Properties.PropertiesChanged":
interfaceName := sig.Body[0].(string)
if interfaceName != "org.bluez.Device1" {
continue
}
if sig.Path != device.device.Path() {
continue
}
changes := sig.Body[1].(map[string]dbus.Variant)
if connected, ok := changes["Connected"].Value().(bool); ok && connected {
close(connectChan)
}
}
}
}()
<-connectChan
}
return device, nil
// TODO: a proper async callback.
a.connectHandler(nil, true)
return &Device{
device: dev,
}, nil
}
// Disconnect from the BLE device. This method is non-blocking and does not
// wait until the connection is fully gone.
func (d Device) Disconnect() error {
// we don't call our cancel function here, instead we wait for the
// property change in `watchForConnect` and cancel things then
return d.device.Call("org.bluez.Device1.Disconnect", 0).Err
}
// RequestConnectionParams requests a different connection latency and timeout
// of the given device connection. Fields that are unset will be left alone.
// Whether or not the device will actually honor this, depends on the device and
// on the specific parameters.
//
// On Linux, this call doesn't do anything because BlueZ doesn't support
// changing the connection latency.
func (d Device) RequestConnectionParams(params ConnectionParams) error {
return nil
func (d *Device) Disconnect() error {
return d.device.Disconnect()
}

View file

@ -1,21 +1,19 @@
//go:build softdevice && s110v8
// +build softdevice,s110v8
package bluetooth
/*
#include "ble_gap.h"
// Define SoftDevice functions as regular function declarations (not inline
// static functions).
#define SVCALL_AS_NORMAL_FUNCTION
// Workaround wrapper function to avoid pointer arguments escaping to heap
static inline uint32_t sd_ble_gap_adv_start_noescape(ble_gap_adv_params_t const p_adv_params) {
return sd_ble_gap_adv_start(&p_adv_params);
}
#include "ble_gap.h"
*/
import "C"
import (
"runtime/volatile"
"time"
"unsafe"
)
// Address contains a Bluetooth MAC address.
@ -53,7 +51,7 @@ func (a *Advertisement) Configure(options AdvertisementOptions) error {
return errAdvertisementPacketTooBig
}
errCode := C.sd_ble_gap_adv_data_set((*C.uint8_t)(unsafe.Pointer(&payload.data[0])), C.uint8_t(payload.len), nil, 0)
errCode := C.sd_ble_gap_adv_data_set(&payload.data[0], payload.len, nil, 0)
a.interval = options.Interval
return makeError(errCode)
}
@ -65,21 +63,14 @@ func (a *Advertisement) Start() error {
return makeError(errCode)
}
// Stop advertisement.
func (a *Advertisement) Stop() error {
a.isAdvertising.Set(0)
errCode := C.sd_ble_gap_adv_stop()
return makeError(errCode)
}
// Low-level version of Start. Used to restart advertisement when a connection
// is lost.
func (a *Advertisement) start() C.uint32_t {
func (a *Advertisement) start() uint32 {
params := C.ble_gap_adv_params_t{
_type: C.BLE_GAP_ADV_TYPE_ADV_IND,
fp: C.BLE_GAP_ADV_FP_ANY,
interval: C.uint16_t(a.interval),
interval: uint16(a.interval),
timeout: 0, // no timeout
}
return C.sd_ble_gap_adv_start_noescape(params)
return C.sd_ble_gap_adv_start(&params)
}

View file

@ -1,86 +0,0 @@
//go:build (softdevice && s113v7) || (softdevice && s132v6) || (softdevice && s140v6) || (softdevice && s140v7)
package bluetooth
import (
"runtime/volatile"
"time"
"unsafe"
)
/*
#include "ble_gap.h"
*/
import "C"
// Address contains a Bluetooth MAC address.
type Address struct {
MACAddress
}
// Advertisement encapsulates a single advertisement instance.
type Advertisement struct {
handle C.uint8_t
isAdvertising volatile.Register8
payload rawAdvertisementPayload
}
// The nrf528xx devices only seem to support one advertisement instance. The way
// multiple advertisements are implemented is by changing the packet data
// frequently.
var defaultAdvertisement = Advertisement{
handle: C.BLE_GAP_ADV_SET_HANDLE_NOT_SET,
}
// DefaultAdvertisement returns the default advertisement instance but does not
// configure it.
func (a *Adapter) DefaultAdvertisement() *Advertisement {
return &defaultAdvertisement
}
// Configure this advertisement.
func (a *Advertisement) Configure(options AdvertisementOptions) error {
// Fill empty options with reasonable defaults.
if options.Interval == 0 {
// Pick an advertisement interval recommended by Apple (section 35.5
// Advertising Interval):
// https://developer.apple.com/accessories/Accessory-Design-Guidelines.pdf
options.Interval = NewDuration(152500 * time.Microsecond) // 152.5ms
}
// Construct payload.
// Note that the payload needs to be part of the Advertisement object as the
// memory is still used after sd_ble_gap_adv_set_configure returns.
a.payload.reset()
if !a.payload.addFromOptions(options) {
return errAdvertisementPacketTooBig
}
data := C.ble_gap_adv_data_t{}
data.adv_data = C.ble_data_t{
p_data: (*C.uint8_t)(unsafe.Pointer(&a.payload.data[0])),
len: C.uint16_t(a.payload.len),
}
params := C.ble_gap_adv_params_t{
properties: C.ble_gap_adv_properties_t{
_type: C.BLE_GAP_ADV_TYPE_CONNECTABLE_SCANNABLE_UNDIRECTED,
},
interval: C.uint32_t(options.Interval),
}
errCode := C.sd_ble_gap_adv_set_configure(&a.handle, &data, &params)
return makeError(errCode)
}
// Start advertisement. May only be called after it has been configured.
func (a *Advertisement) Start() error {
a.isAdvertising.Set(1)
errCode := C.sd_ble_gap_adv_start(a.handle, C.BLE_CONN_CFG_TAG_DEFAULT)
return makeError(errCode)
}
// Stop advertisement.
func (a *Advertisement) Stop() error {
a.isAdvertising.Set(0)
errCode := C.sd_ble_gap_adv_stop(a.handle)
return makeError(errCode)
}

View file

@ -1,4 +1,4 @@
//go:build (softdevice && s132v6) || (softdevice && s140v6) || (softdevice && s140v7)
// +build softdevice,!s110v8
package bluetooth
@ -7,16 +7,18 @@ import (
"errors"
"runtime/volatile"
"time"
"unsafe"
)
/*
// Define SoftDevice functions as regular function declarations (not inline
// static functions).
#define SVCALL_AS_NORMAL_FUNCTION
#include "ble_gap.h"
*/
import "C"
var errAlreadyConnecting = errors.New("bluetooth: already in a connection attempt")
var errConnectionTimeout = errors.New("bluetooth: timeout while connecting")
// Memory buffers needed by sd_ble_gap_scan_start.
var (
@ -25,6 +27,71 @@ var (
globalScanResult ScanResult
)
// Address contains a Bluetooth MAC address.
type Address struct {
MACAddress
}
// Advertisement encapsulates a single advertisement instance.
type Advertisement struct {
handle uint8
isAdvertising volatile.Register8
payload rawAdvertisementPayload
}
// The nrf528xx devices only seem to support one advertisement instance. The way
// multiple advertisements are implemented is by changing the packet data
// frequently.
var defaultAdvertisement = Advertisement{
handle: C.BLE_GAP_ADV_SET_HANDLE_NOT_SET,
}
// DefaultAdvertisement returns the default advertisement instance but does not
// configure it.
func (a *Adapter) DefaultAdvertisement() *Advertisement {
return &defaultAdvertisement
}
// Configure this advertisement.
func (a *Advertisement) Configure(options AdvertisementOptions) error {
// Fill empty options with reasonable defaults.
if options.Interval == 0 {
// Pick an advertisement interval recommended by Apple (section 35.5
// Advertising Interval):
// https://developer.apple.com/accessories/Accessory-Design-Guidelines.pdf
options.Interval = NewDuration(152500 * time.Microsecond) // 152.5ms
}
// Construct payload.
// Note that the payload needs to be part of the Advertisement object as the
// memory is still used after sd_ble_gap_adv_set_configure returns.
a.payload.reset()
if !a.payload.addFromOptions(options) {
return errAdvertisementPacketTooBig
}
data := C.ble_gap_adv_data_t{}
data.adv_data = C.ble_data_t{
p_data: &a.payload.data[0],
len: uint16(a.payload.len),
}
params := C.ble_gap_adv_params_t{
properties: C.ble_gap_adv_properties_t{
_type: C.BLE_GAP_ADV_TYPE_CONNECTABLE_SCANNABLE_UNDIRECTED,
},
interval: uint32(options.Interval),
}
errCode := C.sd_ble_gap_adv_set_configure(&a.handle, &data, &params)
return makeError(errCode)
}
// Start advertisement. May only be called after it has been configured.
func (a *Advertisement) Start() error {
a.isAdvertising.Set(1)
errCode := C.sd_ble_gap_adv_start(a.handle, C.BLE_CONN_CFG_TAG_DEFAULT)
return makeError(errCode)
}
// 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.
//
@ -42,12 +109,12 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
scanParams := C.ble_gap_scan_params_t{}
scanParams.set_bitfield_extended(0)
scanParams.set_bitfield_active(0)
scanParams.interval = C.uint16_t(NewDuration(40 * time.Millisecond))
scanParams.window = C.uint16_t(NewDuration(30 * time.Millisecond))
scanParams.interval = uint16(NewDuration(40 * time.Millisecond))
scanParams.window = uint16(NewDuration(30 * time.Millisecond))
scanParams.timeout = C.BLE_GAP_SCAN_TIMEOUT_UNLIMITED
scanReportBufferInfo := C.ble_data_t{
p_data: (*C.uint8_t)(unsafe.Pointer(&scanReportBuffer.data[0])),
len: C.uint16_t(len(scanReportBuffer.data)),
p_data: &scanReportBuffer.data[0],
len: uint16(len(scanReportBuffer.data)),
}
errCode := C.sd_ble_gap_scan_start(&scanParams, &scanReportBufferInfo)
if errCode != 0 {
@ -93,10 +160,15 @@ func (a *Adapter) StopScan() error {
return nil
}
// Device is a connection to a remote peripheral.
type Device struct {
connectionHandle uint16
}
// In-progress connection attempt.
var connectionAttempt struct {
state volatile.Register8 // 0 means unused, 1 means connecting, 2 means connected, 3 means timeout
connectionHandle C.uint16_t
state volatile.Register8 // 0 means unused, 1 means connecting, 2 means ready (connected or timeout)
connectionHandle uint16
}
// Connect starts a connection attempt to the given peripheral device address.
@ -105,12 +177,13 @@ var connectionAttempt struct {
// connection attempt at once and that the address parameter must have the
// IsRandom bit set correctly. This bit is set correctly for scan results, so
// you can reuse that address directly.
func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, error) {
func (a *Adapter) Connect(address Addresser, params ConnectionParams) (*Device, error) {
adr := address.(Address)
// Construct an address object as used in the SoftDevice.
var addr C.ble_gap_addr_t
addr.addr = makeSDAddress(address.MAC)
addr.addr = adr.MAC
if address.IsRandom() {
switch address.MAC[5] >> 6 {
switch adr.MAC[5] >> 6 {
case 0b11:
addr.set_bitfield_addr_type(C.BLE_GAP_ADDR_TYPE_RANDOM_STATIC)
case 0b01:
@ -139,25 +212,22 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, err
scanParams := C.ble_gap_scan_params_t{}
scanParams.set_bitfield_extended(0)
scanParams.set_bitfield_active(0)
scanParams.interval = C.uint16_t(NewDuration(40 * time.Millisecond))
scanParams.window = C.uint16_t(NewDuration(30 * time.Millisecond))
scanParams.timeout = C.uint16_t(params.ConnectionTimeout / 16) // timeout in 10ms units
scanParams.interval = uint16(NewDuration(40 * time.Millisecond))
scanParams.window = uint16(NewDuration(30 * time.Millisecond))
scanParams.timeout = uint16(params.ConnectionTimeout)
connectionParams := C.ble_gap_conn_params_t{
min_conn_interval: C.uint16_t(params.MinInterval) / 2,
max_conn_interval: C.uint16_t(params.MaxInterval) / 2,
min_conn_interval: uint16(params.MinInterval) / 2,
max_conn_interval: uint16(params.MaxInterval) / 2,
slave_latency: 0, // mostly relevant to connected keyboards etc
conn_sup_timeout: 200, // 2 seconds (in 10ms units), the minimum recommended by Apple
}
if params.Timeout != 0 {
connectionParams.conn_sup_timeout = uint16(params.Timeout / 16)
}
// Flag to the event handler that we are waiting for incoming connections.
// This should be safe as long as Connect is not called concurrently. And
// even then, it should catch most such race conditions.
if connectionAttempt.state.Get() != 0 {
return Device{}, errAlreadyConnecting
return nil, errAlreadyConnecting
}
connectionAttempt.state.Set(1)
@ -165,33 +235,26 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, err
errCode := C.sd_ble_gap_connect(&addr, &scanParams, &connectionParams, C.BLE_CONN_CFG_TAG_DEFAULT)
if errCode != 0 {
connectionAttempt.state.Set(0)
return Device{}, Error(errCode)
return nil, Error(errCode)
}
// Wait until the connection is established.
for {
state := connectionAttempt.state.Get()
if state == 2 {
// Successfully connected.
connectionAttempt.state.Set(0)
connectionHandle := connectionAttempt.connectionHandle
return Device{
connectionHandle: connectionHandle,
}, nil
} else if state == 3 {
// Timeout while connecting.
connectionAttempt.state.Set(0)
return Device{}, errConnectionTimeout
} else {
// TODO: use some sort of condition variable once the scheduler
// supports them.
arm.Asm("wfe")
}
// TODO: use some sort of condition variable once the scheduler supports
// them.
for connectionAttempt.state.Get() != 2 {
arm.Asm("wfe")
}
connectionHandle := connectionAttempt.connectionHandle
connectionAttempt.state.Set(0)
// Connection has been established.
return &Device{
connectionHandle: connectionHandle,
}, nil
}
// Disconnect from the BLE device.
func (d Device) Disconnect() error {
func (d *Device) Disconnect() error {
errCode := C.sd_ble_gap_disconnect(d.connectionHandle, C.BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION)
if errCode != 0 {
return Error(errCode)
@ -199,34 +262,3 @@ func (d Device) Disconnect() error {
return nil
}
// RequestConnectionParams requests a different connection latency and timeout
// of the given device connection. Fields that are unset will be left alone.
// Whether or not the device will actually honor this, depends on the device and
// on the specific parameters.
//
// On the Nordic SoftDevice, this call will also set the slave latency to 0.
func (d Device) RequestConnectionParams(params ConnectionParams) error {
// The default parameters if no specific parameters are picked.
connParams := C.ble_gap_conn_params_t{
min_conn_interval: C.BLE_GAP_CP_MIN_CONN_INTVL_NONE,
max_conn_interval: C.BLE_GAP_CP_MAX_CONN_INTVL_NONE,
slave_latency: 0,
conn_sup_timeout: C.BLE_GAP_CP_CONN_SUP_TIMEOUT_NONE,
}
// Use specified parameters if available.
if params.MinInterval != 0 {
connParams.min_conn_interval = C.uint16_t(params.MinInterval) / 2
}
if params.MaxInterval != 0 {
connParams.max_conn_interval = C.uint16_t(params.MaxInterval) / 2
}
if params.Timeout != 0 {
connParams.conn_sup_timeout = C.uint16_t(params.Timeout) / 16
}
// Send them to peer device.
errCode := C.sd_ble_gap_conn_param_update(d.connectionHandle, &connParams)
return makeError(errCode)
}

View file

@ -1,15 +0,0 @@
//go:build softdevice
package bluetooth
/*
#include "ble_gap.h"
*/
import "C"
// Device is a connection to a remote peripheral or central.
type Device struct {
Address Address
connectionHandle C.uint16_t
}

View file

@ -1,133 +0,0 @@
package bluetooth
import (
"reflect"
"testing"
"time"
)
func TestCreateAdvertisementPayload(t *testing.T) {
type testCase struct {
raw string
parsed AdvertisementOptions
}
tests := []testCase{
{
raw: "\x02\x01\x06", // flags
parsed: AdvertisementOptions{},
},
{
raw: "\x02\x01\x06", // flags
parsed: AdvertisementOptions{
// Interval doesn't affect the advertisement payload.
Interval: NewDuration(100 * time.Millisecond),
},
},
{
raw: "\x02\x01\x06" + // flags
"\x07\x09foobar", // local name
parsed: AdvertisementOptions{
LocalName: "foobar",
},
},
{
raw: "\x02\x01\x06" + // flags
"\x0b\x09Heart rate" + // local name
"\x03\x03\x0d\x18", // service UUID
parsed: AdvertisementOptions{
LocalName: "Heart rate",
ServiceUUIDs: []UUID{
ServiceUUIDHeartRate,
},
},
},
{
// Note: the two service UUIDs should really be merged into one to
// save space.
raw: "\x02\x01\x06" + // flags
"\x0b\x09Heart rate" + // local name
"\x03\x03\x0d\x18" + // heart rate service UUID
"\x03\x03\x0f\x18", // battery service UUID
parsed: AdvertisementOptions{
LocalName: "Heart rate",
ServiceUUIDs: []UUID{
ServiceUUIDHeartRate,
ServiceUUIDBattery,
},
},
},
{
raw: "\x02\x01\x06" + // flags
"\a\xff\x34\x12asdf", // manufacturer data
parsed: AdvertisementOptions{
ManufacturerData: []ManufacturerDataElement{
{0x1234, []byte("asdf")},
},
},
},
{
raw: "\x02\x01\x06" + // flags
"\x04\xff\x34\x12\x05" + // manufacturer data 1
"\x05\xff\xff\xff\x03\x07" + // manufacturer data 2
"\x03\xff\x11\x00", // manufacturer data 3
parsed: AdvertisementOptions{
ManufacturerData: []ManufacturerDataElement{
{0x1234, []byte{5}},
{0xffff, []byte{3, 7}},
{0x0011, []byte{}},
},
},
},
{
raw: "\x02\x01\x06" + // flags
"\x05\x16\xD2\xFC\x40\x02" + // service data 16-Bit UUID
"\x06\x20\xD2\xFC\x40\x02\xC4", // service data 32-Bit UUID
parsed: AdvertisementOptions{
ServiceData: []ServiceDataElement{
{UUID: New16BitUUID(0xFCD2), Data: []byte{0x40, 0x02}},
{UUID: New32BitUUID(0x0240FCD2), Data: []byte{0xC4}},
},
},
},
{
raw: "\x02\x01\x06" + // flags
"\x05\x16\xD2\xFC\x40\x02" + // service data 16-Bit UUID
"\x05\x16\xD3\xFC\x40\x02", // service data 16-Bit UUID
parsed: AdvertisementOptions{
ServiceData: []ServiceDataElement{
{UUID: New16BitUUID(0xFCD2), Data: []byte{0x40, 0x02}},
{UUID: New16BitUUID(0xFCD3), Data: []byte{0x40, 0x02}},
},
},
},
{
raw: "\x02\x01\x06" + // flags
"\x04\x16\xD2\xFC\x40" + // service data 16-Bit UUID
"\x12\x21\xB8\x6C\x75\x05\xE9\x25\xBD\x93\xA8\x42\x32\xC3\x00\x01\xAF\xAD\x09", // service data 128-Bit UUID
parsed: AdvertisementOptions{
ServiceData: []ServiceDataElement{
{UUID: New16BitUUID(0xFCD2), Data: []byte{0x40}},
{
UUID: NewUUID([16]byte{0xad, 0xaf, 0x01, 0x00, 0xc3, 0x32, 0x42, 0xa8, 0x93, 0xbd, 0x25, 0xe9, 0x05, 0x75, 0x6c, 0xb8}),
Data: []byte{0x09},
},
},
},
},
}
for _, tc := range tests {
var expectedRaw rawAdvertisementPayload
expectedRaw.len = uint8(len(tc.raw))
copy(expectedRaw.data[:], tc.raw)
var raw rawAdvertisementPayload
raw.addFromOptions(tc.parsed)
if raw != expectedRaw {
t.Errorf("error when serializing options: %#v\nexpected: %#v\nactual: %#v\n", tc.parsed, tc.raw, string(raw.data[:raw.len]))
}
mdata := raw.ManufacturerData()
if !reflect.DeepEqual(mdata, tc.parsed.ManufacturerData) {
t.Errorf("ManufacturerData was not parsed as expected:\nexpected: %#v\nactual: %#v", tc.parsed.ManufacturerData, mdata)
}
}
}

View file

@ -1,16 +1,7 @@
package bluetooth
import (
"fmt"
"unsafe"
"github.com/go-ole/go-ole"
"github.com/saltosystems/winrt-go"
"github.com/saltosystems/winrt-go/windows/devices/bluetooth"
"github.com/saltosystems/winrt-go/windows/devices/bluetooth/advertisement"
"github.com/saltosystems/winrt-go/windows/devices/bluetooth/genericattributeprofile"
"github.com/saltosystems/winrt-go/windows/foundation"
"github.com/saltosystems/winrt-go/windows/storage/streams"
"tinygo.org/x/bluetooth/winbt"
)
// Address contains a Bluetooth MAC address.
@ -18,108 +9,6 @@ type Address struct {
MACAddress
}
type Advertisement struct {
advertisement *advertisement.BluetoothLEAdvertisement
publisher *advertisement.BluetoothLEAdvertisementPublisher
}
// DefaultAdvertisement returns the default advertisement instance but does not
// configure it.
func (a *Adapter) DefaultAdvertisement() *Advertisement {
if a.defaultAdvertisement == nil {
a.defaultAdvertisement = &Advertisement{}
}
return a.defaultAdvertisement
}
// Configure this advertisement.
// on Windows we're only able to set "Manufacturer Data" for advertisements.
// https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothleadvertisementpublisher?view=winrt-22621#remarks
// following this c# source for this implementation: https://github.com/microsoft/Windows-universal-samples/blob/main/Samples/BluetoothAdvertisement/cs/Scenario2_Publisher.xaml.cs
// adding service data / localname leads to errors when starting the advertisement.
func (a *Advertisement) Configure(options AdvertisementOptions) error {
// we can only advertise manufacturer / company data on windows, so no need to continue if we have none
if len(options.ManufacturerData) == 0 {
return nil
}
if a.publisher != nil {
a.publisher.Release()
}
if a.advertisement != nil {
a.advertisement.Release()
}
pub, err := advertisement.NewBluetoothLEAdvertisementPublisher()
if err != nil {
return err
}
a.publisher = pub
ad, err := a.publisher.GetAdvertisement()
if err != nil {
return err
}
a.advertisement = ad
vec, err := ad.GetManufacturerData()
if err != nil {
return err
}
for _, optManData := range options.ManufacturerData {
writer, err := streams.NewDataWriter()
if err != nil {
return err
}
defer writer.Release()
err = writer.WriteBytes(uint32(len(optManData.Data)), optManData.Data)
if err != nil {
return err
}
buf, err := writer.DetachBuffer()
if err != nil {
return err
}
manData, err := advertisement.BluetoothLEManufacturerDataCreate(optManData.CompanyID, buf)
if err != nil {
return err
}
if err = vec.Append(unsafe.Pointer(&manData.IUnknown.RawVTable)); err != nil {
return err
}
}
return nil
}
// Start advertisement. May only be called after it has been configured.
func (a *Advertisement) Start() error {
// publisher will be present if we actually have manufacturer data to advertise.
if a.publisher != nil {
return a.publisher.Start()
}
return nil
}
// Stop advertisement. May only be called after it has been started.
func (a *Advertisement) Stop() error {
if a.publisher != nil {
return a.publisher.Stop()
}
return nil
}
// Scan starts a BLE scan. It is stopped by a call to StopScan. A common pattern
// is to cancel the scan when a particular device has been found.
func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) {
@ -129,75 +18,49 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) {
return errScanning
}
a.watcher, err = advertisement.NewBluetoothLEAdvertisementWatcher()
if err != nil {
return
}
defer func() {
_ = a.watcher.Release()
a.watcher = nil
}()
// Set scanning mode to active so we receive scan responses
// from devices in advertising mode
err = a.watcher.SetScanningMode(advertisement.BluetoothLEScanningModeActive)
a.watcher, err = winbt.NewBluetoothLEAdvertisementWatcher()
if err != nil {
return
}
defer a.watcher.Release()
// Listen for incoming BLE advertisement packets.
// We need a TypedEventHandler<TSender, TResult> to listen to events, but since this is a parameterized delegate
// its GUID depends on the classes used as sender and result, so we need to compute it:
// TypedEventHandler<BluetoothLEAdvertisementWatcher, BluetoothLEAdvertisementReceivedEventArgs>
eventReceivedGuid := winrt.ParameterizedInstanceGUID(
foundation.GUIDTypedEventHandler,
advertisement.SignatureBluetoothLEAdvertisementWatcher,
advertisement.SignatureBluetoothLEAdvertisementReceivedEventArgs,
)
handler := foundation.NewTypedEventHandler(ole.NewGUID(eventReceivedGuid), func(instance *foundation.TypedEventHandler, sender, arg unsafe.Pointer) {
args := (*advertisement.BluetoothLEAdvertisementReceivedEventArgs)(arg)
result := getScanResultFromArgs(args)
err = a.watcher.AddReceivedEvent(func(watcher *winbt.IBluetoothLEAdvertisementWatcher, args *winbt.IBluetoothLEAdvertisementReceivedEventArgs) {
var result ScanResult
result.RSSI = args.RawSignalStrengthInDBm()
addr := args.BluetoothAddress()
adr := result.Address.(Address)
for i := range adr.MAC {
adr.MAC[i] = byte(addr)
addr >>= 8
}
// Note: the IsRandom bit is never set.
advertisement := args.Advertisement()
result.AdvertisementPayload = &advertisementFields{
AdvertisementFields{
LocalName: advertisement.LocalName(),
},
}
callback(a, result)
})
defer handler.Release()
token, err := a.watcher.AddReceived(handler)
if err != nil {
return
}
defer a.watcher.RemoveReceived(token)
// Wait for when advertisement has stopped by a call to StopScan().
// Advertisement doesn't seem to stop right away, there is an
// intermediate Stopping state.
stoppingChan := make(chan error)
// TypedEventHandler<BluetoothLEAdvertisementWatcher, BluetoothLEAdvertisementWatcherStoppedEventArgs>
eventStoppedGuid := winrt.ParameterizedInstanceGUID(
foundation.GUIDTypedEventHandler,
advertisement.SignatureBluetoothLEAdvertisementWatcher,
advertisement.SignatureBluetoothLEAdvertisementWatcherStoppedEventArgs,
)
stoppedHandler := foundation.NewTypedEventHandler(ole.NewGUID(eventStoppedGuid), func(_ *foundation.TypedEventHandler, _, arg unsafe.Pointer) {
args := (*advertisement.BluetoothLEAdvertisementWatcherStoppedEventArgs)(arg)
errCode, err := args.GetError()
if err != nil {
// Got an error while getting the error value, that shouldn't
// happen.
stoppingChan <- fmt.Errorf("failed to get stopping error value: %w", err)
} else if errCode != bluetooth.BluetoothErrorSuccess {
// Could not stop the scan? I'm not sure when this would actually
// happen.
stoppingChan <- fmt.Errorf("failed to stop scanning (error code %d)", errCode)
}
stoppingChan := make(chan struct{})
err = a.watcher.AddStoppedEvent(func(watcher *winbt.IBluetoothLEAdvertisementWatcher, args *winbt.IBluetoothLEAdvertisementWatcherStoppedEventArgs) {
// Note: the args parameter has an Error property that should
// probably be checked, but I'm not sure when stopping the
// advertisement watcher could ever result in an error (except
// for bugs).
close(stoppingChan)
})
defer stoppedHandler.Release()
token, err = a.watcher.AddStopped(stoppedHandler)
if err != nil {
return
}
defer a.watcher.RemoveStopped(token)
err = a.watcher.Start()
if err != nil {
@ -205,61 +68,9 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) {
}
// Wait until advertisement has stopped, and finish.
return <-stoppingChan
}
func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedEventArgs) ScanResult {
// parse bluetooth address
addr, _ := args.GetBluetoothAddress()
adr := Address{}
for i := range adr.MAC {
adr.MAC[i] = byte(addr)
addr >>= 8
}
sigStrength, _ := args.GetRawSignalStrengthInDBm()
result := ScanResult{
RSSI: sigStrength,
Address: adr,
}
var manufacturerData []ManufacturerDataElement
if winAdv, err := args.GetAdvertisement(); err == nil && winAdv != nil {
vector, _ := winAdv.GetManufacturerData()
size, _ := vector.GetSize()
for i := uint32(0); i < size; i++ {
element, _ := vector.GetAt(i)
manData := (*advertisement.BluetoothLEManufacturerData)(element)
companyID, _ := manData.GetCompanyId()
buffer, _ := manData.GetData()
manufacturerData = append(manufacturerData, ManufacturerDataElement{
CompanyID: companyID,
Data: bufferToSlice(buffer),
})
}
}
// Note: the IsRandom bit is never set.
advertisement, _ := args.GetAdvertisement()
localName, _ := advertisement.GetLocalName()
result.AdvertisementPayload = &advertisementFields{
AdvertisementFields{
LocalName: localName,
ManufacturerData: manufacturerData,
},
}
return result
}
func bufferToSlice(buffer *streams.IBuffer) []byte {
dataReader, _ := streams.DataReaderFromBuffer(buffer)
defer dataReader.Release()
bufferSize, _ := buffer.GetLength()
if bufferSize == 0 {
return nil
}
data, _ := dataReader.ReadBytes(bufferSize)
return data
<-stoppingChan
a.watcher = nil
return nil
}
// StopScan stops any in-progress scan. It can be called from within a Scan
@ -271,104 +82,3 @@ func (a *Adapter) StopScan() error {
}
return a.watcher.Stop()
}
// Device is a connection to a remote peripheral.
type Device struct {
Address Address // the MAC address of the device
device *bluetooth.BluetoothLEDevice
session *genericattributeprofile.GattSession
}
// Connect starts a connection attempt to the given peripheral device address.
//
// On Linux and Windows, the IsRandom part of the address is ignored.
func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, error) {
var winAddr uint64
for i := range address.MAC {
winAddr += uint64(address.MAC[i]) << (8 * i)
}
// IAsyncOperation<BluetoothLEDevice>
bleDeviceOp, err := bluetooth.BluetoothLEDeviceFromBluetoothAddressAsync(winAddr)
if err != nil {
return Device{}, err
}
// We need to pass the signature of the parameter returned by the async operation:
// IAsyncOperation<BluetoothLEDevice>
if err := awaitAsyncOperation(bleDeviceOp, bluetooth.SignatureBluetoothLEDevice); err != nil {
return Device{}, fmt.Errorf("error connecting to device: %w", err)
}
res, err := bleDeviceOp.GetResults()
if err != nil {
return Device{}, err
}
// The returned BluetoothLEDevice is set to null if FromBluetoothAddressAsync can't find the device identified by bluetoothAddress
if uintptr(res) == 0x0 {
return Device{}, fmt.Errorf("device with the given address was not found")
}
bleDevice := (*bluetooth.BluetoothLEDevice)(res)
// Creating a BluetoothLEDevice object by calling this method alone doesn't (necessarily) initiate a connection.
// To initiate a connection, we need to set GattSession.MaintainConnection to true.
dID, err := bleDevice.GetBluetoothDeviceId()
if err != nil {
return Device{}, err
}
// Windows does not support explicitly connecting to a device.
// Instead it has the concept of a GATT session that is owned
// by the calling program.
gattSessionOp, err := genericattributeprofile.GattSessionFromDeviceIdAsync(dID) // IAsyncOperation<GattSession>
if err != nil {
return Device{}, err
}
if err := awaitAsyncOperation(gattSessionOp, genericattributeprofile.SignatureGattSession); err != nil {
return Device{}, fmt.Errorf("error getting gatt session: %w", err)
}
gattRes, err := gattSessionOp.GetResults()
if err != nil {
return Device{}, err
}
newSession := (*genericattributeprofile.GattSession)(gattRes)
// This keeps the device connected until we set maintain_connection = False.
if err := newSession.SetMaintainConnection(true); err != nil {
return Device{}, err
}
return Device{address, bleDevice, newSession}, nil
}
// Disconnect from the BLE device. This method is non-blocking and does not
// wait until the connection is fully gone.
func (d Device) Disconnect() error {
defer d.device.Release()
defer d.session.Release()
if err := d.session.Close(); err != nil {
return err
}
if err := d.device.Close(); err != nil {
return err
}
return nil
}
// RequestConnectionParams requests a different connection latency and timeout
// of the given device connection. Fields that are unset will be left alone.
// Whether or not the device will actually honor this, depends on the device and
// on the specific parameters.
//
// On Windows, this call doesn't do anything.
func (d Device) RequestConnectionParams(params ConnectionParams) error {
// TODO: implement this using
// BluetoothLEDevice.RequestPreferredConnectionParameters.
return nil
}

View file

@ -4,7 +4,7 @@ import (
"errors"
"time"
"github.com/tinygo-org/cbgo"
"github.com/JuulLabs-OSS/cbgo"
)
// DiscoverServices starts a service discovery procedure. Pass a list of service
@ -14,42 +14,31 @@ import (
//
// Passing a nil slice of UUIDs will return a complete list of
// services.
func (d Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
d.prph.DiscoverServices([]cbgo.UUID{})
func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
cbuuids := []cbgo.UUID{}
for _, u := range uuids {
uuid, _ := cbgo.ParseUUID(u.String())
cbuuids = append(cbuuids, uuid)
}
d.prph.DiscoverServices(cbuuids)
// clear cache of services
d.services = make(map[UUID]DeviceService)
d.services = make(map[UUID]*DeviceService)
// wait on channel for service discovery
select {
case <-d.servicesChan:
svcs := []DeviceService{}
for _, dsvc := range d.prph.Services() {
dsvcuuid, _ := ParseUUID(dsvc.UUID().String())
// add if in our original list
if len(uuids) > 0 {
found := false
for _, uuid := range uuids {
if dsvcuuid.String() == uuid.String() {
// one of the services we're looking for.
found = true
break
}
}
if !found {
continue
}
}
uuid, _ := ParseUUID(dsvc.UUID().String())
svc := DeviceService{
deviceService: &deviceService{
uuidWrapper: dsvcuuid,
device: d,
service: dsvc,
},
uuidWrapper: uuid,
device: d,
service: dsvc,
}
svcs = append(svcs, svc)
d.services[svc.uuidWrapper] = svc
d.services[svc.uuidWrapper] = &svc
}
return svcs, nil
case <-time.NewTimer(10 * time.Second).C:
@ -63,20 +52,15 @@ type uuidWrapper = UUID
// DeviceService is a BLE service on a connected peripheral device.
type DeviceService struct {
*deviceService // embdedded as pointer to enable returning by []value in DiscoverServices
}
type deviceService struct {
uuidWrapper
device Device
device *Device
service cbgo.Service
characteristics []DeviceCharacteristic
service cbgo.Service
}
// UUID returns the UUID for this DeviceService.
func (s DeviceService) UUID() UUID {
func (s *DeviceService) UUID() UUID {
return s.uuidWrapper
}
@ -89,51 +73,33 @@ func (s DeviceService) UUID() UUID {
//
// Passing a nil slice of UUIDs will return a complete list of
// characteristics.
func (s DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteristic, error) {
func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteristic, error) {
cbuuids := []cbgo.UUID{}
for _, u := range uuids {
uuid, _ := cbgo.ParseUUID(u.String())
cbuuids = append(cbuuids, uuid)
}
s.device.prph.DiscoverCharacteristics(cbuuids, s.service)
// clear cache of characteristics
s.characteristics = make([]DeviceCharacteristic, 0)
s.device.characteristics = make(map[UUID]*DeviceCharacteristic)
// wait on channel for characteristic discovery
select {
case <-s.device.charsChan:
var chars []DeviceCharacteristic
if len(uuids) > 0 {
// The caller wants to get a list of characteristics in a specific
// order.
chars = make([]DeviceCharacteristic, len(uuids))
}
chars := []DeviceCharacteristic{}
for _, dchar := range s.service.Characteristics() {
dcuuid, _ := ParseUUID(dchar.UUID().String())
if len(uuids) > 0 {
// The caller wants to get a list of characteristics in a
// specific order. Check whether this is one of those.
for i, uuid := range uuids {
if chars[i] != (DeviceCharacteristic{}) {
// To support multiple identical characteristics, we
// need to ignore the characteristics that are already
// found. See:
// https://github.com/tinygo-org/bluetooth/issues/131
continue
}
if dcuuid == uuid {
// one of the characteristics we're looking for.
chars[i] = s.makeCharacteristic(dcuuid, dchar)
break
}
}
} else {
// The caller wants to get all characteristics, in any order.
chars = append(chars, s.makeCharacteristic(dcuuid, dchar))
}
}
for _, char := range chars {
if char == (DeviceCharacteristic{}) {
return nil, errors.New("bluetooth: did not find all requested characteristic")
uuid, _ := ParseUUID(dchar.UUID().String())
char := DeviceCharacteristic{
deviceCharacteristic: &deviceCharacteristic{
uuidWrapper: uuid,
service: s,
characteristic: dchar,
},
}
chars = append(chars, char)
s.device.characteristics[char.uuidWrapper] = &char
}
return chars, nil
case <-time.NewTimer(10 * time.Second).C:
@ -141,19 +107,6 @@ func (s DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteri
}
}
// Small helper to create a DeviceCharacteristic object.
func (s DeviceService) makeCharacteristic(uuid UUID, dchar cbgo.Characteristic) DeviceCharacteristic {
char := DeviceCharacteristic{
deviceCharacteristic: &deviceCharacteristic{
uuidWrapper: uuid,
service: s,
characteristic: dchar,
},
}
s.characteristics = append(s.characteristics, char)
return char
}
// DeviceCharacteristic is a BLE characteristic on a connected peripheral
// device.
type DeviceCharacteristic struct {
@ -163,40 +116,18 @@ type DeviceCharacteristic struct {
type deviceCharacteristic struct {
uuidWrapper
service DeviceService
service *DeviceService
characteristic cbgo.Characteristic
callback func(buf []byte)
readChan chan error
writeChan chan error
}
// UUID returns the UUID for this DeviceCharacteristic.
func (c DeviceCharacteristic) UUID() UUID {
func (c *DeviceCharacteristic) UUID() UUID {
return c.uuidWrapper
}
// Write replaces the characteristic value with a new value. The
// call will return after all data has been written.
func (c DeviceCharacteristic) Write(p []byte) (n int, err error) {
c.writeChan = make(chan error)
c.service.device.prph.WriteCharacteristic(p, c.characteristic, true)
// wait for result
select {
case <-time.NewTimer(10 * time.Second).C:
err = errors.New("timeout on Write()")
case err = <-c.writeChan:
}
c.writeChan = nil
if err != nil {
return 0, err
}
return len(p), nil
}
// WriteWithoutResponse replaces the characteristic value with a new value. The
// call will return before all data has been written. A limited number of such
// writes can be in flight at any given time. This call is also known as a
@ -222,11 +153,6 @@ func (c DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) err
return nil
}
// GetMTU returns the MTU for the characteristic.
func (c DeviceCharacteristic) GetMTU() (uint16, error) {
return uint16(c.service.device.prph.MaximumWriteValueLength(false)), nil
}
// Read reads the current characteristic value.
func (c *deviceCharacteristic) Read(data []byte) (n int, err error) {
c.readChan = make(chan error)

View file

@ -1,330 +0,0 @@
//go:build hci || ninafw || cyw43439
package bluetooth
import "errors"
var (
errNotYetImplemented = errors.New("bluetooth: not yet implemented")
errNoWrite = errors.New("bluetooth: write not permitted")
errNoWriteWithoutResponse = errors.New("bluetooth: write without response not permitted")
errWriteFailed = errors.New("bluetooth: write failed")
errNoRead = errors.New("bluetooth: read not permitted")
errReadFailed = errors.New("bluetooth: read failed")
errNoNotify = errors.New("bluetooth: notify/indicate not permitted")
errEnableNotificationsFailed = errors.New("bluetooth: enable notifications failed")
errServiceNotFound = errors.New("bluetooth: service not found")
errCharacteristicNotFound = errors.New("bluetooth: characteristic not found")
)
const (
maxDefaultServicesToDiscover = 8
maxDefaultCharacteristicsToDiscover = 16
)
const (
charPropertyBroadcast = 0x01
charPropertyRead = 0x02
charPropertyWriteWithoutResponse = 0x04
charPropertyWrite = 0x08
charPropertyNotify = 0x10
charPropertyIndicate = 0x20
)
// DeviceService is a BLE service on a connected peripheral device.
type DeviceService struct {
uuid UUID
device Device
startHandle, endHandle uint16
}
// UUID returns the UUID for this DeviceService.
func (s DeviceService) UUID() UUID {
return s.uuid
}
// DiscoverServices starts a service discovery procedure. Pass a list of service
// UUIDs you are interested in to this function. Either a slice of all services
// is returned (of the same length as the requested UUIDs and in the same
// order), or if some services could not be discovered an error is returned.
//
// Passing a nil slice of UUIDs will return a complete list of
// services.
func (d Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
if debug {
println("DiscoverServices")
}
services := make([]DeviceService, 0, maxDefaultServicesToDiscover)
foundServices := make(map[UUID]DeviceService)
cd, err := d.adapter.att.findConnectionData(d.handle)
if err != nil {
return nil, err
}
startHandle := uint16(0x0001)
endHandle := uint16(0xffff)
for endHandle == uint16(0xffff) {
err := d.adapter.att.readByGroupReq(d.handle, startHandle, endHandle, gattServiceUUID)
if err != nil {
return nil, err
}
if debug {
println("found services", len(cd.services))
}
if len(cd.services) == 0 {
break
}
for _, rawService := range cd.services {
if len(uuids) == 0 || rawService.uuid.isIn(uuids) {
foundServices[rawService.uuid] =
DeviceService{
device: d,
uuid: rawService.uuid,
startHandle: rawService.startHandle,
endHandle: rawService.endHandle,
}
}
startHandle = rawService.endHandle + 1
if startHandle == 0x0000 {
endHandle = 0x0000
}
}
// reset raw services
cd.services = []rawService{}
// did we find them all?
if len(foundServices) == len(uuids) {
break
}
}
switch {
case len(uuids) > 0:
// put into correct order
for _, uuid := range uuids {
s, ok := foundServices[uuid]
if !ok {
return nil, errServiceNotFound
}
services = append(services, s)
}
default:
for _, s := range foundServices {
services = append(services, s)
}
}
return services, nil
}
// DeviceCharacteristic is a BLE characteristic on a connected peripheral
// device.
type DeviceCharacteristic struct {
uuid UUID
service *DeviceService
permissions CharacteristicPermissions
handle uint16
properties uint8
callback func(buf []byte)
}
// UUID returns the UUID for this DeviceCharacteristic.
func (c DeviceCharacteristic) UUID() UUID {
return c.uuid
}
// DiscoverCharacteristics discovers characteristics in this service. Pass a
// list of characteristic UUIDs you are interested in to this function. Either a
// list of all requested services is returned, or if some services could not be
// discovered an error is returned. If there is no error, the characteristics
// slice has the same length as the UUID slice with characteristics in the same
// order in the slice as in the requested UUID list.
//
// Passing a nil slice of UUIDs will return a complete
// list of characteristics.
func (s DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteristic, error) {
if debug {
println("DiscoverCharacteristics")
}
characteristics := make([]DeviceCharacteristic, 0, maxDefaultCharacteristicsToDiscover)
foundCharacteristics := make(map[UUID]DeviceCharacteristic)
cd, err := s.device.adapter.att.findConnectionData(s.device.handle)
if err != nil {
return nil, err
}
startHandle := s.startHandle
endHandle := s.endHandle
for startHandle < endHandle {
err := s.device.adapter.att.readByTypeReq(s.device.handle, startHandle, endHandle, gattCharacteristicUUID)
switch {
case err == ErrATTOp:
opcode, _, errcode := s.device.adapter.att.lastError(s.device.handle)
if opcode == attOpReadByTypeReq && errcode == attErrorAttrNotFound {
// no characteristics found
break
}
case err != nil:
return nil, err
}
if debug {
println("found characteristics", len(cd.characteristics))
}
if len(cd.characteristics) == 0 {
break
}
for _, rawCharacteristic := range cd.characteristics {
if len(uuids) == 0 || rawCharacteristic.uuid.isIn(uuids) {
dc := DeviceCharacteristic{
service: &s,
uuid: rawCharacteristic.uuid,
handle: rawCharacteristic.valueHandle,
properties: rawCharacteristic.properties,
permissions: CharacteristicPermissions(rawCharacteristic.properties),
}
foundCharacteristics[rawCharacteristic.uuid] = dc
}
startHandle = rawCharacteristic.valueHandle + 1
}
// reset raw characteristics
cd.characteristics = []rawCharacteristic{}
// did we find them all?
if len(foundCharacteristics) == len(uuids) {
break
}
}
switch {
case len(uuids) > 0:
// put into correct order
for _, uuid := range uuids {
c, ok := foundCharacteristics[uuid]
if !ok {
return nil, errCharacteristicNotFound
}
characteristics = append(characteristics, c)
}
default:
for _, c := range foundCharacteristics {
characteristics = append(characteristics, c)
}
}
return characteristics, nil
}
// WriteWithoutResponse replaces the characteristic value with a new value. The
// call will return before all data has been written. A limited number of such
// writes can be in flight at any given time. This call is also known as a
// "write command" (as opposed to a write request).
func (c DeviceCharacteristic) WriteWithoutResponse(p []byte) (n int, err error) {
if !c.permissions.WriteWithoutResponse() {
return 0, errNoWriteWithoutResponse
}
err = c.service.device.adapter.att.writeCmd(c.service.device.handle, c.handle, p)
if err != nil {
return 0, err
}
return len(p), nil
}
// EnableNotifications enables notifications in the Client Characteristic
// Configuration Descriptor (CCCD). This means that most peripherals will send a
// notification with a new value every time the value of the characteristic
// changes.
//
// Users may call EnableNotifications with a nil callback to disable notifications.
func (c DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) error {
if !c.permissions.Notify() {
return errNoNotify
}
switch {
case callback == nil:
// disable notifications
if debug {
println("disabling notifications")
}
err := c.service.device.adapter.att.writeReq(c.service.device.handle, c.handle+1, []byte{0x00, 0x00})
if err != nil {
return err
}
default:
// enable notifications
if debug {
println("enabling notifications")
}
err := c.service.device.adapter.att.writeReq(c.service.device.handle, c.handle+1, []byte{0x01, 0x00})
if err != nil {
return err
}
}
c.callback = callback
c.service.device.startNotifications()
c.service.device.addNotificationRegistration(c.handle, c.callback)
return nil
}
// GetMTU returns the MTU for the characteristic.
func (c DeviceCharacteristic) GetMTU() (uint16, error) {
err := c.service.device.adapter.att.mtuReq(c.service.device.handle)
if err != nil {
return 0, err
}
c.service.device.mtu = c.service.device.adapter.att.mtu
return c.service.device.mtu, nil
}
// Read reads the current characteristic value.
func (c DeviceCharacteristic) Read(data []byte) (int, error) {
if !c.permissions.Read() {
return 0, errNoRead
}
err := c.service.device.adapter.att.readReq(c.service.device.handle, c.handle)
if err != nil {
return 0, err
}
cd, err := c.service.device.adapter.att.findConnectionData(c.service.device.handle)
if err != nil {
return 0, err
}
if len(cd.value) == 0 {
return 0, errReadFailed
}
copy(data, cd.value)
return len(cd.value), nil
}

View file

@ -1,18 +1,14 @@
//go:build !baremetal
// +build !baremetal
package bluetooth
import (
"errors"
"sort"
"strings"
"time"
"github.com/godbus/dbus/v5"
)
var (
errDupNotif = errors.New("unclosed notifications")
"github.com/muka/go-bluetooth/bluez"
"github.com/muka/go-bluetooth/bluez/profile/gatt"
)
// UUIDWrapper is a type alias for UUID so we ensure no conflicts with
@ -22,12 +18,12 @@ type uuidWrapper = UUID
// DeviceService is a BLE service on a connected peripheral device.
type DeviceService struct {
uuidWrapper
adapter *Adapter
servicePath string
service *gatt.GattService1
}
// UUID returns the UUID for this DeviceService.
func (s DeviceService) UUID() UUID {
func (s *DeviceService) UUID() UUID {
return s.uuidWrapper
}
@ -41,57 +37,50 @@ func (s DeviceService) UUID() UUID {
//
// On Linux with BlueZ, this just waits for the ServicesResolved signal (if
// services haven't been resolved yet) and uses this list of cached services.
func (d Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
start := time.Now()
func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
for {
resolved, err := d.device.GetProperty("org.bluez.Device1.ServicesResolved")
resolved, err := d.device.GetServicesResolved()
if err != nil {
return nil, err
}
if resolved.Value().(bool) {
if resolved {
break
}
// This is a terrible hack, but I couldn't find another way.
// TODO: actually there is, by waiting for a property change event of
// ServicesResolved.
time.Sleep(10 * time.Millisecond)
if time.Since(start) > 10*time.Second {
return nil, errors.New("timeout on DiscoverServices")
}
}
services := []DeviceService{}
uuidServices := make(map[UUID]struct{})
uuidServices := make(map[string]string)
servicesFound := 0
// Iterate through all objects managed by BlueZ, hoping to find the services
// we're looking for.
var list map[dbus.ObjectPath]map[string]map[string]dbus.Variant
err := d.adapter.bluez.Call("org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0).Store(&list)
om, err := bluez.GetObjectManager()
if err != nil {
return nil, err
}
objects := make([]string, 0, len(list))
for objectPath := range list {
objects = append(objects, string(objectPath))
list, err := om.GetManagedObjects()
if err != nil {
return nil, err
}
sort.Strings(objects)
for _, objectPath := range objects {
if !strings.HasPrefix(objectPath, string(d.device.Path())+"/service") {
for objectPath := range list {
if !strings.HasPrefix(string(objectPath), string(d.device.Path())+"/service") {
continue
}
properties, ok := list[dbus.ObjectPath(objectPath)]["org.bluez.GattService1"]
if !ok {
suffix := string(objectPath)[len(d.device.Path()+"/"):]
if len(strings.Split(suffix, "/")) != 1 {
continue
}
serviceUUID, _ := ParseUUID(properties["UUID"].Value().(string))
service, err := gatt.NewGattService1(objectPath)
if err != nil {
return nil, err
}
if len(uuids) > 0 {
found := false
for _, uuid := range uuids {
if uuid == serviceUUID {
if service.Properties.UUID == uuid.String() {
// One of the services we're looking for.
found = true
break
@ -102,21 +91,20 @@ func (d Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
}
}
if _, ok := uuidServices[serviceUUID]; ok {
if _, ok := uuidServices[service.Properties.UUID]; ok {
// There is more than one service with the same UUID?
// Don't overwrite it, to keep the servicesFound count correct.
continue
}
ds := DeviceService{
uuidWrapper: serviceUUID,
adapter: d.adapter,
servicePath: objectPath,
uuid, _ := ParseUUID(service.Properties.UUID)
ds := DeviceService{uuidWrapper: uuid,
service: service,
}
services = append(services, ds)
servicesFound++
uuidServices[serviceUUID] = struct{}{}
uuidServices[service.Properties.UUID] = service.Properties.UUID
}
if servicesFound < len(uuids) {
@ -130,14 +118,12 @@ func (d Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
// device.
type DeviceCharacteristic struct {
uuidWrapper
adapter *Adapter
characteristic dbus.BusObject
property chan *dbus.Signal // channel where notifications are reported
propertiesChangedMatchOption dbus.MatchOption // the same value must be passed to RemoveMatchSignal
characteristic *gatt.GattCharacteristic1
}
// UUID returns the UUID for this DeviceCharacteristic.
func (c DeviceCharacteristic) UUID() UUID {
func (c *DeviceCharacteristic) UUID() UUID {
return c.uuidWrapper
}
@ -150,68 +136,66 @@ func (c DeviceCharacteristic) UUID() UUID {
//
// Passing a nil slice of UUIDs will return a complete
// list of characteristics.
func (s DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteristic, error) {
var chars []DeviceCharacteristic
if len(uuids) > 0 {
// The caller wants to get a list of characteristics in a specific
// order.
chars = make([]DeviceCharacteristic, len(uuids))
}
func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteristic, error) {
chars := []DeviceCharacteristic{}
uuidChars := make(map[string]string)
characteristicsFound := 0
// Iterate through all objects managed by BlueZ, hoping to find the
// characteristic we're looking for.
var list map[dbus.ObjectPath]map[string]map[string]dbus.Variant
err := s.adapter.bluez.Call("org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0).Store(&list)
om, err := bluez.GetObjectManager()
if err != nil {
return nil, err
}
objects := make([]string, 0, len(list))
for objectPath := range list {
objects = append(objects, string(objectPath))
list, err := om.GetManagedObjects()
if err != nil {
return nil, err
}
sort.Strings(objects)
for _, objectPath := range objects {
if !strings.HasPrefix(objectPath, s.servicePath+"/char") {
for objectPath := range list {
if !strings.HasPrefix(string(objectPath), string(s.service.Path())+"/char") {
continue
}
properties, ok := list[dbus.ObjectPath(objectPath)]["org.bluez.GattCharacteristic1"]
if !ok {
suffix := string(objectPath)[len(s.service.Path()+"/"):]
if len(strings.Split(suffix, "/")) != 1 {
continue
}
cuuid, _ := ParseUUID(properties["UUID"].Value().(string))
char := DeviceCharacteristic{
uuidWrapper: cuuid,
adapter: s.adapter,
characteristic: s.adapter.bus.Object("org.bluez", dbus.ObjectPath(objectPath)),
char, err := gatt.NewGattCharacteristic1(objectPath)
if err != nil {
return nil, err
}
if len(uuids) > 0 {
// The caller wants to get a list of characteristics in a specific
// order. Check whether this is one of those.
for i, uuid := range uuids {
if chars[i] != (DeviceCharacteristic{}) {
// To support multiple identical characteristics, we need to
// ignore the characteristics that are already found. See:
// https://github.com/tinygo-org/bluetooth/issues/131
continue
}
if cuuid == uuid {
// one of the characteristics we're looking for.
chars[i] = char
found := false
for _, uuid := range uuids {
if char.Properties.UUID == uuid.String() {
// One of the services we're looking for.
found = true
break
}
}
} else {
// The caller wants to get all characteristics, in any order.
chars = append(chars, char)
if !found {
continue
}
}
if _, ok := uuidChars[char.Properties.UUID]; ok {
// There is more than one characteristic with the same UUID?
// Don't overwrite it, to keep the servicesFound count correct.
continue
}
uuid, _ := ParseUUID(char.Properties.UUID)
dc := DeviceCharacteristic{uuidWrapper: uuid,
characteristic: char,
}
chars = append(chars, dc)
characteristicsFound++
uuidChars[char.Properties.UUID] = char.Properties.UUID
}
// Check that we have found all characteristics.
for _, char := range chars {
if char == (DeviceCharacteristic{}) {
return nil, errors.New("bluetooth: could not find some characteristics")
}
if characteristicsFound < len(uuids) {
return nil, errors.New("bluetooth: could not find some characteristics")
}
return chars, nil
@ -222,7 +206,7 @@ func (s DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteri
// writes can be in flight at any given time. This call is also known as a
// "write command" (as opposed to a write request).
func (c DeviceCharacteristic) WriteWithoutResponse(p []byte) (n int, err error) {
err = c.characteristic.Call("org.bluez.GattCharacteristic1.WriteValue", 0, p, map[string]dbus.Variant(nil)).Err
err = c.characteristic.WriteValue(p, nil)
if err != nil {
return 0, err
}
@ -233,72 +217,25 @@ func (c DeviceCharacteristic) WriteWithoutResponse(p []byte) (n int, err error)
// Configuration Descriptor (CCCD). This means that most peripherals will send a
// notification with a new value every time the value of the characteristic
// changes.
//
// Users may call EnableNotifications with a nil callback to disable notifications.
func (c DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) error {
switch callback {
default:
if c.property != nil {
return errDupNotif
}
// Start watching for changes in the Value property.
c.property = make(chan *dbus.Signal)
c.adapter.bus.Signal(c.property)
c.propertiesChangedMatchOption = dbus.WithMatchInterface("org.freedesktop.DBus.Properties")
c.adapter.bus.AddMatchSignal(c.propertiesChangedMatchOption)
err := c.characteristic.Call("org.bluez.GattCharacteristic1.StartNotify", 0).Err
if err != nil {
return err
}
go func() {
for sig := range c.property {
if sig.Name == "org.freedesktop.DBus.Properties.PropertiesChanged" {
interfaceName := sig.Body[0].(string)
if interfaceName != "org.bluez.GattCharacteristic1" {
continue
}
if sig.Path != c.characteristic.Path() {
continue
}
changes := sig.Body[1].(map[string]dbus.Variant)
if value, ok := changes["Value"].Value().([]byte); ok {
callback(value)
}
}
}
}()
return nil
case nil:
if c.property == nil {
return nil
}
err := c.adapter.bus.RemoveMatchSignal(c.propertiesChangedMatchOption)
c.adapter.bus.RemoveSignal(c.property)
c.property = nil
ch, err := c.characteristic.WatchProperties()
if err != nil {
return err
}
}
// GetMTU returns the MTU for the characteristic.
func (c DeviceCharacteristic) GetMTU() (uint16, error) {
mtu, err := c.characteristic.GetProperty("org.bluez.GattCharacteristic1.MTU")
if err != nil {
return uint16(0), err
}
return mtu.Value().(uint16), nil
go func() {
for update := range ch {
if update.Interface == "org.bluez.GattCharacteristic1" && update.Name == "Value" {
callback(update.Value.([]byte))
}
}
}()
return c.characteristic.StartNotify()
}
// Read reads the current characteristic value.
func (c DeviceCharacteristic) Read(data []byte) (int, error) {
func (c *DeviceCharacteristic) Read(data []byte) (int, error) {
options := make(map[string]interface{})
var result []byte
err := c.characteristic.Call("org.bluez.GattCharacteristic1.ReadValue", 0, options).Store(&result)
result, err := c.characteristic.ReadValue(options)
if err != nil {
return 0, err
}

View file

@ -1,8 +1,12 @@
//go:build (softdevice && s132v6) || (softdevice && s140v6) || (softdevice && s140v7)
// +build softdevice,!s110v8
package bluetooth
/*
// Define SoftDevice functions as regular function declarations (not inline
// static functions).
#define SVCALL_AS_NORMAL_FUNCTION
#include "ble_gattc.h"
*/
import "C"
@ -11,7 +15,6 @@ import (
"device/arm"
"errors"
"runtime/volatile"
"unsafe"
)
const (
@ -29,8 +32,8 @@ var (
// program and the event handler.
var discoveringService struct {
state volatile.Register8 // 0 means nothing happening, 1 means in progress, 2 means found something
startHandle volatileHandle
endHandle volatileHandle
startHandle volatile.Register16
endHandle volatile.Register16
uuid C.ble_uuid_t
}
@ -39,13 +42,13 @@ var discoveringService struct {
type DeviceService struct {
uuid shortUUID
connectionHandle C.uint16_t
startHandle C.uint16_t
endHandle C.uint16_t
connectionHandle uint16
startHandle uint16
endHandle uint16
}
// UUID returns the UUID for this DeviceService.
func (s DeviceService) UUID() UUID {
func (s *DeviceService) UUID() UUID {
return s.uuid.UUID()
}
@ -59,7 +62,7 @@ func (s DeviceService) UUID() UUID {
//
// On the Nordic SoftDevice, only one service discovery procedure may be done at
// a time.
func (d Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
if discoveringService.state.Get() != 0 {
// Not concurrency safe, but should catch most concurrency misuses.
return nil, errAlreadyDiscovering
@ -77,7 +80,7 @@ func (d Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
if len(uuids) > 0 {
shortUUIDs = make([]C.ble_uuid_t, sz)
for i, uuid := range uuids {
var errCode C.uint32_t
var errCode uint32
shortUUIDs[i], errCode = uuid.shortUUID()
if errCode != 0 {
return nil, Error(errCode)
@ -87,7 +90,7 @@ func (d Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
numFound := 0
var startHandle C.uint16_t = 1
var startHandle uint16 = 1
for i := 0; i < sz; i++ {
var suuid C.ble_uuid_t
@ -97,7 +100,7 @@ func (d Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
// Start discovery of this service.
discoveringService.state.Set(1)
var errCode C.uint32_t
var errCode uint32
if len(uuids) > 0 {
errCode = C.sd_ble_gattc_primary_services_discover(d.connectionHandle, startHandle, &suuid)
} else {
@ -131,7 +134,7 @@ func (d Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
// Store the discovered service.
svc := DeviceService{
uuid: shortUUID(suuid),
uuid: suuid,
connectionHandle: d.connectionHandle,
startHandle: startHandle,
endHandle: endHandle,
@ -160,14 +163,14 @@ func (d Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
type DeviceCharacteristic struct {
uuid shortUUID
connectionHandle C.uint16_t
valueHandle C.uint16_t
cccdHandle C.uint16_t
connectionHandle uint16
valueHandle uint16
cccdHandle uint16
permissions CharacteristicPermissions
}
// UUID returns the UUID for this DeviceCharacteristic.
func (c DeviceCharacteristic) UUID() UUID {
func (c *DeviceCharacteristic) UUID() UUID {
return c.uuid.UUID()
}
@ -176,7 +179,7 @@ func (c DeviceCharacteristic) UUID() UUID {
var discoveringCharacteristic struct {
uuid C.ble_uuid_t
char_props C.ble_gatt_char_props_t
handle_value volatileHandle
handle_value volatile.Register16
}
// DiscoverCharacteristics discovers characteristics in this service. Pass a
@ -188,7 +191,7 @@ var discoveringCharacteristic struct {
//
// Passing a nil slice of UUIDs will return a complete
// list of characteristics.
func (s DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteristic, error) {
func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteristic, error) {
if discoveringCharacteristic.handle_value.Get() != 0 {
return nil, errAlreadyDiscovering
}
@ -205,7 +208,7 @@ func (s DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteri
if len(uuids) > 0 {
shortUUIDs = make([]C.ble_uuid_t, sz)
for i, uuid := range uuids {
var errCode C.uint32_t
var errCode uint32
shortUUIDs[i], errCode = uuid.shortUUID()
if errCode != 0 {
return nil, Error(errCode)
@ -273,7 +276,7 @@ func (s DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteri
permissions |= CharacteristicIndicatePermission
}
dc := DeviceCharacteristic{uuid: shortUUID(discoveringCharacteristic.uuid)}
dc := DeviceCharacteristic{uuid: discoveringCharacteristic.uuid}
dc.permissions = permissions
dc.valueHandle = foundCharacteristicHandle
@ -325,8 +328,8 @@ func (c DeviceCharacteristic) WriteWithoutResponse(p []byte) (n int, err error)
write_op: C.BLE_GATT_OP_WRITE_CMD,
handle: c.valueHandle,
offset: 0,
len: C.uint16_t(len(p)),
p_value: (*C.uint8_t)(unsafe.Pointer(&p[0])),
len: uint16(len(p)),
p_value: &p[0],
})
if errCode != 0 {
return 0, Error(errCode)
@ -335,8 +338,8 @@ func (c DeviceCharacteristic) WriteWithoutResponse(p []byte) (n int, err error)
}
type gattcNotificationCallback struct {
connectionHandle C.uint16_t
valueHandle C.uint16_t // may be 0 if the slot is empty
connectionHandle uint16
valueHandle uint16 // may be 0 if the slot is empty
callback func([]byte)
}
@ -401,7 +404,7 @@ func (c DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) err
}
// Write to the CCCD to enable notifications. Don't wait for a response.
value := [2]C.uint8_t{0x01, 0x00} // 0x0001 enables notifications (and disables indications)
value := [2]byte{0x01, 0x00} // 0x0001 enables notifications (and disables indications)
errCode := C.sd_ble_gattc_write(c.connectionHandle, &C.ble_gattc_write_params_t{
write_op: C.BLE_GATT_OP_WRITE_CMD,
handle: c.cccdHandle,
@ -415,16 +418,16 @@ func (c DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) err
// A global used to pass information from the event handler back to the
// Read function below.
var readingCharacteristic struct {
handle_value volatileHandle
offset C.uint16_t
length C.uint16_t
handle_value volatile.Register16
offset uint16
length uint16
value []byte
}
// Read reads the current characteristic value up to MTU length.
// A future enhancement would be to be able to retrieve a longer
// value by making multiple calls.
func (c DeviceCharacteristic) Read(data []byte) (n int, err error) {
func (c *DeviceCharacteristic) Read(data []byte) (n int, err error) {
// global will copy bytes from read operation into data slice
readingCharacteristic.value = data
@ -447,8 +450,3 @@ func (c DeviceCharacteristic) Read(data []byte) (n int, err error) {
return
}
// GetMTU returns the MTU for the characteristic.
func (c DeviceCharacteristic) GetMTU() (uint16, error) {
return uint16(C.BLE_GATT_ATT_MTU_DEFAULT), nil
}

View file

@ -1,434 +0,0 @@
package bluetooth
import (
"errors"
"fmt"
"syscall"
"unsafe"
"github.com/go-ole/go-ole"
"github.com/saltosystems/winrt-go"
"github.com/saltosystems/winrt-go/windows/devices/bluetooth"
"github.com/saltosystems/winrt-go/windows/devices/bluetooth/genericattributeprofile"
"github.com/saltosystems/winrt-go/windows/foundation"
"github.com/saltosystems/winrt-go/windows/storage/streams"
)
var (
errNoWrite = errors.New("bluetooth: write not supported")
errNoWriteWithoutResponse = errors.New("bluetooth: write without response not supported")
errWriteFailed = errors.New("bluetooth: write failed")
errNoRead = errors.New("bluetooth: read not supported")
errNoNotify = errors.New("bluetooth: notify/indicate not supported")
errEnableNotificationsFailed = errors.New("bluetooth: enable notifications failed")
)
// DiscoverServices starts a service discovery procedure. Pass a list of service
// UUIDs you are interested in to this function. Either a slice of all services
// is returned (of the same length as the requested UUIDs and in the same
// order), or if some services could not be discovered an error is returned.
//
// Passing a nil slice of UUIDs will return a complete list of
// services.
func (d Device) DiscoverServices(filterUUIDs []UUID) ([]DeviceService, error) {
// IAsyncOperation<GattDeviceServicesResult>
getServicesOperation, err := d.device.GetGattServicesWithCacheModeAsync(bluetooth.BluetoothCacheModeUncached)
if err != nil {
return nil, err
}
if err := awaitAsyncOperation(getServicesOperation, genericattributeprofile.SignatureGattDeviceServicesResult); err != nil {
return nil, err
}
res, err := getServicesOperation.GetResults()
if err != nil {
return nil, err
}
servicesResult := (*genericattributeprofile.GattDeviceServicesResult)(res)
status, err := servicesResult.GetStatus()
if err != nil {
return nil, err
} else if status != genericattributeprofile.GattCommunicationStatusSuccess {
return nil, fmt.Errorf("could not retrieve device services, operation failed with code %d", status)
}
// IVectorView<GattDeviceService>
servicesVector, err := servicesResult.GetServices()
if err != nil {
return nil, err
}
// Convert services vector to array
servicesSize, err := servicesVector.GetSize()
if err != nil {
return nil, err
}
var services []DeviceService
for i := uint32(0); i < servicesSize; i++ {
s, err := servicesVector.GetAt(i)
if err != nil {
return nil, err
}
srv := (*genericattributeprofile.GattDeviceService)(s)
guid, err := srv.GetUuid()
if err != nil {
return nil, err
}
serviceUuid := winRTUuidToUuid(guid)
// only include services that are included in the input filter
if len(filterUUIDs) > 0 {
found := false
for _, uuid := range filterUUIDs {
if serviceUuid.String() == uuid.String() {
// One of the services we're looking for.
found = true
break
}
}
if !found {
continue
}
}
services = append(services, DeviceService{
uuidWrapper: serviceUuid,
service: srv,
device: d,
})
}
return services, nil
}
func winRTUuidToUuid(uuid syscall.GUID) UUID {
return NewUUID([16]byte{
byte(uuid.Data1 >> 24),
byte(uuid.Data1 >> 16),
byte(uuid.Data1 >> 8),
byte(uuid.Data1),
byte(uuid.Data2 >> 8),
byte(uuid.Data2),
byte(uuid.Data3 >> 8),
byte(uuid.Data3),
uuid.Data4[0], uuid.Data4[1],
uuid.Data4[2], uuid.Data4[3],
uuid.Data4[4], uuid.Data4[5],
uuid.Data4[6], uuid.Data4[7],
})
}
// uuidWrapper is a type alias for UUID so we ensure no conflicts with
// struct method of the same name.
type uuidWrapper = UUID
// DeviceService is a BLE service on a connected peripheral device.
type DeviceService struct {
uuidWrapper
service *genericattributeprofile.GattDeviceService
device Device
}
// UUID returns the UUID for this DeviceService.
func (s DeviceService) UUID() UUID {
return s.uuidWrapper
}
// DiscoverCharacteristics discovers characteristics in this service. Pass a
// list of characteristic UUIDs you are interested in to this function. Either a
// list of all requested characteristics is returned, or if some characteristics could not be
// discovered an error is returned. If there is no error, the characteristics
// slice has the same length as the UUID slice with characteristics in the same
// order in the slice as in the requested UUID list.
//
// Passing a nil slice of UUIDs will return a complete
// list of characteristics.
func (s DeviceService) DiscoverCharacteristics(filterUUIDs []UUID) ([]DeviceCharacteristic, error) {
getCharacteristicsOp, err := s.service.GetCharacteristicsWithCacheModeAsync(bluetooth.BluetoothCacheModeUncached)
if err != nil {
return nil, err
}
// IAsyncOperation<GattCharacteristicsResult>
if err := awaitAsyncOperation(getCharacteristicsOp, genericattributeprofile.SignatureGattCharacteristicsResult); err != nil {
return nil, err
}
res, err := getCharacteristicsOp.GetResults()
if err != nil {
return nil, err
}
gattCharResult := (*genericattributeprofile.GattCharacteristicsResult)(res)
// IVectorView<GattCharacteristic>
charVector, err := gattCharResult.GetCharacteristics()
if err != nil {
return nil, err
}
// Convert characteristics vector to array
characteristicsSize, err := charVector.GetSize()
if err != nil {
return nil, err
}
var characteristics []DeviceCharacteristic
for i := uint32(0); i < characteristicsSize; i++ {
c, err := charVector.GetAt(i)
if err != nil {
return nil, err
}
characteristic := (*genericattributeprofile.GattCharacteristic)(c)
guid, err := characteristic.GetUuid()
if err != nil {
return nil, err
}
characteristicUUID := winRTUuidToUuid(guid)
properties, err := characteristic.GetCharacteristicProperties()
if err != nil {
return nil, err
}
// only include characteristics that are included in the input filter
if len(filterUUIDs) > 0 {
found := false
for _, uuid := range filterUUIDs {
if characteristicUUID.String() == uuid.String() {
// One of the characteristics we're looking for.
found = true
break
}
}
if !found {
continue
}
}
characteristics = append(characteristics, DeviceCharacteristic{
uuidWrapper: characteristicUUID,
service: s,
characteristic: characteristic,
properties: properties,
})
}
return characteristics, nil
}
// DeviceCharacteristic is a BLE characteristic on a connected peripheral
// device.
type DeviceCharacteristic struct {
uuidWrapper
characteristic *genericattributeprofile.GattCharacteristic
properties genericattributeprofile.GattCharacteristicProperties
service DeviceService
}
// UUID returns the UUID for this DeviceCharacteristic.
func (c DeviceCharacteristic) UUID() UUID {
return c.uuidWrapper
}
func (c DeviceCharacteristic) Properties() uint32 {
return uint32(c.properties)
}
// GetMTU returns the MTU for the characteristic.
func (c DeviceCharacteristic) GetMTU() (uint16, error) {
return c.service.device.session.GetMaxPduSize()
}
// Write replaces the characteristic value with a new value. The
// call will return after all data has been written.
func (c DeviceCharacteristic) Write(p []byte) (n int, err error) {
if c.properties&genericattributeprofile.GattCharacteristicPropertiesWrite == 0 {
return 0, errNoWrite
}
return c.write(p, genericattributeprofile.GattWriteOptionWriteWithResponse)
}
// WriteWithoutResponse replaces the characteristic value with a new value. The
// call will return before all data has been written. A limited number of such
// writes can be in flight at any given time. This call is also known as a
// "write command" (as opposed to a write request).
func (c DeviceCharacteristic) WriteWithoutResponse(p []byte) (n int, err error) {
if c.properties&genericattributeprofile.GattCharacteristicPropertiesWriteWithoutResponse == 0 {
return 0, errNoWriteWithoutResponse
}
return c.write(p, genericattributeprofile.GattWriteOptionWriteWithoutResponse)
}
func (c DeviceCharacteristic) write(p []byte, mode genericattributeprofile.GattWriteOption) (n int, err error) {
// Convert data to buffer
writer, err := streams.NewDataWriter()
if err != nil {
return 0, err
}
defer writer.Release()
// Add bytes to writer
if err := writer.WriteBytes(uint32(len(p)), p); err != nil {
return 0, err
}
value, err := writer.DetachBuffer()
if err != nil {
return 0, err
}
// IAsyncOperation<GattCommunicationStatus>
asyncOp, err := c.characteristic.WriteValueWithOptionAsync(value, mode)
if err := awaitAsyncOperation(asyncOp, genericattributeprofile.SignatureGattCommunicationStatus); err != nil {
return 0, err
}
res, err := asyncOp.GetResults()
if err != nil {
return 0, err
}
status := genericattributeprofile.GattCommunicationStatus(uintptr(res))
// Is the status success?
if status != genericattributeprofile.GattCommunicationStatusSuccess {
return 0, errWriteFailed
}
// Success
return len(p), nil
}
// Read reads the current characteristic value.
func (c DeviceCharacteristic) Read(data []byte) (int, error) {
if c.properties&genericattributeprofile.GattCharacteristicPropertiesRead == 0 {
return 0, errNoRead
}
readOp, err := c.characteristic.ReadValueWithCacheModeAsync(bluetooth.BluetoothCacheModeUncached)
if err != nil {
return 0, err
}
// IAsyncOperation<GattReadResult>
if err := awaitAsyncOperation(readOp, genericattributeprofile.SignatureGattReadResult); err != nil {
return 0, err
}
res, err := readOp.GetResults()
if err != nil {
return 0, err
}
result := (*genericattributeprofile.GattReadResult)(res)
buffer, err := result.GetValue()
if err != nil {
return 0, err
}
datareader, err := streams.DataReaderFromBuffer(buffer)
if err != nil {
return 0, err
}
bufferlen, err := buffer.GetLength()
if err != nil {
return 0, err
}
readBuffer, err := datareader.ReadBytes(bufferlen)
if err != nil {
return 0, err
}
copy(data, readBuffer)
return len(readBuffer), nil
}
// EnableNotifications enables notifications in the Client Characteristic
// Configuration Descriptor (CCCD). This means that most peripherals will send a
// notification with a new value every time the value of the characteristic
// changes.
func (c DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) error {
if (c.properties&genericattributeprofile.GattCharacteristicPropertiesNotify == 0) &&
(c.properties&genericattributeprofile.GattCharacteristicPropertiesIndicate == 0) {
return errNoNotify
}
// listen value changed event
// TypedEventHandler<GattCharacteristic,GattValueChangedEventArgs>
guid := winrt.ParameterizedInstanceGUID(foundation.GUIDTypedEventHandler, genericattributeprofile.SignatureGattCharacteristic, genericattributeprofile.SignatureGattValueChangedEventArgs)
valueChangedEventHandler := foundation.NewTypedEventHandler(ole.NewGUID(guid), func(instance *foundation.TypedEventHandler, sender, args unsafe.Pointer) {
valueChangedEvent := (*genericattributeprofile.GattValueChangedEventArgs)(args)
buf, err := valueChangedEvent.GetCharacteristicValue()
if err != nil {
return
}
reader, err := streams.DataReaderFromBuffer(buf)
if err != nil {
return
}
defer reader.Release()
buflen, err := buf.GetLength()
if err != nil {
return
}
data, err := reader.ReadBytes(buflen)
if err != nil {
return
}
callback(data)
})
_, err := c.characteristic.AddValueChanged(valueChangedEventHandler)
if err != nil {
return err
}
var writeOp *foundation.IAsyncOperation
if c.properties&genericattributeprofile.GattCharacteristicPropertiesNotify != 0 {
writeOp, err = c.characteristic.WriteClientCharacteristicConfigurationDescriptorAsync(genericattributeprofile.GattClientCharacteristicConfigurationDescriptorValueNotify)
} else {
writeOp, err = c.characteristic.WriteClientCharacteristicConfigurationDescriptorAsync(genericattributeprofile.GattClientCharacteristicConfigurationDescriptorValueIndicate)
}
if err != nil {
return err
}
// IAsyncOperation<GattCommunicationStatus>
if err := awaitAsyncOperation(writeOp, genericattributeprofile.SignatureGattCommunicationStatus); err != nil {
return err
}
res, err := writeOp.GetResults()
if err != nil {
return err
}
result := genericattributeprofile.GattCommunicationStatus(uintptr(res))
if result != genericattributeprofile.GattCommunicationStatusSuccess {
return errEnableNotificationsFailed
}
return nil
}

View file

@ -7,8 +7,6 @@ type Service struct {
Characteristics []CharacteristicConfig
}
type WriteEvent = func(client Connection, offset int, value []byte)
// CharacteristicConfig contains some parameters for the configuration of a
// single characteristic.
//
@ -19,7 +17,7 @@ type CharacteristicConfig struct {
UUID
Value []byte
Flags CharacteristicPermissions
WriteEvent WriteEvent
WriteEvent func(client Connection, offset int, value []byte)
}
// CharacteristicPermissions lists a number of basic permissions/capabilities
@ -58,13 +56,3 @@ func (p CharacteristicPermissions) Write() bool {
func (p CharacteristicPermissions) WriteWithoutResponse() bool {
return p&CharacteristicWriteWithoutResponsePermission != 0
}
// Notify returns whether notifications are permitted.
func (p CharacteristicPermissions) Notify() bool {
return p&CharacteristicNotifyPermission != 0
}
// Indicate returns whether indications are permitted.
func (p CharacteristicPermissions) Indicate() bool {
return p&CharacteristicIndicatePermission != 0
}

View file

@ -1,128 +0,0 @@
//go:build hci || ninafw || cyw43439
package bluetooth
type Characteristic struct {
adapter *Adapter
handle uint16
permissions CharacteristicPermissions
value []byte
cccd uint16
}
// AddService creates a new service with the characteristics listed in the
// Service struct.
func (a *Adapter) AddService(service *Service) error {
uuid := service.UUID.Bytes()
serviceHandle := a.att.addLocalAttribute(attributeTypeService, 0, shortUUID(gattServiceUUID).UUID(), 0, uuid[:])
valueHandle := serviceHandle
endHandle := serviceHandle
for i := range service.Characteristics {
data := service.Characteristics[i].UUID.Bytes()
cuuid := append([]byte{}, data[:]...)
// add characteristic declaration
charHandle := a.att.addLocalAttribute(attributeTypeCharacteristic, serviceHandle, shortUUID(gattCharacteristicUUID).UUID(), CharacteristicReadPermission, cuuid[:])
// add characteristic value
vf := CharacteristicPermissions(0)
if service.Characteristics[i].Flags.Read() {
vf |= CharacteristicReadPermission
}
if service.Characteristics[i].Flags.Write() {
vf |= CharacteristicWritePermission
}
valueHandle = a.att.addLocalAttribute(attributeTypeCharacteristicValue, charHandle, service.Characteristics[i].UUID, vf, service.Characteristics[i].Value)
endHandle = valueHandle
// add characteristic descriptor
if service.Characteristics[i].Flags.Notify() ||
service.Characteristics[i].Flags.Indicate() {
endHandle = a.att.addLocalAttribute(attributeTypeDescriptor, charHandle, shortUUID(gattClientCharacteristicConfigUUID).UUID(), CharacteristicReadPermission|CharacteristicWritePermission, []byte{0, 0})
}
if service.Characteristics[i].Handle == nil {
service.Characteristics[i].Handle = &Characteristic{}
}
service.Characteristics[i].Handle.adapter = a
service.Characteristics[i].Handle.handle = valueHandle
service.Characteristics[i].Handle.permissions = service.Characteristics[i].Flags
if len(service.Characteristics[i].Value) > 0 {
service.Characteristics[i].Handle.value = service.Characteristics[i].Value
}
if (service.Characteristics[i].Flags.Write() ||
service.Characteristics[i].Flags.WriteWithoutResponse()) &&
service.Characteristics[i].WriteEvent != nil {
handlers := append(a.charWriteHandlers, charWriteHandler{
handle: valueHandle,
callback: service.Characteristics[i].WriteEvent,
})
a.charWriteHandlers = handlers
}
if debug {
println("added characteristic", charHandle, valueHandle, service.Characteristics[i].UUID.String())
}
a.att.addLocalCharacteristic(charHandle, service.Characteristics[i].Flags, valueHandle, service.Characteristics[i].UUID, service.Characteristics[i].Handle)
}
if debug {
println("added service", serviceHandle, endHandle, service.UUID.String())
}
a.att.addLocalService(serviceHandle, endHandle, service.UUID)
return nil
}
// Write replaces the characteristic value with a new value.
func (c *Characteristic) Write(p []byte) (n int, err error) {
if !(c.permissions.Write() || c.permissions.WriteWithoutResponse() ||
c.permissions.Notify() || c.permissions.Indicate()) {
return 0, errNoWrite
}
hdl := c.adapter.getCharWriteHandler(c.handle)
if hdl != nil {
hdl.callback(Connection(c.handle), 0, p)
}
copy(c.value, p)
if c.cccd&0x01 != 0 {
// send notification
c.adapter.att.sendNotification(c.handle, c.value)
}
return len(c.value), nil
}
func (c *Characteristic) readCCCD() (uint16, error) {
if !c.permissions.Notify() {
return 0, errNoNotify
}
return c.cccd, nil
}
func (c *Characteristic) writeCCCD(val uint16) error {
if !c.permissions.Notify() {
return errNoNotify
}
c.cccd = val
return nil
}
func (c *Characteristic) readValue() ([]byte, error) {
if !c.permissions.Read() {
return nil, errNoRead
}
return c.value, nil
}

View file

@ -1,156 +1,89 @@
//go:build !baremetal
// +build !baremetal
package bluetooth
import (
"fmt"
"strconv"
"sync/atomic"
"github.com/godbus/dbus/v5"
"github.com/godbus/dbus/v5/prop"
"github.com/muka/go-bluetooth/api/service"
"github.com/muka/go-bluetooth/bluez/profile/gatt"
)
// Unique ID per service (to generate a unique object path).
var serviceID uint64
// Characteristic is a single characteristic in a service. It has an UUID and a
// value.
type Characteristic struct {
char *bluezChar
handle *service.Char
permissions CharacteristicPermissions
}
// A small ObjectManager for a single service.
type objectManager struct {
objects map[dbus.ObjectPath]map[string]map[string]*prop.Prop
}
// This method implements org.freedesktop.DBus.ObjectManager.
func (om *objectManager) GetManagedObjects() (map[dbus.ObjectPath]map[string]map[string]dbus.Variant, *dbus.Error) {
// Convert from a map with *prop.Prop keys, to a map with dbus.Variant keys.
objects := map[dbus.ObjectPath]map[string]map[string]dbus.Variant{}
for path, object := range om.objects {
obj := make(map[string]map[string]dbus.Variant)
objects[path] = obj
for iface, props := range object {
ifaceObj := make(map[string]dbus.Variant)
obj[iface] = ifaceObj
for k, v := range props {
ifaceObj[k] = dbus.MakeVariant(v.Value)
}
}
}
return objects, nil
}
// Object that implements org.bluez.GattCharacteristic1 to be exported over
// DBus. Here is the documentation:
// https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/org.bluez.GattCharacteristic.rst
type bluezChar struct {
props *prop.Properties
writeEvent func(client Connection, offset int, value []byte)
}
func (c *bluezChar) ReadValue(options map[string]dbus.Variant) ([]byte, *dbus.Error) {
// TODO: should we use the offset value? The BlueZ documentation doesn't
// clearly specify this. The go-bluetooth library doesn't, but I believe it
// should be respected.
value := c.props.GetMust("org.bluez.GattCharacteristic1", "Value").([]byte)
return value, nil
}
func (c *bluezChar) WriteValue(value []byte, options map[string]dbus.Variant) *dbus.Error {
if c.writeEvent != nil {
// BlueZ doesn't seem to tell who did the write, so pass 0 always as the
// connection ID.
client := Connection(0)
offset, _ := options["offset"].Value().(uint16)
c.writeEvent(client, int(offset), value)
}
return nil
}
// AddService creates a new service with the characteristics listed in the
// Service struct.
func (a *Adapter) AddService(s *Service) error {
// Create a unique DBus path for this service.
id := atomic.AddUint64(&serviceID, 1)
path := dbus.ObjectPath(fmt.Sprintf("/org/tinygo/bluetooth/service%d", id))
// All objects that will be part of the ObjectManager.
objects := map[dbus.ObjectPath]map[string]map[string]*prop.Prop{}
// Define the service to be exported over DBus.
serviceSpec := map[string]map[string]*prop.Prop{
"org.bluez.GattService1": {
"UUID": {Value: s.UUID.String()},
"Primary": {Value: true},
},
}
objects[path] = serviceSpec
for i, char := range s.Characteristics {
// Calculate Flags field.
bluezCharFlags := []string{
"broadcast", // bit 0
"read", // bit 1
"write-without-response", // bit 2
"write", // bit 3
"notify", // bit 4
"indicate", // bit 5
}
var flags []string
for i := 0; i < len(bluezCharFlags); i++ {
if (char.Flags>>i)&1 != 0 {
flags = append(flags, bluezCharFlags[i])
}
}
// Export the properties of this characteristic.
charPath := path + dbus.ObjectPath("/char"+strconv.Itoa(i))
propsSpec := map[string]map[string]*prop.Prop{
"org.bluez.GattCharacteristic1": {
"UUID": {Value: char.UUID.String()},
"Service": {Value: path},
"Flags": {Value: flags},
"Value": {Value: char.Value, Writable: true, Emit: prop.EmitTrue},
},
}
objects[charPath] = propsSpec
props, err := prop.Export(a.bus, charPath, propsSpec)
if err != nil {
return err
}
// Export the methods of this characteristic.
obj := &bluezChar{
props: props,
writeEvent: char.WriteEvent,
}
err = a.bus.Export(obj, charPath, "org.bluez.GattCharacteristic1")
if err != nil {
return err
}
// Keep the object around for Characteristic.Write.
if char.Handle != nil {
char.Handle.permissions = char.Flags
char.Handle.char = obj
}
}
// Export all objects that are part of our service.
om := &objectManager{
objects: objects,
}
err := a.bus.Export(om, path, "org.freedesktop.DBus.ObjectManager")
app, err := service.NewApp(service.AppOptions{
AdapterID: a.id,
})
if err != nil {
return err
}
// Register our service.
return a.adapter.Call("org.bluez.GattManager1.RegisterApplication", 0, path, map[string]dbus.Variant(nil)).Err
bluezService, err := app.NewService(s.UUID.String())
if err != nil {
return err
}
err = app.AddService(bluezService)
if err != nil {
return err
}
for _, char := range s.Characteristics {
// Create characteristic handle.
bluezChar, err := bluezService.NewChar(char.UUID.String())
if err != nil {
return err
}
// Set properties.
bluezCharFlags := []string{
gatt.FlagCharacteristicBroadcast, // bit 0
gatt.FlagCharacteristicRead, // bit 1
gatt.FlagCharacteristicWriteWithoutResponse, // bit 2
gatt.FlagCharacteristicWrite, // bit 3
gatt.FlagCharacteristicNotify, // bit 4
gatt.FlagCharacteristicIndicate, // bit 5
}
for i := uint(0); i < 5; i++ {
if (char.Flags>>i)&1 != 0 {
bluezChar.Properties.Flags = append(bluezChar.Properties.Flags, bluezCharFlags[i])
}
}
bluezChar.Properties.Value = char.Value
if char.Handle != nil {
char.Handle.handle = bluezChar
char.Handle.permissions = char.Flags
}
// Do a callback when the value changes.
if char.WriteEvent != nil {
callback := char.WriteEvent
bluezChar.OnWrite(func(c *service.Char, value []byte) ([]byte, error) {
// BlueZ doesn't seem to tell who did the write, so pass 0
// always.
// It also doesn't provide which part of the value was written,
// so pretend the entire characteristic was updated (which might
// not be the case).
callback(0, 0, value)
return nil, nil
})
}
// Add characteristic to the service, to activate it.
err = bluezService.AddChar(bluezChar)
if err != nil {
return err
}
}
return app.Run()
}
// Write replaces the characteristic value with a new value.
@ -159,10 +92,7 @@ func (c *Characteristic) Write(p []byte) (n int, err error) {
return 0, nil // nothing to do
}
if c.char.writeEvent != nil {
c.char.writeEvent(0, 0, p)
}
gattError := c.char.props.Set("org.bluez.GattCharacteristic1", "Value", dbus.MakeVariant(p))
gattError := c.handle.WriteValue(p, nil)
if gattError != nil {
return 0, gattError
}

View file

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

View file

@ -1,33 +1,20 @@
//go:build softdevice
// +build softdevice
package bluetooth
/*
// Define SoftDevice functions as regular function declarations (not inline
// static functions).
#define SVCALL_AS_NORMAL_FUNCTION
#include "ble_gap.h"
#include "ble_gatts.h"
// Workaround wrapper functions which prevent pointer arguments escaping to heap
static inline uint32_t sd_ble_gatts_hvx_noescape(uint16_t conn_handle, uint16_t handle, uint8_t type, uint16_t offset, uint16_t len, uint8_t *p_data) {
ble_gatts_hvx_params_t p_hvx_params = {handle, type, offset, &len, p_data};
return sd_ble_gatts_hvx(conn_handle, &p_hvx_params);
}
static inline uint32_t sd_ble_gatts_value_set_noescape(uint16_t conn_handle, uint16_t handle, uint16_t len, uint8_t *value) {
ble_gatts_value_t p_value = {
.len = len,
.offset = 0,
.p_value = value,
};
return sd_ble_gatts_value_set(conn_handle, handle, &p_value);
}
*/
import "C"
import "unsafe"
// Characteristic is a single characteristic in a service. It has an UUID and a
// value.
type Characteristic struct {
handle C.uint16_t
handle uint16
permissions CharacteristicPermissions
}
@ -38,18 +25,18 @@ func (a *Adapter) AddService(service *Service) error {
if errCode != 0 {
return Error(errCode)
}
errCode = C.sd_ble_gatts_service_add(C.BLE_GATTS_SRVC_TYPE_PRIMARY, &uuid, (*C.uint16_t)(unsafe.Pointer(&service.handle)))
errCode = C.sd_ble_gatts_service_add(C.BLE_GATTS_SRVC_TYPE_PRIMARY, &uuid, &service.handle)
if errCode != 0 {
return Error(errCode)
}
for _, char := range service.Characteristics {
metadata := C.ble_gatts_char_md_t{}
metadata.char_props.set_bitfield_broadcast(C.uint8_t(char.Flags>>0) & 1)
metadata.char_props.set_bitfield_read(C.uint8_t(char.Flags>>1) & 1)
metadata.char_props.set_bitfield_write_wo_resp(C.uint8_t(char.Flags>>2) & 1)
metadata.char_props.set_bitfield_write(C.uint8_t(char.Flags>>3) & 1)
metadata.char_props.set_bitfield_notify(C.uint8_t(char.Flags>>4) & 1)
metadata.char_props.set_bitfield_indicate(C.uint8_t(char.Flags>>5) & 1)
metadata.char_props.set_bitfield_broadcast(uint8(char.Flags>>0) & 1)
metadata.char_props.set_bitfield_read(uint8(char.Flags>>1) & 1)
metadata.char_props.set_bitfield_write_wo_resp(uint8(char.Flags>>2) & 1)
metadata.char_props.set_bitfield_write(uint8(char.Flags>>3) & 1)
metadata.char_props.set_bitfield_notify(uint8(char.Flags>>4) & 1)
metadata.char_props.set_bitfield_indicate(uint8(char.Flags>>5) & 1)
handles := C.ble_gatts_char_handles_t{}
charUUID, errCode := char.UUID.shortUUID()
if errCode != 0 {
@ -61,16 +48,16 @@ func (a *Adapter) AddService(service *Service) error {
read_perm: secModeOpen,
write_perm: secModeOpen,
},
init_len: C.uint16_t(len(char.Value)),
init_len: uint16(len(char.Value)),
init_offs: 0,
max_len: 20, // This is a conservative maximum length.
}
if len(char.Value) != 0 {
value.p_value = (*C.uint8_t)(unsafe.Pointer(&char.Value[0]))
value.p_value = &char.Value[0]
}
value.p_attr_md.set_bitfield_vloc(C.BLE_GATTS_VLOC_STACK)
value.p_attr_md.set_bitfield_vlen(1)
errCode = C.sd_ble_gatts_characteristic_add(C.uint16_t(service.handle), &metadata, &value, &handles)
errCode = C.sd_ble_gatts_characteristic_add(service.handle, &metadata, &value, &handles)
if errCode != 0 {
return Error(errCode)
}
@ -94,13 +81,13 @@ func (a *Adapter) AddService(service *Service) error {
// charWriteHandler contains a handler->callback mapping for characteristic
// writes.
type charWriteHandler struct {
handle C.uint16_t
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 C.uint16_t) *charWriteHandler {
func (a *Adapter) getCharWriteHandler(handle uint16) *charWriteHandler {
// Look through all handlers for a match.
// There is probably a way to do this more efficiently (with a hashmap for
// example) but the number of event handlers is likely low and improving
@ -123,16 +110,15 @@ func (c *Characteristic) Write(p []byte) (n int, err error) {
}
connHandle := currentConnection.Get()
if connHandle != C.BLE_CONN_HANDLE_INVALID && c.permissions&(CharacteristicNotifyPermission|CharacteristicIndicatePermission) != 0 {
if connHandle != C.BLE_CONN_HANDLE_INVALID {
// There is a connected central.
p_len := uint16(len(p))
errCode := C.sd_ble_gatts_hvx_noescape(connHandle,
c.handle,
C.BLE_GATT_HVX_NOTIFICATION,
0,
C.uint16_t(p_len),
(*C.uint8_t)(unsafe.Pointer(&p[0])),
)
errCode := C.sd_ble_gatts_hvx(connHandle, &C.ble_gatts_hvx_params_t{
handle: c.handle,
_type: C.BLE_GATT_HVX_NOTIFICATION,
p_len: &p_len,
p_data: &p[0],
})
// Check for some expected errors. Don't report them as errors, but
// instead fall through and do a normal characteristic value update.
@ -150,7 +136,10 @@ 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(C.BLE_CONN_HANDLE_INVALID, c.handle, &C.ble_gatts_value_t{
len: uint16(len(p)),
p_value: &p[0],
})
if errCode != 0 {
return 0, Error(errCode)
}

View file

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

33
go.mod
View file

@ -1,25 +1,16 @@
module gitrepo.ru/neonxp/bluetooth
module tinygo.org/x/bluetooth
go 1.20
go 1.15
require (
github.com/go-ole/go-ole v1.2.6
github.com/godbus/dbus/v5 v5.1.0
github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b
github.com/soypat/cyw43439 v0.0.0-20240609122733-da9153086796
github.com/tinygo-org/cbgo v0.0.4
golang.org/x/crypto v0.12.0
tinygo.org/x/drivers v0.26.1-0.20230922160320-ed51435c2ef6
tinygo.org/x/tinyfont v0.4.0
tinygo.org/x/tinyterm v0.3.0
)
require (
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef // indirect
github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899 // indirect
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/term v0.11.0 // indirect
github.com/JuulLabs-OSS/cbgo v0.0.2
github.com/go-ole/go-ole v1.2.4
github.com/godbus/dbus/v5 v5.0.3
github.com/kr/pretty v0.1.0 // indirect
github.com/muka/go-bluetooth v0.0.0-20200928120822-44d49b402aee
github.com/sirupsen/logrus v1.6.0 // indirect
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
tinygo.org/x/drivers v0.13.0
)

104
go.sum
View file

@ -1,49 +1,75 @@
github.com/JuulLabs-OSS/cbgo v0.0.2 h1:gCDyT0+EPuI8GOFyvAksFcVD2vF4CXBAVwT6uVnD9oo=
github.com/JuulLabs-OSS/cbgo v0.0.2/go.mod h1:L4YtGP+gnyD84w7+jN66ncspFRfOYB5aj9QSXaFHmBA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/muka/go-bluetooth v0.0.0-20200619025933-f6113f7141c5 h1:xnTS/7y0g28W2SJeWNLMYTiTOmfW2P/YdPByoQnPvVo=
github.com/muka/go-bluetooth v0.0.0-20200619025933-f6113f7141c5/go.mod h1:yV39+EVOWdnoTe75NyKdo9iuyI3Slyh4t7eQvElUbWE=
github.com/muka/go-bluetooth v0.0.0-20200926181701-4ca7d8dd0ff5 h1:2n6xusPU4MxghLXhYNpGSYYov4AjnARQfA2xIPfqelA=
github.com/muka/go-bluetooth v0.0.0-20200926181701-4ca7d8dd0ff5/go.mod h1:dMCjicU6vRBk34dqOmIZm0aod6gUwZXOXzBROqGous0=
github.com/muka/go-bluetooth v0.0.0-20200928120822-44d49b402aee h1:viY7YR4j7hyLKFK9peIU+FysyOJ47ezp4oBwhiEW9Hk=
github.com/muka/go-bluetooth v0.0.0-20200928120822-44d49b402aee/go.mod h1:dMCjicU6vRBk34dqOmIZm0aod6gUwZXOXzBROqGous0=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/paypal/gatt v0.0.0-20151011220935-4ae819d591cf/go.mod h1:+AwQL2mK3Pd3S+TUwg0tYQjid0q1txyNUJuuSmz8Kdk=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b h1:du3zG5fd8snsFN6RBoLA7fpaYV9ZQIsyH9snlk2Zvik=
github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA=
github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q=
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/soypat/cyw43439 v0.0.0-20240609122733-da9153086796 h1:1/r2URInjjFtWqT61gU7YGVCq3BRyXt/C7z4oLRF9Lo=
github.com/soypat/cyw43439 v0.0.0-20240609122733-da9153086796/go.mod h1:1Otjk6PRhfzfcVHeWMEeku/VntFqWghUwuSQyivb2vE=
github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef h1:phH95I9wANjTYw6bSYLZDQfNvao+HqYDom8owbNa0P4=
github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef/go.mod h1:oCVCNGCHMKoBj97Zp9znLbQ1nHxpkmOY9X+UAGzOxc8=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q=
github.com/tinygo-org/cbgo v0.0.4 h1:3D76CRYbH03Rudi8sEgs/YO0x3JIMdyq8jlQtk/44fU=
github.com/tinygo-org/cbgo v0.0.4/go.mod h1:7+HgWIHd4nbAz0ESjGlJ1/v9LDU1Ox8MGzP9mah/fLk=
github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899 h1:/DyaXDEWMqoVUVEJVJIlNk1bXTbFs8s3Q4GdPInSKTQ=
github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899/go.mod h1:LU7Dw00NJ+N86QkeTGjMLNkYcEYMor6wTDpTCu0EaH8=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 h1:/yRP+0AN7mf5DkD3BAI6TOFnd51gEoDEb8o35jIFtgw=
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/suapapa/go_eddystone v1.3.1/go.mod h1:bXC11TfJOS+3g3q/Uzd7FKd5g62STQEfeEIhcKe4Qy8=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200602180216-279210d13fed h1:g4KENRiCMEx58Q7/ecwfT0N2o8z35Fnbsjig/Alf2T4=
golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 h1:DvY3Zkh7KabQE/kfzMvYvKirSiguP9Q/veMtkYyf0o8=
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
tinygo.org/x/drivers v0.26.1-0.20230922160320-ed51435c2ef6 h1:w18u47MirULgAl+bP0piUGu5VUZDs7TvXwHASEVXqHk=
tinygo.org/x/drivers v0.26.1-0.20230922160320-ed51435c2ef6/go.mod h1:X7utcg3yfFUFuKLOMTZD56eztXMjpkcf8OHldfTBsjw=
tinygo.org/x/tinyfont v0.4.0 h1:XexPKEKiHInf6p4CMCJwsIheVPY0T46HUs6ictYyZfE=
tinygo.org/x/tinyfont v0.4.0/go.mod h1:7nVj3j3geqBoPDzpFukAhF1C8AP9YocMsZy0HSAcGCA=
tinygo.org/x/tinyterm v0.3.0 h1:4MMZoMyrbWbjru1KP/Z2TGhaguy/Uh5Mdhf/niemM8c=
tinygo.org/x/tinyterm v0.3.0/go.mod h1:F1pQjxEwNZQIc5czeJSBtk57ucEvbR4u7vHaLhWhHtg=
tinygo.org/x/drivers v0.13.0 h1:ohzhFiPb/5dcRop3X+Gdvsr6uswmnpfMX9KsAMtgtTM=
tinygo.org/x/drivers v0.13.0/go.mod h1:mShi1lpVtJFpApkZgwyrzDKHToeGfWIuB08utyHxZ7g=

787
hci.go
View file

@ -1,787 +0,0 @@
//go:build ninafw || hci || cyw43439
package bluetooth
import (
"encoding/binary"
"encoding/hex"
"errors"
"time"
)
const (
ogfCommandPos = 10
ogfLinkCtl = 0x01
ogfHostCtl = 0x03
ogfInfoParam = 0x04
ogfStatusParam = 0x05
ogfLECtrl = 0x08
// ogfLinkCtl
ocfDisconnect = 0x0006
// ogfHostCtl
ocfSetEventMask = 0x0001
ocfReset = 0x0003
// ogfInfoParam
ocfReadLocalVersion = 0x0001
ocfReadBDAddr = 0x0009
// ogfStatusParam
ocfReadRSSI = 0x0005
// ogfLECtrl
ocfLEReadBufferSize = 0x0002
ocfLESetRandomAddress = 0x0005
ocfLESetAdvertisingParameters = 0x0006
ocfLESetAdvertisingData = 0x0008
ocfLESetScanResponseData = 0x0009
ocfLESetAdvertiseEnable = 0x000a
ocfLESetScanParameters = 0x000b
ocfLESetScanEnable = 0x000c
ocfLECreateConn = 0x000d
ocfLECancelConn = 0x000e
ocfLEConnUpdate = 0x0013
ocfLEParamRequestReply = 0x0020
leCommandEncrypt = 0x0017
leCommandRandom = 0x0018
leCommandLongTermKeyReply = 0x001A
leCommandLongTermKeyNegativeReply = 0x001B
leCommandReadLocalP256 = 0x0025
leCommandGenerateDHKeyV1 = 0x0026
leCommandGenerateDHKeyV2 = 0x005E
leMetaEventConnComplete = 0x01
leMetaEventAdvertisingReport = 0x02
leMetaEventConnectionUpdateComplete = 0x03
leMetaEventReadRemoteUsedFeaturesComplete = 0x04
leMetaEventLongTermKeyRequest = 0x05
leMetaEventRemoteConnParamReq = 0x06
leMetaEventDataLengthChange = 0x07
leMetaEventReadLocalP256Complete = 0x08
leMetaEventGenerateDHKeyComplete = 0x09
leMetaEventEnhancedConnectionComplete = 0x0A
leMetaEventDirectAdvertisingReport = 0x0B
hciCommandPkt = 0x01
hciACLDataPkt = 0x02
hciSynchronousDataPkt = 0x03
hciEventPkt = 0x04
hciSecurityPkt = 0x06
evtDisconnComplete = 0x05
evtEncryptionChange = 0x08
evtCmdComplete = 0x0e
evtCmdStatus = 0x0f
evtHardwareError = 0x10
evtNumCompPkts = 0x13
evtReturnLinkKeys = 0x15
evtLEMetaEvent = 0x3e
hciOEUserEndedConnection = 0x13
)
const (
hciACLLenPos = 4
hciEvtLenPos = 2
attCID = 0x0004
bleCTL = 0x0008
signalingCID = 0x0005
securityCID = 0x0006
)
var (
ErrHCITimeout = errors.New("bluetooth: HCI timeout")
ErrHCIUnknownEvent = errors.New("bluetooth: HCI unknown event")
ErrHCIUnknown = errors.New("bluetooth: HCI unknown error")
ErrHCIInvalidPacket = errors.New("bluetooth: HCI invalid packet")
ErrHCIHardware = errors.New("bluetooth: HCI hardware error")
)
type leAdvertisingReport struct {
reported bool
numReports, typ, peerBdaddrType uint8
peerBdaddr [6]uint8
eirLength uint8
eirData [31]uint8
rssi int8
}
type leConnectData struct {
connected bool
status uint8
handle uint16
role uint8
peerBdaddrType 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 {
transport hciTransport
att *att
l2cap *l2cap
buf []byte
address [6]byte
cmdCompleteOpcode uint16
cmdCompleteStatus uint8
cmdResponse []byte
scanning bool
advData leAdvertisingReport
connectData leConnectData
maxPkt uint16
pendingPkt uint16
}
func newHCI(t hciTransport) *hci {
return &hci{
transport: t,
buf: make([]byte, 256),
}
}
func (h *hci) start() error {
h.transport.startRead()
defer h.transport.endRead()
var data [32]byte
for {
if i := h.transport.Buffered(); i > 0 {
if i > len(data) {
i = len(data)
}
if _, err := h.transport.Read(data[:i]); err != nil {
return err
}
continue
}
return nil
}
return nil
}
func (h *hci) stop() error {
return nil
}
func (h *hci) reset() error {
return h.sendCommand(ogfHostCtl<<10 | ocfReset)
}
func (h *hci) poll() error {
h.transport.startRead()
defer h.transport.endRead()
i := 0
for h.transport.Buffered() > 0 {
sz := h.transport.Buffered()
c := sz + 4 - (sz % 4)
_, err := h.transport.Read(h.buf[i : i+c])
if err != nil {
return err
}
i += sz
done, err := h.processPacket(i)
switch {
case err == ErrHCIUnknown || err == ErrHCIInvalidPacket || err == ErrHCIUnknownEvent:
if debug {
println("hci error:", err.Error(), hex.EncodeToString(h.buf[:i]))
}
i = 0
time.Sleep(5 * time.Millisecond)
case err != nil:
return err
case done:
return nil
case i+1 >= len(h.buf):
if debug {
println("hci error: buffer overflow")
}
i = 0
time.Sleep(5 * time.Millisecond)
default:
time.Sleep(1 * time.Millisecond)
}
}
return nil
}
func (h *hci) processPacket(i int) (bool, error) {
switch h.buf[0] {
case hciACLDataPkt:
if i > hciACLLenPos {
pktlen := int(binary.LittleEndian.Uint16(h.buf[3:5]))
switch {
case pktlen > len(h.buf):
return true, ErrHCIInvalidPacket
case i >= (hciACLLenPos + pktlen):
if debug {
println("hci acl data:", i, hex.EncodeToString(h.buf[:1+hciACLLenPos+pktlen]))
}
return true, h.handleACLData(h.buf[1 : 1+hciACLLenPos+pktlen])
}
}
case hciEventPkt:
if i > hciEvtLenPos {
pktlen := int(h.buf[hciEvtLenPos])
switch {
case pktlen > len(h.buf):
return true, ErrHCIInvalidPacket
case i >= (hciEvtLenPos + pktlen):
if debug {
println("hci event data:", i, hex.EncodeToString(h.buf[:1+hciEvtLenPos+pktlen]))
}
return true, h.handleEventData(h.buf[1 : 1+hciEvtLenPos+pktlen])
}
}
case hciSynchronousDataPkt:
// not supported by BLE, so ignore
if i > 3 {
pktlen := int(h.buf[3])
if debug {
println("hci synchronous data:", i, pktlen, hex.EncodeToString(h.buf[:1+3+pktlen]))
}
return true, nil
}
default:
if debug {
println("unknown packet data:", hex.EncodeToString(h.buf[0:i]))
}
return true, ErrHCIUnknown
}
return false, nil
}
func (h *hci) readBdAddr() error {
if err := h.sendCommand(ogfInfoParam<<ogfCommandPos | ocfReadBDAddr); err != nil {
return err
}
copy(h.address[:], h.cmdResponse[:7])
return nil
}
func (h *hci) setEventMask(eventMask uint64) error {
var b [8]byte
binary.LittleEndian.PutUint64(b[:], eventMask)
return h.sendCommandWithParams(ogfHostCtl<<ogfCommandPos|ocfSetEventMask, b[:])
}
func (h *hci) setLeEventMask(eventMask uint64) error {
var b [8]byte
binary.LittleEndian.PutUint64(b[:], eventMask)
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 {
h.scanning = enabled
var data [2]byte
if enabled {
data[0] = 1
}
if duplicates {
data[1] = 1
}
return h.sendCommandWithParams(ogfLECtrl<<ogfCommandPos|ocfLESetScanEnable, data[:])
}
func (h *hci) leSetScanParameters(typ uint8, interval, window uint16, ownBdaddrType, filter uint8) error {
var data [7]byte
data[0] = typ
binary.LittleEndian.PutUint16(data[1:], interval)
binary.LittleEndian.PutUint16(data[3:], window)
data[5] = ownBdaddrType
data[6] = filter
return h.sendCommandWithParams(ogfLECtrl<<ogfCommandPos|ocfLESetScanParameters, data[:])
}
func (h *hci) leSetAdvertiseEnable(enabled bool) error {
var data [1]byte
if enabled {
data[0] = 1
}
return h.sendWithoutResponse(ogfLECtrl<<ogfCommandPos|ocfLESetAdvertiseEnable, data[:])
}
func (h *hci) leSetAdvertisingParameters(minInterval, maxInterval uint16,
advType, ownBdaddrType uint8,
directBdaddrType uint8, directBdaddr [6]byte,
chanMap, filter uint8) error {
var b [15]byte
binary.LittleEndian.PutUint16(b[0:], minInterval)
binary.LittleEndian.PutUint16(b[2:], maxInterval)
b[4] = advType
b[5] = ownBdaddrType
b[6] = directBdaddrType
copy(b[7:], directBdaddr[:])
b[13] = chanMap
b[14] = filter
return h.sendCommandWithParams(ogfLECtrl<<ogfCommandPos|ocfLESetAdvertisingParameters, b[:])
}
func (h *hci) leSetAdvertisingData(data []byte) error {
var b [32]byte
b[0] = byte(len(data))
copy(b[1:], data)
return h.sendCommandWithParams(ogfLECtrl<<ogfCommandPos|ocfLESetAdvertisingData, b[:])
}
func (h *hci) leSetScanResponseData(data []byte) error {
var b [32]byte
b[0] = byte(len(data))
copy(b[1:], data)
return h.sendCommandWithParams(ogfLECtrl<<ogfCommandPos|ocfLESetScanResponseData, b[:])
}
func (h *hci) leCreateConn(interval, window uint16,
initiatorFilter, peerBdaddrType uint8, peerBdaddr [6]byte, ownBdaddrType uint8,
minInterval, maxInterval, latency, supervisionTimeout,
minCeLength, maxCeLength uint16) error {
var b [25]byte
binary.LittleEndian.PutUint16(b[0:], interval)
binary.LittleEndian.PutUint16(b[2:], window)
b[4] = initiatorFilter
b[5] = peerBdaddrType
copy(b[6:], peerBdaddr[:])
b[12] = ownBdaddrType
binary.LittleEndian.PutUint16(b[13:], minInterval)
binary.LittleEndian.PutUint16(b[15:], maxInterval)
binary.LittleEndian.PutUint16(b[17:], latency)
binary.LittleEndian.PutUint16(b[19:], supervisionTimeout)
binary.LittleEndian.PutUint16(b[21:], minCeLength)
binary.LittleEndian.PutUint16(b[23:], maxCeLength)
return h.sendCommandWithParams(ogfLECtrl<<ogfCommandPos|ocfLECreateConn, b[:])
}
func (h *hci) leCancelConn() error {
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 {
var b [3]byte
binary.LittleEndian.PutUint16(b[0:], handle)
b[2] = hciOEUserEndedConnection
return h.sendCommandWithParams(ogfLinkCtl<<ogfCommandPos|ocfDisconnect, b[:])
}
func (h *hci) sendCommand(opcode uint16) error {
return h.sendCommandWithParams(opcode, []byte{})
}
func (h *hci) sendCommandWithParams(opcode uint16, params []byte) error {
if debug {
println("hci send command", opcode, hex.EncodeToString(params))
}
h.buf[0] = hciCommandPkt
binary.LittleEndian.PutUint16(h.buf[1:], opcode)
h.buf[3] = byte(len(params))
copy(h.buf[4:], params)
if _, err := h.write(h.buf[:4+len(params)]); err != nil {
return err
}
h.cmdCompleteOpcode = 0xffff
h.cmdCompleteStatus = 0xff
start := time.Now().UnixNano()
for h.cmdCompleteOpcode != opcode {
if err := h.poll(); err != nil {
return err
}
if (time.Now().UnixNano()-start)/int64(time.Second) > 3 {
return ErrHCITimeout
}
}
return nil
}
func (h *hci) sendWithoutResponse(opcode uint16, params []byte) error {
if debug {
println("hci send without response command", opcode, hex.EncodeToString(params))
}
h.buf[0] = hciCommandPkt
binary.LittleEndian.PutUint16(h.buf[1:], opcode)
h.buf[3] = byte(len(params))
copy(h.buf[4:], params)
if _, err := h.write(h.buf[:4+len(params)]); err != nil {
return err
}
h.cmdCompleteOpcode = 0xffff
h.cmdCompleteStatus = 0xff
return nil
}
func (h *hci) sendAclPkt(handle uint16, cid uint8, data []byte) error {
h.buf[0] = hciACLDataPkt
binary.LittleEndian.PutUint16(h.buf[1:], handle)
binary.LittleEndian.PutUint16(h.buf[3:], uint16(len(data)+4))
binary.LittleEndian.PutUint16(h.buf[5:], uint16(len(data)))
binary.LittleEndian.PutUint16(h.buf[7:], uint16(cid))
copy(h.buf[9:], data)
if debug {
println("hci send acl data", handle, cid, hex.EncodeToString(h.buf[:9+len(data)]))
}
if _, err := h.write(h.buf[:9+len(data)]); err != nil {
return err
}
h.pendingPkt++
return nil
}
func (h *hci) write(buf []byte) (int, error) {
return h.transport.Write(buf)
}
type aclDataHeader struct {
handle uint16
dlen uint16
len uint16
cid uint16
}
func (h *hci) handleACLData(buf []byte) error {
aclHdr := aclDataHeader{
handle: binary.LittleEndian.Uint16(buf[0:]),
dlen: binary.LittleEndian.Uint16(buf[2:]),
len: binary.LittleEndian.Uint16(buf[4:]),
cid: binary.LittleEndian.Uint16(buf[6:]),
}
aclFlags := (aclHdr.handle & 0xf000) >> 12
if aclHdr.dlen-4 != aclHdr.len {
return errors.New("fragmented packet")
}
switch aclHdr.cid {
case attCID:
if aclFlags == 0x01 {
// TODO: use buffered packet
if debug {
println("WARNING: att.handleACLData needs buffered packet")
}
return h.att.handleData(aclHdr.handle&0x0fff, buf[8:aclHdr.len+8])
} else {
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:
if debug {
println("unknown acl data cid", aclHdr.cid)
}
}
return nil
}
func (h *hci) handleEventData(buf []byte) error {
evt := buf[0]
plen := buf[1]
switch evt {
case evtDisconnComplete:
if debug {
println("evtDisconnComplete")
}
handle := binary.LittleEndian.Uint16(buf[3:])
h.att.removeConnection(handle)
h.l2cap.removeConnection(handle)
return h.leSetAdvertiseEnable(true)
case evtEncryptionChange:
if debug {
println("evtEncryptionChange")
}
case evtCmdComplete:
h.cmdCompleteOpcode = binary.LittleEndian.Uint16(buf[3:])
h.cmdCompleteStatus = buf[5]
if plen > 0 {
h.cmdResponse = buf[1 : plen+2]
} else {
h.cmdResponse = buf[:0]
}
if debug {
println("evtCmdComplete", h.cmdCompleteOpcode, h.cmdCompleteStatus)
}
return nil
case evtCmdStatus:
h.cmdCompleteStatus = buf[2]
h.cmdCompleteOpcode = binary.LittleEndian.Uint16(buf[4:])
if debug {
println("evtCmdStatus", h.cmdCompleteOpcode, h.cmdCompleteOpcode, h.cmdCompleteStatus)
}
h.cmdResponse = buf[:0]
return nil
case evtNumCompPkts:
if debug {
println("evtNumCompPkts", hex.EncodeToString(buf))
}
// count of handles
c := buf[2]
pkts := uint16(0)
for i := byte(0); i < c; i++ {
pkts += binary.LittleEndian.Uint16(buf[5+i*4:])
}
if pkts > 0 && h.pendingPkt > pkts {
h.pendingPkt -= pkts
} else {
h.pendingPkt = 0
}
if debug {
println("evtNumCompPkts", pkts, h.pendingPkt)
}
return nil
case evtLEMetaEvent:
if debug {
println("evtLEMetaEvent")
}
switch buf[2] {
case leMetaEventConnComplete, leMetaEventEnhancedConnectionComplete:
if debug {
println("leMetaEventConnComplete")
}
h.connectData.connected = true
h.connectData.status = buf[3]
h.connectData.handle = binary.LittleEndian.Uint16(buf[4:])
h.connectData.role = buf[6]
h.connectData.peerBdaddrType = buf[7]
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)
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)
case leMetaEventAdvertisingReport:
h.advData.reported = true
h.advData.numReports = buf[3]
h.advData.typ = buf[4]
h.advData.peerBdaddrType = buf[5]
copy(h.advData.peerBdaddr[0:], buf[6:])
h.advData.eirLength = buf[12]
h.advData.rssi = 0
if debug {
println("leMetaEventAdvertisingReport", plen, h.advData.numReports,
h.advData.typ, h.advData.peerBdaddrType, h.advData.eirLength)
}
if int(13+h.advData.eirLength+1) > len(buf) || h.advData.eirLength > 31 {
if debug {
println("invalid packet length", h.advData.eirLength, len(buf))
}
return ErrHCIInvalidPacket
}
copy(h.advData.eirData[0:h.advData.eirLength], buf[13:13+h.advData.eirLength])
// TODO: handle multiple reports
if h.advData.numReports == 0x01 {
h.advData.rssi = int8(buf[int(13+h.advData.eirLength)])
}
return nil
case leMetaEventLongTermKeyRequest:
if debug {
println("leMetaEventLongTermKeyRequest")
}
case leMetaEventRemoteConnParamReq:
if debug {
println("leMetaEventRemoteConnParamReq")
}
connectionHandle := binary.LittleEndian.Uint16(buf[3:])
intervalMin := binary.LittleEndian.Uint16(buf[5:])
intervalMax := binary.LittleEndian.Uint16(buf[7:])
latency := binary.LittleEndian.Uint16(buf[9:])
timeOut := binary.LittleEndian.Uint16(buf[11:])
var b [14]byte
binary.LittleEndian.PutUint16(b[0:], connectionHandle)
binary.LittleEndian.PutUint16(b[2:], intervalMin)
binary.LittleEndian.PutUint16(b[4:], intervalMax)
binary.LittleEndian.PutUint16(b[6:], latency)
binary.LittleEndian.PutUint16(b[8:], timeOut)
binary.LittleEndian.PutUint16(b[10:], 0x000F)
binary.LittleEndian.PutUint16(b[12:], 0x0FFF)
return h.sendWithoutResponse(ogfLECtrl<<10|ocfLEParamRequestReply, b[:])
case leMetaEventConnectionUpdateComplete:
if debug {
println("leMetaEventConnectionUpdateComplete")
}
case leMetaEventReadLocalP256Complete:
if debug {
println("leMetaEventReadLocalP256Complete")
}
case leMetaEventGenerateDHKeyComplete:
if debug {
println("leMetaEventGenerateDHKeyComplete")
}
case leMetaEventDataLengthChange:
if debug {
println("leMetaEventDataLengthChange")
}
default:
if debug {
println("unknown metaevent", buf[2], buf[3], buf[4], buf[5])
}
h.clearAdvData()
return ErrHCIUnknownEvent
}
case evtHardwareError:
if debug {
println("evtHardwareError", hex.EncodeToString(buf))
}
return ErrHCIUnknownEvent
}
return nil
}
func (h *hci) clearAdvData() error {
h.advData.reported = false
h.advData.numReports = 0
h.advData.typ = 0
h.advData.peerBdaddrType = 0
h.advData.peerBdaddr = [6]uint8{}
h.advData.eirLength = 0
h.advData.eirData = [31]uint8{}
h.advData.rssi = 0
return nil
}
func (h *hci) clearConnectData() error {
h.connectData.connected = false
h.connectData.status = 0
h.connectData.handle = 0
h.connectData.role = 0
h.connectData.peerBdaddrType = 0
h.connectData.peerBdaddr = [6]uint8{}
return nil
}

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)
}

2
mac.go
View file

@ -36,7 +36,7 @@ func ParseMAC(s string) (mac MAC, err error) {
}
macIndex--
}
if macIndex != -1 {
if macIndex != 0 {
err = errInvalidMAC
}
return

View file

@ -1,5 +0,0 @@
//go:build !bledebug
package bluetooth
var debug = false

View file

@ -1,4 +1,4 @@
//go:build (linux && !baremetal) || darwin
// +build linux,!baremetal darwin
// Package rawterm provides some sort of raw terminal interface, both on hosted
// systems and baremetal. It is intended only for use by examples.
@ -43,9 +43,9 @@ func Putchar(ch byte) {
// Getchar/Putchar). It must be restored after use with Restore. You can do this
// with the following code:
//
// rawterm.Configure()
// defer rawterm.Restore()
// // use raw terminal features
// rawterm.Configure()
// defer rawterm.Restore()
// // use raw terminal features
func Configure() {
terminalState, _ = terminal.MakeRaw(0)
}

View file

@ -1,4 +1,4 @@
//go:build nrf
// +build nrf
package rawterm
@ -42,9 +42,9 @@ func Putchar(ch byte) {
// Getchar/Putchar). It must be restored after use with Restore. You can do this
// with the following code:
//
// rawterm.Configure()
// defer rawterm.Restore()
// // use raw terminal features
// rawterm.Configure()
// defer rawterm.Restore()
// // use raw terminal features
func Configure() {
}

File diff suppressed because it is too large Load diff

View file

@ -1,507 +0,0 @@
/*
* Copyright (c) 2011 - 2020, Nordic Semiconductor ASA
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form, except as embedded into a Nordic
* Semiconductor ASA integrated circuit in a product or a software update for
* such product, must reproduce the above copyright notice, this list of
* conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. Neither the name of Nordic Semiconductor ASA nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* 4. This software, with or without modification, must only be used with a
* Nordic Semiconductor ASA integrated circuit.
*
* 5. Any software provided in binary form under this license must not be reverse
* engineered, decompiled, modified and/or disassembled.
*
* THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
@addtogroup BLE_L2CAP Logical Link Control and Adaptation Protocol (L2CAP)
@{
@brief Definitions and prototypes for the L2CAP interface.
*/
#ifndef BLE_L2CAP_H__
#define BLE_L2CAP_H__
#include <stdint.h>
#include "nrf_svc.h"
#include "nrf_error.h"
#include "ble_ranges.h"
#include "ble_types.h"
#include "ble_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/**@addtogroup BLE_L2CAP_TERMINOLOGY Terminology
* @{
* @details
*
* L2CAP SDU
* - A data unit that the application can send/receive to/from a peer.
*
* L2CAP PDU
* - A data unit that is exchanged between local and remote L2CAP entities.
* It consists of L2CAP protocol control information and payload fields.
* The payload field can contain an L2CAP SDU or a part of an L2CAP SDU.
*
* L2CAP MTU
* - The maximum length of an L2CAP SDU.
*
* L2CAP MPS
* - The maximum length of an L2CAP PDU payload field.
*
* Credits
* - A value indicating the number of L2CAP PDUs that the receiver of the credit can send to the peer.
* @} */
/**@addtogroup BLE_L2CAP_ENUMERATIONS Enumerations
* @{ */
/**@brief L2CAP API SVC numbers. */
enum BLE_L2CAP_SVCS
{
SD_BLE_L2CAP_CH_SETUP = BLE_L2CAP_SVC_BASE + 0, /**< Set up an L2CAP channel. */
SD_BLE_L2CAP_CH_RELEASE = BLE_L2CAP_SVC_BASE + 1, /**< Release an L2CAP channel. */
SD_BLE_L2CAP_CH_RX = BLE_L2CAP_SVC_BASE + 2, /**< Receive an SDU on an L2CAP channel. */
SD_BLE_L2CAP_CH_TX = BLE_L2CAP_SVC_BASE + 3, /**< Transmit an SDU on an L2CAP channel. */
SD_BLE_L2CAP_CH_FLOW_CONTROL = BLE_L2CAP_SVC_BASE + 4, /**< Advanced SDU reception flow control. */
};
/**@brief L2CAP Event IDs. */
enum BLE_L2CAP_EVTS
{
BLE_L2CAP_EVT_CH_SETUP_REQUEST = BLE_L2CAP_EVT_BASE + 0, /**< L2CAP Channel Setup Request event.
\n Reply with @ref sd_ble_l2cap_ch_setup.
\n See @ref ble_l2cap_evt_ch_setup_request_t. */
BLE_L2CAP_EVT_CH_SETUP_REFUSED = BLE_L2CAP_EVT_BASE + 1, /**< L2CAP Channel Setup Refused event.
\n See @ref ble_l2cap_evt_ch_setup_refused_t. */
BLE_L2CAP_EVT_CH_SETUP = BLE_L2CAP_EVT_BASE + 2, /**< L2CAP Channel Setup Completed event.
\n See @ref ble_l2cap_evt_ch_setup_t. */
BLE_L2CAP_EVT_CH_RELEASED = BLE_L2CAP_EVT_BASE + 3, /**< L2CAP Channel Released event.
\n No additional event structure applies. */
BLE_L2CAP_EVT_CH_SDU_BUF_RELEASED = BLE_L2CAP_EVT_BASE + 4, /**< L2CAP Channel SDU data buffer released event.
\n See @ref ble_l2cap_evt_ch_sdu_buf_released_t. */
BLE_L2CAP_EVT_CH_CREDIT = BLE_L2CAP_EVT_BASE + 5, /**< L2CAP Channel Credit received.
\n See @ref ble_l2cap_evt_ch_credit_t. */
BLE_L2CAP_EVT_CH_RX = BLE_L2CAP_EVT_BASE + 6, /**< L2CAP Channel SDU received.
\n See @ref ble_l2cap_evt_ch_rx_t. */
BLE_L2CAP_EVT_CH_TX = BLE_L2CAP_EVT_BASE + 7, /**< L2CAP Channel SDU transmitted.
\n See @ref ble_l2cap_evt_ch_tx_t. */
};
/** @} */
/**@addtogroup BLE_L2CAP_DEFINES Defines
* @{ */
/**@brief Maximum number of L2CAP channels per connection. */
#define BLE_L2CAP_CH_COUNT_MAX (64)
/**@brief Minimum L2CAP MTU, in bytes. */
#define BLE_L2CAP_MTU_MIN (23)
/**@brief Minimum L2CAP MPS, in bytes. */
#define BLE_L2CAP_MPS_MIN (23)
/**@brief Invalid CID. */
#define BLE_L2CAP_CID_INVALID (0x0000)
/**@brief Default number of credits for @ref sd_ble_l2cap_ch_flow_control. */
#define BLE_L2CAP_CREDITS_DEFAULT (1)
/**@defgroup BLE_L2CAP_CH_SETUP_REFUSED_SRCS L2CAP channel setup refused sources
* @{ */
#define BLE_L2CAP_CH_SETUP_REFUSED_SRC_LOCAL (0x01) /**< Local. */
#define BLE_L2CAP_CH_SETUP_REFUSED_SRC_REMOTE (0x02) /**< Remote. */
/** @} */
/** @defgroup BLE_L2CAP_CH_STATUS_CODES L2CAP channel status codes
* @{ */
#define BLE_L2CAP_CH_STATUS_CODE_SUCCESS (0x0000) /**< Success. */
#define BLE_L2CAP_CH_STATUS_CODE_LE_PSM_NOT_SUPPORTED (0x0002) /**< LE_PSM not supported. */
#define BLE_L2CAP_CH_STATUS_CODE_NO_RESOURCES (0x0004) /**< No resources available. */
#define BLE_L2CAP_CH_STATUS_CODE_INSUFF_AUTHENTICATION (0x0005) /**< Insufficient authentication. */
#define BLE_L2CAP_CH_STATUS_CODE_INSUFF_AUTHORIZATION (0x0006) /**< Insufficient authorization. */
#define BLE_L2CAP_CH_STATUS_CODE_INSUFF_ENC_KEY_SIZE (0x0007) /**< Insufficient encryption key size. */
#define BLE_L2CAP_CH_STATUS_CODE_INSUFF_ENC (0x0008) /**< Insufficient encryption. */
#define BLE_L2CAP_CH_STATUS_CODE_INVALID_SCID (0x0009) /**< Invalid Source CID. */
#define BLE_L2CAP_CH_STATUS_CODE_SCID_ALLOCATED (0x000A) /**< Source CID already allocated. */
#define BLE_L2CAP_CH_STATUS_CODE_UNACCEPTABLE_PARAMS (0x000B) /**< Unacceptable parameters. */
#define BLE_L2CAP_CH_STATUS_CODE_NOT_UNDERSTOOD (0x8000) /**< Command Reject received instead of LE Credit Based Connection Response. */
#define BLE_L2CAP_CH_STATUS_CODE_TIMEOUT (0xC000) /**< Operation timed out. */
/** @} */
/** @} */
/**@addtogroup BLE_L2CAP_STRUCTURES Structures
* @{ */
/**
* @brief BLE L2CAP connection configuration parameters, set with @ref sd_ble_cfg_set.
*
* @note These parameters are set per connection, so all L2CAP channels created on this connection
* will have the same parameters.
*
* @retval ::NRF_ERROR_INVALID_PARAM One or more of the following is true:
* - rx_mps is smaller than @ref BLE_L2CAP_MPS_MIN.
* - tx_mps is smaller than @ref BLE_L2CAP_MPS_MIN.
* - ch_count is greater than @ref BLE_L2CAP_CH_COUNT_MAX.
* @retval ::NRF_ERROR_NO_MEM rx_mps or tx_mps is set too high.
*/
typedef struct
{
uint16_t rx_mps; /**< The maximum L2CAP PDU payload size, in bytes, that L2CAP shall
be able to receive on L2CAP channels on connections with this
configuration. The minimum value is @ref BLE_L2CAP_MPS_MIN. */
uint16_t tx_mps; /**< The maximum L2CAP PDU payload size, in bytes, that L2CAP shall
be able to transmit on L2CAP channels on connections with this
configuration. The minimum value is @ref BLE_L2CAP_MPS_MIN. */
uint8_t rx_queue_size; /**< Number of SDU data buffers that can be queued for reception per
L2CAP channel. The minimum value is one. */
uint8_t tx_queue_size; /**< Number of SDU data buffers that can be queued for transmission
per L2CAP channel. The minimum value is one. */
uint8_t ch_count; /**< Number of L2CAP channels the application can create per connection
with this configuration. The default value is zero, the maximum
value is @ref BLE_L2CAP_CH_COUNT_MAX.
@note if this parameter is set to zero, all other parameters in
@ref ble_l2cap_conn_cfg_t are ignored. */
} ble_l2cap_conn_cfg_t;
/**@brief L2CAP channel RX parameters. */
typedef struct
{
uint16_t rx_mtu; /**< The maximum L2CAP SDU size, in bytes, that L2CAP shall be able to
receive on this L2CAP channel.
- Must be equal to or greater than @ref BLE_L2CAP_MTU_MIN. */
uint16_t rx_mps; /**< The maximum L2CAP PDU payload size, in bytes, that L2CAP shall be
able to receive on this L2CAP channel.
- Must be equal to or greater than @ref BLE_L2CAP_MPS_MIN.
- Must be equal to or less than @ref ble_l2cap_conn_cfg_t::rx_mps. */
ble_data_t sdu_buf; /**< SDU data buffer for reception.
- If @ref ble_data_t::p_data is non-NULL, initial credits are
issued to the peer.
- If @ref ble_data_t::p_data is NULL, no initial credits are
issued to the peer. */
} ble_l2cap_ch_rx_params_t;
/**@brief L2CAP channel setup parameters. */
typedef struct
{
ble_l2cap_ch_rx_params_t rx_params; /**< L2CAP channel RX parameters. */
uint16_t le_psm; /**< LE Protocol/Service Multiplexer. Used when requesting
setup of an L2CAP channel, ignored otherwise. */
uint16_t status; /**< Status code, see @ref BLE_L2CAP_CH_STATUS_CODES.
Used when replying to a setup request of an L2CAP
channel, ignored otherwise. */
} ble_l2cap_ch_setup_params_t;
/**@brief L2CAP channel TX parameters. */
typedef struct
{
uint16_t tx_mtu; /**< The maximum L2CAP SDU size, in bytes, that L2CAP is able to
transmit on this L2CAP channel. */
uint16_t peer_mps; /**< The maximum L2CAP PDU payload size, in bytes, that the peer is
able to receive on this L2CAP channel. */
uint16_t tx_mps; /**< The maximum L2CAP PDU payload size, in bytes, that L2CAP is able
to transmit on this L2CAP channel. This is effective tx_mps,
selected by the SoftDevice as
MIN( @ref ble_l2cap_ch_tx_params_t::peer_mps, @ref ble_l2cap_conn_cfg_t::tx_mps ) */
uint16_t credits; /**< Initial credits given by the peer. */
} ble_l2cap_ch_tx_params_t;
/**@brief L2CAP Channel Setup Request event. */
typedef struct
{
ble_l2cap_ch_tx_params_t tx_params; /**< L2CAP channel TX parameters. */
uint16_t le_psm; /**< LE Protocol/Service Multiplexer. */
} ble_l2cap_evt_ch_setup_request_t;
/**@brief L2CAP Channel Setup Refused event. */
typedef struct
{
uint8_t source; /**< Source, see @ref BLE_L2CAP_CH_SETUP_REFUSED_SRCS */
uint16_t status; /**< Status code, see @ref BLE_L2CAP_CH_STATUS_CODES */
} ble_l2cap_evt_ch_setup_refused_t;
/**@brief L2CAP Channel Setup Completed event. */
typedef struct
{
ble_l2cap_ch_tx_params_t tx_params; /**< L2CAP channel TX parameters. */
} ble_l2cap_evt_ch_setup_t;
/**@brief L2CAP Channel SDU Data Buffer Released event. */
typedef struct
{
ble_data_t sdu_buf; /**< Returned reception or transmission SDU data buffer. The SoftDevice
returns SDU data buffers supplied by the application, which have
not yet been returned previously via a @ref BLE_L2CAP_EVT_CH_RX or
@ref BLE_L2CAP_EVT_CH_TX event. */
} ble_l2cap_evt_ch_sdu_buf_released_t;
/**@brief L2CAP Channel Credit received event. */
typedef struct
{
uint16_t credits; /**< Additional credits given by the peer. */
} ble_l2cap_evt_ch_credit_t;
/**@brief L2CAP Channel received SDU event. */
typedef struct
{
uint16_t sdu_len; /**< Total SDU length, in bytes. */
ble_data_t sdu_buf; /**< SDU data buffer.
@note If there is not enough space in the buffer
(sdu_buf.len < sdu_len) then the rest of the SDU will be
silently discarded by the SoftDevice. */
} ble_l2cap_evt_ch_rx_t;
/**@brief L2CAP Channel transmitted SDU event. */
typedef struct
{
ble_data_t sdu_buf; /**< SDU data buffer. */
} ble_l2cap_evt_ch_tx_t;
/**@brief L2CAP event structure. */
typedef struct
{
uint16_t conn_handle; /**< Connection Handle on which the event occured. */
uint16_t local_cid; /**< Local Channel ID of the L2CAP channel, or
@ref BLE_L2CAP_CID_INVALID if not present. */
union
{
ble_l2cap_evt_ch_setup_request_t ch_setup_request; /**< L2CAP Channel Setup Request Event Parameters. */
ble_l2cap_evt_ch_setup_refused_t ch_setup_refused; /**< L2CAP Channel Setup Refused Event Parameters. */
ble_l2cap_evt_ch_setup_t ch_setup; /**< L2CAP Channel Setup Completed Event Parameters. */
ble_l2cap_evt_ch_sdu_buf_released_t ch_sdu_buf_released;/**< L2CAP Channel SDU Data Buffer Released Event Parameters. */
ble_l2cap_evt_ch_credit_t credit; /**< L2CAP Channel Credit Received Event Parameters. */
ble_l2cap_evt_ch_rx_t rx; /**< L2CAP Channel SDU Received Event Parameters. */
ble_l2cap_evt_ch_tx_t tx; /**< L2CAP Channel SDU Transmitted Event Parameters. */
} params; /**< Event Parameters. */
} ble_l2cap_evt_t;
/** @} */
/**@addtogroup BLE_L2CAP_FUNCTIONS Functions
* @{ */
/**@brief Set up an L2CAP channel.
*
* @details This function is used to:
* - Request setup of an L2CAP channel: sends an LE Credit Based Connection Request packet to a peer.
* - Reply to a setup request of an L2CAP channel (if called in response to a
* @ref BLE_L2CAP_EVT_CH_SETUP_REQUEST event): sends an LE Credit Based Connection
* Response packet to a peer.
*
* @note A call to this function will require the application to keep the SDU data buffer alive
* until the SDU data buffer is returned in @ref BLE_L2CAP_EVT_CH_RX or
* @ref BLE_L2CAP_EVT_CH_SDU_BUF_RELEASED event.
*
* @events
* @event{@ref BLE_L2CAP_EVT_CH_SETUP, Setup successful.}
* @event{@ref BLE_L2CAP_EVT_CH_SETUP_REFUSED, Setup failed.}
* @endevents
*
* @mscs
* @mmsc{@ref BLE_L2CAP_CH_SETUP_MSC}
* @endmscs
*
* @param[in] conn_handle Connection Handle.
* @param[in,out] p_local_cid Pointer to a uint16_t containing Local Channel ID of the L2CAP channel:
* - As input: @ref BLE_L2CAP_CID_INVALID when requesting setup of an L2CAP
* channel or local_cid provided in the @ref BLE_L2CAP_EVT_CH_SETUP_REQUEST
* event when replying to a setup request of an L2CAP channel.
* - As output: local_cid for this channel.
* @param[in] p_params L2CAP channel parameters.
*
* @retval ::NRF_SUCCESS Successfully queued request or response for transmission.
* @retval ::NRF_ERROR_BUSY The stack is busy, process pending events and retry.
* @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied.
* @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle.
* @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied.
* @retval ::NRF_ERROR_INVALID_LENGTH Supplied higher rx_mps than has been configured on this link.
* @retval ::NRF_ERROR_INVALID_STATE Invalid State to perform operation (L2CAP channel already set up).
* @retval ::NRF_ERROR_NOT_FOUND CID not found.
* @retval ::NRF_ERROR_RESOURCES The limit has been reached for available L2CAP channels,
* see @ref ble_l2cap_conn_cfg_t::ch_count.
*/
SVCALL(SD_BLE_L2CAP_CH_SETUP, uint32_t, sd_ble_l2cap_ch_setup(uint16_t conn_handle, uint16_t *p_local_cid, ble_l2cap_ch_setup_params_t const *p_params));
/**@brief Release an L2CAP channel.
*
* @details This sends a Disconnection Request packet to a peer.
*
* @events
* @event{@ref BLE_L2CAP_EVT_CH_RELEASED, Release complete.}
* @endevents
*
* @mscs
* @mmsc{@ref BLE_L2CAP_CH_RELEASE_MSC}
* @endmscs
*
* @param[in] conn_handle Connection Handle.
* @param[in] local_cid Local Channel ID of the L2CAP channel.
*
* @retval ::NRF_SUCCESS Successfully queued request for transmission.
* @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle.
* @retval ::NRF_ERROR_INVALID_STATE Invalid State to perform operation (Setup or release is
* in progress for the L2CAP channel).
* @retval ::NRF_ERROR_NOT_FOUND CID not found.
*/
SVCALL(SD_BLE_L2CAP_CH_RELEASE, uint32_t, sd_ble_l2cap_ch_release(uint16_t conn_handle, uint16_t local_cid));
/**@brief Receive an SDU on an L2CAP channel.
*
* @details This may issue additional credits to the peer using an LE Flow Control Credit packet.
*
* @note A call to this function will require the application to keep the memory pointed by
* @ref ble_data_t::p_data alive until the SDU data buffer is returned in @ref BLE_L2CAP_EVT_CH_RX
* or @ref BLE_L2CAP_EVT_CH_SDU_BUF_RELEASED event.
*
* @note The SoftDevice can queue up to @ref ble_l2cap_conn_cfg_t::rx_queue_size SDU data buffers
* for reception per L2CAP channel.
*
* @events
* @event{@ref BLE_L2CAP_EVT_CH_RX, The SDU is received.}
* @endevents
*
* @mscs
* @mmsc{@ref BLE_L2CAP_CH_RX_MSC}
* @endmscs
*
* @param[in] conn_handle Connection Handle.
* @param[in] local_cid Local Channel ID of the L2CAP channel.
* @param[in] p_sdu_buf Pointer to the SDU data buffer.
*
* @retval ::NRF_SUCCESS Buffer accepted.
* @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied.
* @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle.
* @retval ::NRF_ERROR_INVALID_STATE Invalid State to perform operation (Setup or release is
* in progress for an L2CAP channel).
* @retval ::NRF_ERROR_NOT_FOUND CID not found.
* @retval ::NRF_ERROR_RESOURCES Too many SDU data buffers supplied. Wait for a
* @ref BLE_L2CAP_EVT_CH_RX event and retry.
*/
SVCALL(SD_BLE_L2CAP_CH_RX, uint32_t, sd_ble_l2cap_ch_rx(uint16_t conn_handle, uint16_t local_cid, ble_data_t const *p_sdu_buf));
/**@brief Transmit an SDU on an L2CAP channel.
*
* @note A call to this function will require the application to keep the memory pointed by
* @ref ble_data_t::p_data alive until the SDU data buffer is returned in @ref BLE_L2CAP_EVT_CH_TX
* or @ref BLE_L2CAP_EVT_CH_SDU_BUF_RELEASED event.
*
* @note The SoftDevice can queue up to @ref ble_l2cap_conn_cfg_t::tx_queue_size SDUs for
* transmission per L2CAP channel.
*
* @note The application can keep track of the available credits for transmission by following
* the procedure below:
* - Store initial credits given by the peer in a variable.
* (Initial credits are provided in a @ref BLE_L2CAP_EVT_CH_SETUP event.)
* - Decrement the variable, which stores the currently available credits, by
* ceiling((@ref ble_data_t::len + 2) / tx_mps) when a call to this function returns
* @ref NRF_SUCCESS. (tx_mps is provided in a @ref BLE_L2CAP_EVT_CH_SETUP event.)
* - Increment the variable, which stores the currently available credits, by additional
* credits given by the peer in a @ref BLE_L2CAP_EVT_CH_CREDIT event.
*
* @events
* @event{@ref BLE_L2CAP_EVT_CH_TX, The SDU is transmitted.}
* @endevents
*
* @mscs
* @mmsc{@ref BLE_L2CAP_CH_TX_MSC}
* @endmscs
*
* @param[in] conn_handle Connection Handle.
* @param[in] local_cid Local Channel ID of the L2CAP channel.
* @param[in] p_sdu_buf Pointer to the SDU data buffer.
*
* @retval ::NRF_SUCCESS Successfully queued L2CAP SDU for transmission.
* @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied.
* @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle.
* @retval ::NRF_ERROR_INVALID_STATE Invalid State to perform operation (Setup or release is
* in progress for the L2CAP channel).
* @retval ::NRF_ERROR_NOT_FOUND CID not found.
* @retval ::NRF_ERROR_DATA_SIZE Invalid SDU length supplied, must not be more than
* @ref ble_l2cap_ch_tx_params_t::tx_mtu provided in
* @ref BLE_L2CAP_EVT_CH_SETUP event.
* @retval ::NRF_ERROR_RESOURCES Too many SDUs queued for transmission. Wait for a
* @ref BLE_L2CAP_EVT_CH_TX event and retry.
*/
SVCALL(SD_BLE_L2CAP_CH_TX, uint32_t, sd_ble_l2cap_ch_tx(uint16_t conn_handle, uint16_t local_cid, ble_data_t const *p_sdu_buf));
/**@brief Advanced SDU reception flow control.
*
* @details Adjust the way the SoftDevice issues credits to the peer.
* This may issue additional credits to the peer using an LE Flow Control Credit packet.
*
* @mscs
* @mmsc{@ref BLE_L2CAP_CH_FLOW_CONTROL_MSC}
* @endmscs
*
* @param[in] conn_handle Connection Handle.
* @param[in] local_cid Local Channel ID of the L2CAP channel or @ref BLE_L2CAP_CID_INVALID to set
* the value that will be used for newly created channels.
* @param[in] credits Number of credits that the SoftDevice will make sure the peer has every
* time it starts using a new reception buffer.
* - @ref BLE_L2CAP_CREDITS_DEFAULT is the default value the SoftDevice will
* use if this function is not called.
* - If set to zero, the SoftDevice will stop issuing credits for new reception
* buffers the application provides or has provided. SDU reception that is
* currently ongoing will be allowed to complete.
* @param[out] p_credits NULL or pointer to a uint16_t. If a valid pointer is provided, it will be
* written by the SoftDevice with the number of credits that is or will be
* available to the peer. If the value written by the SoftDevice is 0 when
* credits parameter was set to 0, the peer will not be able to send more
* data until more credits are provided by calling this function again with
* credits > 0. This parameter is ignored when local_cid is set to
* @ref BLE_L2CAP_CID_INVALID.
*
* @note Application should take care when setting number of credits higher than default value. In
* this case the application must make sure that the SoftDevice always has reception buffers
* available (see @ref sd_ble_l2cap_ch_rx) for that channel. If the SoftDevice does not have
* such buffers available, packets may be NACKed on the Link Layer and all Bluetooth traffic
* on the connection handle may be stalled until the SoftDevice again has an available
* reception buffer. This applies even if the application has used this call to set the
* credits back to default, or zero.
*
* @retval ::NRF_SUCCESS Flow control parameters accepted.
* @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied.
* @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle.
* @retval ::NRF_ERROR_INVALID_STATE Invalid State to perform operation (Setup or release is
* in progress for an L2CAP channel).
* @retval ::NRF_ERROR_NOT_FOUND CID not found.
*/
SVCALL(SD_BLE_L2CAP_CH_FLOW_CONTROL, uint32_t, sd_ble_l2cap_ch_flow_control(uint16_t conn_handle, uint16_t local_cid, uint16_t credits, uint16_t *p_credits));
/** @} */
#ifdef __cplusplus
}
#endif
#endif // BLE_L2CAP_H__
/**
@}
*/

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2012 - 2020, Nordic Semiconductor ASA
* Copyright (c) 2012 - 2019, Nordic Semiconductor ASA
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
@ -89,9 +89,8 @@ enum BLE_COMMON_SVCS
*/
enum BLE_COMMON_EVTS
{
BLE_EVT_USER_MEM_REQUEST = BLE_EVT_BASE + 0, /**< User Memory request. See @ref ble_evt_user_mem_request_t
\n Reply with @ref sd_ble_user_mem_reply. */
BLE_EVT_USER_MEM_RELEASE = BLE_EVT_BASE + 1, /**< User Memory release. See @ref ble_evt_user_mem_release_t */
BLE_EVT_USER_MEM_REQUEST = BLE_EVT_BASE + 0, /**< User Memory request. @ref ble_evt_user_mem_request_t */
BLE_EVT_USER_MEM_RELEASE = BLE_EVT_BASE + 1, /**< User Memory release. @ref ble_evt_user_mem_release_t */
};
/**@brief BLE Connection Configuration IDs.
@ -329,6 +328,7 @@ typedef union
* place of @ref ble_conn_cfg_t::conn_cfg_tag.
*
* @sa sd_ble_gap_adv_start()
* @sa sd_ble_gap_connect()
*
* @mscs
* @mmsc{@ref BLE_CONN_CFG}
@ -338,7 +338,7 @@ typedef union
typedef struct
{
uint8_t conn_cfg_tag; /**< The application chosen tag it can use with the
@ref sd_ble_gap_adv_start() call
@ref sd_ble_gap_adv_start() and @ref sd_ble_gap_connect() calls
to select this configuration when creating a connection.
Must be different for all connection configurations added and not @ref BLE_CONN_CFG_TAG_DEFAULT. */
union {
@ -388,19 +388,6 @@ typedef union
* application RAM region (APP_RAM_BASE). On return, this will
* contain the minimum start address of the application RAM region
* required by the SoftDevice for this configuration.
* @warning After this call, the SoftDevice may generate several events. The list of events provided
* below require the application to initiate a SoftDevice API call. The corresponding API call
* is referenced in the event documentation.
* If the application fails to do so, the BLE connection may timeout, or the SoftDevice may stop
* communicating with the peer device.
* - @ref BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST
* - @ref BLE_GAP_EVT_PHY_UPDATE_REQUEST
* - @ref BLE_GAP_EVT_SEC_PARAMS_REQUEST
* - @ref BLE_GAP_EVT_SEC_INFO_REQUEST
* - @ref BLE_GAP_EVT_AUTH_KEY_REQUEST
* - @ref BLE_GATTS_EVT_EXCHANGE_MTU_REQUEST
* - @ref BLE_EVT_USER_MEM_REQUEST
* - @ref BLE_L2CAP_EVT_CH_SETUP_REQUEST
*
* @note The memory requirement for a specific configuration will not increase between SoftDevices
* with the same major version number.

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) Nordic Semiconductor ASA
* Copyright (c) 2011 - 2019, Nordic Semiconductor ASA
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
@ -128,9 +128,7 @@ enum BLE_GAP_EVTS
BLE_GAP_EVT_TIMEOUT = BLE_GAP_EVT_BASE + 11, /**< Timeout expired. \n See @ref ble_gap_evt_timeout_t. */
BLE_GAP_EVT_RSSI_CHANGED = BLE_GAP_EVT_BASE + 12, /**< RSSI report. \n See @ref ble_gap_evt_rssi_changed_t. */
BLE_GAP_EVT_ADV_REPORT = BLE_GAP_EVT_BASE + 13, /**< Advertising report. \n See @ref ble_gap_evt_adv_report_t. */
BLE_GAP_EVT_SEC_REQUEST = BLE_GAP_EVT_BASE + 14, /**< Security Request. \n Reply with @ref sd_ble_gap_authenticate
\n or with @ref sd_ble_gap_encrypt if required security information is available
. \n See @ref ble_gap_evt_sec_request_t. */
BLE_GAP_EVT_SEC_REQUEST = BLE_GAP_EVT_BASE + 14, /**< Security Request. \n See @ref ble_gap_evt_sec_request_t. */
BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST = BLE_GAP_EVT_BASE + 15, /**< Connection Parameter Update Request. \n Reply with @ref sd_ble_gap_conn_param_update. \n See @ref ble_gap_evt_conn_param_update_request_t. */
BLE_GAP_EVT_SCAN_REQ_REPORT = BLE_GAP_EVT_BASE + 16, /**< Scan request report. \n See @ref ble_gap_evt_scan_req_report_t. */
BLE_GAP_EVT_PHY_UPDATE_REQUEST = BLE_GAP_EVT_BASE + 17, /**< PHY Update Request. \n Reply with @ref sd_ble_gap_phy_update. \n See @ref ble_gap_evt_phy_update_request_t. */
@ -677,20 +675,6 @@ enum BLE_GAP_TX_POWER_ROLES
#define BLE_GAP_CAR_INCL_CONFIG_DEFAULT (BLE_GAP_CHAR_INCL_CONFIG_INCLUDE) /**< Included by default. */
/**@} */
/** @defgroup BLE_GAP_SLAVE_LATENCY Slave latency configuration options
* @{ */
#define BLE_GAP_SLAVE_LATENCY_ENABLE (0) /**< Slave latency is enabled. When slave latency is enabled,
the slave will wake up every time it has data to send,
and/or every slave latency number of connection events. */
#define BLE_GAP_SLAVE_LATENCY_DISABLE (1) /**< Disable slave latency. The slave will wake up every connection event
regardless of the requested slave latency.
This option consumes the most power. */
#define BLE_GAP_SLAVE_LATENCY_WAIT_FOR_ACK (2) /**< The slave will wake up every connection event if it has not received
an ACK from the master for at least slave latency events. This
configuration may increase the power consumption in environments
with a lot of radio activity. */
/**@} */
/**@addtogroup BLE_GAP_STRUCTURES Structures
* @{ */
@ -1258,7 +1242,7 @@ typedef struct
typedef struct
{
int8_t rssi; /**< Received Signal Strength Indication in dBm.
@note ERRATA-153 and ERRATA-225 require the rssi sample to be compensated based on a temperature measurement. */
@note ERRATA-153 requires the rssi sample to be compensated based on a temperature measurement. */
uint8_t ch_index; /**< Data Channel Index on which the Signal Strength is measured (0-36). */
} ble_gap_evt_rssi_changed_t;
@ -1308,7 +1292,7 @@ typedef struct
last received packet did not contain the Tx Power field.
@note TX Power is only included in extended advertising packets. */
int8_t rssi; /**< Received Signal Strength Indication in dBm of the last packet received.
@note ERRATA-153 and ERRATA-225 require the rssi sample to be compensated based on a temperature measurement. */
@note ERRATA-153 requires the rssi sample to be compensated based on a temperature measurement. */
uint8_t ch_index; /**< Channel Index on which the last advertising packet is received (0-39). */
uint8_t set_id; /**< Set ID of the received advertising data. Set ID is not present
if set to @ref BLE_GAP_ADV_REPORT_SET_ID_NOT_AVAILABLE. */
@ -1347,7 +1331,7 @@ typedef struct
{
uint8_t adv_handle; /**< Advertising handle for the advertising set which received the Scan Request */
int8_t rssi; /**< Received Signal Strength Indication in dBm.
@note ERRATA-153 and ERRATA-225 require the rssi sample to be compensated based on a temperature measurement. */
@note ERRATA-153 requires the rssi sample to be compensated based on a temperature measurement. */
ble_gap_addr_t peer_addr; /**< Bluetooth address of the peer device. If the peer_addr resolved: @ref ble_gap_addr_t::addr_id_peer is set to 1
and the address is the device's identity address. */
} ble_gap_evt_scan_req_report_t;
@ -1594,7 +1578,7 @@ typedef struct
typedef struct
{
uint16_t conn_handle; /**< Connection Handle */
uint8_t disable; /**< For allowed values see @ref BLE_GAP_SLAVE_LATENCY */
uint8_t disable : 1; /**< Set to 1 to disable slave latency. Set to 0 enable it again.*/
} ble_gap_opt_slave_latency_disable_t;
/**@brief Passkey Option.
@ -1751,7 +1735,7 @@ SVCALL(SD_BLE_GAP_ADDR_GET, uint32_t, sd_ble_gap_addr_get(ble_gap_addr_t *p_addr
*
* @retval ::NRF_SUCCESS Address successfully retrieved.
* @retval ::NRF_ERROR_INVALID_ADDR Invalid or NULL pointer supplied.
* @retval ::BLE_ERROR_INVALID_ADV_HANDLE The provided advertising handle was not found.
* @retval ::BLE_ERROR_INVALID_ADV_HANDLE The provided advertising handle was not found.
* @retval ::NRF_ERROR_INVALID_STATE The advertising set is currently not advertising.
*/
SVCALL(SD_BLE_GAP_ADV_ADDR_GET, uint32_t, sd_ble_gap_adv_addr_get(uint8_t adv_handle, ble_gap_addr_t *p_addr));
@ -1907,9 +1891,6 @@ SVCALL(SD_BLE_GAP_ADV_SET_CONFIGURE, uint32_t, sd_ble_gap_adv_set_configure(uint
*
* @note Only one advertiser may be active at any time.
*
* @note If privacy is enabled, the advertiser's private address will be refreshed when this function is called.
* See @ref sd_ble_gap_privacy_set().
*
* @events
* @event{@ref BLE_GAP_EVT_CONNECTED, Generated after connection has been established through connectable advertising.}
* @event{@ref BLE_GAP_EVT_ADV_SET_TERMINATED, Advertising set has terminated.}
@ -2042,9 +2023,7 @@ SVCALL(SD_BLE_GAP_DISCONNECT, uint32_t, sd_ble_gap_disconnect(uint16_t conn_hand
* - For all other roles handle is ignored.
* @param[in] tx_power Radio transmit power in dBm (see note for accepted values).
*
* @note Supported tx_power values: -40dBm, -20dBm, -16dBm, -12dBm, -8dBm, -4dBm, 0dBm, +3dBm and +4dBm.
* In addition, on some chips following values are supported: +2dBm, +5dBm, +6dBm, +7dBm and +8dBm.
* Setting these values on a chip that does not support them will result in undefined behaviour.
* @note Supported tx_power values: -40dBm, -20dBm, -16dBm, -12dBm, -8dBm, -4dBm, 0dBm, +2dBm, +3dBm, +4dBm, +5dBm, +6dBm, +7dBm and +8dBm.
* @note The initiator will have the same transmit power as the scanner.
* @note When a connection is created it will inherit the transmit power from the initiator or
* advertiser leading to the connection.
@ -2449,7 +2428,7 @@ SVCALL(SD_BLE_GAP_ENCRYPT, uint32_t, sd_ble_gap_encrypt(uint16_t conn_handle, bl
* @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied.
* @retval ::NRF_ERROR_INVALID_STATE Invalid state to perform operation. Either:
* - No link has been established.
* - No @ref BLE_GAP_EVT_SEC_INFO_REQUEST pending.
* - No @ref BLE_GAP_EVT_SEC_REQUEST pending.
* - Encryption information provided by the app without being requested. See @ref ble_gap_evt_sec_info_request_t::enc_info.
* @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid connection handle supplied.
*/
@ -2517,7 +2496,7 @@ SVCALL(SD_BLE_GAP_RSSI_STOP, uint32_t, sd_ble_gap_rssi_stop(uint16_t conn_handle
*
* @ref sd_ble_gap_rssi_start must be called to start reporting RSSI before using this function. @ref NRF_ERROR_NOT_FOUND
* will be returned until RSSI was sampled for the first time after calling @ref sd_ble_gap_rssi_start.
* @note ERRATA-153 and ERRATA-225 require the rssi sample to be compensated based on a temperature measurement.
* @note ERRATA-153 requires the rssi sample to be compensated based on a temperature measurement.
* @mscs
* @mmsc{@ref BLE_GAP_CENTRAL_RSSI_READ_MSC}
* @endmscs

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) Nordic Semiconductor ASA
* Copyright (c) 2011 - 2018, Nordic Semiconductor ASA
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
@ -95,7 +95,6 @@ enum BLE_L2CAP_SVCS
enum BLE_L2CAP_EVTS
{
BLE_L2CAP_EVT_CH_SETUP_REQUEST = BLE_L2CAP_EVT_BASE + 0, /**< L2CAP Channel Setup Request event.
\n Reply with @ref sd_ble_l2cap_ch_setup.
\n See @ref ble_l2cap_evt_ch_setup_request_t. */
BLE_L2CAP_EVT_CH_SETUP_REFUSED = BLE_L2CAP_EVT_BASE + 1, /**< L2CAP Channel Setup Refused event.
\n See @ref ble_l2cap_evt_ch_setup_refused_t. */

Some files were not shown because too many files have changed in this diff Show more