Compare commits

..

68 commits

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

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

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

* gap: expose ServiceData() in AdvertisementFields

* macos: include ServiceData in AdvertisementFields

* gap/linux: include ServiceData in AdvertisementFields

* gap: add unimplemented ServiceData() to raw advertisement

* added ServiceData advertising element also to the sending pieces

* more explicitly use the ad element type ids

* added a test case for ServiceData

* linux: added ServiceData advertising element

* sd: fix: handle no servicedata present

* linux: bluez uses string uuids for service data

* linux: fix: correct datatype for advertise with ServiceData

* uuid: add 32-Bit functions

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

* Revert unnessesary changes

* formatting

* remove extra check

---------

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

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

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

Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-23 21:38:07 -05:00
deadprogram
0a9bffe397 ninafw: should support muliple connections as a central
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-23 21:38:07 -05:00
deadprogram
00a475adf1 hci: add check for poll buffer overflow
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-23 21:38:07 -05:00
deadprogram
4c90cf4ab6 sd: replace unsafe.SliceData call with expression that is still supported in older Go versions
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-22 18:24:47 +01:00
deadprogram
bf647ecd57 ninafw: this PR contains several fixes and improvements for the NINAFW implementation including:
- 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

Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-17 10:21:32 +01:00
deadprogram
564b0ba58f all: use 'debug' variable protected by build tags for debug logging
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-16 17:25:37 +01:00
deadprogram
dc7d1b4d4c build: add nina-fw smoketest as peripheral
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-15 22:57:46 -05:00
deadprogram
5c62ee4645 docs: complete README info about nina-fw support
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-15 22:57:46 -05:00
deadprogram
10d1c71078 ninafw: implement BLE peripheral support
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-15 22:57:46 -05:00
Ayke van Laethem
b8c79250c7 softdevice: add support for connection timeout on connect
This adds support for the "connection supervision timeout", basically
the connection timeout while the connection is active (as opposed to
while connecting).
2024-01-12 14:56:32 +01:00
Ayke van Laethem
ecf09759ac softdevice: fix connect timeout
This fixes the connection timeout. Previously it would try to connect
for a time 16 times as much as specified by the user.
2024-01-12 14:56:32 +01:00
Ayke van Laethem
3f8f8a6622 softdevice: return an error on a connection timeout
This makes sure an error is reported on a connection timeout. Previously
it would just block forever.
2024-01-12 14:56:32 +01:00
Ayke van Laethem
d74f6a1009 all: add RequestConnectionParams to request new connection parameters
This allows changing the connection latency, slave latency, and
connection timeout of an active connection - whether in the central or
peripheral role. This is especially helpful on battery operated BLE
devices that don't have a lot of power and need to lower the connection
latency for improved speed. It might also be useful for devices that
need high speed, as the defaults might be too low.
2024-01-11 15:53:20 +01:00
Ayke van Laethem
5d805a929c all: use Device instead of Address in SetConnectHandler
This makes it possible to discover services on a connected central while
in peripheral mode, for example.
2024-01-11 15:53:20 +01:00
Ayke van Laethem
c9eafaff20 all: make Device a value instead of a pointer
This is a refactor that is necessary to make it easier to work with
connected central devices on a SoftDevice.
2024-01-11 15:53:20 +01:00
Ayke van Laethem
6e0df0ec3c softdevice: add address of connecting device
I thought it wasn't available, but in fact it is. So let's make it
available in the connect handler.
2024-01-11 15:53:20 +01:00
Ayke van Laethem
735333aa1a softdevice: print connection parameters when debug is enabled
This is very useful for debugging, though we should probably expose this
in some way to users of the bluetooth package without changing a
constant.
2024-01-11 15:53:20 +01:00
deadprogram
56e56f3647 build: add arduino-nano33 and pyportal to smoke tests
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-07 08:59:59 +01:00
deadprogram
f639d80012 ninafw: add support for software RTS/CTS flow control for boards where hardware support is not available
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-07 08:59:59 +01:00
Ayke van Laethem
cd3c0c4835 linux: fix characteristic value
I left in a debugging value. Oops. Let's fix that quickly.
2024-01-06 15:49:26 +01:00
deadprogram
c5ab6a9b65 ninafw: use NINA settings from board file in main TinyGo repo
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-05 20:09:54 +01:00
Ayke van Laethem
eb30760e41 ninafw: fix connection timeout
The break statement didn't actually break the for loop, it just exited
the switch case.

Discovered because VS Code flagged the code after the loop as dead code.
2024-01-05 18:13:33 +01:00
Fabianexe
190c4be423
darwin: add Write command to the gattc implementation 2024-01-05 14:03:30 +01:00
Ayke van Laethem
5746ccfb60 all: don't use a pointer receiver for many method calls
This is unnecessary because the values are passed by value in other
cases, and can in some cases lead to more (heap or stack) allocation
than is necessary.
2024-01-04 20:43:16 +01:00
Anton Onipko
83fba1b809
Release AsyncOperationCompletedHandler (#208)
windows: release AsyncOperationCompletedHandler
2024-01-04 20:28:49 +01:00
deadprogram
b8a4a54d5f build: add some ninafw examples to smoketest
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-04 14:35:20 +01:00
deadprogram
30138095e1 ninafw: implement GetMTU()
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-04 14:35:20 +01:00
deadprogram
044320ea69 ninafw: remove some pointer receivers from method calls
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-04 14:35:20 +01:00
deadprogram
930a5c7a88 docs: a small mention of the NINA BLE support
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-04 14:35:20 +01:00
deadprogram
92c12af54f ninafw: BLE central implementation on nina-fw co-processors
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-04 14:35:20 +01:00
Ayke van Laethem
1860e505b9 examples/discover: add MTU
This is useful for testing.
2024-01-03 22:08:13 +01:00
Ayke van Laethem
d77521461d linux: rewrite everything to use DBus directly
This is a big rewrite to use DBus calls directly instead of going
through go-bluetooth first.

This is a big change, but I believe it is an improvement. While the
go-bluetooth works for many cases, it's a layer in between that I
believe hurts more than it helps. Without it, we can just program
directly against the BlueZ D-Bus API. The end result is about 10% more
code.

With this rewrite, I fixed the following issues:

  * All MapToStruct warnings are gone, like in
    https://github.com/tinygo-org/bluetooth/issues/193.
  * Advertisements can be restarted after they were stopped. Previously
    this resulted in a panic.
  * Looking at the source code of go-bluetooth, it appears that it
    includes devices from a different Bluetooth adapter than the one
    that's currently scanning. This is fixed with the rewrite.
  * Fix a bug in Adapter.AddService where it would only allow adding a
    single service. Multiple services can now be added.
    This was actually the motivating bug that led me down to rewrite the
    whole thing because I couldn't figure out where the bug was in
    go-bluetooth (it's many layers deep).
  * The `WriteEvent` callback in a characteristic now also gets the
    'offset' parameter which wasn't provided by go-bluetooth.

This rewrite also avoids go-bluetooth specific workarounds like
https://github.com/tinygo-org/bluetooth/pull/74 and
https://github.com/tinygo-org/bluetooth/pull/121.

I have tested all examples in the smoketest-linux Makefile target. They
all still work with this rewrite.
2024-01-03 20:40:48 +01:00
Ayke van Laethem
b278e2b932 go fmt 2024-01-03 20:33:10 +01:00
deadprogram
8f92747a18 examples: update MCU central examples to use ldflags to pass the desired device to connect to
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-03 16:12:16 +01:00
Ayke van Laethem
ec80e0111e softdevice: don't send a notify/indicate without a CCCD
sd_ble_gatts_hvx_noescape can only be called when the notify/indicate
permission is set (and therefore a CCCD has been added). Without it, it
will just return an error.

This fixes a problem I found on the PineTime, while implementing the
battery service.
2023-12-25 12:48:31 +01:00
Ayke van Laethem
b5d4e3f82a softdevice: fix writing to a characteristic
This seems to have been broken since
https://github.com/tinygo-org/bluetooth/pull/192, I suspect it's a
problem with the struct calling convention (which most certainly is a
TinyGo CGo bug, but at least this works around the problem).
2023-12-25 12:41:39 +01:00
Ayke van Laethem
01243181c3 sd: update to prepare for changes in the TinyGo CGo implementation
For details, see: https://github.com/tinygo-org/tinygo/pull/3927

I ran the smoke tests and the binaries are exactly identical to what
they were before, so this change cannot have had an effect on these
smoke tests (which is expected, as this is mostly just changing some
types without changing the machine data type).
2023-10-05 19:11:46 +02:00
deadprogram
d0c7887b81 Update for release 0.8.0
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-09-21 13:45:40 +02:00
deadprogram
195d418876 build: remove CGo dependencies for Windows cross-compiler tests
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-09-21 12:54:07 +02:00
deadprogram
0cc860c018 docs: update README to remove CGo requirement for Windows
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-09-21 12:54:07 +02:00
deadprogram
1f58ec1fb4 windows: update github.com/saltosystems/winrt-go to no longer require CGo
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-09-21 12:54:07 +02:00
Filip Vranesevic
d34d15d830 Noescape workaround 2023-09-01 20:01:24 +02:00
deadprogram
20ccbeb113 build: add Windows to GH actions build jobs
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-08-29 22:57:42 +02:00
deadprogram
0403d51c8a build: add macOS 12 to GH actions build jobs
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-08-29 21:41:43 +02:00
77 changed files with 6026 additions and 1010 deletions

View file

@ -11,25 +11,20 @@ on:
jobs:
build:
runs-on: ubuntu-latest
container: ghcr.io/tinygo-org/tinygo-dev: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.5.3
uses: actions/checkout@v3
- name: TinyGo version check
run: tinygo version
- name: Download Go modules
run: go mod download
- 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: Install Windows cross compiler
run: |
apt-get install -y gcc-mingw-w64-x86-64
- name: "Run Windows smoke tests"
- name: "Run Windows cross-compiled smoke tests"
run: make smoketest-windows

View file

@ -12,35 +12,46 @@ concurrency:
cancel-in-progress: true
jobs:
build-go1-18:
name: build-go1-18
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'
go-version: '1.18.3'
- name: Checkout
uses: actions/checkout@v3.5.3
- name: Download Go modules
run: go mod download
uses: actions/checkout@v3.6.0
- name: Run unit tests
run: go test
- name: "Run macOS smoke tests"
run: make smoketest-macos
build-go1-21:
name: build-go1-21
runs-on: macos-11
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'
go-version: '1.21.0'
- name: Checkout
uses: actions/checkout@v3.5.3
- name: Download Go modules
run: go mod download
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"

24
.github/workflows/windows.yml vendored Normal file
View file

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

View file

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

View file

@ -11,6 +11,8 @@ 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
@ -32,10 +34,25 @@ smoketest-tinygo:
@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
@ -44,9 +61,11 @@ smoketest-linux:
smoketest-windows:
# Test on Windows.
GOOS=windows CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc go build -o /tmp/go-build-discard ./examples/scanner
GOOS=windows CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc go build -o /tmp/go-build-discard ./examples/discover
GOOS=windows CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc go build -o /tmp/go-build-discard ./examples/heartrate-monitor
GOOS=windows go build -o /tmp/go-build-discard ./examples/scanner
GOOS=windows go build -o /tmp/go-build-discard ./examples/discover
GOOS=windows go build -o /tmp/go-build-discard ./examples/heartrate-monitor
GOOS=windows go build -o /tmp/go-build-discard ./examples/advertisement
GOOS=windows go build -o /tmp/go-build-discard ./examples/heartrate
smoketest-macos:
# Test on macos.

View file

@ -2,13 +2,13 @@
[![Go Bluetooth](./images/gobluetooth.png)](https://tinygo.org/bluetooth)
[![PkgGoDev](https://pkg.go.dev/badge/pkg.go.dev/tinygo.org/x/bluetooth)](https://pkg.go.dev/tinygo.org/x/bluetooth) [![Linux](https://github.com/tinygo-org/bluetooth/actions/workflows/linux.yml/badge.svg?branch=dev)](https://github.com/tinygo-org/bluetooth/actions/workflows/linux.yml) [![macOS](https://github.com/tinygo-org/bluetooth/actions/workflows/macos.yml/badge.svg?branch=dev)](https://github.com/tinygo-org/bluetooth/actions/workflows/macos.yml)
[![PkgGoDev](https://pkg.go.dev/badge/pkg.go.dev/gitrepo.ru/neonxp/bluetooth)](https://pkg.go.dev/gitrepo.ru/neonxp/bluetooth) [![Linux](https://github.com/tinygo-org/bluetooth/actions/workflows/linux.yml/badge.svg?branch=dev)](https://github.com/tinygo-org/bluetooth/actions/workflows/linux.yml) [![macOS](https://github.com/tinygo-org/bluetooth/actions/workflows/macos.yml/badge.svg?branch=dev)](https://github.com/tinygo-org/bluetooth/actions/workflows/macos.yml)
Go Bluetooth is a cross-platform package for using [Bluetooth Low Energy](https://en.wikipedia.org/wiki/Bluetooth_Low_Energy) hardware from the Go programming language.
It works on typical operating systems such as [Linux](#linux), [macOS](#macos), and [Windows](#windows).
It can also be used running "bare metal" on microcontrollers produced by [Nordic Semiconductor](https://www.nordicsemi.com/) by using [TinyGo](https://tinygo.org/).
It can also be used running "bare metal" on microcontrollers produced by [Nordic Semiconductor](https://www.nordicsemi.com/) or using the Bluetooth Host Controller Interface (HCI) by using [TinyGo](https://tinygo.org/).
The Go Bluetooth package can be used to create both Bluetooth Low Energy Centrals as well as to create Bluetooth Low Energy Peripherals.
@ -22,7 +22,7 @@ This example shows a central that scans for peripheral devices and then displays
package main
import (
"tinygo.org/x/bluetooth"
"gitrepo.ru/neonxp/bluetooth"
)
var adapter = bluetooth.DefaultAdapter
@ -58,7 +58,7 @@ package main
import (
"time"
"tinygo.org/x/bluetooth"
"gitrepo.ru/neonxp/bluetooth"
)
var adapter = bluetooth.DefaultAdapter
@ -92,21 +92,21 @@ func must(action string, err error) {
## Current support
| | 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: | :heavy_check_mark: | :heavy_check_mark: |
| Write peripheral characteristics | :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: |
| 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 | 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
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.
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.
Linux can be used both as a BLE Central or as a BLE Peripheral.
@ -165,11 +165,9 @@ The Windows support only can only act as a BLE Central at this time, with some a
### Installation
In order to compile Go Bluetooth code targeting Windows, you must have a GCC compiler installed.
Only the Go compiler itself is needed to compile Go Bluetooth code targeting Windows.
On Windows, you can download and install mingw-w64 (https://github.com/mingw-w64/mingw-w64)
Once you have done this, you can obtain the Go Bluetooth package using Git:
You can obtain the Go Bluetooth package using Git:
git clone https://github.com/tinygo-org/bluetooth.git
@ -266,6 +264,45 @@ 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,11 +1,8 @@
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 Address, connected bool)) {
func (a *Adapter) SetConnectHandler(c func(device Device, connected bool)) {
a.connectHandler = c
}

116
adapter_cyw43439.go Normal file
View file

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

View file

@ -24,7 +24,7 @@ type Adapter struct {
// used to allow multiple callers to call Connect concurrently.
connectMap sync.Map
connectHandler func(device Address, connected bool)
connectHandler func(device Device, connected bool)
}
// DefaultAdapter is the default adapter on the system.
@ -35,7 +35,7 @@ var DefaultAdapter = &Adapter{
pm: cbgo.NewPeripheralManager(nil),
connectMap: sync.Map{},
connectHandler: func(device Address, connected bool) {
connectHandler: func(device Device, connected bool) {
return
},
}
@ -106,7 +106,7 @@ func (cmd *centralManagerDelegate) DidDisconnectPeripheral(cmgr cbgo.CentralMana
addr := Address{}
uuid, _ := ParseUUID(id)
addr.UUID = uuid
cmd.a.connectHandler(addr, false)
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
@ -141,11 +141,32 @@ func makeScanResult(prph cbgo.Peripheral, advFields cbgo.AdvFields, rssi int) Sc
serviceUUIDs = append(serviceUUIDs, parsedUUID)
}
manufacturerData := make(map[uint16][]byte)
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[manufacturerID] = advFields.ManufacturerData[2:]
manufacturerData = append(manufacturerData, ManufacturerDataElement{
CompanyID: manufacturerID,
Data: advFields.ManufacturerData[2:],
})
}
var serviceData []ServiceDataElement
for _, svcData := range advFields.ServiceData {
cbgoUUID := svcData.UUID
uuid, err := ParseUUID(cbgoUUID.String())
if err != nil {
continue
}
serviceData = append(serviceData, ServiceDataElement{
UUID: uuid,
Data: svcData.Data,
})
}
// Peripheral UUID is randomized on macOS, which means to
@ -160,6 +181,7 @@ func makeScanResult(prph cbgo.Peripheral, advFields cbgo.AdvFields, rssi int) Sc
LocalName: advFields.LocalName,
ServiceUUIDs: serviceUUIDs,
ManufacturerData: manufacturerData,
ServiceData: serviceData,
},
},
}

205
adapter_hci.go Normal file
View file

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

122
adapter_hci_uart.go Normal file
View file

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

View file

@ -7,18 +7,23 @@ package bluetooth
import (
"errors"
"fmt"
"github.com/muka/go-bluetooth/api"
"github.com/muka/go-bluetooth/bluez/profile/adapter"
"github.com/godbus/dbus/v5"
)
const defaultAdapter = "hci0"
type Adapter struct {
adapter *adapter.Adapter1
id string
cancelChan chan struct{}
scanCancelChan chan struct{}
bus *dbus.Conn
bluez dbus.BusObject // object at /
adapter dbus.BusObject // object at /org/bluez/hciX
address string
defaultAdvertisement *Advertisement
connectHandler func(device Address, connected bool)
connectHandler func(device Device, connected bool)
}
// DefaultAdapter is the default adapter on the system. On Linux, it is the
@ -26,29 +31,38 @@ type Adapter struct {
//
// Make sure to call Enable() before using it to initialize the adapter.
var DefaultAdapter = &Adapter{
connectHandler: func(device Address, connected bool) {
return
id: defaultAdapter,
connectHandler: func(device Device, connected bool) {
},
}
// Enable configures the BLE stack. It must be called before any
// Bluetooth-related calls (unless otherwise indicated).
func (a *Adapter) Enable() (err error) {
if a.id == "" {
a.adapter, err = api.GetDefaultAdapter()
bus, err := dbus.SystemBus()
if err != nil {
return
return err
}
a.id, err = a.adapter.GetAdapterID()
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())
}
return fmt.Errorf("could not activate BlueZ adapter: %w", err)
}
addr.Store(&a.address)
return nil
}
func (a *Adapter) Address() (MACAddress, error) {
if a.adapter == nil {
if a.address == "" {
return MACAddress{}, errors.New("adapter not enabled")
}
mac, err := ParseMAC(a.adapter.Properties.Address)
mac, err := ParseMAC(a.address)
if err != nil {
return MACAddress{}, err
}

134
adapter_ninafw.go Normal file
View file

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

@ -42,8 +42,13 @@ func handleEvent() {
gapEvent := eventBuf.evt.unionfield_gap_evt()
switch id {
case C.BLE_GAP_EVT_CONNECTED:
currentConnection.Reg = gapEvent.conn_handle
DefaultAdapter.connectHandler(Address{}, true)
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 defaultAdvertisement.isAdvertising.Get() != 0 {
// The advertisement was running but was automatically stopped
@ -54,8 +59,11 @@ func handleEvent() {
// necessary.
defaultAdvertisement.start()
}
currentConnection.Reg = C.BLE_CONN_HANDLE_INVALID
DefaultAdapter.connectHandler(Address{}, false)
currentConnection.handle.Reg = C.BLE_CONN_HANDLE_INVALID
device := Device{
connectionHandle: gapEvent.conn_handle,
}
DefaultAdapter.connectHandler(device, false)
case C.BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST:
// Respond with the default PPCP connection parameters by passing
// nil:
@ -109,5 +117,13 @@ func (a *Adapter) Address() (MACAddress, error) {
if errCode != 0 {
return MACAddress{}, Error(errCode)
}
return MACAddress{MAC: addr.addr}, nil
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

@ -25,20 +25,24 @@ func handleEvent() {
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.Reg = gapEvent.conn_handle
DefaultAdapter.connectHandler(Address{}, true)
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(Address{}, true)
DefaultAdapter.connectHandler(device, true)
}
case C.BLE_GAP_EVT_DISCONNECTED:
if debug {
@ -46,11 +50,11 @@ func handleEvent() {
}
// Clean up state for this connection.
for i, cb := range gattcNotificationCallbacks {
if cb.connectionHandle == currentConnection.Reg {
if uint16(cb.connectionHandle) == currentConnection.handle.Reg {
gattcNotificationCallbacks[i].valueHandle = 0 // 0 means invalid
}
}
currentConnection.Reg = C.BLE_CONN_HANDLE_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
@ -61,10 +65,21 @@ func handleEvent() {
// necessary.
C.sd_ble_gap_adv_start(defaultAdvertisement.handle, C.BLE_CONN_CFG_TAG_DEFAULT)
}
DefaultAdapter.connectHandler(Address{}, false)
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] != advReport.data.p_data {
if debug && &scanReportBuffer.data[0] != (*byte)(unsafe.Pointer(advReport.data.p_data)) {
// Sanity check.
panic("scanReportBuffer != advReport.p_data")
}
@ -73,8 +88,7 @@ func handleEvent() {
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},
makeMACAddress(advReport.peer_addr),
}
globalScanResult.AdvertisementPayload = &scanReportBuffer
// Signal to the main thread that there was a scan report.
@ -101,6 +115,21 @@ func handleEvent() {
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)

View file

@ -27,13 +27,18 @@ func handleEvent() {
if debug {
println("evt: connected in peripheral role")
}
currentConnection.Reg = gapEvent.conn_handle
DefaultAdapter.connectHandler(Address{}, true)
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.Reg = C.BLE_CONN_HANDLE_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
@ -44,7 +49,10 @@ func handleEvent() {
// necessary.
C.sd_ble_gap_adv_start(defaultAdvertisement.handle, C.BLE_CONN_CFG_TAG_DEFAULT)
}
DefaultAdapter.connectHandler(Address{}, false)
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.

View file

@ -46,7 +46,7 @@ func (a *Adapter) enable() error {
}
// Enable the BLE stack.
appRAMBase := uint32(uintptr(unsafe.Pointer(&appRAMBase)))
appRAMBase := C.uint32_t(uintptr(unsafe.Pointer(&appRAMBase)))
errCode = C.sd_ble_enable(&appRAMBase)
return makeError(errCode)
}
@ -57,5 +57,13 @@ func (a *Adapter) Address() (MACAddress, error) {
if errCode != 0 {
return MACAddress{}, Error(errCode)
}
return MACAddress{MAC: addr.addr}, nil
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.bitfield_addr_type() != 0,
}
}

View file

@ -28,7 +28,7 @@ var (
)
// There can only be one connection at a time in the default configuration.
var currentConnection = volatile.Register16{C.BLE_CONN_HANDLE_INVALID}
var currentConnection = volatileHandle{handle: volatile.Register16{C.BLE_CONN_HANDLE_INVALID}}
// Globally allocated buffer for incoming SoftDevice events.
var eventBuf struct {
@ -48,7 +48,7 @@ type Adapter struct {
scanning bool
charWriteHandlers []charWriteHandler
connectHandler func(device Address, connected bool)
connectHandler func(device Device, connected bool)
}
// DefaultAdapter is the default adapter on the current system. On Nordic chips,
@ -56,11 +56,11 @@ type Adapter struct {
//
// Make sure to call Enable() before using it to initialize the adapter.
var DefaultAdapter = &Adapter{isDefault: true,
connectHandler: func(device Address, connected bool) {
connectHandler: func(device Device, connected bool) {
return
}}
var eventBufLen uint16
var eventBufLen C.uint16_t
// Enable configures the BLE stack. It must be called before any
// Bluetooth-related calls (unless otherwise indicated).
@ -72,8 +72,8 @@ func (a *Adapter) Enable() error {
// Enable the IRQ that handles all events.
intr := interrupt.New(nrf.IRQ_SWI2, func(interrupt.Interrupt) {
for {
eventBufLen = uint16(unsafe.Sizeof(eventBuf))
errCode := C.sd_ble_evt_get((*uint8)(unsafe.Pointer(&eventBuf)), &eventBufLen)
eventBufLen = C.uint16_t(unsafe.Sizeof(eventBuf))
errCode := C.sd_ble_evt_get((*C.uint8_t)(unsafe.Pointer(&eventBuf)), &eventBufLen)
if errCode != 0 {
// Possible error conditions:
// * NRF_ERROR_NOT_FOUND: no events left, break
@ -97,7 +97,7 @@ func (a *Adapter) Enable() error {
return err
}
errCode := C.sd_ble_gap_device_name_set(&secModeOpen, &defaultDeviceName[0], uint16(len(defaultDeviceName)))
errCode := C.sd_ble_gap_device_name_set(&secModeOpen, (*C.uint8_t)(unsafe.Pointer(&defaultDeviceName[0])), C.uint16_t(len(defaultDeviceName)))
if errCode != 0 {
return Error(errCode)
}
@ -116,7 +116,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 uint8
var is_nested_critical_region C.uint8_t
C.sd_nvic_critical_region_enter(&is_nested_critical_region)
return uintptr(is_nested_critical_region)
}
@ -125,5 +125,43 @@ func DisableInterrupts() uintptr {
// DisableInterrupts. The mask parameter must be the value returned by
// DisableInterrupts.
func RestoreInterrupts(mask uintptr) {
C.sd_nvic_critical_region_exit(uint8(mask))
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]),
}
}

View file

@ -1,6 +1,7 @@
package bluetooth
import (
"errors"
"fmt"
"github.com/go-ole/go-ole"
@ -12,14 +13,16 @@ import (
type Adapter struct {
watcher *advertisement.BluetoothLEAdvertisementWatcher
connectHandler func(device Address, connected bool)
connectHandler func(device Device, connected bool)
defaultAdvertisement *Advertisement
}
// 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 Address, connected bool) {
connectHandler: func(device Device, connected bool) {
return
},
}
@ -40,10 +43,14 @@ func awaitAsyncOperation(asyncOperation *foundation.IAsyncOperation, genericPara
// Wait until the async operation completes.
waitChan := make(chan struct{})
asyncOperation.SetCompleted(foundation.NewAsyncOperationCompletedHandler(ole.NewGUID(iid), func(instance *foundation.AsyncOperationCompletedHandler, asyncInfo *foundation.IAsyncOperation, asyncStatus foundation.AsyncStatus) {
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
@ -52,3 +59,8 @@ func awaitAsyncOperation(asyncOperation *foundation.IAsyncOperation, genericPara
}
return nil
}
func (a *Adapter) Address() (MACAddress, error) {
// TODO: get mac address
return MACAddress{}, errors.New("not implemented")
}

1203
att_hci.go Normal file

File diff suppressed because it is too large Load diff

View file

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

File diff suppressed because it is too large Load diff

5
debug.go Normal file
View file

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

View file

@ -2,6 +2,7 @@
package bluetooth
// #include <stdint.h>
// #include "nrf_error.h"
// #include "nrf_error_sdm.h"
import "C"
@ -83,7 +84,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 uint32) error {
func makeError(code C.uint32_t) error {
if code != 0 {
return Error(code)
}

View file

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

View file

@ -8,7 +8,7 @@ import (
"machine"
"time"
"tinygo.org/x/bluetooth"
"gitrepo.ru/neonxp/bluetooth"
"tinygo.org/x/drivers/ws2812"
)
@ -38,7 +38,7 @@ func main() {
neo.Configure(machine.PinConfig{Mode: machine.PinOutput})
ws = ws2812.New(neo)
adapter.SetConnectHandler(func(d bluetooth.Address, c bool) {
adapter.SetConnectHandler(func(d bluetooth.Device, c bool) {
connected = c
if !connected && !disconnected {

View file

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

View file

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

@ -13,13 +13,12 @@
//
// Once the program is flashed to the board, connect to the USB port
// via serial to view the output.
//
package main
import (
"strconv"
"tinygo.org/x/bluetooth"
"gitrepo.ru/neonxp/bluetooth"
)
var adapter = bluetooth.DefaultAdapter
@ -44,7 +43,7 @@ func main() {
}
})
var device *bluetooth.Device
var device bluetooth.Device
select {
case result := <-ch:
device, err = adapter.Connect(result.Address, bluetooth.ConnectionParams{})
@ -73,6 +72,12 @@ 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

@ -6,11 +6,15 @@ import (
"time"
)
// replace this with the MAC address of the Bluetooth peripheral you want to connect to.
const deviceAddress = "E4:B7:F4:11:8D:33"
// 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
func connectAddress() string {
return deviceAddress
return DeviceAddress
}
// wait on baremetal, proceed immediately on desktop OS.

View file

@ -24,7 +24,7 @@
package main
import (
"tinygo.org/x/bluetooth"
"gitrepo.ru/neonxp/bluetooth"
)
var (
@ -52,7 +52,7 @@ func main() {
}
})
var device *bluetooth.Device
var device bluetooth.Device
select {
case result := <-ch:
device, err = adapter.Connect(result.Address, bluetooth.ConnectionParams{})

View file

@ -6,11 +6,15 @@ import (
"time"
)
// replace this with the MAC address of the Bluetooth peripheral you want to connect to.
const deviceAddress = "E4:B7:F4:11:8D:33"
// 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
func connectAddress() string {
return deviceAddress
return DeviceAddress
}
// done just blocks forever, allows USB CDC reset for flashing new software.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@ package main
import (
"time"
"tinygo.org/x/bluetooth"
"gitrepo.ru/neonxp/bluetooth"
)
var adapter = bluetooth.DefaultAdapter
@ -21,7 +21,7 @@ func main() {
must("config adv", adv.Configure(bluetooth.AdvertisementOptions{
LocalName: "Go Bluetooth",
}))
adapter.SetConnectHandler(func(device bluetooth.Address, connected bool) {
adapter.SetConnectHandler(func(device bluetooth.Device, connected bool) {
if connected {
println("connected, not advertising...")
advState = false

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

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

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

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

View file

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

View file

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

214
gap.go
View file

@ -55,8 +55,34 @@ type AdvertisementOptions struct {
Interval Duration
// ManufacturerData stores Advertising Data.
// Keys are the Manufacturer ID to associate with the data.
ManufacturerData map[uint16]interface{}
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
@ -79,7 +105,7 @@ type ScanResult struct {
// Bluetooth address of the scanned device.
Address Address
// RSSI the last time a packet from this device has been received.
// Signal strength of the advertisement packet.
RSSI int16
// The data obtained from the advertisement data, which may contain many
@ -110,9 +136,13 @@ type AdvertisementPayload interface {
// if this data is not available.
Bytes() []byte
// ManufacturerData returns a map with all the manufacturer data present in the
//advertising. IT may be empty.
ManufacturerData() map[uint16][]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.
@ -127,7 +157,10 @@ type AdvertisementFields struct {
ServiceUUIDs []UUID
// ManufacturerData is the manufacturer data of the advertisement.
ManufacturerData map[uint16][]byte
ManufacturerData []ManufacturerDataElement
// ServiceData is the service data of the advertisement.
ServiceData []ServiceDataElement
}
// advertisementFields wraps AdvertisementFields to implement the
@ -161,10 +194,15 @@ func (p *advertisementFields) Bytes() []byte {
}
// ManufacturerData returns the underlying ManufacturerData field.
func (p *advertisementFields) ManufacturerData() map[uint16][]byte {
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
@ -254,22 +292,58 @@ func (buf *rawAdvertisementPayload) HasServiceUUID(uuid UUID) bool {
}
// ManufacturerData returns the manufacturer data in the advertisement payload.
func (buf *rawAdvertisementPayload) ManufacturerData() map[uint16][]byte {
mData := make(map[uint16][]byte)
data := buf.Bytes()
for len(data) >= 2 {
fieldLength := data[0]
if int(fieldLength)+1 > len(data) {
// Invalid field length.
return nil
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
}
// If this is the manufacturer data
if byte(0xFF) == data[1] {
mData[uint16(data[2])+(uint16(data[3])<<8)] = data[4 : fieldLength+1]
fieldType := buf.data[index+1]
if fieldType != 0xff {
continue
}
data = data[fieldLength+1:]
key := uint16(buf.data[index+2]) | uint16(buf.data[index+3])<<8
manufacturerData = append(manufacturerData, ManufacturerDataElement{
CompanyID: key,
Data: buf.data[index+4 : index+fieldLength+1],
})
}
return mData
return manufacturerData
}
// ServiceData returns the service data in the advertisment payload
func (buf *rawAdvertisementPayload) ServiceData() []ServiceDataElement {
var serviceData []ServiceDataElement
for index := 0; index < int(buf.len)+4; index += int(buf.data[index]) + 1 {
fieldLength := int(buf.data[index+0])
if fieldLength < 3 { // field has only length and type and no data
continue
}
fieldType := buf.data[index+1]
switch fieldType {
case 0x16: // 16-bit uuid
serviceData = append(serviceData, ServiceDataElement{
UUID: New16BitUUID(uint16(buf.data[index+2]) + (uint16(buf.data[index+3]) << 8)),
Data: buf.data[index+4 : index+fieldLength+1],
})
case 0x20: // 32-bit uuid
serviceData = append(serviceData, ServiceDataElement{
UUID: New32BitUUID(uint32(buf.data[index+2]) + (uint32(buf.data[index+3]) << 8) + (uint32(buf.data[index+4]) << 16) + (uint32(buf.data[index+5]) << 24)),
Data: buf.data[index+6 : index+fieldLength+1],
})
case 0x21: // 128-bit uuid
var uuidArray [16]byte
copy(uuidArray[:], buf.data[index+2:index+18])
serviceData = append(serviceData, ServiceDataElement{
UUID: NewUUID(uuidArray),
Data: buf.data[index+18 : index+fieldLength+1],
})
default:
continue
}
}
return serviceData
}
// reset restores this buffer to the original state.
@ -300,36 +374,88 @@ func (buf *rawAdvertisementPayload) addFromOptions(options AdvertisementOptions)
}
}
if len(options.ManufacturerData) > 0 {
buf.addManufacturerData(options.ManufacturerData)
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(manufacturerData map[uint16]interface{}) (ok bool) {
payloadData := buf.Bytes()
for manufacturerID, rawData := range manufacturerData {
data := rawData.([]byte)
// Check if the manufacturer ID is within the range of 16 bits (0-65535).
if manufacturerID > 0xFFFF {
// Invalid manufacturer ID.
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
}
fieldLength := len(data) + 3
// Add the data.
buf.data[buf.len+0] = uint8(fieldLength - 1)
buf.data[buf.len+1] = 0xff
buf.data[buf.len+2] = uint8(key)
buf.data[buf.len+3] = uint8(key >> 8)
copy(buf.data[buf.len+4:], value)
buf.len += uint8(fieldLength)
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)
// Build manufacturer ID parts
manufacturerDataBit := byte(0xFF)
manufacturerIDPart1 := byte(manufacturerID & 0xFF)
manufacturerIDPart2 := byte((manufacturerID >> 8) & 0xFF)
payloadData = append(payloadData, byte(fieldLength), manufacturerDataBit, manufacturerIDPart1, manufacturerIDPart2)
payloadData = append(payloadData, data...)
}
buf.len = uint8(len(payloadData))
copy(buf.data[:], payloadData)
return true
}
@ -391,7 +517,8 @@ func (buf *rawAdvertisementPayload) addServiceUUID(uuid UUID) (ok bool) {
}
}
// ConnectionParams are used when connecting to a peripherals.
// ConnectionParams are used when connecting to a peripherals or when changing
// the parameters of an active connection.
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.
@ -403,4 +530,9 @@ 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

@ -85,6 +85,12 @@ 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
@ -97,14 +103,14 @@ type Device struct {
}
// Connect starts a connection attempt to the given peripheral device address.
func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, error) {
func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, error) {
uuid, err := cbgo.ParseUUID(address.UUID.String())
if err != nil {
return nil, err
return Device{}, err
}
prphs := a.cm.RetrievePeripheralsWithIdentifiers([]cbgo.UUID{uuid})
if len(prphs) == 0 {
return nil, fmt.Errorf("Connect failed: no peer with address: %s", address.UUID.String())
return Device{}, fmt.Errorf("Connect failed: no peer with address: %s", address.UUID.String())
}
timeout := defaultConnectionTimeout
@ -129,20 +135,23 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, er
// check if we have received a disconnected peripheral
if p.State() == cbgo.PeripheralStateDisconnected {
return nil, connectionError
return Device{}, connectionError
}
d := &Device{
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(address, true)
a.connectHandler(d, true)
return d, nil
@ -162,17 +171,29 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, er
// 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
@ -211,3 +232,20 @@ func (pd *peripheralDelegate) DidUpdateValueForCharacteristic(prph cbgo.Peripher
}
}
// 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
}
}
}
}
}

422
gap_hci.go Normal file
View file

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

@ -3,18 +3,20 @@
package bluetooth
import (
"context"
"errors"
"fmt"
"strings"
"sync/atomic"
"github.com/godbus/dbus/v5"
"github.com/muka/go-bluetooth/api"
"github.com/muka/go-bluetooth/bluez"
"github.com/muka/go-bluetooth/bluez/profile/advertising"
"github.com/muka/go-bluetooth/bluez/profile/device"
"github.com/godbus/dbus/v5/prop"
)
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 {
@ -24,9 +26,8 @@ type Address struct {
// Advertisement encapsulates a single advertisement instance.
type Advertisement struct {
adapter *Adapter
advertisement *api.Advertisement
properties *advertising.LEAdvertisement1Properties
cancel func()
properties *prop.Properties
path dbus.ObjectPath
}
// DefaultAdvertisement returns the default advertisement instance but does not
@ -44,42 +45,83 @@ 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.advertisement != nil {
if a.properties != nil {
panic("todo: configure advertisement a second time")
}
a.properties = &advertising.LEAdvertisement1Properties{
Type: advertising.AdvertisementTypeBroadcast,
Timeout: 1<<16 - 1,
LocalName: options.LocalName,
ManufacturerData: options.ManufacturerData,
}
var serviceUUIDs []string
for _, uuid := range options.ServiceUUIDs {
a.properties.ServiceUUIDs = append(a.properties.ServiceUUIDs, uuid.String())
serviceUUIDs = append(serviceUUIDs, uuid.String())
}
var serviceData = make(map[string]interface{})
for _, element := range options.ServiceData {
serviceData[element.UUID.String()] = element.Data
}
// Convert map[uint16][]byte to map[uint16]any because that's what BlueZ needs.
manufacturerData := map[uint16]any{}
for _, element := range options.ManufacturerData {
manufacturerData[element.CompanyID] = element.Data
}
// Build an org.bluez.LEAdvertisement1 object, to be exported over DBus.
// 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 {
if a.advertisement != nil {
panic("todo: start advertisement a second time")
}
cancel, err := api.ExposeAdvertisement(a.adapter.id, a.properties, uint32(a.properties.Timeout))
// 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 {
return err
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)
}
// Make us discoverable.
err = a.adapter.adapter.SetProperty("org.bluez.Adapter1.Discoverable", dbus.MakeVariant(true))
if err != nil {
return fmt.Errorf("bluetooth: could not start advertisement: %w", err)
}
a.cancel = cancel
return nil
}
// Stop advertisement. May only be called after it has been started.
func (a *Advertisement) Stop() error {
if a.cancel == nil {
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
}
a.cancel()
return fmt.Errorf("bluetooth: could not stop advertisement: %w", err)
}
return nil
}
@ -92,7 +134,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.cancelChan != nil {
if a.scanCancelChan != nil {
return errScanning
}
@ -100,58 +142,61 @@ 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.cancelChan = cancelChan
a.scanCancelChan = cancelChan
// This appears to be necessary to receive any BLE discovery results at all.
defer a.adapter.SetDiscoveryFilter(nil)
err := a.adapter.SetDiscoveryFilter(map[string]interface{}{
defer a.adapter.Call("org.bluez.Adapter1.SetDiscoveryFilter", 0)
err := a.adapter.Call("org.bluez.Adapter1.SetDiscoveryFilter", 0, map[string]interface{}{
"Transport": "le",
})
if err != nil {
return err
}
bus, err := dbus.SystemBus()
}).Err
if err != nil {
return err
}
signal := make(chan *dbus.Signal)
bus.Signal(signal)
defer bus.RemoveSignal(signal)
a.bus.Signal(signal)
defer a.bus.RemoveSignal(signal)
propertiesChangedMatchOptions := []dbus.MatchOption{dbus.WithMatchInterface("org.freedesktop.DBus.Properties")}
bus.AddMatchSignal(propertiesChangedMatchOptions...)
defer bus.RemoveMatchSignal(propertiesChangedMatchOptions...)
a.bus.AddMatchSignal(propertiesChangedMatchOptions...)
defer a.bus.RemoveMatchSignal(propertiesChangedMatchOptions...)
newObjectMatchOptions := []dbus.MatchOption{dbus.WithMatchInterface("org.freedesktop.DBus.ObjectManager")}
bus.AddMatchSignal(newObjectMatchOptions...)
defer bus.RemoveMatchSignal(newObjectMatchOptions...)
a.bus.AddMatchSignal(newObjectMatchOptions...)
defer a.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.
deviceList, err := a.adapter.GetDevices()
var deviceList map[dbus.ObjectPath]map[string]map[string]dbus.Variant
err = a.bluez.Call("org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0).Store(&deviceList)
if err != nil {
return err
}
devices := make(map[dbus.ObjectPath]*device.Device1Properties)
for _, dev := range deviceList {
if dev.Properties.Connected {
callback(a, makeScanResult(dev.Properties))
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))
select {
case <-cancelChan:
return nil
default:
}
}
devices[dev.Path()] = dev.Properties
devices[path] = device
}
// Instruct BlueZ to start discovering.
err = a.adapter.StartDiscovery()
err = a.adapter.Call("org.bluez.Adapter1.StartDiscovery", 0).Err
if err != nil {
return err
}
@ -163,8 +208,7 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
// StopScan is called).
select {
case <-cancelChan:
a.adapter.StopDiscovery()
return nil
return a.adapter.Call("org.bluez.Adapter1.StopDiscovery", 0).Err
default:
}
@ -180,35 +224,24 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
if !ok {
continue
}
var props *device.Device1Properties
props, _ = props.FromDBusMap(rawprops)
devices[objectPath] = props
callback(a, makeScanResult(props))
devices[objectPath] = rawprops
callback(a, makeScanResult(rawprops))
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)
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)
case "ManufacturerData":
// work around for https://github.com/muka/go-bluetooth/issues/163
mData := make(map[uint16]interface{})
for k, v := range val.Value().(map[uint16]dbus.Variant) {
mData[k] = v.Value().(interface{})
device, ok := devices[sig.Path]
if !ok {
// This shouldn't happen, but protect against it just in
// case.
continue
}
props.ManufacturerData = mData
for k, v := range changes {
device[k] = v
}
}
callback(a, makeScanResult(props))
callback(a, makeScanResult(device))
}
case <-cancelChan:
continue
@ -222,49 +255,67 @@ 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.cancelChan == nil {
if a.scanCancelChan == nil {
return errNotScanning
}
close(a.cancelChan)
a.cancelChan = nil
close(a.scanCancelChan)
a.scanCancelChan = nil
return nil
}
// makeScanResult creates a ScanResult from a Device1 object.
func makeScanResult(props *device.Device1Properties) ScanResult {
// makeScanResult creates a ScanResult from a raw DBus device.
func makeScanResult(props map[string]dbus.Variant) ScanResult {
// Assume the Address property is well-formed.
addr, _ := ParseMAC(props.Address)
addr, _ := ParseMAC(props["Address"].Value().(string))
// Create a list of UUIDs.
var serviceUUIDs []UUID
for _, uuid := range props.UUIDs {
for _, uuid := range props["UUIDs"].Value().([]string) {
// Assume the UUID is well-formed.
parsedUUID, _ := ParseUUID(uuid)
serviceUUIDs = append(serviceUUIDs, parsedUUID)
}
a := Address{MACAddress{MAC: addr}}
a.SetRandom(props.AddressType == "random")
a.SetRandom(props["AddressType"].Value().(string) == "random")
mData := make(map[uint16][]byte)
for k, v := range props.ManufacturerData {
// can be either variant or just byte value
switch val := v.(type) {
case dbus.Variant:
mData[k] = val.Value().([]byte)
case []byte:
mData[k] = val
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),
})
}
}
return ScanResult{
RSSI: props.RSSI,
RSSI: rssi,
Address: a,
AdvertisementPayload: &advertisementFields{
AdvertisementFields{
LocalName: props.Name,
LocalName: localName,
ServiceUUIDs: serviceUUIDs,
ManufacturerData: mData,
ManufacturerData: manufacturerData,
ServiceData: serviceData,
},
},
}
@ -272,40 +323,68 @@ func makeScanResult(props *device.Device1Properties) ScanResult {
// Device is a connection to a remote peripheral.
type Device struct {
device *device.Device1 // bluez device interface
ctx context.Context // context for our event watcher, canceled on disconnect event
cancel context.CancelFunc // cancel function to halt our event watcher context
propchanged chan *bluez.PropertyChanged // channel that device property changes will show up on
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
address Address // the address of the device
}
// 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) {
func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, error) {
devicePath := dbus.ObjectPath(string(a.adapter.Path()) + "/dev_" + strings.Replace(address.MAC.String(), ":", "_", -1))
dev, err := device.NewDevice1(devicePath)
if err != nil {
return nil, err
}
device := &Device{
device: dev,
device := Device{
Address: address,
device: a.bus.Object("org.bluez", devicePath),
adapter: a,
address: address,
}
device.ctx, device.cancel = context.WithCancel(context.Background())
device.watchForConnect() // Set this up before we trigger a connection so we can capture the connect event
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()
// 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")
if err != nil {
device.cancel() // cancel our watcher routine
return nil, err
return Device{}, 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 err != nil {
return Device{}, fmt.Errorf("bluetooth: failed to connect: %w", 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
@ -313,51 +392,19 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, er
// 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 {
// we don't call our cancel function here, instead we wait for the
// property change in `watchForConnect` and cancel things then
return d.device.Disconnect()
return d.device.Call("org.bluez.Device1.Disconnect", 0).Err
}
// watchForConnect watches for a signal from the bluez device interface that indicates a Connection/Disconnection.
// 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.
//
// We can add extra signals to watch for here,
// see https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/device-api.txt, for a full list
func (d *Device) watchForConnect() error {
var err error
d.propchanged, err = d.device.WatchProperties()
if err != nil {
return err
}
go func() {
for {
select {
case changed := <-d.propchanged:
// we will receive a nil if bluez.UnwatchProperties(a, ch) is called, if so we can stop watching
if changed == nil {
d.cancel()
return
}
switch changed.Name {
case "Connected":
// Send off a notification indicating we have connected or disconnected
d.adapter.connectHandler(d.address, d.device.Properties.Connected)
if !d.device.Properties.Connected {
d.cancel()
return
}
}
continue
case <-d.ctx.Done():
return
}
}
}()
// 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
}

View file

@ -4,12 +4,18 @@ package bluetooth
/*
#include "ble_gap.h"
// 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);
}
*/
import "C"
import (
"runtime/volatile"
"time"
"unsafe"
)
// Address contains a Bluetooth MAC address.
@ -47,7 +53,7 @@ func (a *Advertisement) Configure(options AdvertisementOptions) error {
return errAdvertisementPacketTooBig
}
errCode := C.sd_ble_gap_adv_data_set(&payload.data[0], payload.len, nil, 0)
errCode := C.sd_ble_gap_adv_data_set((*C.uint8_t)(unsafe.Pointer(&payload.data[0])), C.uint8_t(payload.len), nil, 0)
a.interval = options.Interval
return makeError(errCode)
}
@ -68,12 +74,12 @@ func (a *Advertisement) Stop() error {
// Low-level version of Start. Used to restart advertisement when a connection
// is lost.
func (a *Advertisement) start() uint32 {
func (a *Advertisement) start() C.uint32_t {
params := C.ble_gap_adv_params_t{
_type: C.BLE_GAP_ADV_TYPE_ADV_IND,
fp: C.BLE_GAP_ADV_FP_ANY,
interval: uint16(a.interval),
interval: C.uint16_t(a.interval),
timeout: 0, // no timeout
}
return C.sd_ble_gap_adv_start(&params)
return C.sd_ble_gap_adv_start_noescape(params)
}

View file

@ -5,6 +5,7 @@ package bluetooth
import (
"runtime/volatile"
"time"
"unsafe"
)
/*
@ -19,7 +20,7 @@ type Address struct {
// Advertisement encapsulates a single advertisement instance.
type Advertisement struct {
handle uint8
handle C.uint8_t
isAdvertising volatile.Register8
payload rawAdvertisementPayload
}
@ -57,14 +58,14 @@ func (a *Advertisement) Configure(options AdvertisementOptions) error {
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),
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: uint32(options.Interval),
interval: C.uint32_t(options.Interval),
}
errCode := C.sd_ble_gap_adv_set_configure(&a.handle, &data, &params)
return makeError(errCode)

View file

@ -7,6 +7,7 @@ import (
"errors"
"runtime/volatile"
"time"
"unsafe"
)
/*
@ -15,6 +16,7 @@ import (
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 (
@ -40,12 +42,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 = uint16(NewDuration(40 * time.Millisecond))
scanParams.window = uint16(NewDuration(30 * time.Millisecond))
scanParams.interval = C.uint16_t(NewDuration(40 * time.Millisecond))
scanParams.window = C.uint16_t(NewDuration(30 * time.Millisecond))
scanParams.timeout = C.BLE_GAP_SCAN_TIMEOUT_UNLIMITED
scanReportBufferInfo := C.ble_data_t{
p_data: &scanReportBuffer.data[0],
len: uint16(len(scanReportBuffer.data)),
p_data: (*C.uint8_t)(unsafe.Pointer(&scanReportBuffer.data[0])),
len: C.uint16_t(len(scanReportBuffer.data)),
}
errCode := C.sd_ble_gap_scan_start(&scanParams, &scanReportBufferInfo)
if errCode != 0 {
@ -91,15 +93,10 @@ 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 ready (connected or timeout)
connectionHandle uint16
state volatile.Register8 // 0 means unused, 1 means connecting, 2 means connected, 3 means timeout
connectionHandle C.uint16_t
}
// Connect starts a connection attempt to the given peripheral device address.
@ -108,10 +105,10 @@ 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 Address, params ConnectionParams) (Device, error) {
// Construct an address object as used in the SoftDevice.
var addr C.ble_gap_addr_t
addr.addr = address.MAC
addr.addr = makeSDAddress(address.MAC)
if address.IsRandom() {
switch address.MAC[5] >> 6 {
case 0b11:
@ -142,22 +139,25 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, er
scanParams := C.ble_gap_scan_params_t{}
scanParams.set_bitfield_extended(0)
scanParams.set_bitfield_active(0)
scanParams.interval = uint16(NewDuration(40 * time.Millisecond))
scanParams.window = uint16(NewDuration(30 * time.Millisecond))
scanParams.timeout = uint16(params.ConnectionTimeout)
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
connectionParams := C.ble_gap_conn_params_t{
min_conn_interval: uint16(params.MinInterval) / 2,
max_conn_interval: uint16(params.MaxInterval) / 2,
min_conn_interval: C.uint16_t(params.MinInterval) / 2,
max_conn_interval: C.uint16_t(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 nil, errAlreadyConnecting
return Device{}, errAlreadyConnecting
}
connectionAttempt.state.Set(1)
@ -165,26 +165,33 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, er
errCode := C.sd_ble_gap_connect(&addr, &scanParams, &connectionParams, C.BLE_CONN_CFG_TAG_DEFAULT)
if errCode != 0 {
connectionAttempt.state.Set(0)
return nil, Error(errCode)
return Device{}, Error(errCode)
}
// Wait until the connection is established.
// TODO: use some sort of condition variable once the scheduler supports
// them.
for connectionAttempt.state.Get() != 2 {
arm.Asm("wfe")
}
connectionHandle := connectionAttempt.connectionHandle
for {
state := connectionAttempt.state.Get()
if state == 2 {
// Successfully connected.
connectionAttempt.state.Set(0)
// Connection has been established.
return &Device{
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")
}
}
}
// 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)
@ -192,3 +199,34 @@ 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)
}

15
gap_sd.go Normal file
View file

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

View file

@ -18,6 +18,108 @@ 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) {
@ -68,18 +170,25 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) {
// Wait for when advertisement has stopped by a call to StopScan().
// Advertisement doesn't seem to stop right away, there is an
// intermediate Stopping state.
stoppingChan := make(chan struct{})
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, _, _ unsafe.Pointer) {
// Note: the args parameter has an Error property that should
// probably be checked, but I'm not sure when stopping the
// advertisement watcher could ever result in an error (except
// for bugs).
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)
}
close(stoppingChan)
})
defer stoppedHandler.Release()
@ -96,8 +205,7 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) {
}
// Wait until advertisement has stopped, and finish.
<-stoppingChan
return nil
return <-stoppingChan
}
func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedEventArgs) ScanResult {
@ -114,7 +222,7 @@ func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedE
Address: adr,
}
var manufacturerData map[uint16][]byte = make(map[uint16][]byte)
var manufacturerData []ManufacturerDataElement
if winAdv, err := args.GetAdvertisement(); err == nil && winAdv != nil {
vector, _ := winAdv.GetManufacturerData()
size, _ := vector.GetSize()
@ -123,7 +231,10 @@ func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedE
manData := (*advertisement.BluetoothLEManufacturerData)(element)
companyID, _ := manData.GetCompanyId()
buffer, _ := manData.GetData()
manufacturerData[companyID] = bufferToSlice(buffer)
manufacturerData = append(manufacturerData, ManufacturerDataElement{
CompanyID: companyID,
Data: bufferToSlice(buffer),
})
}
}
@ -141,7 +252,7 @@ func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedE
}
func bufferToSlice(buffer *streams.IBuffer) []byte {
dataReader, _ := streams.FromBuffer(buffer)
dataReader, _ := streams.DataReaderFromBuffer(buffer)
defer dataReader.Release()
bufferSize, _ := buffer.GetLength()
if bufferSize == 0 {
@ -163,6 +274,8 @@ func (a *Adapter) StopScan() error {
// Device is a connection to a remote peripheral.
type Device struct {
Address Address // the MAC address of the device
device *bluetooth.BluetoothLEDevice
session *genericattributeprofile.GattSession
}
@ -170,32 +283,32 @@ type Device struct {
// 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) {
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.FromBluetoothAddressAsync(winAddr)
bleDeviceOp, err := bluetooth.BluetoothLEDeviceFromBluetoothAddressAsync(winAddr)
if err != nil {
return nil, err
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 nil, fmt.Errorf("error connecting to device: %w", err)
return Device{}, fmt.Errorf("error connecting to device: %w", err)
}
res, err := bleDeviceOp.GetResults()
if err != nil {
return nil, err
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 nil, fmt.Errorf("device with the given address was not found")
return Device{}, fmt.Errorf("device with the given address was not found")
}
bleDevice := (*bluetooth.BluetoothLEDevice)(res)
@ -204,37 +317,37 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, er
// To initiate a connection, we need to set GattSession.MaintainConnection to true.
dID, err := bleDevice.GetBluetoothDeviceId()
if err != nil {
return nil, err
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.FromDeviceIdAsync(dID) // IAsyncOperation<GattSession>
gattSessionOp, err := genericattributeprofile.GattSessionFromDeviceIdAsync(dID) // IAsyncOperation<GattSession>
if err != nil {
return nil, err
return Device{}, err
}
if err := awaitAsyncOperation(gattSessionOp, genericattributeprofile.SignatureGattSession); err != nil {
return nil, fmt.Errorf("error getting gatt session: %w", err)
return Device{}, fmt.Errorf("error getting gatt session: %w", err)
}
gattRes, err := gattSessionOp.GetResults()
if err != nil {
return nil, err
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 nil, err
return Device{}, err
}
return &Device{bleDevice, newSession}, nil
return Device{address, bleDevice, newSession}, nil
}
// Disconnect from the BLE device. This method is non-blocking and does not
// wait until the connection is fully gone.
func (d *Device) Disconnect() error {
func (d Device) Disconnect() error {
defer d.device.Release()
defer d.session.Release()
@ -247,3 +360,15 @@ 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 Windows, this call doesn't do anything.
func (d Device) RequestConnectionParams(params ConnectionParams) error {
// TODO: implement this using
// BluetoothLEDevice.RequestPreferredConnectionParameters.
return nil
}

View file

@ -14,7 +14,7 @@ import (
//
// Passing a nil slice of UUIDs will return a complete list of
// services.
func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
func (d Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
d.prph.DiscoverServices([]cbgo.UUID{})
// clear cache of services
@ -69,14 +69,14 @@ type DeviceService struct {
type deviceService struct {
uuidWrapper
device *Device
device Device
service cbgo.Service
characteristics []DeviceCharacteristic
}
// UUID returns the UUID for this DeviceService.
func (s *DeviceService) UUID() UUID {
func (s DeviceService) UUID() UUID {
return s.uuidWrapper
}
@ -89,7 +89,7 @@ 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{}
s.device.prph.DiscoverCharacteristics(cbuuids, s.service)
@ -142,7 +142,7 @@ func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacter
}
// Small helper to create a DeviceCharacteristic object.
func (s *DeviceService) makeCharacteristic(uuid UUID, dchar cbgo.Characteristic) DeviceCharacteristic {
func (s DeviceService) makeCharacteristic(uuid UUID, dchar cbgo.Characteristic) DeviceCharacteristic {
char := DeviceCharacteristic{
deviceCharacteristic: &deviceCharacteristic{
uuidWrapper: uuid,
@ -163,18 +163,40 @@ 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

330
gattc_hci.go Normal file
View file

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

@ -9,8 +9,6 @@ import (
"time"
"github.com/godbus/dbus/v5"
"github.com/muka/go-bluetooth/bluez"
"github.com/muka/go-bluetooth/bluez/profile/gatt"
)
var (
@ -24,12 +22,12 @@ type uuidWrapper = UUID
// DeviceService is a BLE service on a connected peripheral device.
type DeviceService struct {
uuidWrapper
service *gatt.GattService1
adapter *Adapter
servicePath string
}
// UUID returns the UUID for this DeviceService.
func (s *DeviceService) UUID() UUID {
func (s DeviceService) UUID() UUID {
return s.uuidWrapper
}
@ -43,18 +41,20 @@ 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) {
func (d Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
start := time.Now()
for {
resolved, err := d.device.GetServicesResolved()
resolved, err := d.device.GetProperty("org.bluez.Device1.ServicesResolved")
if err != nil {
return nil, err
}
if resolved {
if resolved.Value().(bool) {
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")
@ -62,16 +62,13 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
}
services := []DeviceService{}
uuidServices := make(map[string]string)
uuidServices := make(map[UUID]struct{})
servicesFound := 0
// Iterate through all objects managed by BlueZ, hoping to find the services
// we're looking for.
om, err := bluez.GetObjectManager()
if err != nil {
return nil, err
}
list, err := om.GetManagedObjects()
var list map[dbus.ObjectPath]map[string]map[string]dbus.Variant
err := d.adapter.bluez.Call("org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0).Store(&list)
if err != nil {
return nil, err
}
@ -84,19 +81,17 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
if !strings.HasPrefix(objectPath, string(d.device.Path())+"/service") {
continue
}
suffix := objectPath[len(d.device.Path()+"/"):]
if len(strings.Split(suffix, "/")) != 1 {
properties, ok := list[dbus.ObjectPath(objectPath)]["org.bluez.GattService1"]
if !ok {
continue
}
service, err := gatt.NewGattService1(dbus.ObjectPath(objectPath))
if err != nil {
return nil, err
}
serviceUUID, _ := ParseUUID(properties["UUID"].Value().(string))
if len(uuids) > 0 {
found := false
for _, uuid := range uuids {
if service.Properties.UUID == uuid.String() {
if uuid == serviceUUID {
// One of the services we're looking for.
found = true
break
@ -107,20 +102,21 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
}
}
if _, ok := uuidServices[service.Properties.UUID]; ok {
if _, ok := uuidServices[serviceUUID]; ok {
// There is more than one service with the same UUID?
// Don't overwrite it, to keep the servicesFound count correct.
continue
}
uuid, _ := ParseUUID(service.Properties.UUID)
ds := DeviceService{uuidWrapper: uuid,
service: service,
ds := DeviceService{
uuidWrapper: serviceUUID,
adapter: d.adapter,
servicePath: objectPath,
}
services = append(services, ds)
servicesFound++
uuidServices[service.Properties.UUID] = service.Properties.UUID
uuidServices[serviceUUID] = struct{}{}
}
if servicesFound < len(uuids) {
@ -134,13 +130,14 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
// device.
type DeviceCharacteristic struct {
uuidWrapper
characteristic *gatt.GattCharacteristic1
property chan *bluez.PropertyChanged // channel where notifications are reported
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
}
// UUID returns the UUID for this DeviceCharacteristic.
func (c *DeviceCharacteristic) UUID() UUID {
func (c DeviceCharacteristic) UUID() UUID {
return c.uuidWrapper
}
@ -153,7 +150,7 @@ 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) {
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
@ -163,11 +160,8 @@ func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacter
// Iterate through all objects managed by BlueZ, hoping to find the
// characteristic we're looking for.
om, err := bluez.GetObjectManager()
if err != nil {
return nil, err
}
list, err := om.GetManagedObjects()
var list map[dbus.ObjectPath]map[string]map[string]dbus.Variant
err := s.adapter.bluez.Call("org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0).Store(&list)
if err != nil {
return nil, err
}
@ -177,21 +171,18 @@ func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacter
}
sort.Strings(objects)
for _, objectPath := range objects {
if !strings.HasPrefix(objectPath, string(s.service.Path())+"/char") {
if !strings.HasPrefix(objectPath, s.servicePath+"/char") {
continue
}
suffix := objectPath[len(s.service.Path()+"/"):]
if len(strings.Split(suffix, "/")) != 1 {
properties, ok := list[dbus.ObjectPath(objectPath)]["org.bluez.GattCharacteristic1"]
if !ok {
continue
}
characteristic, err := gatt.NewGattCharacteristic1(dbus.ObjectPath(objectPath))
if err != nil {
return nil, err
}
cuuid, _ := ParseUUID(characteristic.Properties.UUID)
cuuid, _ := ParseUUID(properties["UUID"].Value().(string))
char := DeviceCharacteristic{
uuidWrapper: cuuid,
characteristic: characteristic,
adapter: s.adapter,
characteristic: s.adapter.bus.Object("org.bluez", dbus.ObjectPath(objectPath)),
}
if len(uuids) > 0 {
@ -231,7 +222,7 @@ func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacter
// 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.WriteValue(p, nil)
err = c.characteristic.Call("org.bluez.GattCharacteristic1.WriteValue", 0, p, map[string]dbus.Variant(nil)).Err
if err != nil {
return 0, err
}
@ -244,32 +235,38 @@ func (c DeviceCharacteristic) WriteWithoutResponse(p []byte) (n int, err error)
// changes.
//
// Users may call EnableNotifications with a nil callback to disable notifications.
func (c *DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) error {
func (c DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) error {
switch callback {
default:
if c.property != nil {
return errDupNotif
}
ch, err := c.characteristic.WatchProperties()
if err != nil {
return err
}
// 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.StartNotify()
err := c.characteristic.Call("org.bluez.GattCharacteristic1.StartNotify", 0).Err
if err != nil {
_ = c.characteristic.UnwatchProperties(ch)
return err
}
c.property = ch
go func() {
for update := range ch {
if update == nil {
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 update.Interface == "org.bluez.GattCharacteristic1" && update.Name == "Value" {
callback(update.Value.([]byte))
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)
}
}
}
}()
@ -281,26 +278,16 @@ func (c *DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) er
return nil
}
e1 := c.characteristic.StopNotify()
e2 := c.characteristic.UnwatchProperties(c.property)
err := c.adapter.bus.RemoveMatchSignal(c.propertiesChangedMatchOption)
c.adapter.bus.RemoveSignal(c.property)
c.property = nil
// FIXME(sbinet): use errors.Join(e1, e2)
if e1 != nil {
return e1
}
if e2 != nil {
return e2
}
return nil
return err
}
}
// GetMTU returns the MTU for the characteristic.
func (c DeviceCharacteristic) GetMTU() (uint16, error) {
mtu, err := c.characteristic.GetProperty("MTU")
mtu, err := c.characteristic.GetProperty("org.bluez.GattCharacteristic1.MTU")
if err != nil {
return uint16(0), err
}
@ -308,9 +295,10 @@ func (c DeviceCharacteristic) GetMTU() (uint16, error) {
}
// 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{})
result, err := c.characteristic.ReadValue(options)
var result []byte
err := c.characteristic.Call("org.bluez.GattCharacteristic1.ReadValue", 0, options).Store(&result)
if err != nil {
return 0, err
}

View file

@ -11,6 +11,7 @@ import (
"device/arm"
"errors"
"runtime/volatile"
"unsafe"
)
const (
@ -28,8 +29,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 volatile.Register16
endHandle volatile.Register16
startHandle volatileHandle
endHandle volatileHandle
uuid C.ble_uuid_t
}
@ -38,13 +39,13 @@ var discoveringService struct {
type DeviceService struct {
uuid shortUUID
connectionHandle uint16
startHandle uint16
endHandle uint16
connectionHandle C.uint16_t
startHandle C.uint16_t
endHandle C.uint16_t
}
// UUID returns the UUID for this DeviceService.
func (s *DeviceService) UUID() UUID {
func (s DeviceService) UUID() UUID {
return s.uuid.UUID()
}
@ -58,7 +59,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
@ -76,7 +77,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 uint32
var errCode C.uint32_t
shortUUIDs[i], errCode = uuid.shortUUID()
if errCode != 0 {
return nil, Error(errCode)
@ -86,7 +87,7 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
numFound := 0
var startHandle uint16 = 1
var startHandle C.uint16_t = 1
for i := 0; i < sz; i++ {
var suuid C.ble_uuid_t
@ -96,7 +97,7 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
// Start discovery of this service.
discoveringService.state.Set(1)
var errCode uint32
var errCode C.uint32_t
if len(uuids) > 0 {
errCode = C.sd_ble_gattc_primary_services_discover(d.connectionHandle, startHandle, &suuid)
} else {
@ -159,14 +160,14 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
type DeviceCharacteristic struct {
uuid shortUUID
connectionHandle uint16
valueHandle uint16
cccdHandle uint16
connectionHandle C.uint16_t
valueHandle C.uint16_t
cccdHandle C.uint16_t
permissions CharacteristicPermissions
}
// UUID returns the UUID for this DeviceCharacteristic.
func (c *DeviceCharacteristic) UUID() UUID {
func (c DeviceCharacteristic) UUID() UUID {
return c.uuid.UUID()
}
@ -175,7 +176,7 @@ func (c *DeviceCharacteristic) UUID() UUID {
var discoveringCharacteristic struct {
uuid C.ble_uuid_t
char_props C.ble_gatt_char_props_t
handle_value volatile.Register16
handle_value volatileHandle
}
// DiscoverCharacteristics discovers characteristics in this service. Pass a
@ -187,7 +188,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
}
@ -204,7 +205,7 @@ func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacter
if len(uuids) > 0 {
shortUUIDs = make([]C.ble_uuid_t, sz)
for i, uuid := range uuids {
var errCode uint32
var errCode C.uint32_t
shortUUIDs[i], errCode = uuid.shortUUID()
if errCode != 0 {
return nil, Error(errCode)
@ -324,8 +325,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: uint16(len(p)),
p_value: &p[0],
len: C.uint16_t(len(p)),
p_value: (*C.uint8_t)(unsafe.Pointer(&p[0])),
})
if errCode != 0 {
return 0, Error(errCode)
@ -334,8 +335,8 @@ func (c DeviceCharacteristic) WriteWithoutResponse(p []byte) (n int, err error)
}
type gattcNotificationCallback struct {
connectionHandle uint16
valueHandle uint16 // may be 0 if the slot is empty
connectionHandle C.uint16_t
valueHandle C.uint16_t // may be 0 if the slot is empty
callback func([]byte)
}
@ -400,7 +401,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]byte{0x01, 0x00} // 0x0001 enables notifications (and disables indications)
value := [2]C.uint8_t{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,
@ -414,16 +415,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 volatile.Register16
offset uint16
length uint16
handle_value volatileHandle
offset C.uint16_t
length C.uint16_t
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

View file

@ -30,7 +30,7 @@ var (
//
// Passing a nil slice of UUIDs will return a complete list of
// services.
func (d *Device) DiscoverServices(filterUUIDs []UUID) ([]DeviceService, error) {
func (d Device) DiscoverServices(filterUUIDs []UUID) ([]DeviceService, error) {
// IAsyncOperation<GattDeviceServicesResult>
getServicesOperation, err := d.device.GetGattServicesWithCacheModeAsync(bluetooth.BluetoothCacheModeUncached)
if err != nil {
@ -133,11 +133,11 @@ type DeviceService struct {
uuidWrapper
service *genericattributeprofile.GattDeviceService
device *Device
device Device
}
// UUID returns the UUID for this DeviceService.
func (s *DeviceService) UUID() UUID {
func (s DeviceService) UUID() UUID {
return s.uuidWrapper
}
@ -150,7 +150,7 @@ func (s *DeviceService) UUID() UUID {
//
// Passing a nil slice of UUIDs will return a complete
// list of characteristics.
func (s *DeviceService) DiscoverCharacteristics(filterUUIDs []UUID) ([]DeviceCharacteristic, error) {
func (s DeviceService) DiscoverCharacteristics(filterUUIDs []UUID) ([]DeviceCharacteristic, error) {
getCharacteristicsOp, err := s.service.GetCharacteristicsWithCacheModeAsync(bluetooth.BluetoothCacheModeUncached)
if err != nil {
return nil, err
@ -234,20 +234,20 @@ type DeviceCharacteristic struct {
characteristic *genericattributeprofile.GattCharacteristic
properties genericattributeprofile.GattCharacteristicProperties
service *DeviceService
service DeviceService
}
// UUID returns the UUID for this DeviceCharacteristic.
func (c *DeviceCharacteristic) UUID() UUID {
func (c DeviceCharacteristic) UUID() UUID {
return c.uuidWrapper
}
func (c *DeviceCharacteristic) Properties() uint32 {
func (c DeviceCharacteristic) Properties() uint32 {
return uint32(c.properties)
}
// GetMTU returns the MTU for the characteristic.
func (c *DeviceCharacteristic) GetMTU() (uint16, error) {
func (c DeviceCharacteristic) GetMTU() (uint16, error) {
return c.service.device.session.GetMaxPduSize()
}
@ -314,7 +314,7 @@ func (c DeviceCharacteristic) write(p []byte, mode genericattributeprofile.GattW
}
// Read reads the current characteristic value.
func (c *DeviceCharacteristic) Read(data []byte) (int, error) {
func (c DeviceCharacteristic) Read(data []byte) (int, error) {
if c.properties&genericattributeprofile.GattCharacteristicPropertiesRead == 0 {
return 0, errNoRead
}
@ -341,7 +341,7 @@ func (c *DeviceCharacteristic) Read(data []byte) (int, error) {
return 0, err
}
datareader, err := streams.FromBuffer(buffer)
datareader, err := streams.DataReaderFromBuffer(buffer)
if err != nil {
return 0, err
}
@ -381,7 +381,7 @@ func (c DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) err
return
}
reader, err := streams.FromBuffer(buf)
reader, err := streams.DataReaderFromBuffer(buf)
if err != nil {
return
}

View file

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

128
gatts_hci.go Normal file
View file

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

@ -3,94 +3,154 @@
package bluetooth
import (
"github.com/muka/go-bluetooth/api/service"
"github.com/muka/go-bluetooth/bluez/profile/gatt"
"fmt"
"strconv"
"sync/atomic"
"github.com/godbus/dbus/v5"
"github.com/godbus/dbus/v5/prop"
)
// 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 {
handle *service.Char
char *bluezChar
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 {
app, err := service.NewApp(service.AppOptions{
AdapterID: a.id,
})
if err != nil {
return err
// 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
// disable magic uuid generation because we send through a fully formed UUID.
// muka/go-bluetooth does some magic so you can use short UUIDs and it'll auto
// expand them to the full 128 bit uuid.
// setting these flags disables that behavior.
app.Options.UUIDSuffix = ""
app.Options.UUID = ""
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.
for i, char := range s.Characteristics {
// Calculate Flags field.
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
"broadcast", // bit 0
"read", // bit 1
"write-without-response", // bit 2
"write", // bit 3
"notify", // bit 4
"indicate", // bit 5
}
for i := uint(0); i < 5; i++ {
var flags []string
for i := 0; i < len(bluezCharFlags); i++ {
if (char.Flags>>i)&1 != 0 {
bluezChar.Properties.Flags = append(bluezChar.Properties.Flags, bluezCharFlags[i])
flags = append(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
})
// 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},
},
}
// Add characteristic to the service, to activate it.
err = bluezService.AddChar(bluezChar)
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
}
return app.Run()
// 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")
if err != nil {
return err
}
// Register our service.
return a.adapter.Call("org.bluez.GattManager1.RegisterApplication", 0, path, map[string]dbus.Variant(nil)).Err
}
// Write replaces the characteristic value with a new value.
@ -99,7 +159,10 @@ func (c *Characteristic) Write(p []byte) (n int, err error) {
return 0, nil // nothing to do
}
gattError := c.handle.WriteValue(p, nil)
if c.char.writeEvent != nil {
c.char.writeEvent(0, 0, p)
}
gattError := c.char.props.Set("org.bluez.GattCharacteristic1", "Value", dbus.MakeVariant(p))
if gattError != nil {
return 0, gattError
}

View file

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

View file

@ -5,13 +5,29 @@ package bluetooth
/*
#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 uint16
handle C.uint16_t
permissions CharacteristicPermissions
}
@ -22,18 +38,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, &service.handle)
errCode = C.sd_ble_gatts_service_add(C.BLE_GATTS_SRVC_TYPE_PRIMARY, &uuid, (*C.uint16_t)(unsafe.Pointer(&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(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)
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)
handles := C.ble_gatts_char_handles_t{}
charUUID, errCode := char.UUID.shortUUID()
if errCode != 0 {
@ -45,16 +61,16 @@ func (a *Adapter) AddService(service *Service) error {
read_perm: secModeOpen,
write_perm: secModeOpen,
},
init_len: uint16(len(char.Value)),
init_len: C.uint16_t(len(char.Value)),
init_offs: 0,
max_len: 20, // This is a conservative maximum length.
}
if len(char.Value) != 0 {
value.p_value = &char.Value[0]
value.p_value = (*C.uint8_t)(unsafe.Pointer(&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(service.handle, &metadata, &value, &handles)
errCode = C.sd_ble_gatts_characteristic_add(C.uint16_t(service.handle), &metadata, &value, &handles)
if errCode != 0 {
return Error(errCode)
}
@ -78,13 +94,13 @@ func (a *Adapter) AddService(service *Service) error {
// charWriteHandler contains a handler->callback mapping for characteristic
// writes.
type charWriteHandler struct {
handle uint16
handle C.uint16_t
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 {
func (a *Adapter) getCharWriteHandler(handle C.uint16_t) *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
@ -107,15 +123,16 @@ func (c *Characteristic) Write(p []byte) (n int, err error) {
}
connHandle := currentConnection.Get()
if connHandle != C.BLE_CONN_HANDLE_INVALID {
if connHandle != C.BLE_CONN_HANDLE_INVALID && c.permissions&(CharacteristicNotifyPermission|CharacteristicIndicatePermission) != 0 {
// There is a connected central.
p_len := uint16(len(p))
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],
})
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])),
)
// Check for some expected errors. Don't report them as errors, but
// instead fall through and do a normal characteristic value update.
@ -133,10 +150,7 @@ func (c *Characteristic) Write(p []byte) (n int, err error) {
}
}
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],
})
errCode := C.sd_ble_gatts_value_set_noescape(C.BLE_CONN_HANDLE_INVALID, c.handle, C.uint16_t(len(p)), (*C.uint8_t)(unsafe.Pointer(&p[0])))
if errCode != 0 {
return 0, Error(errCode)
}

309
gatts_windows.go Normal file
View file

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

14
go.mod
View file

@ -1,23 +1,25 @@
module tinygo.org/x/bluetooth
module gitrepo.ru/neonxp/bluetooth
go 1.18
go 1.20
require (
github.com/go-ole/go-ole v1.2.6
github.com/godbus/dbus/v5 v5.1.0
github.com/muka/go-bluetooth v0.0.0-20221213043340-85dc80edc4e1
github.com/saltosystems/winrt-go v0.0.0-20230710111611-a39229b5054c
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.25.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/fatih/structs v1.1.0 // indirect
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
)

76
go.sum
View file

@ -1,101 +1,49 @@
github.com/bgould/http v0.0.0-20190627042742-d268792bdee7/go.mod h1:BTqvVegvwifopl4KTEDth6Zezs9eR+lCWhvGKvkxJHE=
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/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/frankban/quicktest v1.10.2/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P6txr3mVT54s=
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.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
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/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hajimehoshi/go-jisx0208 v1.0.0/go.mod h1:yYxEStHL7lt9uL+AbdWgW9gBumwieDoZCiB1f/0X0as=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
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-20221213043340-85dc80edc4e1 h1:BuVRHr4HHJbk1DHyWkArJ7E8J/VA8ncCr/VLnQFazBo=
github.com/muka/go-bluetooth v0.0.0-20221213043340-85dc80edc4e1/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sago35/go-bdf v0.0.0-20200313142241-6c17821c91c4/go.mod h1:rOebXGuMLsXhZAC6mF/TjxONsm45498ZyzVhel++6KM=
github.com/saltosystems/winrt-go v0.0.0-20230710111611-a39229b5054c h1:GmnSNqDHyCqwGT5a3cOhMO8+yXzQW7VUdUGFAOSaEkw=
github.com/saltosystems/winrt-go v0.0.0-20230710111611-a39229b5054c/go.mod h1:UvKm1lyhg+8ehk99i8g5Q7AX1LXUJgks0lRyAkG/ahQ=
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/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/soypat/cyw43439 v0.0.0-20240609122733-da9153086796 h1:1/r2URInjjFtWqT61gU7YGVCq3BRyXt/C7z4oLRF9Lo=
github.com/soypat/cyw43439 v0.0.0-20240609122733-da9153086796/go.mod h1:1Otjk6PRhfzfcVHeWMEeku/VntFqWghUwuSQyivb2vE=
github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef h1:phH95I9wANjTYw6bSYLZDQfNvao+HqYDom8owbNa0P4=
github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef/go.mod h1:oCVCNGCHMKoBj97Zp9znLbQ1nHxpkmOY9X+UAGzOxc8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q=
github.com/suapapa/go_eddystone v1.3.1/go.mod h1:bXC11TfJOS+3g3q/Uzd7FKd5g62STQEfeEIhcKe4Qy8=
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/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
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/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
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/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
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/exp v0.0.0-20230728194245-b0cb94b80691 h1:/yRP+0AN7mf5DkD3BAI6TOFnd51gEoDEb8o35jIFtgw=
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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-20191204190536-9bdfabe68543/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-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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.14.0/go.mod h1:uT2svMq3EpBZpKkGO+NQHjxjGf1f42ra4OnMMwQL2aI=
tinygo.org/x/drivers v0.15.1/go.mod h1:uT2svMq3EpBZpKkGO+NQHjxjGf1f42ra4OnMMwQL2aI=
tinygo.org/x/drivers v0.16.0/go.mod h1:uT2svMq3EpBZpKkGO+NQHjxjGf1f42ra4OnMMwQL2aI=
tinygo.org/x/drivers v0.19.0/go.mod h1:uJD/l1qWzxzLx+vcxaW0eY464N5RAgFi1zTVzASFdqI=
tinygo.org/x/drivers v0.25.0 h1:MFnec5lY8Sxk1bIfqQWsflIbxcpAFbohWhg/qZ7psdM=
tinygo.org/x/drivers v0.25.0/go.mod h1:v+mXaA4cgpz/YZJ3ZPm/86bYQJAXTaYtMkHlVwbodbw=
tinygo.org/x/tinyfont v0.2.1/go.mod h1:eLqnYSrFRjt5STxWaMeOWJTzrKhXqpWw7nU3bPfKOAM=
tinygo.org/x/tinyfont v0.3.0/go.mod h1:+TV5q0KpwSGRWnN+ITijsIhrWYJkoUCp9MYELjKpAXk=
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/tinyfs v0.1.0/go.mod h1:ysc8Y92iHfhTXeyEM9+c7zviUQ4fN9UCFgSOFfMWv20=
tinygo.org/x/tinyterm v0.1.0/go.mod h1:/DDhNnGwNF2/tNgHywvyZuCGnbH3ov49Z/6e8LPLRR4=
tinygo.org/x/tinyterm v0.3.0 h1:4MMZoMyrbWbjru1KP/Z2TGhaguy/Uh5Mdhf/niemM8c=
tinygo.org/x/tinyterm v0.3.0/go.mod h1:F1pQjxEwNZQIc5czeJSBtk57ucEvbR4u7vHaLhWhHtg=

787
hci.go Normal file
View file

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

156
l2cap_hci.go Normal file
View file

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

5
nodebug.go Normal file
View file

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

View file

@ -1,19 +1,18 @@
// Code generated by bin/gen-service-uuids; DO NOT EDIT.
// This file was generated on 2022-12-21 19:21:51.011665984 +0100 CET m=+0.000615122 using the list of standard service UUIDs from
// https://github.com/NordicSemiconductor/bluetooth-numbers-database/blob/master/v1/service_uuids.json
//
package bluetooth
var (
// ServiceUUIDExperimentalButtonlessDFU - Experimental Buttonless DFU Service
ServiceUUIDExperimentalButtonlessDFU = NewUUID([16]byte{0x8e,0x40,0x00,0x01,0xf3,0x15,0x4f,0x60,0x9f,0xb8,0x83,0x88,0x30,0xda,0xea,0x50,})
ServiceUUIDExperimentalButtonlessDFU = NewUUID([16]byte{0x8e, 0x40, 0x00, 0x01, 0xf3, 0x15, 0x4f, 0x60, 0x9f, 0xb8, 0x83, 0x88, 0x30, 0xda, 0xea, 0x50})
// ServiceUUIDAdafruitTemperature - Adafruit Temperature Service
ServiceUUIDAdafruitTemperature = NewUUID([16]byte{0xad,0xaf,0x01,0x00,0xc3,0x32,0x42,0xa8,0x93,0xbd,0x25,0xe9,0x05,0x75,0x6c,0xb8,})
ServiceUUIDAdafruitTemperature = NewUUID([16]byte{0xad, 0xaf, 0x01, 0x00, 0xc3, 0x32, 0x42, 0xa8, 0x93, 0xbd, 0x25, 0xe9, 0x05, 0x75, 0x6c, 0xb8})
// ServiceUUIDAdafruitHumidity - Adafruit Humidity Service
ServiceUUIDAdafruitHumidity = NewUUID([16]byte{0xad,0xaf,0x07,0x00,0xc3,0x32,0x42,0xa8,0x93,0xbd,0x25,0xe9,0x05,0x75,0x6c,0xb8,})
ServiceUUIDAdafruitHumidity = NewUUID([16]byte{0xad, 0xaf, 0x07, 0x00, 0xc3, 0x32, 0x42, 0xa8, 0x93, 0xbd, 0x25, 0xe9, 0x05, 0x75, 0x6c, 0xb8})
// ServiceUUIDUserData - User Data
ServiceUUIDUserData = New16BitUUID(0x181C)
@ -22,16 +21,16 @@ var (
ServiceUUIDAudioStreamControl = New16BitUUID(0x184E)
// ServiceUUIDSMP - SMP Service
ServiceUUIDSMP = NewUUID([16]byte{0x8d,0x53,0xdc,0x1d,0x1d,0xb7,0x4c,0xd3,0x86,0x8b,0x8a,0x52,0x74,0x60,0xaa,0x84,})
ServiceUUIDSMP = NewUUID([16]byte{0x8d, 0x53, 0xdc, 0x1d, 0x1d, 0xb7, 0x4c, 0xd3, 0x86, 0x8b, 0x8a, 0x52, 0x74, 0x60, 0xaa, 0x84})
// ServiceUUIDAdafruitSound - Adafruit Sound Service
ServiceUUIDAdafruitSound = NewUUID([16]byte{0xad,0xaf,0x0b,0x00,0xc3,0x32,0x42,0xa8,0x93,0xbd,0x25,0xe9,0x05,0x75,0x6c,0xb8,})
ServiceUUIDAdafruitSound = NewUUID([16]byte{0xad, 0xaf, 0x0b, 0x00, 0xc3, 0x32, 0x42, 0xa8, 0x93, 0xbd, 0x25, 0xe9, 0x05, 0x75, 0x6c, 0xb8})
// ServiceUUIDPulseOximeter - Pulse Oximeter Service
ServiceUUIDPulseOximeter = New16BitUUID(0x1822)
// ServiceUUIDEddystoneConfiguration - Eddystone Configuration Service
ServiceUUIDEddystoneConfiguration = NewUUID([16]byte{0xa3,0xc8,0x75,0x00,0x8e,0xd3,0x4b,0xdf,0x8a,0x39,0xa0,0x1b,0xeb,0xed,0xe2,0x95,})
ServiceUUIDEddystoneConfiguration = NewUUID([16]byte{0xa3, 0xc8, 0x75, 0x00, 0x8e, 0xd3, 0x4b, 0xdf, 0x8a, 0x39, 0xa0, 0x1b, 0xeb, 0xed, 0xe2, 0x95})
// ServiceUUIDLocationAndNavigation - Location and Navigation
ServiceUUIDLocationAndNavigation = New16BitUUID(0x1819)
@ -46,10 +45,10 @@ var (
ServiceUUIDSignifyNetherlandsBVFormerlyPhilipsLighting = New16BitUUID(0xFE0F)
// ServiceUUIDPhilipsHueLightControl - Philips Hue Light Control Service
ServiceUUIDPhilipsHueLightControl = NewUUID([16]byte{0x93,0x2c,0x32,0xbd,0x00,0x00,0x47,0xa2,0x83,0x5a,0xa8,0xd4,0x55,0xb8,0x59,0xdd,})
ServiceUUIDPhilipsHueLightControl = NewUUID([16]byte{0x93, 0x2c, 0x32, 0xbd, 0x00, 0x00, 0x47, 0xa2, 0x83, 0x5a, 0xa8, 0xd4, 0x55, 0xb8, 0x59, 0xdd})
// ServiceUUIDThingyUI - Thingy UI Service
ServiceUUIDThingyUI = NewUUID([16]byte{0xef,0x68,0x03,0x00,0x9b,0x35,0x49,0x33,0x9b,0x10,0x52,0xff,0xa9,0x74,0x00,0x42,})
ServiceUUIDThingyUI = NewUUID([16]byte{0xef, 0x68, 0x03, 0x00, 0x9b, 0x35, 0x49, 0x33, 0x9b, 0x10, 0x52, 0xff, 0xa9, 0x74, 0x00, 0x42})
// ServiceUUIDAlertNotification - Alert Notification Service
ServiceUUIDAlertNotification = New16BitUUID(0x1811)
@ -64,16 +63,16 @@ var (
ServiceUUIDGenericAttribute = New16BitUUID(0x1801)
// ServiceUUIDAdafruitMagnetometer - Adafruit Magnetometer Service
ServiceUUIDAdafruitMagnetometer = NewUUID([16]byte{0xad,0xaf,0x05,0x00,0xc3,0x32,0x42,0xa8,0x93,0xbd,0x25,0xe9,0x05,0x75,0x6c,0xb8,})
ServiceUUIDAdafruitMagnetometer = NewUUID([16]byte{0xad, 0xaf, 0x05, 0x00, 0xc3, 0x32, 0x42, 0xa8, 0x93, 0xbd, 0x25, 0xe9, 0x05, 0x75, 0x6c, 0xb8})
// ServiceUUIDMicrophoneControl - Microphone Control
ServiceUUIDMicrophoneControl = New16BitUUID(0x184D)
// ServiceUUIDLEGOWirelessProtocolV3Bootloader - LEGO® Wireless Protocol v3 Bootloader Service
ServiceUUIDLEGOWirelessProtocolV3Bootloader = NewUUID([16]byte{0x00,0x00,0x16,0x25,0x12,0x12,0xef,0xde,0x16,0x23,0x78,0x5f,0xea,0xbc,0xd1,0x23,})
ServiceUUIDLEGOWirelessProtocolV3Bootloader = NewUUID([16]byte{0x00, 0x00, 0x16, 0x25, 0x12, 0x12, 0xef, 0xde, 0x16, 0x23, 0x78, 0x5f, 0xea, 0xbc, 0xd1, 0x23})
// ServiceUUIDAdafruitColor - Adafruit Color Service
ServiceUUIDAdafruitColor = NewUUID([16]byte{0xad,0xaf,0x0a,0x00,0xc3,0x32,0x42,0xa8,0x93,0xbd,0x25,0xe9,0x05,0x75,0x6c,0xb8,})
ServiceUUIDAdafruitColor = NewUUID([16]byte{0xad, 0xaf, 0x0a, 0x00, 0xc3, 0x32, 0x42, 0xa8, 0x93, 0xbd, 0x25, 0xe9, 0x05, 0x75, 0x6c, 0xb8})
// ServiceUUIDMeshProvisioning - Mesh Provisioning Service
ServiceUUIDMeshProvisioning = New16BitUUID(0x1827)
@ -82,22 +81,22 @@ var (
ServiceUUIDTransportDiscovery = New16BitUUID(0x1824)
// ServiceUUIDAppleReserved1 - Apple Reserved Service 1
ServiceUUIDAppleReserved1 = NewUUID([16]byte{0x7d,0xfc,0x60,0x00,0x7d,0x1c,0x49,0x51,0x86,0xaa,0x8d,0x97,0x28,0xf8,0xd6,0x6c,})
ServiceUUIDAppleReserved1 = NewUUID([16]byte{0x7d, 0xfc, 0x60, 0x00, 0x7d, 0x1c, 0x49, 0x51, 0x86, 0xaa, 0x8d, 0x97, 0x28, 0xf8, 0xd6, 0x6c})
// ServiceUUIDAppleReserved2 - Apple Reserved Service 2
ServiceUUIDAppleReserved2 = NewUUID([16]byte{0x7d,0xfc,0x70,0x00,0x7d,0x1c,0x49,0x51,0x86,0xaa,0x8d,0x97,0x28,0xf8,0xd6,0x6c,})
ServiceUUIDAppleReserved2 = NewUUID([16]byte{0x7d, 0xfc, 0x70, 0x00, 0x7d, 0x1c, 0x49, 0x51, 0x86, 0xaa, 0x8d, 0x97, 0x28, 0xf8, 0xd6, 0x6c})
// ServiceUUIDAppleReserved3 - Apple Reserved Service 3
ServiceUUIDAppleReserved3 = NewUUID([16]byte{0x7d,0xfc,0x80,0x00,0x7d,0x1c,0x49,0x51,0x86,0xaa,0x8d,0x97,0x28,0xf8,0xd6,0x6c,})
ServiceUUIDAppleReserved3 = NewUUID([16]byte{0x7d, 0xfc, 0x80, 0x00, 0x7d, 0x1c, 0x49, 0x51, 0x86, 0xaa, 0x8d, 0x97, 0x28, 0xf8, 0xd6, 0x6c})
// ServiceUUIDAppleReserved4 - Apple Reserved Service 4
ServiceUUIDAppleReserved4 = NewUUID([16]byte{0x7d,0xfc,0x90,0x00,0x7d,0x1c,0x49,0x51,0x86,0xaa,0x8d,0x97,0x28,0xf8,0xd6,0x6c,})
ServiceUUIDAppleReserved4 = NewUUID([16]byte{0x7d, 0xfc, 0x90, 0x00, 0x7d, 0x1c, 0x49, 0x51, 0x86, 0xaa, 0x8d, 0x97, 0x28, 0xf8, 0xd6, 0x6c})
// ServiceUUIDAdafruitButton - Adafruit Button Service
ServiceUUIDAdafruitButton = NewUUID([16]byte{0xad,0xaf,0x06,0x00,0xc3,0x32,0x42,0xa8,0x93,0xbd,0x25,0xe9,0x05,0x75,0x6c,0xb8,})
ServiceUUIDAdafruitButton = NewUUID([16]byte{0xad, 0xaf, 0x06, 0x00, 0xc3, 0x32, 0x42, 0xa8, 0x93, 0xbd, 0x25, 0xe9, 0x05, 0x75, 0x6c, 0xb8})
// ServiceUUIDMemfaultDiagnostic - Memfault Diagnostic Service
ServiceUUIDMemfaultDiagnostic = NewUUID([16]byte{0x54,0x22,0x00,0x00,0xf6,0xa5,0x40,0x07,0xa3,0x71,0x72,0x2f,0x4e,0xbd,0x84,0x36,})
ServiceUUIDMemfaultDiagnostic = NewUUID([16]byte{0x54, 0x22, 0x00, 0x00, 0xf6, 0xa5, 0x40, 0x07, 0xa3, 0x71, 0x72, 0x2f, 0x4e, 0xbd, 0x84, 0x36})
// ServiceUUIDScanParameters - Scan Parameters
ServiceUUIDScanParameters = New16BitUUID(0x1813)
@ -106,13 +105,13 @@ var (
ServiceUUIDCoordinatedSetIdentification = New16BitUUID(0x1846)
// ServiceUUIDMicrobitAccelerometer - micro:bit Accelerometer Service
ServiceUUIDMicrobitAccelerometer = NewUUID([16]byte{0xe9,0x5d,0x07,0x53,0x25,0x1d,0x47,0x0a,0xa0,0x62,0xfa,0x19,0x22,0xdf,0xa9,0xa8,})
ServiceUUIDMicrobitAccelerometer = NewUUID([16]byte{0xe9, 0x5d, 0x07, 0x53, 0x25, 0x1d, 0x47, 0x0a, 0xa0, 0x62, 0xfa, 0x19, 0x22, 0xdf, 0xa9, 0xa8})
// ServiceUUIDMicrobitEvent - micro:bit Event Service
ServiceUUIDMicrobitEvent = NewUUID([16]byte{0xe9,0x5d,0x93,0xaf,0x25,0x1d,0x47,0x0a,0xa0,0x62,0xfa,0x19,0x22,0xdf,0xa9,0xa8,})
ServiceUUIDMicrobitEvent = NewUUID([16]byte{0xe9, 0x5d, 0x93, 0xaf, 0x25, 0x1d, 0x47, 0x0a, 0xa0, 0x62, 0xfa, 0x19, 0x22, 0xdf, 0xa9, 0xa8})
// ServiceUUIDThingySound - Thingy Sound Service
ServiceUUIDThingySound = NewUUID([16]byte{0xef,0x68,0x05,0x00,0x9b,0x35,0x49,0x33,0x9b,0x10,0x52,0xff,0xa9,0x74,0x00,0x42,})
ServiceUUIDThingySound = NewUUID([16]byte{0xef, 0x68, 0x05, 0x00, 0x9b, 0x35, 0x49, 0x33, 0x9b, 0x10, 0x52, 0xff, 0xa9, 0x74, 0x00, 0x42})
// ServiceUUIDImmediateAlert - Immediate Alert
ServiceUUIDImmediateAlert = New16BitUUID(0x1802)
@ -127,10 +126,10 @@ var (
ServiceUUIDVolumeControl = New16BitUUID(0x1844)
// ServiceUUIDMicrobitMagnetometer - micro:bit Magnetometer Service
ServiceUUIDMicrobitMagnetometer = NewUUID([16]byte{0xe9,0x5d,0xf2,0xd8,0x25,0x1d,0x47,0x0a,0xa0,0x62,0xfa,0x19,0x22,0xdf,0xa9,0xa8,})
ServiceUUIDMicrobitMagnetometer = NewUUID([16]byte{0xe9, 0x5d, 0xf2, 0xd8, 0x25, 0x1d, 0x47, 0x0a, 0xa0, 0x62, 0xfa, 0x19, 0x22, 0xdf, 0xa9, 0xa8})
// ServiceUUIDMicrobitLED - micro:bit LED Service
ServiceUUIDMicrobitLED = NewUUID([16]byte{0xe9,0x5d,0xd9,0x1d,0x25,0x1d,0x47,0x0a,0xa0,0x62,0xfa,0x19,0x22,0xdf,0xa9,0xa8,})
ServiceUUIDMicrobitLED = NewUUID([16]byte{0xe9, 0x5d, 0xd9, 0x1d, 0x25, 0x1d, 0x47, 0x0a, 0xa0, 0x62, 0xfa, 0x19, 0x22, 0xdf, 0xa9, 0xa8})
// ServiceUUIDFileTransferByAdafruit - File Transfer Service by Adafruit
ServiceUUIDFileTransferByAdafruit = New16BitUUID(0xFEBB)
@ -151,16 +150,16 @@ var (
ServiceUUIDFastPair = New16BitUUID(0xFE2C)
// ServiceUUIDAdafruitGyroscope - Adafruit Gyroscope Service
ServiceUUIDAdafruitGyroscope = NewUUID([16]byte{0xad,0xaf,0x04,0x00,0xc3,0x32,0x42,0xa8,0x93,0xbd,0x25,0xe9,0x05,0x75,0x6c,0xb8,})
ServiceUUIDAdafruitGyroscope = NewUUID([16]byte{0xad, 0xaf, 0x04, 0x00, 0xc3, 0x32, 0x42, 0xa8, 0x93, 0xbd, 0x25, 0xe9, 0x05, 0x75, 0x6c, 0xb8})
// ServiceUUIDAdafruitProximity - Adafruit Proximity Service
ServiceUUIDAdafruitProximity = NewUUID([16]byte{0xad,0xaf,0x0e,0x00,0xc3,0x32,0x42,0xa8,0x93,0xbd,0x25,0xe9,0x05,0x75,0x6c,0xb8,})
ServiceUUIDAdafruitProximity = NewUUID([16]byte{0xad, 0xaf, 0x0e, 0x00, 0xc3, 0x32, 0x42, 0xa8, 0x93, 0xbd, 0x25, 0xe9, 0x05, 0x75, 0x6c, 0xb8})
// ServiceUUIDTelephoneBearer - Telephone Bearer
ServiceUUIDTelephoneBearer = New16BitUUID(0x184B)
// ServiceUUIDNordicLEDAndButton - Nordic LED and Button Service
ServiceUUIDNordicLEDAndButton = NewUUID([16]byte{0x00,0x00,0x15,0x23,0x12,0x12,0xef,0xde,0x15,0x23,0x78,0x5f,0xea,0xbc,0xd1,0x23,})
ServiceUUIDNordicLEDAndButton = NewUUID([16]byte{0x00, 0x00, 0x15, 0x23, 0x12, 0x12, 0xef, 0xde, 0x15, 0x23, 0x78, 0x5f, 0xea, 0xbc, 0xd1, 0x23})
// ServiceUUIDInternetProtocolSupport - Internet Protocol Support Service
ServiceUUIDInternetProtocolSupport = New16BitUUID(0x1820)
@ -169,7 +168,7 @@ var (
ServiceUUIDPublishedAudioCapabilities = New16BitUUID(0x1850)
// ServiceUUIDNordicUART - Nordic UART Service
ServiceUUIDNordicUART = NewUUID([16]byte{0x6e,0x40,0x00,0x01,0xb5,0xa3,0xf3,0x93,0xe0,0xa9,0xe5,0x0e,0x24,0xdc,0xca,0x9e,})
ServiceUUIDNordicUART = NewUUID([16]byte{0x6e, 0x40, 0x00, 0x01, 0xb5, 0xa3, 0xf3, 0x93, 0xe0, 0xa9, 0xe5, 0x0e, 0x24, 0xdc, 0xca, 0x9e})
// ServiceUUIDExposureNotification - Exposure Notification Service
ServiceUUIDExposureNotification = New16BitUUID(0xFD6F)
@ -187,7 +186,7 @@ var (
ServiceUUIDConstantToneExtension = New16BitUUID(0x184A)
// ServiceUUIDThingyWeatherStation - Thingy Weather Station Service
ServiceUUIDThingyWeatherStation = NewUUID([16]byte{0xef,0x68,0x02,0x00,0x9b,0x35,0x49,0x33,0x9b,0x10,0x52,0xff,0xa9,0x74,0x00,0x42,})
ServiceUUIDThingyWeatherStation = NewUUID([16]byte{0xef, 0x68, 0x02, 0x00, 0x9b, 0x35, 0x49, 0x33, 0x9b, 0x10, 0x52, 0xff, 0xa9, 0x74, 0x00, 0x42})
// ServiceUUIDHealthThermometer - Health Thermometer
ServiceUUIDHealthThermometer = New16BitUUID(0x1809)
@ -202,19 +201,19 @@ var (
ServiceUUIDReconnectionConfiguration = New16BitUUID(0x1829)
// ServiceUUIDAdafruitQuaternion - Adafruit Quaternion Service
ServiceUUIDAdafruitQuaternion = NewUUID([16]byte{0xad,0xaf,0x0d,0x00,0xc3,0x32,0x42,0xa8,0x93,0xbd,0x25,0xe9,0x05,0x75,0x6c,0xb8,})
ServiceUUIDAdafruitQuaternion = NewUUID([16]byte{0xad, 0xaf, 0x0d, 0x00, 0xc3, 0x32, 0x42, 0xa8, 0x93, 0xbd, 0x25, 0xe9, 0x05, 0x75, 0x6c, 0xb8})
// ServiceUUIDMeshProxy - Mesh Proxy Service
ServiceUUIDMeshProxy = New16BitUUID(0x1828)
// ServiceUUIDThingyMotion - Thingy Motion Service
ServiceUUIDThingyMotion = NewUUID([16]byte{0xef,0x68,0x04,0x00,0x9b,0x35,0x49,0x33,0x9b,0x10,0x52,0xff,0xa9,0x74,0x00,0x42,})
ServiceUUIDThingyMotion = NewUUID([16]byte{0xef, 0x68, 0x04, 0x00, 0x9b, 0x35, 0x49, 0x33, 0x9b, 0x10, 0x52, 0xff, 0xa9, 0x74, 0x00, 0x42})
// ServiceUUIDIndoorPositioning - Indoor Positioning
ServiceUUIDIndoorPositioning = New16BitUUID(0x1821)
// ServiceUUIDAdafruitAddressable - Adafruit Addressable Service
ServiceUUIDAdafruitAddressable = NewUUID([16]byte{0xad,0xaf,0x09,0x00,0xc3,0x32,0x42,0xa8,0x93,0xbd,0x25,0xe9,0x05,0x75,0x6c,0xb8,})
ServiceUUIDAdafruitAddressable = NewUUID([16]byte{0xad, 0xaf, 0x09, 0x00, 0xc3, 0x32, 0x42, 0xa8, 0x93, 0xbd, 0x25, 0xe9, 0x05, 0x75, 0x6c, 0xb8})
// ServiceUUIDBloodPressure - Blood Pressure
ServiceUUIDBloodPressure = New16BitUUID(0x1810)
@ -238,7 +237,7 @@ var (
ServiceUUIDBroadcastAudioAnnouncement = New16BitUUID(0x1852)
// ServiceUUIDMicrobitDFUControl - micro:bit DFU Control Service
ServiceUUIDMicrobitDFUControl = NewUUID([16]byte{0xe9,0x5d,0x93,0xb0,0x25,0x1d,0x47,0x0a,0xa0,0x62,0xfa,0x19,0x22,0xdf,0xa9,0xa8,})
ServiceUUIDMicrobitDFUControl = NewUUID([16]byte{0xe9, 0x5d, 0x93, 0xb0, 0x25, 0x1d, 0x47, 0x0a, 0xa0, 0x62, 0xfa, 0x19, 0x22, 0xdf, 0xa9, 0xa8})
// ServiceUUIDBondManagement - Bond Management Service
ServiceUUIDBondManagement = New16BitUUID(0x181E)
@ -247,7 +246,7 @@ var (
ServiceUUIDLinkLoss = New16BitUUID(0x1803)
// ServiceUUIDAdafruitAccelerometer - Adafruit Accelerometer Service
ServiceUUIDAdafruitAccelerometer = NewUUID([16]byte{0xad,0xaf,0x02,0x00,0xc3,0x32,0x42,0xa8,0x93,0xbd,0x25,0xe9,0x05,0x75,0x6c,0xb8,})
ServiceUUIDAdafruitAccelerometer = NewUUID([16]byte{0xad, 0xaf, 0x02, 0x00, 0xc3, 0x32, 0x42, 0xa8, 0x93, 0xbd, 0x25, 0xe9, 0x05, 0x75, 0x6c, 0xb8})
// ServiceUUIDReferenceTimeUpdate - Reference Time Update Service
ServiceUUIDReferenceTimeUpdate = New16BitUUID(0x1806)
@ -256,7 +255,7 @@ var (
ServiceUUIDTxPower = New16BitUUID(0x1804)
// ServiceUUIDMicrobitTemperature - micro:bit Temperature Service
ServiceUUIDMicrobitTemperature = NewUUID([16]byte{0xe9,0x5d,0x61,0x00,0x25,0x1d,0x47,0x0a,0xa0,0x62,0xfa,0x19,0x22,0xdf,0xa9,0xa8,})
ServiceUUIDMicrobitTemperature = NewUUID([16]byte{0xe9, 0x5d, 0x61, 0x00, 0x25, 0x1d, 0x47, 0x0a, 0xa0, 0x62, 0xfa, 0x19, 0x22, 0xdf, 0xa9, 0xa8})
// ServiceUUIDCyclingSpeedAndCadence - Cycling Speed and Cadence
ServiceUUIDCyclingSpeedAndCadence = New16BitUUID(0x1816)
@ -268,10 +267,10 @@ var (
ServiceUUIDTMAS = New16BitUUID(0x1855)
// ServiceUUIDMicrobitButton - micro:bit Button Service
ServiceUUIDMicrobitButton = NewUUID([16]byte{0xe9,0x5d,0x98,0x82,0x25,0x1d,0x47,0x0a,0xa0,0x62,0xfa,0x19,0x22,0xdf,0xa9,0xa8,})
ServiceUUIDMicrobitButton = NewUUID([16]byte{0xe9, 0x5d, 0x98, 0x82, 0x25, 0x1d, 0x47, 0x0a, 0xa0, 0x62, 0xfa, 0x19, 0x22, 0xdf, 0xa9, 0xa8})
// ServiceUUIDMicrobitIOPin - micro:bit IO Pin Service
ServiceUUIDMicrobitIOPin = NewUUID([16]byte{0xe9,0x5d,0x12,0x7b,0x25,0x1d,0x47,0x0a,0xa0,0x62,0xfa,0x19,0x22,0xdf,0xa9,0xa8,})
ServiceUUIDMicrobitIOPin = NewUUID([16]byte{0xe9, 0x5d, 0x12, 0x7b, 0x25, 0x1d, 0x47, 0x0a, 0xa0, 0x62, 0xfa, 0x19, 0x22, 0xdf, 0xa9, 0xa8})
// ServiceUUIDAutomationIO - Automation IO
ServiceUUIDAutomationIO = New16BitUUID(0x1815)
@ -280,28 +279,28 @@ var (
ServiceUUIDGlucose = New16BitUUID(0x1808)
// ServiceUUIDAppleMedia - Apple Media Service
ServiceUUIDAppleMedia = NewUUID([16]byte{0x89,0xd3,0x50,0x2b,0x0f,0x36,0x43,0x3a,0x8e,0xf4,0xc5,0x02,0xad,0x55,0xf8,0xdc,})
ServiceUUIDAppleMedia = NewUUID([16]byte{0x89, 0xd3, 0x50, 0x2b, 0x0f, 0x36, 0x43, 0x3a, 0x8e, 0xf4, 0xc5, 0x02, 0xad, 0x55, 0xf8, 0xdc})
// ServiceUUIDEdgeImpulseRemoteManagement - Edge Impulse Remote Management Service
ServiceUUIDEdgeImpulseRemoteManagement = NewUUID([16]byte{0xe2,0xa0,0x00,0x01,0xec,0x31,0x4e,0xc3,0xa9,0x7a,0x1c,0x34,0xd8,0x7e,0x98,0x78,})
ServiceUUIDEdgeImpulseRemoteManagement = NewUUID([16]byte{0xe2, 0xa0, 0x00, 0x01, 0xec, 0x31, 0x4e, 0xc3, 0xa9, 0x7a, 0x1c, 0x34, 0xd8, 0x7e, 0x98, 0x78})
// ServiceUUIDHeartRate - Heart Rate
ServiceUUIDHeartRate = New16BitUUID(0x180D)
// ServiceUUIDPhilipsHueLightUpdate - Philips Hue Light Update Service
ServiceUUIDPhilipsHueLightUpdate = NewUUID([16]byte{0xb8,0x84,0x3a,0xdd,0x00,0x00,0x4a,0xa1,0x87,0x94,0xc3,0xf4,0x62,0x03,0x0b,0xda,})
ServiceUUIDPhilipsHueLightUpdate = NewUUID([16]byte{0xb8, 0x84, 0x3a, 0xdd, 0x00, 0x00, 0x4a, 0xa1, 0x87, 0x94, 0xc3, 0xf4, 0x62, 0x03, 0x0b, 0xda})
// ServiceUUIDLegacyDFU - Legacy DFU Service
ServiceUUIDLegacyDFU = NewUUID([16]byte{0x00,0x00,0x15,0x30,0x12,0x12,0xef,0xde,0x15,0x23,0x78,0x5f,0xea,0xbc,0xd1,0x23,})
ServiceUUIDLegacyDFU = NewUUID([16]byte{0x00, 0x00, 0x15, 0x30, 0x12, 0x12, 0xef, 0xde, 0x15, 0x23, 0x78, 0x5f, 0xea, 0xbc, 0xd1, 0x23})
// ServiceUUIDLEGOWirelessProtocolV3Hub - LEGO® Wireless Protocol v3 Hub Service
ServiceUUIDLEGOWirelessProtocolV3Hub = NewUUID([16]byte{0x00,0x00,0x16,0x23,0x12,0x12,0xef,0xde,0x16,0x23,0x78,0x5f,0xea,0xbc,0xd1,0x23,})
ServiceUUIDLEGOWirelessProtocolV3Hub = NewUUID([16]byte{0x00, 0x00, 0x16, 0x23, 0x12, 0x12, 0xef, 0xde, 0x16, 0x23, 0x78, 0x5f, 0xea, 0xbc, 0xd1, 0x23})
// ServiceUUIDTexasInstrumentsOvertheAirDownloadOAD - Texas Instruments Over-the-Air Download (OAD) Service
ServiceUUIDTexasInstrumentsOvertheAirDownloadOAD = NewUUID([16]byte{0xf0,0x00,0xff,0xc0,0x04,0x51,0x40,0x00,0xb0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,})
ServiceUUIDTexasInstrumentsOvertheAirDownloadOAD = NewUUID([16]byte{0xf0, 0x00, 0xff, 0xc0, 0x04, 0x51, 0x40, 0x00, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
// ServiceUUIDHeliumHotspotCustom - Helium Hotspot Custom Service
ServiceUUIDHeliumHotspotCustom = NewUUID([16]byte{0x0f,0xda,0x92,0xb2,0x44,0xa2,0x4a,0xf2,0x84,0xf5,0xfa,0x68,0x2b,0xaa,0x2b,0x8d,})
ServiceUUIDHeliumHotspotCustom = NewUUID([16]byte{0x0f, 0xda, 0x92, 0xb2, 0x44, 0xa2, 0x4a, 0xf2, 0x84, 0xf5, 0xfa, 0x68, 0x2b, 0xaa, 0x2b, 0x8d})
// ServiceUUIDObjectTransfer - Object Transfer Service
ServiceUUIDObjectTransfer = New16BitUUID(0x1825)
@ -316,16 +315,16 @@ var (
ServiceUUIDVolumeOffsetControl = New16BitUUID(0x1845)
// ServiceUUIDThingyConfiguration - Thingy Configuration Service
ServiceUUIDThingyConfiguration = NewUUID([16]byte{0xef,0x68,0x01,0x00,0x9b,0x35,0x49,0x33,0x9b,0x10,0x52,0xff,0xa9,0x74,0x00,0x42,})
ServiceUUIDThingyConfiguration = NewUUID([16]byte{0xef, 0x68, 0x01, 0x00, 0x9b, 0x35, 0x49, 0x33, 0x9b, 0x10, 0x52, 0xff, 0xa9, 0x74, 0x00, 0x42})
// ServiceUUIDAdafruitLight - Adafruit Light Service
ServiceUUIDAdafruitLight = NewUUID([16]byte{0xad,0xaf,0x03,0x00,0xc3,0x32,0x42,0xa8,0x93,0xbd,0x25,0xe9,0x05,0x75,0x6c,0xb8,})
ServiceUUIDAdafruitLight = NewUUID([16]byte{0xad, 0xaf, 0x03, 0x00, 0xc3, 0x32, 0x42, 0xa8, 0x93, 0xbd, 0x25, 0xe9, 0x05, 0x75, 0x6c, 0xb8})
// ServiceUUIDAdafruitBarometric - Adafruit Barometric Service
ServiceUUIDAdafruitBarometric = NewUUID([16]byte{0xad,0xaf,0x08,0x00,0xc3,0x32,0x42,0xa8,0x93,0xbd,0x25,0xe9,0x05,0x75,0x6c,0xb8,})
ServiceUUIDAdafruitBarometric = NewUUID([16]byte{0xad, 0xaf, 0x08, 0x00, 0xc3, 0x32, 0x42, 0xa8, 0x93, 0xbd, 0x25, 0xe9, 0x05, 0x75, 0x6c, 0xb8})
// ServiceUUIDAdafruitTone - Adafruit Tone Service
ServiceUUIDAdafruitTone = NewUUID([16]byte{0xad,0xaf,0x0c,0x00,0xc3,0x32,0x42,0xa8,0x93,0xbd,0x25,0xe9,0x05,0x75,0x6c,0xb8,})
ServiceUUIDAdafruitTone = NewUUID([16]byte{0xad, 0xaf, 0x0c, 0x00, 0xc3, 0x32, 0x42, 0xa8, 0x93, 0xbd, 0x25, 0xe9, 0x05, 0x75, 0x6c, 0xb8})
// ServiceUUIDBattery - Battery Service
ServiceUUIDBattery = New16BitUUID(0x180F)
@ -337,7 +336,7 @@ var (
ServiceUUIDAudioInputControl = New16BitUUID(0x1843)
// ServiceUUIDAppleNotificationCenter - Apple Notification Center Service
ServiceUUIDAppleNotificationCenter = NewUUID([16]byte{0x79,0x05,0xf4,0x31,0xb5,0xce,0x4e,0x99,0xa4,0x0f,0x4b,0x1e,0x12,0x2d,0x00,0xd0,})
ServiceUUIDAppleNotificationCenter = NewUUID([16]byte{0x79, 0x05, 0xf4, 0x31, 0xb5, 0xce, 0x4e, 0x99, 0xa4, 0x0f, 0x4b, 0x1e, 0x12, 0x2d, 0x00, 0xd0})
// ServiceUUIDGenericAccess - Generic Access
ServiceUUIDGenericAccess = New16BitUUID(0x1800)
@ -350,5 +349,4 @@ var (
// ServiceUUIDHTTPProxy - HTTP Proxy
ServiceUUIDHTTPProxy = New16BitUUID(0x1823)
)

View file

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

View file

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

22
uuid.go
View file

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

20
uuid_hci.go Normal file
View file

@ -0,0 +1,20 @@
//go:build hci || ninafw || cyw43439
package bluetooth
type shortUUID uint16
// UUID returns the full length UUID for this short UUID.
func (s shortUUID) UUID() UUID {
return New16BitUUID(uint16(s))
}
// isIn checks the passed in slice of UUIDs to see if this uuid is in it.
func (uuid UUID) isIn(uuids []UUID) bool {
for _, u := range uuids {
if u == uuid {
return true
}
}
return false
}

View file

@ -10,9 +10,9 @@ import "unsafe"
type shortUUID C.ble_uuid_t
func (uuid UUID) shortUUID() (C.ble_uuid_t, uint32) {
func (uuid UUID) shortUUID() (C.ble_uuid_t, C.uint32_t) {
var short C.ble_uuid_t
short.uuid = uint16(uuid[3])
short.uuid = C.uint16_t(uuid[3])
if uuid.Is16Bit() {
short._type = C.BLE_UUID_TYPE_BLE
return short, 0
@ -24,7 +24,7 @@ func (uuid UUID) shortUUID() (C.ble_uuid_t, uint32) {
// UUID returns the full length UUID for this short UUID.
func (s shortUUID) UUID() UUID {
if s._type == C.BLE_UUID_TYPE_BLE {
return New16BitUUID(s.uuid)
return New16BitUUID(uint16(s.uuid))
}
var outLen C.uint8_t
var outUUID UUID

View file

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