Compare commits

...

79 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
Hari Bhaskaran
4da7f58124 Include WriteWithoutResponse permission, for examples, where Write exists 2023-08-11 14:26:37 +02:00
Erik de Vries
d9490f73ea Update dependencies - add documentation to heartrate-monitor 2023-08-11 12:49:03 +02:00
Binozo
d5276e5aed added manufacturer data support on SoftDevices 2023-08-11 11:52:12 +02:00
Binozo
da2032de42 Added option to add ManufacturerData to Advertisement 2023-08-11 11:52:12 +02:00
Ayke van Laethem
c3f9d593de sd: test creation of raw BLE advertisement packets
I realized we didn't have any tests for this yet, which we really should
have. So here they are.
2023-08-05 23:09:52 +02:00
Yurii Soldak
f9436906c1 all: go 1.18 and remove old-style build tags 2023-07-31 15:54:58 +02:00
Baden Parr
3c9cf83de2 macos: enable support for duplicate chars by moving from a map to a slice 2023-07-16 20:53:17 +02:00
Jagoba Gascón
4d067bc2b3 winrt-go: bump to latest
This version fixes critical bugs that caused random errors at runtime.
For more details see saltosystems/winrt-go/issues#71 and
saltosystems/winrt-go/issues#72. Diff:
e096b9a...c792451
2023-06-14 08:42:31 +02:00
deadprogram
1c44c024fd modules: update to latest TinyGo drivers and friends
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-06-12 01:46:46 +02:00
deadprogram
ba63457646 release: preparing for v0.7.0 release
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-06-12 01:46:46 +02:00
deadprogram
7f67fa0275 Update LICENSE year
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-06-12 01:46:46 +02:00
85 changed files with 6210 additions and 1075 deletions

View file

@ -26,8 +26,5 @@ jobs:
run: make smoketest-tinygo run: make smoketest-tinygo
- name: Run Linux smoke tests - name: Run Linux smoke tests
run: make smoketest-linux run: make smoketest-linux
- name: Install Windows cross compiler - name: "Run Windows cross-compiled smoke tests"
run: |
apt-get install -y gcc-mingw-w64-x86-64
- name: "Run Windows smoke tests"
run: make smoketest-windows run: make smoketest-windows

View file

@ -12,16 +12,46 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
build: macos-11:
name: build name: macos-11
runs-on: macos-11 runs-on: macos-11
steps: steps:
- name: Install Go - name: Install Go
uses: actions/setup-go@v2 uses: actions/setup-go@v4.1.0
with: with:
go-version: '1.18.3' go-version: '1.18.3'
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3.6.0
- name: Run unit tests
run: go test
- name: "Run macOS smoke tests"
run: make smoketest-macos
macos-12:
name: macos-12
runs-on: macos-12
steps:
- name: Install Go
uses: actions/setup-go@v4.1.0
with:
go-version: '1.21.0'
- name: Checkout
uses: actions/checkout@v3.6.0
- name: Run unit tests
run: go test
- name: "Run macOS smoke tests"
run: make smoketest-macos
macos-13:
name: macos-13
runs-on: macos-13
steps:
- name: Install Go
uses: actions/setup-go@v4.1.0
with:
go-version: '1.21.0'
- name: Checkout
uses: actions/checkout@v3.6.0
- name: Run unit tests - name: Run unit tests
run: go test run: go test
- name: "Run macOS smoke tests" - 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,149 @@
0.10.0
---
* **core**
- gap: fix ServiceDataElement.UUID comment
* **docs**
- add mention of support for rp2040-W to README
- Improve documentation of RSSI Fixes https://github.com/tinygo-org/bluetooth/issues/272
* **hci**
- cyw43439: HCI implementation
- refactor to separate HCI transport implementation from interface to not always assume UART.
- update for cyw43439 HCI functionality
* **windows**
- Add Address field to Windows Device struct
- Winrt full support (#266)
- winrt-go: bump to latest
- assign char handle write event (#274)
* **test**
- add hci_uart based implementation to smoke tests
0.9.0
---
* **build**
- add arduino-nano33 and pyportal to smoke tests
- add nina-fw smoketest as peripheral
- add some ninafw examples to smoketest
* **core**
- add ServiceData advertising element (#243)
- add RequestConnectionParams to request new connection parameters
- change ManufacturerData from a map to a slice
- don't use a pointer receiver for many method calls
- make Device a value instead of a pointer
- use 'debug' variable protected by build tags for debug logging
- use Device instead of Address in SetConnectHandler
* **docs**
- a small mention of the NINA BLE support
- complete README info about nina-fw support
* **linux**
- fix characteristic value
- rewrite everything to use DBus directly
* **macos**
- add Write command to the gattc implementation
* **examples**
- tinyscan to replace clue-scanner, also works on pyportal and pybadge+airlift
- update MCU central examples to use ldflags to pass the desired device to connect to
- discover: add MTU
* **hci**
- add check for poll buffer overflow
- allow for both ninafw and pure hci uart adapter implementations
- implement Characteristic WriteHandler
- multiple connections
- return service UUIDs with scan results
- add l2cap signaling support
- implement evtNumCompPkts to count in-flight packets
- correct implementation for WriteWithoutReponse
- speed up time waiting for hardware - corrections to MTU exchange
- add support for software RTS/CTS flow control for boards where hardware support is not available
- BLE central implementation on nina-fw co-processors
- fix connection timeout
- implement BLE peripheral support
- implement GetMTU()
- remove some pointer receivers from method calls
- should support muliple connections as a central
- correctly return from read requests instead of returning spurious error
- move some steps previously being done during Configure() into Start() where they more correctly belonged.
- use advertising display name as the correct default value for the generic access characteristic.
- speed up the polling for new notifications for Centrals
- use NINA settings from board file in main TinyGo repo
* **nordic semi**
- replace unsafe.SliceData call with expression that is still supported in older Go versions
- update to prepare for changes in the TinyGo CGo implementation
- add address of connecting device
- add support for connection timeout on connect
- don't send a notify/indicate without a CCCD
- fix connect timeout
- fix writing to a characteristic
- print connection parameters when debug is enabled
- return an error on a connection timeout
* **windows**
- Release AsyncOperationCompletedHandler (#208)
- check for error when scanning
- bump to latest winrt
0.8.0
---
* **build**
- remove CGo dependencies for Windows cross-compiler tests
- add Windows to GH actions build jobs
- add macOS 12 to GH actions build jobs
* **core**
- go 1.18 and remove old-style build tags
- Noescape workaround
* **docs**
- update README to remove CGo requirement for Windows
- add documentation to heartrate-monitor
* **linux**
- Added option to add ManufacturerData to Advertisement
* **macos**
- enable support for duplicate chars by moving from a map to a slice
* **examples**
- Include WriteWithoutResponse permission, for examples, where Write exists
* **nordic semi**
- softdevice: added manufacturer data support
- softdevice: test creation of raw BLE advertisement packets
* **windows**
- update github.com/saltosystems/winrt-go to no longer require CGo
0.7.0
---
* **build**
- switch to ghcr.io for docker container
- update to actions/checkout@v3
- work around for CVE-2022-24765
* **core**
- gap: Set and SetRandom methods should have a pointer receiver
- mtu-{darwin,linux,windows,sd}: add get mtu function
- remove Addresser
- update uuid generation
* **docs**
- CONTRIBUTING: add note on new APIs
- correct badge link for GH actions
- README: add note on macOS Big Sur and iTerm2
* **linux**
- do not randomize order of returned discovered services/chars
- fix characteristic scan order
- implement disconnect handling
* **macos**
- implement disconnect handling
- fix characteristic scan order
* **examples**
- add examples/stop-advertisement
* **nordic semi**
- nrf528xx: handle BLE_GAP_EVT_PHY_UPDATE_REQUEST and explicitly ignore some other events
- softdevice: avoid a heap allocation in the SoftDevice event handler
* **windows**
- Added Indicate support to Windows driver
- gap/windows: Scan should set scanning mode to active to match other platforms
- support empty manufacturer data
- winrt-go: bump to latest
0.6.0 0.6.0
--- ---
* **core** * **core**

View file

@ -1,4 +1,4 @@
Copyright (c) 2019-2022 TinyGo Authors. All rights reserved. Copyright (c) 2019-2023 TinyGo Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are modification, are permitted provided that the following conditions are

View file

@ -11,6 +11,8 @@ smoketest-tinygo:
@md5sum test.hex @md5sum test.hex
$(TINYGO) build -o test.uf2 -size=short -target=circuitplay-bluefruit ./examples/circuitplay $(TINYGO) build -o test.uf2 -size=short -target=circuitplay-bluefruit ./examples/circuitplay
@md5sum test.hex @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 $(TINYGO) build -o test.uf2 -size=short -target=circuitplay-bluefruit ./examples/discover
@md5sum test.hex @md5sum test.hex
$(TINYGO) build -o test.hex -size=short -target=pca10040-s132v6 ./examples/heartrate $(TINYGO) build -o test.hex -size=short -target=pca10040-s132v6 ./examples/heartrate
@ -32,10 +34,25 @@ smoketest-tinygo:
@md5sum test.hex @md5sum test.hex
$(TINYGO) build -o test.hex -size=short -target=microbit-v2-s113v7 ./examples/nusserver $(TINYGO) build -o test.hex -size=short -target=microbit-v2-s113v7 ./examples/nusserver
@md5sum test.hex @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: smoketest-linux:
# Test on 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/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
GOOS=linux go build -o /tmp/go-build-discard ./examples/heartrate-monitor GOOS=linux go build -o /tmp/go-build-discard ./examples/heartrate-monitor
GOOS=linux go build -o /tmp/go-build-discard ./examples/nusserver GOOS=linux go build -o /tmp/go-build-discard ./examples/nusserver
@ -44,9 +61,11 @@ smoketest-linux:
smoketest-windows: smoketest-windows:
# Test on 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 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 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/heartrate-monitor
GOOS=windows go build -o /tmp/go-build-discard ./examples/advertisement
GOOS=windows go build -o /tmp/go-build-discard ./examples/heartrate
smoketest-macos: smoketest-macos:
# Test on macos. # Test on macos.

View file

@ -2,13 +2,13 @@
[![Go Bluetooth](./images/gobluetooth.png)](https://tinygo.org/bluetooth) [![Go Bluetooth](./images/gobluetooth.png)](https://tinygo.org/bluetooth)
[![PkgGoDev](https://pkg.go.dev/badge/pkg.go.dev/tinygo.org/x/bluetooth)](https://pkg.go.dev/tinygo.org/x/bluetooth) [![Linux](https://github.com/tinygo-org/bluetooth/actions/workflows/linux.yml/badge.svg?branch=dev)](https://github.com/tinygo-org/bluetooth/actions/workflows/linux.yml) [![macOS](https://github.com/tinygo-org/bluetooth/actions/workflows/macos.yml/badge.svg?branch=dev)](https://github.com/tinygo-org/bluetooth/actions/workflows/macos.yml) [![PkgGoDev](https://pkg.go.dev/badge/pkg.go.dev/gitrepo.ru/neonxp/bluetooth)](https://pkg.go.dev/gitrepo.ru/neonxp/bluetooth) [![Linux](https://github.com/tinygo-org/bluetooth/actions/workflows/linux.yml/badge.svg?branch=dev)](https://github.com/tinygo-org/bluetooth/actions/workflows/linux.yml) [![macOS](https://github.com/tinygo-org/bluetooth/actions/workflows/macos.yml/badge.svg?branch=dev)](https://github.com/tinygo-org/bluetooth/actions/workflows/macos.yml)
Go Bluetooth is a cross-platform package for using [Bluetooth Low Energy](https://en.wikipedia.org/wiki/Bluetooth_Low_Energy) hardware from the Go programming language. Go Bluetooth is a cross-platform package for using [Bluetooth Low Energy](https://en.wikipedia.org/wiki/Bluetooth_Low_Energy) hardware from the Go programming language.
It works on typical operating systems such as [Linux](#linux), [macOS](#macos), and [Windows](#windows). It works on typical operating systems such as [Linux](#linux), [macOS](#macos), and [Windows](#windows).
It can also be used running "bare metal" on microcontrollers produced by [Nordic Semiconductor](https://www.nordicsemi.com/) by using [TinyGo](https://tinygo.org/). It can also be used running "bare metal" on microcontrollers produced by [Nordic Semiconductor](https://www.nordicsemi.com/) or using the Bluetooth Host Controller Interface (HCI) by using [TinyGo](https://tinygo.org/).
The Go Bluetooth package can be used to create both Bluetooth Low Energy Centrals as well as to create Bluetooth Low Energy Peripherals. The Go Bluetooth package can be used to create both Bluetooth Low Energy Centrals as well as to create Bluetooth Low Energy Peripherals.
@ -22,7 +22,7 @@ This example shows a central that scans for peripheral devices and then displays
package main package main
import ( import (
"tinygo.org/x/bluetooth" "gitrepo.ru/neonxp/bluetooth"
) )
var adapter = bluetooth.DefaultAdapter var adapter = bluetooth.DefaultAdapter
@ -58,7 +58,7 @@ package main
import ( import (
"time" "time"
"tinygo.org/x/bluetooth" "gitrepo.ru/neonxp/bluetooth"
) )
var adapter = bluetooth.DefaultAdapter var adapter = bluetooth.DefaultAdapter
@ -92,21 +92,21 @@ func must(action string, err error) {
## Current support ## Current support
| | Linux | macOS | Windows | Nordic Semi | | | Linux | macOS | Windows | Nordic Semi | ESP32 (NINA-FW) | CYW43439 (RP2040-W) |
| -------------------------------- | ------------------ | ------------------ | ------------------ | ------------------ | | -------------------------------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------- |
| API used | BlueZ | CoreBluetooth | WinRT | SoftDevice | | API used | BlueZ | CoreBluetooth | WinRT | SoftDevice | HCI | HCI |
| Scanning | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | Scanning | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Connect to peripheral | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | 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: | | 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: | | 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: | | 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: | | 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: | | 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: | | Send notifications | :heavy_check_mark: | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
## Linux ## 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. 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 ### 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) You can obtain the Go Bluetooth package using Git:
Once you have done this, you can obtain the Go Bluetooth package using Git:
git clone https://github.com/tinygo-org/bluetooth.git 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. 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 ## API stability
**The API is not stable!** Because many features are not yet implemented and some platforms (e.g. Windows and macOS) are not yet fully supported, it's hard to say what a good API will be. Therefore, if you want stability you should pick a particular git commit and use that. Go modules can be useful for this purpose. **The API is not stable!** Because many features are not yet implemented and some platforms (e.g. Windows and macOS) are not yet fully supported, it's hard to say what a good API will be. Therefore, if you want stability you should pick a particular git commit and use that. Go modules can be useful for this purpose.

View file

@ -1,11 +1,8 @@
package bluetooth 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 // 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 disconnects. You must call this before you call adaptor.Connect() for centrals
// or adaptor.Start() for peripherals in order for it to work. // 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 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. // used to allow multiple callers to call Connect concurrently.
connectMap sync.Map connectMap sync.Map
connectHandler func(device Address, connected bool) connectHandler func(device Device, connected bool)
} }
// DefaultAdapter is the default adapter on the system. // DefaultAdapter is the default adapter on the system.
@ -35,7 +35,7 @@ var DefaultAdapter = &Adapter{
pm: cbgo.NewPeripheralManager(nil), pm: cbgo.NewPeripheralManager(nil),
connectMap: sync.Map{}, connectMap: sync.Map{},
connectHandler: func(device Address, connected bool) { connectHandler: func(device Device, connected bool) {
return return
}, },
} }
@ -106,7 +106,7 @@ func (cmd *centralManagerDelegate) DidDisconnectPeripheral(cmgr cbgo.CentralMana
addr := Address{} addr := Address{}
uuid, _ := ParseUUID(id) uuid, _ := ParseUUID(id)
addr.UUID = uuid 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 // 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 // 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) serviceUUIDs = append(serviceUUIDs, parsedUUID)
} }
manufacturerData := make(map[uint16][]byte) var manufacturerData []ManufacturerDataElement
if len(advFields.ManufacturerData) > 2 { if len(advFields.ManufacturerData) > 2 {
// Note: CoreBluetooth seems to assume there can be only one
// manufacturer data fields in an advertisement packet, while the
// specification allows multiple such fields. See the Bluetooth Core
// Specification Supplement, table 1.1:
// https://www.bluetooth.com/specifications/css-11/
manufacturerID := uint16(advFields.ManufacturerData[0]) manufacturerID := uint16(advFields.ManufacturerData[0])
manufacturerID += uint16(advFields.ManufacturerData[1]) << 8 manufacturerID += uint16(advFields.ManufacturerData[1]) << 8
manufacturerData[manufacturerID] = advFields.ManufacturerData[2:] manufacturerData = append(manufacturerData, ManufacturerDataElement{
CompanyID: manufacturerID,
Data: advFields.ManufacturerData[2:],
})
}
var serviceData []ServiceDataElement
for _, svcData := range advFields.ServiceData {
cbgoUUID := svcData.UUID
uuid, err := ParseUUID(cbgoUUID.String())
if err != nil {
continue
}
serviceData = append(serviceData, ServiceDataElement{
UUID: uuid,
Data: svcData.Data,
})
} }
// Peripheral UUID is randomized on macOS, which means to // Peripheral UUID is randomized on macOS, which means to
@ -160,6 +181,7 @@ func makeScanResult(prph cbgo.Peripheral, advFields cbgo.AdvFields, rssi int) Sc
LocalName: advFields.LocalName, LocalName: advFields.LocalName,
ServiceUUIDs: serviceUUIDs, ServiceUUIDs: serviceUUIDs,
ManufacturerData: manufacturerData, ManufacturerData: manufacturerData,
ServiceData: serviceData,
}, },
}, },
} }

205
adapter_hci.go Normal file
View file

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

122
adapter_hci_uart.go Normal file
View file

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

View file

@ -1,5 +1,4 @@
//go:build !baremetal //go:build !baremetal
// +build !baremetal
// Some documentation for the BlueZ D-Bus interface: // Some documentation for the BlueZ D-Bus interface:
// https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc // https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc
@ -8,18 +7,23 @@ package bluetooth
import ( import (
"errors" "errors"
"fmt"
"github.com/muka/go-bluetooth/api" "github.com/godbus/dbus/v5"
"github.com/muka/go-bluetooth/bluez/profile/adapter"
) )
const defaultAdapter = "hci0"
type Adapter struct { type Adapter struct {
adapter *adapter.Adapter1
id string 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 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 // DefaultAdapter is the default adapter on the system. On Linux, it is the
@ -27,29 +31,38 @@ type Adapter struct {
// //
// Make sure to call Enable() before using it to initialize the adapter. // Make sure to call Enable() before using it to initialize the adapter.
var DefaultAdapter = &Adapter{ var DefaultAdapter = &Adapter{
connectHandler: func(device Address, connected bool) { id: defaultAdapter,
return connectHandler: func(device Device, connected bool) {
}, },
} }
// Enable configures the BLE stack. It must be called before any // Enable configures the BLE stack. It must be called before any
// Bluetooth-related calls (unless otherwise indicated). // Bluetooth-related calls (unless otherwise indicated).
func (a *Adapter) Enable() (err error) { func (a *Adapter) Enable() (err error) {
if a.id == "" { bus, err := dbus.SystemBus()
a.adapter, err = api.GetDefaultAdapter()
if err != nil { 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 return nil
} }
func (a *Adapter) Address() (MACAddress, error) { func (a *Adapter) Address() (MACAddress, error) {
if a.adapter == nil { if a.address == "" {
return MACAddress{}, errors.New("adapter not enabled") return MACAddress{}, errors.New("adapter not enabled")
} }
mac, err := ParseMAC(a.adapter.Properties.Address) mac, err := ParseMAC(a.address)
if err != nil { if err != nil {
return MACAddress{}, err 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

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

@ -1,5 +1,4 @@
//go:build (softdevice && s132v6) || (softdevice && s140v6) || (softdevice && s140v7) //go:build (softdevice && s132v6) || (softdevice && s140v6) || (softdevice && s140v7)
// +build softdevice,s132v6 softdevice,s140v6 softdevice,s140v7
package bluetooth package bluetooth
@ -26,20 +25,24 @@ func handleEvent() {
switch id { switch id {
case C.BLE_GAP_EVT_CONNECTED: case C.BLE_GAP_EVT_CONNECTED:
connectEvent := gapEvent.params.unionfield_connected() connectEvent := gapEvent.params.unionfield_connected()
device := Device{
Address: Address{makeMACAddress(connectEvent.peer_addr)},
connectionHandle: gapEvent.conn_handle,
}
switch connectEvent.role { switch connectEvent.role {
case C.BLE_GAP_ROLE_PERIPH: case C.BLE_GAP_ROLE_PERIPH:
if debug { if debug {
println("evt: connected in peripheral role") println("evt: connected in peripheral role")
} }
currentConnection.Reg = gapEvent.conn_handle currentConnection.handle.Reg = uint16(gapEvent.conn_handle)
DefaultAdapter.connectHandler(Address{}, true) DefaultAdapter.connectHandler(device, true)
case C.BLE_GAP_ROLE_CENTRAL: case C.BLE_GAP_ROLE_CENTRAL:
if debug { if debug {
println("evt: connected in central role") println("evt: connected in central role")
} }
connectionAttempt.connectionHandle = gapEvent.conn_handle connectionAttempt.connectionHandle = gapEvent.conn_handle
connectionAttempt.state.Set(2) // connection was successful connectionAttempt.state.Set(2) // connection was successful
DefaultAdapter.connectHandler(Address{}, true) DefaultAdapter.connectHandler(device, true)
} }
case C.BLE_GAP_EVT_DISCONNECTED: case C.BLE_GAP_EVT_DISCONNECTED:
if debug { if debug {
@ -47,11 +50,11 @@ func handleEvent() {
} }
// Clean up state for this connection. // Clean up state for this connection.
for i, cb := range gattcNotificationCallbacks { for i, cb := range gattcNotificationCallbacks {
if cb.connectionHandle == currentConnection.Reg { if uint16(cb.connectionHandle) == currentConnection.handle.Reg {
gattcNotificationCallbacks[i].valueHandle = 0 // 0 means invalid 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. // Auto-restart advertisement if needed.
if defaultAdvertisement.isAdvertising.Get() != 0 { if defaultAdvertisement.isAdvertising.Get() != 0 {
// The advertisement was running but was automatically stopped // The advertisement was running but was automatically stopped
@ -62,10 +65,21 @@ func handleEvent() {
// necessary. // necessary.
C.sd_ble_gap_adv_start(defaultAdvertisement.handle, C.BLE_CONN_CFG_TAG_DEFAULT) 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: case C.BLE_GAP_EVT_ADV_REPORT:
advReport := gapEvent.params.unionfield_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. // Sanity check.
panic("scanReportBuffer != advReport.p_data") panic("scanReportBuffer != advReport.p_data")
} }
@ -74,8 +88,7 @@ func handleEvent() {
scanReportBuffer.len = byte(advReport.data.len) scanReportBuffer.len = byte(advReport.data.len)
globalScanResult.RSSI = int16(advReport.rssi) globalScanResult.RSSI = int16(advReport.rssi)
globalScanResult.Address = Address{ globalScanResult.Address = Address{
MACAddress{MAC: advReport.peer_addr.addr, makeMACAddress(advReport.peer_addr),
isRandom: advReport.peer_addr.bitfield_addr_type() != 0},
} }
globalScanResult.AdvertisementPayload = &scanReportBuffer globalScanResult.AdvertisementPayload = &scanReportBuffer
// Signal to the main thread that there was a scan report. // Signal to the main thread that there was a scan report.
@ -102,6 +115,21 @@ func handleEvent() {
C.sd_ble_gap_phy_update(gapEvent.conn_handle, &phyUpdateRequest.peer_preferred_phys) C.sd_ble_gap_phy_update(gapEvent.conn_handle, &phyUpdateRequest.peer_preferred_phys)
case C.BLE_GAP_EVT_PHY_UPDATE: case C.BLE_GAP_EVT_PHY_UPDATE:
// ignore confirmation of phy successfully updated // 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: default:
if debug { if debug {
println("unknown GAP event:", id) println("unknown GAP event:", id)

View file

@ -1,5 +1,4 @@
//go:build softdevice && s113v7 //go:build softdevice && s113v7
// +build softdevice,s113v7
package bluetooth package bluetooth
@ -28,13 +27,18 @@ func handleEvent() {
if debug { if debug {
println("evt: connected in peripheral role") println("evt: connected in peripheral role")
} }
currentConnection.Reg = gapEvent.conn_handle currentConnection.handle.Reg = uint16(gapEvent.conn_handle)
DefaultAdapter.connectHandler(Address{}, true) 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: case C.BLE_GAP_EVT_DISCONNECTED:
if debug { if debug {
println("evt: disconnected") println("evt: disconnected")
} }
currentConnection.Reg = C.BLE_CONN_HANDLE_INVALID currentConnection.handle.Reg = C.BLE_CONN_HANDLE_INVALID
// Auto-restart advertisement if needed. // Auto-restart advertisement if needed.
if defaultAdvertisement.isAdvertising.Get() != 0 { if defaultAdvertisement.isAdvertising.Get() != 0 {
// The advertisement was running but was automatically stopped // The advertisement was running but was automatically stopped
@ -45,7 +49,10 @@ func handleEvent() {
// necessary. // necessary.
C.sd_ble_gap_adv_start(defaultAdvertisement.handle, C.BLE_CONN_CFG_TAG_DEFAULT) 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: case C.BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST:
// We need to respond with sd_ble_gap_data_length_update. Setting // We need to respond with sd_ble_gap_data_length_update. Setting
// both parameters to nil will make sure we send the default values. // both parameters to nil will make sure we send the default values.

View file

@ -1,5 +1,4 @@
//go:build (softdevice && s113v7) || (softdevice && s132v6) || (softdevice && s140v6) || (softdevice && s140v7) //go:build (softdevice && s113v7) || (softdevice && s132v6) || (softdevice && s140v6) || (softdevice && s140v7)
// +build softdevice,s113v7 softdevice,s132v6 softdevice,s140v6 softdevice,s140v7
package bluetooth package bluetooth
@ -47,7 +46,7 @@ func (a *Adapter) enable() error {
} }
// Enable the BLE stack. // Enable the BLE stack.
appRAMBase := uint32(uintptr(unsafe.Pointer(&appRAMBase))) appRAMBase := C.uint32_t(uintptr(unsafe.Pointer(&appRAMBase)))
errCode = C.sd_ble_enable(&appRAMBase) errCode = C.sd_ble_enable(&appRAMBase)
return makeError(errCode) return makeError(errCode)
} }
@ -58,5 +57,13 @@ func (a *Adapter) Address() (MACAddress, error) {
if errCode != 0 { if errCode != 0 {
return MACAddress{}, Error(errCode) 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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,4 @@
//go:build softdevice //go:build softdevice
// +build softdevice
package bluetooth package bluetooth
@ -29,7 +28,7 @@ var (
) )
// There can only be one connection at a time in the default configuration. // 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. // Globally allocated buffer for incoming SoftDevice events.
var eventBuf struct { var eventBuf struct {
@ -49,7 +48,7 @@ type Adapter struct {
scanning bool scanning bool
charWriteHandlers []charWriteHandler 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, // DefaultAdapter is the default adapter on the current system. On Nordic chips,
@ -57,11 +56,11 @@ type Adapter struct {
// //
// Make sure to call Enable() before using it to initialize the adapter. // Make sure to call Enable() before using it to initialize the adapter.
var DefaultAdapter = &Adapter{isDefault: true, var DefaultAdapter = &Adapter{isDefault: true,
connectHandler: func(device Address, connected bool) { connectHandler: func(device Device, connected bool) {
return return
}} }}
var eventBufLen uint16 var eventBufLen C.uint16_t
// Enable configures the BLE stack. It must be called before any // Enable configures the BLE stack. It must be called before any
// Bluetooth-related calls (unless otherwise indicated). // Bluetooth-related calls (unless otherwise indicated).
@ -73,8 +72,8 @@ func (a *Adapter) Enable() error {
// Enable the IRQ that handles all events. // Enable the IRQ that handles all events.
intr := interrupt.New(nrf.IRQ_SWI2, func(interrupt.Interrupt) { intr := interrupt.New(nrf.IRQ_SWI2, func(interrupt.Interrupt) {
for { for {
eventBufLen = uint16(unsafe.Sizeof(eventBuf)) eventBufLen = C.uint16_t(unsafe.Sizeof(eventBuf))
errCode := C.sd_ble_evt_get((*uint8)(unsafe.Pointer(&eventBuf)), &eventBufLen) errCode := C.sd_ble_evt_get((*C.uint8_t)(unsafe.Pointer(&eventBuf)), &eventBufLen)
if errCode != 0 { if errCode != 0 {
// Possible error conditions: // Possible error conditions:
// * NRF_ERROR_NOT_FOUND: no events left, break // * NRF_ERROR_NOT_FOUND: no events left, break
@ -98,7 +97,7 @@ func (a *Adapter) Enable() error {
return err 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 { if errCode != 0 {
return Error(errCode) return Error(errCode)
} }
@ -117,7 +116,7 @@ func (a *Adapter) Enable() error {
// play well with the SoftDevice. Restore interrupts to the previous state with // play well with the SoftDevice. Restore interrupts to the previous state with
// RestoreInterrupts. // RestoreInterrupts.
func DisableInterrupts() uintptr { 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) C.sd_nvic_critical_region_enter(&is_nested_critical_region)
return uintptr(is_nested_critical_region) return uintptr(is_nested_critical_region)
} }
@ -126,5 +125,43 @@ func DisableInterrupts() uintptr {
// DisableInterrupts. The mask parameter must be the value returned by // DisableInterrupts. The mask parameter must be the value returned by
// DisableInterrupts. // DisableInterrupts.
func RestoreInterrupts(mask uintptr) { 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 package bluetooth
import ( import (
"errors"
"fmt" "fmt"
"github.com/go-ole/go-ole" "github.com/go-ole/go-ole"
@ -12,14 +13,16 @@ import (
type Adapter struct { type Adapter struct {
watcher *advertisement.BluetoothLEAdvertisementWatcher 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. // DefaultAdapter is the default adapter on the system.
// //
// Make sure to call Enable() before using it to initialize the adapter. // Make sure to call Enable() before using it to initialize the adapter.
var DefaultAdapter = &Adapter{ var DefaultAdapter = &Adapter{
connectHandler: func(device Address, connected bool) { connectHandler: func(device Device, connected bool) {
return return
}, },
} }
@ -40,10 +43,14 @@ func awaitAsyncOperation(asyncOperation *foundation.IAsyncOperation, genericPara
// Wait until the async operation completes. // Wait until the async operation completes.
waitChan := make(chan struct{}) 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 status = asyncStatus
close(waitChan) close(waitChan)
})) })
defer handler.Release()
asyncOperation.SetCompleted(handler)
// Wait until async operation has stopped, and finish. // Wait until async operation has stopped, and finish.
<-waitChan <-waitChan
@ -52,3 +59,8 @@ func awaitAsyncOperation(asyncOperation *foundation.IAsyncOperation, genericPara
} }
return nil return nil
} }
func (a *Adapter) Address() (MACAddress, error) {
// TODO: get mac address
return MACAddress{}, errors.New("not implemented")
}

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. // those produced by Nordic Semiconductor.
// //
// This package can be use to create Bluetooth Low Energy centrals as well as peripherals. // This package can be use to create Bluetooth Low Energy centrals as well as peripherals.
// package bluetooth // import "gitrepo.ru/neonxp/bluetooth"
package bluetooth // import "tinygo.org/x/bluetooth"

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

@ -1,8 +1,8 @@
//go:build softdevice //go:build softdevice
// +build softdevice
package bluetooth package bluetooth
// #include <stdint.h>
// #include "nrf_error.h" // #include "nrf_error.h"
// #include "nrf_error_sdm.h" // #include "nrf_error_sdm.h"
import "C" import "C"
@ -84,7 +84,7 @@ func (e Error) Error() string {
// makeError returns an error (using the Error type) if the error code is // 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. // 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 { if code != 0 {
return Error(code) return Error(code)
} }

View file

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

View file

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

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/tinyterm"
"tinygo.org/x/tinyterm/fonts/proggy"
)
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 // Once the program is flashed to the board, connect to the USB port
// via serial to view the output. // via serial to view the output.
//
package main package main
import ( import (
"strconv" "strconv"
"tinygo.org/x/bluetooth" "gitrepo.ru/neonxp/bluetooth"
) )
var adapter = bluetooth.DefaultAdapter var adapter = bluetooth.DefaultAdapter
@ -44,7 +43,7 @@ func main() {
} }
}) })
var device *bluetooth.Device var device bluetooth.Device
select { select {
case result := <-ch: case result := <-ch:
device, err = adapter.Connect(result.Address, bluetooth.ConnectionParams{}) device, err = adapter.Connect(result.Address, bluetooth.ConnectionParams{})
@ -73,6 +72,12 @@ func main() {
} }
for _, char := range chars { for _, char := range chars {
println("-- characteristic", char.UUID().String()) 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) n, err := char.Read(buf)
if err != nil { if err != nil {
println(" ", err.Error()) println(" ", err.Error())

View file

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

View file

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

View file

@ -3,6 +3,11 @@
// //
// Once connected, it subscribes to notifications for the data value, and // Once connected, it subscribes to notifications for the data value, and
// displays it. // displays it.
// The Heart Rate Measurement characteristic is a variable-length structure (array) containing a Flags field, a Heart
// Rate Measurement Value field and, based on the contents of the Flags field, may contain additional fields
// such as Energy Expended or RR-Interval.
// More info can be found here: https://www.bluetooth.com/specifications/specs/gatt-specification-supplement-6/
// In this example only the heart rate is used, this is the second element in the array of bytes.
// //
// To run this on a desktop system: // To run this on a desktop system:
// //
@ -16,11 +21,10 @@
// //
// Once the program is flashed to the board, connect to the USB port // Once the program is flashed to the board, connect to the USB port
// via serial to view the output. // via serial to view the output.
//
package main package main
import ( import (
"tinygo.org/x/bluetooth" "gitrepo.ru/neonxp/bluetooth"
) )
var ( var (
@ -48,7 +52,7 @@ func main() {
} }
}) })
var device *bluetooth.Device var device bluetooth.Device
select { select {
case result := <-ch: case result := <-ch:
device, err = adapter.Connect(result.Address, bluetooth.ConnectionParams{}) device, err = adapter.Connect(result.Address, bluetooth.ConnectionParams{})

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

207
gap.go
View file

@ -53,6 +53,36 @@ type AdvertisementOptions struct {
// Interval in BLE-specific units. Create an interval by using NewDuration. // Interval in BLE-specific units. Create an interval by using NewDuration.
Interval Duration Interval Duration
// ManufacturerData stores Advertising Data.
ManufacturerData []ManufacturerDataElement
// ServiceData stores Advertising Data.
ServiceData []ServiceDataElement
}
// Manufacturer data that's part of an advertisement packet.
type ManufacturerDataElement struct {
// The company ID, which must be one of the assigned company IDs.
// The full list is in here:
// https://www.bluetooth.com/specifications/assigned-numbers/
// The list can also be viewed here:
// https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/company_identifiers/company_identifiers.yaml
// The value 0xffff can also be used for testing.
CompanyID uint16
// The value, which can be any value but can't be very large.
Data []byte
}
// ServiceDataElement strores a uuid/byte-array pair used as ServiceData advertisment elements
type ServiceDataElement struct {
// Service UUID.
// The list can also be viewed here:
// https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/uuids/service_uuids.yaml
UUID UUID
// the data byte array
Data []byte
} }
// Duration is the unit of time used in BLE, in 0.625µs units. This unit of time // Duration is the unit of time used in BLE, in 0.625µs units. This unit of time
@ -75,7 +105,7 @@ type ScanResult struct {
// Bluetooth address of the scanned device. // Bluetooth address of the scanned device.
Address Address Address Address
// RSSI the last time a packet from this device has been received. // Signal strength of the advertisement packet.
RSSI int16 RSSI int16
// The data obtained from the advertisement data, which may contain many // The data obtained from the advertisement data, which may contain many
@ -106,9 +136,13 @@ type AdvertisementPayload interface {
// if this data is not available. // if this data is not available.
Bytes() []byte Bytes() []byte
// ManufacturerData returns a map with all the manufacturer data present in the // ManufacturerData returns a slice with all the manufacturer data present in the
//advertising. IT may be empty. // advertising. It may be empty.
ManufacturerData() map[uint16][]byte ManufacturerData() []ManufacturerDataElement
// ServiceData returns a slice with all the service data present in the
// advertising. It may be empty.
ServiceData() []ServiceDataElement
} }
// AdvertisementFields contains advertisement fields in structured form. // AdvertisementFields contains advertisement fields in structured form.
@ -123,7 +157,10 @@ type AdvertisementFields struct {
ServiceUUIDs []UUID ServiceUUIDs []UUID
// ManufacturerData is the manufacturer data of the advertisement. // ManufacturerData is the manufacturer data of the advertisement.
ManufacturerData map[uint16][]byte ManufacturerData []ManufacturerDataElement
// ServiceData is the service data of the advertisement.
ServiceData []ServiceDataElement
} }
// advertisementFields wraps AdvertisementFields to implement the // advertisementFields wraps AdvertisementFields to implement the
@ -157,10 +194,15 @@ func (p *advertisementFields) Bytes() []byte {
} }
// ManufacturerData returns the underlying ManufacturerData field. // ManufacturerData returns the underlying ManufacturerData field.
func (p *advertisementFields) ManufacturerData() map[uint16][]byte { func (p *advertisementFields) ManufacturerData() []ManufacturerDataElement {
return p.AdvertisementFields.ManufacturerData return p.AdvertisementFields.ManufacturerData
} }
// ServiceData returns the underlying ServiceData field.
func (p *advertisementFields) ServiceData() []ServiceDataElement {
return p.AdvertisementFields.ServiceData
}
// rawAdvertisementPayload encapsulates a raw advertisement packet. Methods to // rawAdvertisementPayload encapsulates a raw advertisement packet. Methods to
// get the data (such as LocalName()) will parse just the needed field. Scanning // get the data (such as LocalName()) will parse just the needed field. Scanning
// the data should be fast as most advertisement packets only have a very small // the data should be fast as most advertisement packets only have a very small
@ -250,22 +292,58 @@ func (buf *rawAdvertisementPayload) HasServiceUUID(uuid UUID) bool {
} }
// ManufacturerData returns the manufacturer data in the advertisement payload. // ManufacturerData returns the manufacturer data in the advertisement payload.
func (buf *rawAdvertisementPayload) ManufacturerData() map[uint16][]byte { func (buf *rawAdvertisementPayload) ManufacturerData() []ManufacturerDataElement {
mData := make(map[uint16][]byte) var manufacturerData []ManufacturerDataElement
data := buf.Bytes() for index := 0; index < int(buf.len)+4; index += int(buf.data[index]) + 1 {
for len(data) >= 2 { fieldLength := int(buf.data[index+0])
fieldLength := data[0] if fieldLength < 3 {
if int(fieldLength)+1 > len(data) { continue
// Invalid field length.
return nil
} }
// If this is the manufacturer data fieldType := buf.data[index+1]
if byte(0xFF) == data[1] { if fieldType != 0xff {
mData[uint16(data[2])+(uint16(data[3])<<8)] = data[4 : fieldLength+1] continue
} }
data = data[fieldLength+1:] key := uint16(buf.data[index+2]) | uint16(buf.data[index+3])<<8
manufacturerData = append(manufacturerData, ManufacturerDataElement{
CompanyID: key,
Data: buf.data[index+4 : index+fieldLength+1],
})
} }
return mData return manufacturerData
}
// ServiceData returns the service data in the advertisment payload
func (buf *rawAdvertisementPayload) ServiceData() []ServiceDataElement {
var serviceData []ServiceDataElement
for index := 0; index < int(buf.len)+4; index += int(buf.data[index]) + 1 {
fieldLength := int(buf.data[index+0])
if fieldLength < 3 { // field has only length and type and no data
continue
}
fieldType := buf.data[index+1]
switch fieldType {
case 0x16: // 16-bit uuid
serviceData = append(serviceData, ServiceDataElement{
UUID: New16BitUUID(uint16(buf.data[index+2]) + (uint16(buf.data[index+3]) << 8)),
Data: buf.data[index+4 : index+fieldLength+1],
})
case 0x20: // 32-bit uuid
serviceData = append(serviceData, ServiceDataElement{
UUID: New32BitUUID(uint32(buf.data[index+2]) + (uint32(buf.data[index+3]) << 8) + (uint32(buf.data[index+4]) << 16) + (uint32(buf.data[index+5]) << 24)),
Data: buf.data[index+6 : index+fieldLength+1],
})
case 0x21: // 128-bit uuid
var uuidArray [16]byte
copy(uuidArray[:], buf.data[index+2:index+18])
serviceData = append(serviceData, ServiceDataElement{
UUID: NewUUID(uuidArray),
Data: buf.data[index+18 : index+fieldLength+1],
})
default:
continue
}
}
return serviceData
} }
// reset restores this buffer to the original state. // reset restores this buffer to the original state.
@ -295,6 +373,89 @@ func (buf *rawAdvertisementPayload) addFromOptions(options AdvertisementOptions)
return false return false
} }
} }
for _, element := range options.ManufacturerData {
if !buf.addManufacturerData(element.CompanyID, element.Data) {
return false
}
}
for _, element := range options.ServiceData {
if !buf.addServiceData(element.UUID, element.Data) {
return false
}
}
return true
}
// addManufacturerData adds manufacturer data ([]byte) entries to the advertisement payload.
func (buf *rawAdvertisementPayload) addManufacturerData(key uint16, value []byte) (ok bool) {
// Check whether the field can fit this manufacturer data.
fieldLength := len(value) + 4
if int(buf.len)+fieldLength > len(buf.data) {
return false
}
// Add the data.
buf.data[buf.len+0] = uint8(fieldLength - 1)
buf.data[buf.len+1] = 0xff
buf.data[buf.len+2] = uint8(key)
buf.data[buf.len+3] = uint8(key >> 8)
copy(buf.data[buf.len+4:], value)
buf.len += uint8(fieldLength)
return true
}
// addServiceData adds service data ([]byte) entries to the advertisement payload.
func (buf *rawAdvertisementPayload) addServiceData(uuid UUID, data []byte) (ok bool) {
switch {
case uuid.Is16Bit():
// check if it fits
fieldLength := 1 + 1 + 2 + len(data) // 1 byte length, 1 byte ad type, 2 bytes uuid, actual service data
if int(buf.len)+fieldLength > len(buf.data) {
return false
}
// Add the data.
buf.data[buf.len+0] = byte(fieldLength - 1)
buf.data[buf.len+1] = 0x16
buf.data[buf.len+2] = byte(uuid.Get16Bit())
buf.data[buf.len+3] = byte(uuid.Get16Bit() >> 8)
copy(buf.data[buf.len+4:], data)
buf.len += uint8(fieldLength)
case uuid.Is32Bit():
// check if it fits
fieldLength := 1 + 1 + 4 + len(data) // 1 byte length, 1 byte ad type, 4 bytes uuid, actual service data
if int(buf.len)+fieldLength > len(buf.data) {
return false
}
// Add the data.
buf.data[buf.len+0] = byte(fieldLength - 1)
buf.data[buf.len+1] = 0x20
buf.data[buf.len+2] = byte(uuid.Get32Bit())
buf.data[buf.len+3] = byte(uuid.Get32Bit() >> 8)
buf.data[buf.len+4] = byte(uuid.Get32Bit() >> 16)
buf.data[buf.len+5] = byte(uuid.Get32Bit() >> 24)
copy(buf.data[buf.len+6:], data)
buf.len += uint8(fieldLength)
default: // must be 128-bit uuid
// check if it fits
fieldLength := 1 + 1 + 16 + len(data) // 1 byte length, 1 byte ad type, 16 bytes uuid, actual service data
if int(buf.len)+fieldLength > len(buf.data) {
return false
}
// Add the data.
buf.data[buf.len+0] = byte(fieldLength - 1)
buf.data[buf.len+1] = 0x21
uuid_bytes := uuid.Bytes()
copy(buf.data[buf.len+2:], uuid_bytes[:])
copy(buf.data[buf.len+2+16:], data)
buf.len += uint8(fieldLength)
}
return true return true
} }
@ -356,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 { type ConnectionParams struct {
// The timeout for the connection attempt. Not used during the rest of the // 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. // connection. If no duration is specified, a default timeout will be used.
@ -368,4 +530,9 @@ type ConnectionParams struct {
// will be used. // will be used.
MinInterval Duration MinInterval Duration
MaxInterval 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. // Device is a connection to a remote peripheral.
type Device struct { type Device struct {
Address Address
*deviceInternal
}
type deviceInternal struct {
delegate *peripheralDelegate delegate *peripheralDelegate
cm cbgo.CentralManager cm cbgo.CentralManager
@ -97,14 +103,14 @@ type Device struct {
} }
// Connect starts a connection attempt to the given peripheral device address. // 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()) uuid, err := cbgo.ParseUUID(address.UUID.String())
if err != nil { if err != nil {
return nil, err return Device{}, err
} }
prphs := a.cm.RetrievePeripheralsWithIdentifiers([]cbgo.UUID{uuid}) prphs := a.cm.RetrievePeripheralsWithIdentifiers([]cbgo.UUID{uuid})
if len(prphs) == 0 { 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 timeout := defaultConnectionTimeout
@ -129,20 +135,23 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, er
// check if we have received a disconnected peripheral // check if we have received a disconnected peripheral
if p.State() == cbgo.PeripheralStateDisconnected { if p.State() == cbgo.PeripheralStateDisconnected {
return nil, connectionError return Device{}, connectionError
} }
d := &Device{ d := Device{
Address: address,
deviceInternal: &deviceInternal{
cm: a.cm, cm: a.cm,
prph: p, prph: p,
servicesChan: make(chan error), servicesChan: make(chan error),
charsChan: make(chan error), charsChan: make(chan error),
},
} }
d.delegate = &peripheralDelegate{d: d} d.delegate = &peripheralDelegate{d: d}
p.SetDelegate(d.delegate) p.SetDelegate(d.delegate)
a.connectHandler(address, true) a.connectHandler(d, true)
return d, nil 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 // Disconnect from the BLE device. This method is non-blocking and does not
// wait until the connection is fully gone. // wait until the connection is fully gone.
func (d *Device) Disconnect() error { func (d Device) Disconnect() error {
d.cm.CancelConnect(d.prph) d.cm.CancelConnect(d.prph)
return nil 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 // Peripheral delegate functions
type peripheralDelegate struct { type peripheralDelegate struct {
cbgo.PeripheralDelegateBase cbgo.PeripheralDelegateBase
d *Device d Device
} }
// DidDiscoverServices is called when the services for a Peripheral // DidDiscoverServices is called when the services for a Peripheral
@ -195,7 +216,9 @@ func (pd *peripheralDelegate) DidUpdateValueForCharacteristic(prph cbgo.Peripher
svcuuid, _ := ParseUUID(chr.Service().UUID().String()) svcuuid, _ := ParseUUID(chr.Service().UUID().String())
if svc, ok := pd.d.services[svcuuid]; ok { if svc, ok := pd.d.services[svcuuid]; ok {
if char, ok := svc.characteristics[uuid]; ok { for _, char := range svc.characteristics {
if char.characteristic == chr && uuid == char.UUID() { // compare pointers
if err == nil && char.callback != nil { if err == nil && char.callback != nil {
go char.callback(chr.Value()) go char.callback(chr.Value())
} }
@ -204,5 +227,25 @@ func (pd *peripheralDelegate) DidUpdateValueForCharacteristic(prph cbgo.Peripher
char.readChan <- err char.readChan <- err
} }
} }
}
}
}
// 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

@ -1,21 +1,22 @@
//go:build !baremetal //go:build !baremetal
// +build !baremetal
package bluetooth package bluetooth
import ( import (
"context"
"errors" "errors"
"fmt"
"strings" "strings"
"sync/atomic"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
"github.com/muka/go-bluetooth/api" "github.com/godbus/dbus/v5/prop"
"github.com/muka/go-bluetooth/bluez"
"github.com/muka/go-bluetooth/bluez/profile/advertising"
"github.com/muka/go-bluetooth/bluez/profile/device"
) )
var errAdvertisementNotStarted = errors.New("bluetooth: stop advertisement that was not started") var 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. // Address contains a Bluetooth MAC address.
type Address struct { type Address struct {
@ -25,9 +26,8 @@ type Address struct {
// Advertisement encapsulates a single advertisement instance. // Advertisement encapsulates a single advertisement instance.
type Advertisement struct { type Advertisement struct {
adapter *Adapter adapter *Adapter
advertisement *api.Advertisement properties *prop.Properties
properties *advertising.LEAdvertisement1Properties path dbus.ObjectPath
cancel func()
} }
// DefaultAdvertisement returns the default advertisement instance but does not // DefaultAdvertisement returns the default advertisement instance but does not
@ -45,41 +45,83 @@ func (a *Adapter) DefaultAdvertisement() *Advertisement {
// //
// On Linux with BlueZ, it is not possible to set the advertisement interval. // On Linux with BlueZ, it is not possible to set the advertisement interval.
func (a *Advertisement) Configure(options AdvertisementOptions) error { func (a *Advertisement) Configure(options AdvertisementOptions) error {
if a.advertisement != nil { if a.properties != nil {
panic("todo: configure advertisement a second time") panic("todo: configure advertisement a second time")
} }
a.properties = &advertising.LEAdvertisement1Properties{ var serviceUUIDs []string
Type: advertising.AdvertisementTypeBroadcast,
Timeout: 1<<16 - 1,
LocalName: options.LocalName,
}
for _, uuid := range options.ServiceUUIDs { 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 return nil
} }
// Start advertisement. May only be called after it has been configured. // Start advertisement. May only be called after it has been configured.
func (a *Advertisement) Start() error { func (a *Advertisement) Start() error {
if a.advertisement != nil { // Register our advertisement object to start advertising.
panic("todo: start advertisement a second time") err := a.adapter.adapter.Call("org.bluez.LEAdvertisingManager1.RegisterAdvertisement", 0, a.path, map[string]interface{}{}).Err
}
cancel, err := api.ExposeAdvertisement(a.adapter.id, a.properties, uint32(a.properties.Timeout))
if err != nil { 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 return nil
} }
// Stop advertisement. May only be called after it has been started. // Stop advertisement. May only be called after it has been started.
func (a *Advertisement) Stop() error { 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 return errAdvertisementNotStarted
} }
a.cancel() return fmt.Errorf("bluetooth: could not stop advertisement: %w", err)
}
return nil return nil
} }
@ -92,7 +134,7 @@ func (a *Advertisement) Stop() error {
// possible some events are missed and perhaps even possible that some events // possible some events are missed and perhaps even possible that some events
// are duplicated. // are duplicated.
func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error { func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
if a.cancelChan != nil { if a.scanCancelChan != nil {
return errScanning 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 // Detecting whether the scan is stopped can be done by doing a non-blocking
// read from it. If it succeeds, the scan is stopped. // read from it. If it succeeds, the scan is stopped.
cancelChan := make(chan struct{}) cancelChan := make(chan struct{})
a.cancelChan = cancelChan a.scanCancelChan = cancelChan
// This appears to be necessary to receive any BLE discovery results at all. // This appears to be necessary to receive any BLE discovery results at all.
defer a.adapter.SetDiscoveryFilter(nil) defer a.adapter.Call("org.bluez.Adapter1.SetDiscoveryFilter", 0)
err := a.adapter.SetDiscoveryFilter(map[string]interface{}{ err := a.adapter.Call("org.bluez.Adapter1.SetDiscoveryFilter", 0, map[string]interface{}{
"Transport": "le", "Transport": "le",
}) }).Err
if err != nil {
return err
}
bus, err := dbus.SystemBus()
if err != nil { if err != nil {
return err return err
} }
signal := make(chan *dbus.Signal) signal := make(chan *dbus.Signal)
bus.Signal(signal) a.bus.Signal(signal)
defer bus.RemoveSignal(signal) defer a.bus.RemoveSignal(signal)
propertiesChangedMatchOptions := []dbus.MatchOption{dbus.WithMatchInterface("org.freedesktop.DBus.Properties")} propertiesChangedMatchOptions := []dbus.MatchOption{dbus.WithMatchInterface("org.freedesktop.DBus.Properties")}
bus.AddMatchSignal(propertiesChangedMatchOptions...) a.bus.AddMatchSignal(propertiesChangedMatchOptions...)
defer bus.RemoveMatchSignal(propertiesChangedMatchOptions...) defer a.bus.RemoveMatchSignal(propertiesChangedMatchOptions...)
newObjectMatchOptions := []dbus.MatchOption{dbus.WithMatchInterface("org.freedesktop.DBus.ObjectManager")} newObjectMatchOptions := []dbus.MatchOption{dbus.WithMatchInterface("org.freedesktop.DBus.ObjectManager")}
bus.AddMatchSignal(newObjectMatchOptions...) a.bus.AddMatchSignal(newObjectMatchOptions...)
defer bus.RemoveMatchSignal(newObjectMatchOptions...) defer a.bus.RemoveMatchSignal(newObjectMatchOptions...)
// Go through all connected devices and present the connected devices as // Go through all connected devices and present the connected devices as
// scan results. Also save the properties so that the full list of // scan results. Also save the properties so that the full list of
// properties is known on a PropertiesChanged signal. We can't present the // 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 // list of cached devices as scan results as devices may be cached for a
// long time, long after they have moved out of range. // 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 { if err != nil {
return err return err
} }
devices := make(map[dbus.ObjectPath]*device.Device1Properties) devices := make(map[dbus.ObjectPath]map[string]dbus.Variant)
for _, dev := range deviceList { for path, v := range deviceList {
if dev.Properties.Connected { device, ok := v["org.bluez.Device1"]
callback(a, makeScanResult(dev.Properties)) 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 { select {
case <-cancelChan: case <-cancelChan:
return nil return nil
default: default:
} }
} }
devices[dev.Path()] = dev.Properties devices[path] = device
} }
// Instruct BlueZ to start discovering. // Instruct BlueZ to start discovering.
err = a.adapter.StartDiscovery() err = a.adapter.Call("org.bluez.Adapter1.StartDiscovery", 0).Err
if err != nil { if err != nil {
return err return err
} }
@ -163,8 +208,7 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
// StopScan is called). // StopScan is called).
select { select {
case <-cancelChan: case <-cancelChan:
a.adapter.StopDiscovery() return a.adapter.Call("org.bluez.Adapter1.StopDiscovery", 0).Err
return nil
default: default:
} }
@ -180,35 +224,24 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
if !ok { if !ok {
continue continue
} }
var props *device.Device1Properties devices[objectPath] = rawprops
props, _ = props.FromDBusMap(rawprops) callback(a, makeScanResult(rawprops))
devices[objectPath] = props
callback(a, makeScanResult(props))
case "org.freedesktop.DBus.Properties.PropertiesChanged": case "org.freedesktop.DBus.Properties.PropertiesChanged":
interfaceName := sig.Body[0].(string) interfaceName := sig.Body[0].(string)
if interfaceName != "org.bluez.Device1" { if interfaceName != "org.bluez.Device1" {
continue continue
} }
changes := sig.Body[1].(map[string]dbus.Variant) changes := sig.Body[1].(map[string]dbus.Variant)
props := devices[sig.Path] device, ok := devices[sig.Path]
for field, val := range changes { if !ok {
switch field { // This shouldn't happen, but protect against it just in
case "RSSI": // case.
props.RSSI = val.Value().(int16) continue
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{})
} }
props.ManufacturerData = mData for k, v := range changes {
device[k] = v
} }
} callback(a, makeScanResult(device))
callback(a, makeScanResult(props))
} }
case <-cancelChan: case <-cancelChan:
continue 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 // callback to stop the current scan. If no scan is in progress, an error will
// be returned. // be returned.
func (a *Adapter) StopScan() error { func (a *Adapter) StopScan() error {
if a.cancelChan == nil { if a.scanCancelChan == nil {
return errNotScanning return errNotScanning
} }
close(a.cancelChan) close(a.scanCancelChan)
a.cancelChan = nil a.scanCancelChan = nil
return nil return nil
} }
// makeScanResult creates a ScanResult from a Device1 object. // makeScanResult creates a ScanResult from a raw DBus device.
func makeScanResult(props *device.Device1Properties) ScanResult { func makeScanResult(props map[string]dbus.Variant) ScanResult {
// Assume the Address property is well-formed. // Assume the Address property is well-formed.
addr, _ := ParseMAC(props.Address) addr, _ := ParseMAC(props["Address"].Value().(string))
// Create a list of UUIDs. // Create a list of UUIDs.
var serviceUUIDs []UUID var serviceUUIDs []UUID
for _, uuid := range props.UUIDs { for _, uuid := range props["UUIDs"].Value().([]string) {
// Assume the UUID is well-formed. // Assume the UUID is well-formed.
parsedUUID, _ := ParseUUID(uuid) parsedUUID, _ := ParseUUID(uuid)
serviceUUIDs = append(serviceUUIDs, parsedUUID) serviceUUIDs = append(serviceUUIDs, parsedUUID)
} }
a := Address{MACAddress{MAC: addr}} a := Address{MACAddress{MAC: addr}}
a.SetRandom(props.AddressType == "random") a.SetRandom(props["AddressType"].Value().(string) == "random")
mData := make(map[uint16][]byte) var manufacturerData []ManufacturerDataElement
for k, v := range props.ManufacturerData { if mdata, ok := props["ManufacturerData"].Value().(map[uint16]dbus.Variant); ok {
// can be either variant or just byte value for k, v := range mdata {
switch val := v.(type) { manufacturerData = append(manufacturerData, ManufacturerDataElement{
case dbus.Variant: CompanyID: k,
mData[k] = val.Value().([]byte) Data: v.Value().([]byte),
case []byte: })
mData[k] = val }
}
// 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{ return ScanResult{
RSSI: props.RSSI, RSSI: rssi,
Address: a, Address: a,
AdvertisementPayload: &advertisementFields{ AdvertisementPayload: &advertisementFields{
AdvertisementFields{ AdvertisementFields{
LocalName: props.Name, LocalName: localName,
ServiceUUIDs: serviceUUIDs, 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. // Device is a connection to a remote peripheral.
type Device struct { type Device struct {
device *device.Device1 // bluez device interface Address Address // the MAC address of the device
ctx context.Context // context for our event watcher, canceled on disconnect event
cancel context.CancelFunc // cancel function to halt our event watcher context device dbus.BusObject // bluez device interface
propchanged chan *bluez.PropertyChanged // channel that device property changes will show up on
adapter *Adapter // the adapter that was used to form this device connection 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. // Connect starts a connection attempt to the given peripheral device address.
// //
// On Linux and Windows, the IsRandom part of the address is ignored. // 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)) devicePath := dbus.ObjectPath(string(a.adapter.Path()) + "/dev_" + strings.Replace(address.MAC.String(), ":", "_", -1))
dev, err := device.NewDevice1(devicePath) device := Device{
if err != nil { Address: address,
return nil, err device: a.bus.Object("org.bluez", devicePath),
}
device := &Device{
device: dev,
adapter: a, 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 { // Already start watching for property changes. We do this before reading
// Not yet connected, so do it now. // the Connected property below to avoid a race condition: if the device
// The properties have just been read so this is fresh data. // were connected between the two calls the signal wouldn't be picked up.
err := dev.Connect() 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 { if err != nil {
device.cancel() // cancel our watcher routine return Device{}, err
return nil, err
} }
// Connect to the device, if not already connected.
if !connected.Value().(bool) {
// Start connecting (async).
err := device.device.Call("org.bluez.Device1.Connect", 0).Err
if 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 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 // Disconnect from the BLE device. This method is non-blocking and does not
// wait until the connection is fully gone. // 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 // we don't call our cancel function here, instead we wait for the
// property change in `watchForConnect` and cancel things then // 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, // On Linux, this call doesn't do anything because BlueZ doesn't support
// see https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/device-api.txt, for a full list // changing the connection latency.
func (d *Device) watchForConnect() error { func (d Device) RequestConnectionParams(params ConnectionParams) 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
}
}
}()
return nil return nil
} }

View file

@ -1,16 +1,21 @@
//go:build softdevice && s110v8 //go:build softdevice && s110v8
// +build softdevice,s110v8
package bluetooth package bluetooth
/* /*
#include "ble_gap.h" #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 "C"
import ( import (
"runtime/volatile" "runtime/volatile"
"time" "time"
"unsafe"
) )
// Address contains a Bluetooth MAC address. // Address contains a Bluetooth MAC address.
@ -48,7 +53,7 @@ func (a *Advertisement) Configure(options AdvertisementOptions) error {
return errAdvertisementPacketTooBig 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 a.interval = options.Interval
return makeError(errCode) return makeError(errCode)
} }
@ -69,12 +74,12 @@ func (a *Advertisement) Stop() error {
// Low-level version of Start. Used to restart advertisement when a connection // Low-level version of Start. Used to restart advertisement when a connection
// is lost. // is lost.
func (a *Advertisement) start() uint32 { func (a *Advertisement) start() C.uint32_t {
params := C.ble_gap_adv_params_t{ params := C.ble_gap_adv_params_t{
_type: C.BLE_GAP_ADV_TYPE_ADV_IND, _type: C.BLE_GAP_ADV_TYPE_ADV_IND,
fp: C.BLE_GAP_ADV_FP_ANY, fp: C.BLE_GAP_ADV_FP_ANY,
interval: uint16(a.interval), interval: C.uint16_t(a.interval),
timeout: 0, // no timeout timeout: 0, // no timeout
} }
return C.sd_ble_gap_adv_start(&params) return C.sd_ble_gap_adv_start_noescape(params)
} }

View file

@ -1,11 +1,11 @@
//go:build (softdevice && s113v7) || (softdevice && s132v6) || (softdevice && s140v6) || (softdevice && s140v7) //go:build (softdevice && s113v7) || (softdevice && s132v6) || (softdevice && s140v6) || (softdevice && s140v7)
// +build softdevice,s113v7 softdevice,s132v6 softdevice,s140v6 softdevice,s140v7
package bluetooth package bluetooth
import ( import (
"runtime/volatile" "runtime/volatile"
"time" "time"
"unsafe"
) )
/* /*
@ -20,7 +20,7 @@ type Address struct {
// Advertisement encapsulates a single advertisement instance. // Advertisement encapsulates a single advertisement instance.
type Advertisement struct { type Advertisement struct {
handle uint8 handle C.uint8_t
isAdvertising volatile.Register8 isAdvertising volatile.Register8
payload rawAdvertisementPayload payload rawAdvertisementPayload
} }
@ -58,14 +58,14 @@ func (a *Advertisement) Configure(options AdvertisementOptions) error {
data := C.ble_gap_adv_data_t{} data := C.ble_gap_adv_data_t{}
data.adv_data = C.ble_data_t{ data.adv_data = C.ble_data_t{
p_data: &a.payload.data[0], p_data: (*C.uint8_t)(unsafe.Pointer(&a.payload.data[0])),
len: uint16(a.payload.len), len: C.uint16_t(a.payload.len),
} }
params := C.ble_gap_adv_params_t{ params := C.ble_gap_adv_params_t{
properties: C.ble_gap_adv_properties_t{ properties: C.ble_gap_adv_properties_t{
_type: C.BLE_GAP_ADV_TYPE_CONNECTABLE_SCANNABLE_UNDIRECTED, _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) errCode := C.sd_ble_gap_adv_set_configure(&a.handle, &data, &params)
return makeError(errCode) return makeError(errCode)

View file

@ -1,5 +1,4 @@
//go:build (softdevice && s132v6) || (softdevice && s140v6) || (softdevice && s140v7) //go:build (softdevice && s132v6) || (softdevice && s140v6) || (softdevice && s140v7)
// +build softdevice,s132v6 softdevice,s140v6 softdevice,s140v7
package bluetooth package bluetooth
@ -8,6 +7,7 @@ import (
"errors" "errors"
"runtime/volatile" "runtime/volatile"
"time" "time"
"unsafe"
) )
/* /*
@ -16,6 +16,7 @@ import (
import "C" import "C"
var errAlreadyConnecting = errors.New("bluetooth: already in a connection attempt") 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. // Memory buffers needed by sd_ble_gap_scan_start.
var ( var (
@ -41,12 +42,12 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
scanParams := C.ble_gap_scan_params_t{} scanParams := C.ble_gap_scan_params_t{}
scanParams.set_bitfield_extended(0) scanParams.set_bitfield_extended(0)
scanParams.set_bitfield_active(0) scanParams.set_bitfield_active(0)
scanParams.interval = uint16(NewDuration(40 * time.Millisecond)) scanParams.interval = C.uint16_t(NewDuration(40 * time.Millisecond))
scanParams.window = uint16(NewDuration(30 * time.Millisecond)) scanParams.window = C.uint16_t(NewDuration(30 * time.Millisecond))
scanParams.timeout = C.BLE_GAP_SCAN_TIMEOUT_UNLIMITED scanParams.timeout = C.BLE_GAP_SCAN_TIMEOUT_UNLIMITED
scanReportBufferInfo := C.ble_data_t{ scanReportBufferInfo := C.ble_data_t{
p_data: &scanReportBuffer.data[0], p_data: (*C.uint8_t)(unsafe.Pointer(&scanReportBuffer.data[0])),
len: uint16(len(scanReportBuffer.data)), len: C.uint16_t(len(scanReportBuffer.data)),
} }
errCode := C.sd_ble_gap_scan_start(&scanParams, &scanReportBufferInfo) errCode := C.sd_ble_gap_scan_start(&scanParams, &scanReportBufferInfo)
if errCode != 0 { if errCode != 0 {
@ -92,15 +93,10 @@ func (a *Adapter) StopScan() error {
return nil return nil
} }
// Device is a connection to a remote peripheral.
type Device struct {
connectionHandle uint16
}
// In-progress connection attempt. // In-progress connection attempt.
var connectionAttempt struct { var connectionAttempt struct {
state volatile.Register8 // 0 means unused, 1 means connecting, 2 means ready (connected or timeout) state volatile.Register8 // 0 means unused, 1 means connecting, 2 means connected, 3 means timeout
connectionHandle uint16 connectionHandle C.uint16_t
} }
// Connect starts a connection attempt to the given peripheral device address. // Connect starts a connection attempt to the given peripheral device address.
@ -109,10 +105,10 @@ var connectionAttempt struct {
// connection attempt at once and that the address parameter must have the // 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 // IsRandom bit set correctly. This bit is set correctly for scan results, so
// you can reuse that address directly. // 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. // Construct an address object as used in the SoftDevice.
var addr C.ble_gap_addr_t var addr C.ble_gap_addr_t
addr.addr = address.MAC addr.addr = makeSDAddress(address.MAC)
if address.IsRandom() { if address.IsRandom() {
switch address.MAC[5] >> 6 { switch address.MAC[5] >> 6 {
case 0b11: case 0b11:
@ -143,22 +139,25 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (*Device, er
scanParams := C.ble_gap_scan_params_t{} scanParams := C.ble_gap_scan_params_t{}
scanParams.set_bitfield_extended(0) scanParams.set_bitfield_extended(0)
scanParams.set_bitfield_active(0) scanParams.set_bitfield_active(0)
scanParams.interval = uint16(NewDuration(40 * time.Millisecond)) scanParams.interval = C.uint16_t(NewDuration(40 * time.Millisecond))
scanParams.window = uint16(NewDuration(30 * time.Millisecond)) scanParams.window = C.uint16_t(NewDuration(30 * time.Millisecond))
scanParams.timeout = uint16(params.ConnectionTimeout) scanParams.timeout = C.uint16_t(params.ConnectionTimeout / 16) // timeout in 10ms units
connectionParams := C.ble_gap_conn_params_t{ connectionParams := C.ble_gap_conn_params_t{
min_conn_interval: uint16(params.MinInterval) / 2, min_conn_interval: C.uint16_t(params.MinInterval) / 2,
max_conn_interval: uint16(params.MaxInterval) / 2, max_conn_interval: C.uint16_t(params.MaxInterval) / 2,
slave_latency: 0, // mostly relevant to connected keyboards etc slave_latency: 0, // mostly relevant to connected keyboards etc
conn_sup_timeout: 200, // 2 seconds (in 10ms units), the minimum recommended by Apple 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. // 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 // This should be safe as long as Connect is not called concurrently. And
// even then, it should catch most such race conditions. // even then, it should catch most such race conditions.
if connectionAttempt.state.Get() != 0 { if connectionAttempt.state.Get() != 0 {
return nil, errAlreadyConnecting return Device{}, errAlreadyConnecting
} }
connectionAttempt.state.Set(1) connectionAttempt.state.Set(1)
@ -166,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) errCode := C.sd_ble_gap_connect(&addr, &scanParams, &connectionParams, C.BLE_CONN_CFG_TAG_DEFAULT)
if errCode != 0 { if errCode != 0 {
connectionAttempt.state.Set(0) connectionAttempt.state.Set(0)
return nil, Error(errCode) return Device{}, Error(errCode)
} }
// Wait until the connection is established. // Wait until the connection is established.
// TODO: use some sort of condition variable once the scheduler supports for {
// them. state := connectionAttempt.state.Get()
for connectionAttempt.state.Get() != 2 { if state == 2 {
arm.Asm("wfe") // Successfully connected.
}
connectionHandle := connectionAttempt.connectionHandle
connectionAttempt.state.Set(0) connectionAttempt.state.Set(0)
connectionHandle := connectionAttempt.connectionHandle
// Connection has been established. return Device{
return &Device{
connectionHandle: connectionHandle, connectionHandle: connectionHandle,
}, nil }, 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. // 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) errCode := C.sd_ble_gap_disconnect(d.connectionHandle, C.BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION)
if errCode != 0 { if errCode != 0 {
return Error(errCode) return Error(errCode)
@ -193,3 +199,34 @@ func (d *Device) Disconnect() error {
return nil 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
}

133
gap_test.go Normal file
View file

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

View file

@ -18,6 +18,108 @@ type Address struct {
MACAddress MACAddress
} }
type Advertisement struct {
advertisement *advertisement.BluetoothLEAdvertisement
publisher *advertisement.BluetoothLEAdvertisementPublisher
}
// DefaultAdvertisement returns the default advertisement instance but does not
// configure it.
func (a *Adapter) DefaultAdvertisement() *Advertisement {
if a.defaultAdvertisement == nil {
a.defaultAdvertisement = &Advertisement{}
}
return a.defaultAdvertisement
}
// Configure this advertisement.
// on Windows we're only able to set "Manufacturer Data" for advertisements.
// https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothleadvertisementpublisher?view=winrt-22621#remarks
// following this c# source for this implementation: https://github.com/microsoft/Windows-universal-samples/blob/main/Samples/BluetoothAdvertisement/cs/Scenario2_Publisher.xaml.cs
// adding service data / localname leads to errors when starting the advertisement.
func (a *Advertisement) Configure(options AdvertisementOptions) error {
// we can only advertise manufacturer / company data on windows, so no need to continue if we have none
if len(options.ManufacturerData) == 0 {
return nil
}
if a.publisher != nil {
a.publisher.Release()
}
if a.advertisement != nil {
a.advertisement.Release()
}
pub, err := advertisement.NewBluetoothLEAdvertisementPublisher()
if err != nil {
return err
}
a.publisher = pub
ad, err := a.publisher.GetAdvertisement()
if err != nil {
return err
}
a.advertisement = ad
vec, err := ad.GetManufacturerData()
if err != nil {
return err
}
for _, optManData := range options.ManufacturerData {
writer, err := streams.NewDataWriter()
if err != nil {
return err
}
defer writer.Release()
err = writer.WriteBytes(uint32(len(optManData.Data)), optManData.Data)
if err != nil {
return err
}
buf, err := writer.DetachBuffer()
if err != nil {
return err
}
manData, err := advertisement.BluetoothLEManufacturerDataCreate(optManData.CompanyID, buf)
if err != nil {
return err
}
if err = vec.Append(unsafe.Pointer(&manData.IUnknown.RawVTable)); err != nil {
return err
}
}
return nil
}
// Start advertisement. May only be called after it has been configured.
func (a *Advertisement) Start() error {
// publisher will be present if we actually have manufacturer data to advertise.
if a.publisher != nil {
return a.publisher.Start()
}
return nil
}
// Stop advertisement. May only be called after it has been started.
func (a *Advertisement) Stop() error {
if a.publisher != nil {
return a.publisher.Stop()
}
return nil
}
// Scan starts a BLE scan. It is stopped by a call to StopScan. A common pattern // Scan starts a BLE scan. It is stopped by a call to StopScan. A common pattern
// is to cancel the scan when a particular device has been found. // is to cancel the scan when a particular device has been found.
func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) { func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) {
@ -68,18 +170,25 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) {
// Wait for when advertisement has stopped by a call to StopScan(). // Wait for when advertisement has stopped by a call to StopScan().
// Advertisement doesn't seem to stop right away, there is an // Advertisement doesn't seem to stop right away, there is an
// intermediate Stopping state. // intermediate Stopping state.
stoppingChan := make(chan struct{}) stoppingChan := make(chan error)
// TypedEventHandler<BluetoothLEAdvertisementWatcher, BluetoothLEAdvertisementWatcherStoppedEventArgs> // TypedEventHandler<BluetoothLEAdvertisementWatcher, BluetoothLEAdvertisementWatcherStoppedEventArgs>
eventStoppedGuid := winrt.ParameterizedInstanceGUID( eventStoppedGuid := winrt.ParameterizedInstanceGUID(
foundation.GUIDTypedEventHandler, foundation.GUIDTypedEventHandler,
advertisement.SignatureBluetoothLEAdvertisementWatcher, advertisement.SignatureBluetoothLEAdvertisementWatcher,
advertisement.SignatureBluetoothLEAdvertisementWatcherStoppedEventArgs, advertisement.SignatureBluetoothLEAdvertisementWatcherStoppedEventArgs,
) )
stoppedHandler := foundation.NewTypedEventHandler(ole.NewGUID(eventStoppedGuid), func(_ *foundation.TypedEventHandler, _, _ unsafe.Pointer) { stoppedHandler := foundation.NewTypedEventHandler(ole.NewGUID(eventStoppedGuid), func(_ *foundation.TypedEventHandler, _, arg unsafe.Pointer) {
// Note: the args parameter has an Error property that should args := (*advertisement.BluetoothLEAdvertisementWatcherStoppedEventArgs)(arg)
// probably be checked, but I'm not sure when stopping the errCode, err := args.GetError()
// advertisement watcher could ever result in an error (except if err != nil {
// for bugs). // Got an error while getting the error value, that shouldn't
// happen.
stoppingChan <- fmt.Errorf("failed to get stopping error value: %w", err)
} else if errCode != bluetooth.BluetoothErrorSuccess {
// Could not stop the scan? I'm not sure when this would actually
// happen.
stoppingChan <- fmt.Errorf("failed to stop scanning (error code %d)", errCode)
}
close(stoppingChan) close(stoppingChan)
}) })
defer stoppedHandler.Release() defer stoppedHandler.Release()
@ -96,8 +205,7 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) {
} }
// Wait until advertisement has stopped, and finish. // Wait until advertisement has stopped, and finish.
<-stoppingChan return <-stoppingChan
return nil
} }
func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedEventArgs) ScanResult { func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedEventArgs) ScanResult {
@ -114,7 +222,7 @@ func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedE
Address: adr, Address: adr,
} }
var manufacturerData map[uint16][]byte = make(map[uint16][]byte) var manufacturerData []ManufacturerDataElement
if winAdv, err := args.GetAdvertisement(); err == nil && winAdv != nil { if winAdv, err := args.GetAdvertisement(); err == nil && winAdv != nil {
vector, _ := winAdv.GetManufacturerData() vector, _ := winAdv.GetManufacturerData()
size, _ := vector.GetSize() size, _ := vector.GetSize()
@ -123,7 +231,10 @@ func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedE
manData := (*advertisement.BluetoothLEManufacturerData)(element) manData := (*advertisement.BluetoothLEManufacturerData)(element)
companyID, _ := manData.GetCompanyId() companyID, _ := manData.GetCompanyId()
buffer, _ := manData.GetData() buffer, _ := manData.GetData()
manufacturerData[companyID] = bufferToSlice(buffer) manufacturerData = append(manufacturerData, ManufacturerDataElement{
CompanyID: companyID,
Data: bufferToSlice(buffer),
})
} }
} }
@ -141,7 +252,7 @@ func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedE
} }
func bufferToSlice(buffer *streams.IBuffer) []byte { func bufferToSlice(buffer *streams.IBuffer) []byte {
dataReader, _ := streams.FromBuffer(buffer) dataReader, _ := streams.DataReaderFromBuffer(buffer)
defer dataReader.Release() defer dataReader.Release()
bufferSize, _ := buffer.GetLength() bufferSize, _ := buffer.GetLength()
if bufferSize == 0 { if bufferSize == 0 {
@ -163,6 +274,8 @@ func (a *Adapter) StopScan() error {
// Device is a connection to a remote peripheral. // Device is a connection to a remote peripheral.
type Device struct { type Device struct {
Address Address // the MAC address of the device
device *bluetooth.BluetoothLEDevice device *bluetooth.BluetoothLEDevice
session *genericattributeprofile.GattSession session *genericattributeprofile.GattSession
} }
@ -170,32 +283,32 @@ type Device struct {
// Connect starts a connection attempt to the given peripheral device address. // Connect starts a connection attempt to the given peripheral device address.
// //
// On Linux and Windows, the IsRandom part of the address is ignored. // 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 var winAddr uint64
for i := range address.MAC { for i := range address.MAC {
winAddr += uint64(address.MAC[i]) << (8 * i) winAddr += uint64(address.MAC[i]) << (8 * i)
} }
// IAsyncOperation<BluetoothLEDevice> // IAsyncOperation<BluetoothLEDevice>
bleDeviceOp, err := bluetooth.FromBluetoothAddressAsync(winAddr) bleDeviceOp, err := bluetooth.BluetoothLEDeviceFromBluetoothAddressAsync(winAddr)
if err != nil { if err != nil {
return nil, err return Device{}, err
} }
// We need to pass the signature of the parameter returned by the async operation: // We need to pass the signature of the parameter returned by the async operation:
// IAsyncOperation<BluetoothLEDevice> // IAsyncOperation<BluetoothLEDevice>
if err := awaitAsyncOperation(bleDeviceOp, bluetooth.SignatureBluetoothLEDevice); err != nil { 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() res, err := bleDeviceOp.GetResults()
if err != nil { 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 // The returned BluetoothLEDevice is set to null if FromBluetoothAddressAsync can't find the device identified by bluetoothAddress
if uintptr(res) == 0x0 { 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) 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. // To initiate a connection, we need to set GattSession.MaintainConnection to true.
dID, err := bleDevice.GetBluetoothDeviceId() dID, err := bleDevice.GetBluetoothDeviceId()
if err != nil { if err != nil {
return nil, err return Device{}, err
} }
// Windows does not support explicitly connecting to a device. // Windows does not support explicitly connecting to a device.
// Instead it has the concept of a GATT session that is owned // Instead it has the concept of a GATT session that is owned
// by the calling program. // by the calling program.
gattSessionOp, err := genericattributeprofile.FromDeviceIdAsync(dID) // IAsyncOperation<GattSession> gattSessionOp, err := genericattributeprofile.GattSessionFromDeviceIdAsync(dID) // IAsyncOperation<GattSession>
if err != nil { if err != nil {
return nil, err return Device{}, err
} }
if err := awaitAsyncOperation(gattSessionOp, genericattributeprofile.SignatureGattSession); err != nil { 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() gattRes, err := gattSessionOp.GetResults()
if err != nil { if err != nil {
return nil, err return Device{}, err
} }
newSession := (*genericattributeprofile.GattSession)(gattRes) newSession := (*genericattributeprofile.GattSession)(gattRes)
// This keeps the device connected until we set maintain_connection = False. // This keeps the device connected until we set maintain_connection = False.
if err := newSession.SetMaintainConnection(true); err != nil { 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 // Disconnect from the BLE device. This method is non-blocking and does not
// wait until the connection is fully gone. // wait until the connection is fully gone.
func (d *Device) Disconnect() error { func (d Device) Disconnect() error {
defer d.device.Release() defer d.device.Release()
defer d.session.Release() defer d.session.Release()
@ -247,3 +360,15 @@ func (d *Device) Disconnect() error {
return nil 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 // Passing a nil slice of UUIDs will return a complete list of
// services. // services.
func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) { func (d Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
d.prph.DiscoverServices([]cbgo.UUID{}) d.prph.DiscoverServices([]cbgo.UUID{})
// clear cache of services // clear cache of services
@ -69,14 +69,14 @@ type DeviceService struct {
type deviceService struct { type deviceService struct {
uuidWrapper uuidWrapper
device *Device device Device
service cbgo.Service service cbgo.Service
characteristics map[UUID]DeviceCharacteristic characteristics []DeviceCharacteristic
} }
// UUID returns the UUID for this DeviceService. // UUID returns the UUID for this DeviceService.
func (s *DeviceService) UUID() UUID { func (s DeviceService) UUID() UUID {
return s.uuidWrapper return s.uuidWrapper
} }
@ -89,13 +89,13 @@ func (s *DeviceService) UUID() UUID {
// //
// Passing a nil slice of UUIDs will return a complete list of // Passing a nil slice of UUIDs will return a complete list of
// characteristics. // characteristics.
func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteristic, error) { func (s DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteristic, error) {
cbuuids := []cbgo.UUID{} cbuuids := []cbgo.UUID{}
s.device.prph.DiscoverCharacteristics(cbuuids, s.service) s.device.prph.DiscoverCharacteristics(cbuuids, s.service)
// clear cache of characteristics // clear cache of characteristics
s.characteristics = make(map[UUID]DeviceCharacteristic) s.characteristics = make([]DeviceCharacteristic, 0)
// wait on channel for characteristic discovery // wait on channel for characteristic discovery
select { select {
@ -142,7 +142,7 @@ func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacter
} }
// Small helper to create a DeviceCharacteristic object. // 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{ char := DeviceCharacteristic{
deviceCharacteristic: &deviceCharacteristic{ deviceCharacteristic: &deviceCharacteristic{
uuidWrapper: uuid, uuidWrapper: uuid,
@ -150,7 +150,7 @@ func (s *DeviceService) makeCharacteristic(uuid UUID, dchar cbgo.Characteristic)
characteristic: dchar, characteristic: dchar,
}, },
} }
s.characteristics[char.uuidWrapper] = char s.characteristics = append(s.characteristics, char)
return char return char
} }
@ -163,18 +163,40 @@ type DeviceCharacteristic struct {
type deviceCharacteristic struct { type deviceCharacteristic struct {
uuidWrapper uuidWrapper
service *DeviceService service DeviceService
characteristic cbgo.Characteristic characteristic cbgo.Characteristic
callback func(buf []byte) callback func(buf []byte)
readChan chan error readChan chan error
writeChan chan error
} }
// UUID returns the UUID for this DeviceCharacteristic. // UUID returns the UUID for this DeviceCharacteristic.
func (c *DeviceCharacteristic) UUID() UUID { func (c DeviceCharacteristic) UUID() UUID {
return c.uuidWrapper 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 // WriteWithoutResponse replaces the characteristic value with a new value. The
// call will return before all data has been written. A limited number of such // 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 // 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

@ -1,5 +1,4 @@
//go:build !baremetal //go:build !baremetal
// +build !baremetal
package bluetooth package bluetooth
@ -10,8 +9,6 @@ import (
"time" "time"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
"github.com/muka/go-bluetooth/bluez"
"github.com/muka/go-bluetooth/bluez/profile/gatt"
) )
var ( var (
@ -25,12 +22,12 @@ type uuidWrapper = UUID
// DeviceService is a BLE service on a connected peripheral device. // DeviceService is a BLE service on a connected peripheral device.
type DeviceService struct { type DeviceService struct {
uuidWrapper uuidWrapper
adapter *Adapter
service *gatt.GattService1 servicePath string
} }
// UUID returns the UUID for this DeviceService. // UUID returns the UUID for this DeviceService.
func (s *DeviceService) UUID() UUID { func (s DeviceService) UUID() UUID {
return s.uuidWrapper return s.uuidWrapper
} }
@ -44,18 +41,20 @@ func (s *DeviceService) UUID() UUID {
// //
// On Linux with BlueZ, this just waits for the ServicesResolved signal (if // 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. // 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() start := time.Now()
for { for {
resolved, err := d.device.GetServicesResolved() resolved, err := d.device.GetProperty("org.bluez.Device1.ServicesResolved")
if err != nil { if err != nil {
return nil, err return nil, err
} }
if resolved { if resolved.Value().(bool) {
break break
} }
// This is a terrible hack, but I couldn't find another way. // 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) time.Sleep(10 * time.Millisecond)
if time.Since(start) > 10*time.Second { if time.Since(start) > 10*time.Second {
return nil, errors.New("timeout on DiscoverServices") return nil, errors.New("timeout on DiscoverServices")
@ -63,16 +62,13 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
} }
services := []DeviceService{} services := []DeviceService{}
uuidServices := make(map[string]string) uuidServices := make(map[UUID]struct{})
servicesFound := 0 servicesFound := 0
// Iterate through all objects managed by BlueZ, hoping to find the services // Iterate through all objects managed by BlueZ, hoping to find the services
// we're looking for. // we're looking for.
om, err := bluez.GetObjectManager() var list map[dbus.ObjectPath]map[string]map[string]dbus.Variant
if err != nil { err := d.adapter.bluez.Call("org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0).Store(&list)
return nil, err
}
list, err := om.GetManagedObjects()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -85,19 +81,17 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
if !strings.HasPrefix(objectPath, string(d.device.Path())+"/service") { if !strings.HasPrefix(objectPath, string(d.device.Path())+"/service") {
continue continue
} }
suffix := objectPath[len(d.device.Path()+"/"):] properties, ok := list[dbus.ObjectPath(objectPath)]["org.bluez.GattService1"]
if len(strings.Split(suffix, "/")) != 1 { if !ok {
continue continue
} }
service, err := gatt.NewGattService1(dbus.ObjectPath(objectPath))
if err != nil { serviceUUID, _ := ParseUUID(properties["UUID"].Value().(string))
return nil, err
}
if len(uuids) > 0 { if len(uuids) > 0 {
found := false found := false
for _, uuid := range uuids { for _, uuid := range uuids {
if service.Properties.UUID == uuid.String() { if uuid == serviceUUID {
// One of the services we're looking for. // One of the services we're looking for.
found = true found = true
break break
@ -108,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? // There is more than one service with the same UUID?
// Don't overwrite it, to keep the servicesFound count correct. // Don't overwrite it, to keep the servicesFound count correct.
continue continue
} }
uuid, _ := ParseUUID(service.Properties.UUID) ds := DeviceService{
ds := DeviceService{uuidWrapper: uuid, uuidWrapper: serviceUUID,
service: service, adapter: d.adapter,
servicePath: objectPath,
} }
services = append(services, ds) services = append(services, ds)
servicesFound++ servicesFound++
uuidServices[service.Properties.UUID] = service.Properties.UUID uuidServices[serviceUUID] = struct{}{}
} }
if servicesFound < len(uuids) { if servicesFound < len(uuids) {
@ -135,13 +130,14 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
// device. // device.
type DeviceCharacteristic struct { type DeviceCharacteristic struct {
uuidWrapper uuidWrapper
adapter *Adapter
characteristic *gatt.GattCharacteristic1 characteristic dbus.BusObject
property chan *bluez.PropertyChanged // channel where notifications are reported 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. // UUID returns the UUID for this DeviceCharacteristic.
func (c *DeviceCharacteristic) UUID() UUID { func (c DeviceCharacteristic) UUID() UUID {
return c.uuidWrapper return c.uuidWrapper
} }
@ -154,7 +150,7 @@ func (c *DeviceCharacteristic) UUID() UUID {
// //
// Passing a nil slice of UUIDs will return a complete // Passing a nil slice of UUIDs will return a complete
// list of characteristics. // list of characteristics.
func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteristic, error) { func (s DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteristic, error) {
var chars []DeviceCharacteristic var chars []DeviceCharacteristic
if len(uuids) > 0 { if len(uuids) > 0 {
// The caller wants to get a list of characteristics in a specific // The caller wants to get a list of characteristics in a specific
@ -164,11 +160,8 @@ func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacter
// Iterate through all objects managed by BlueZ, hoping to find the // Iterate through all objects managed by BlueZ, hoping to find the
// characteristic we're looking for. // characteristic we're looking for.
om, err := bluez.GetObjectManager() var list map[dbus.ObjectPath]map[string]map[string]dbus.Variant
if err != nil { err := s.adapter.bluez.Call("org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0).Store(&list)
return nil, err
}
list, err := om.GetManagedObjects()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -178,21 +171,18 @@ func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacter
} }
sort.Strings(objects) sort.Strings(objects)
for _, objectPath := range objects { for _, objectPath := range objects {
if !strings.HasPrefix(objectPath, string(s.service.Path())+"/char") { if !strings.HasPrefix(objectPath, s.servicePath+"/char") {
continue continue
} }
suffix := objectPath[len(s.service.Path()+"/"):] properties, ok := list[dbus.ObjectPath(objectPath)]["org.bluez.GattCharacteristic1"]
if len(strings.Split(suffix, "/")) != 1 { if !ok {
continue continue
} }
characteristic, err := gatt.NewGattCharacteristic1(dbus.ObjectPath(objectPath)) cuuid, _ := ParseUUID(properties["UUID"].Value().(string))
if err != nil {
return nil, err
}
cuuid, _ := ParseUUID(characteristic.Properties.UUID)
char := DeviceCharacteristic{ char := DeviceCharacteristic{
uuidWrapper: cuuid, uuidWrapper: cuuid,
characteristic: characteristic, adapter: s.adapter,
characteristic: s.adapter.bus.Object("org.bluez", dbus.ObjectPath(objectPath)),
} }
if len(uuids) > 0 { if len(uuids) > 0 {
@ -232,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 // writes can be in flight at any given time. This call is also known as a
// "write command" (as opposed to a write request). // "write command" (as opposed to a write request).
func (c DeviceCharacteristic) WriteWithoutResponse(p []byte) (n int, err error) { 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 { if err != nil {
return 0, err return 0, err
} }
@ -245,32 +235,38 @@ func (c DeviceCharacteristic) WriteWithoutResponse(p []byte) (n int, err error)
// changes. // changes.
// //
// Users may call EnableNotifications with a nil callback to disable notifications. // 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 { switch callback {
default: default:
if c.property != nil { if c.property != nil {
return errDupNotif return errDupNotif
} }
ch, err := c.characteristic.WatchProperties() // Start watching for changes in the Value property.
if err != nil { c.property = make(chan *dbus.Signal)
return err 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 { if err != nil {
_ = c.characteristic.UnwatchProperties(ch)
return err return err
} }
c.property = ch
go func() { go func() {
for update := range ch { for sig := range c.property {
if update == nil { if sig.Name == "org.freedesktop.DBus.Properties.PropertiesChanged" {
interfaceName := sig.Body[0].(string)
if interfaceName != "org.bluez.GattCharacteristic1" {
continue continue
} }
if update.Interface == "org.bluez.GattCharacteristic1" && update.Name == "Value" { if sig.Path != c.characteristic.Path() {
callback(update.Value.([]byte)) continue
}
changes := sig.Body[1].(map[string]dbus.Variant)
if value, ok := changes["Value"].Value().([]byte); ok {
callback(value)
}
} }
} }
}() }()
@ -282,26 +278,16 @@ func (c *DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) er
return nil return nil
} }
e1 := c.characteristic.StopNotify() err := c.adapter.bus.RemoveMatchSignal(c.propertiesChangedMatchOption)
e2 := c.characteristic.UnwatchProperties(c.property) c.adapter.bus.RemoveSignal(c.property)
c.property = nil c.property = nil
return err
// FIXME(sbinet): use errors.Join(e1, e2)
if e1 != nil {
return e1
}
if e2 != nil {
return e2
}
return nil
} }
} }
// GetMTU returns the MTU for the characteristic. // GetMTU returns the MTU for the characteristic.
func (c DeviceCharacteristic) GetMTU() (uint16, error) { func (c DeviceCharacteristic) GetMTU() (uint16, error) {
mtu, err := c.characteristic.GetProperty("MTU") mtu, err := c.characteristic.GetProperty("org.bluez.GattCharacteristic1.MTU")
if err != nil { if err != nil {
return uint16(0), err return uint16(0), err
} }
@ -309,9 +295,10 @@ func (c DeviceCharacteristic) GetMTU() (uint16, error) {
} }
// Read reads the current characteristic value. // 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{}) 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 { if err != nil {
return 0, err return 0, err
} }

View file

@ -1,5 +1,4 @@
//go:build (softdevice && s132v6) || (softdevice && s140v6) || (softdevice && s140v7) //go:build (softdevice && s132v6) || (softdevice && s140v6) || (softdevice && s140v7)
// +build softdevice,s132v6 softdevice,s140v6 softdevice,s140v7
package bluetooth package bluetooth
@ -12,6 +11,7 @@ import (
"device/arm" "device/arm"
"errors" "errors"
"runtime/volatile" "runtime/volatile"
"unsafe"
) )
const ( const (
@ -29,8 +29,8 @@ var (
// program and the event handler. // program and the event handler.
var discoveringService struct { var discoveringService struct {
state volatile.Register8 // 0 means nothing happening, 1 means in progress, 2 means found something state volatile.Register8 // 0 means nothing happening, 1 means in progress, 2 means found something
startHandle volatile.Register16 startHandle volatileHandle
endHandle volatile.Register16 endHandle volatileHandle
uuid C.ble_uuid_t uuid C.ble_uuid_t
} }
@ -39,13 +39,13 @@ var discoveringService struct {
type DeviceService struct { type DeviceService struct {
uuid shortUUID uuid shortUUID
connectionHandle uint16 connectionHandle C.uint16_t
startHandle uint16 startHandle C.uint16_t
endHandle uint16 endHandle C.uint16_t
} }
// UUID returns the UUID for this DeviceService. // UUID returns the UUID for this DeviceService.
func (s *DeviceService) UUID() UUID { func (s DeviceService) UUID() UUID {
return s.uuid.UUID() return s.uuid.UUID()
} }
@ -59,7 +59,7 @@ func (s *DeviceService) UUID() UUID {
// //
// On the Nordic SoftDevice, only one service discovery procedure may be done at // On the Nordic SoftDevice, only one service discovery procedure may be done at
// a time. // a time.
func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) { func (d Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
if discoveringService.state.Get() != 0 { if discoveringService.state.Get() != 0 {
// Not concurrency safe, but should catch most concurrency misuses. // Not concurrency safe, but should catch most concurrency misuses.
return nil, errAlreadyDiscovering return nil, errAlreadyDiscovering
@ -77,7 +77,7 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
if len(uuids) > 0 { if len(uuids) > 0 {
shortUUIDs = make([]C.ble_uuid_t, sz) shortUUIDs = make([]C.ble_uuid_t, sz)
for i, uuid := range uuids { for i, uuid := range uuids {
var errCode uint32 var errCode C.uint32_t
shortUUIDs[i], errCode = uuid.shortUUID() shortUUIDs[i], errCode = uuid.shortUUID()
if errCode != 0 { if errCode != 0 {
return nil, Error(errCode) return nil, Error(errCode)
@ -87,7 +87,7 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
numFound := 0 numFound := 0
var startHandle uint16 = 1 var startHandle C.uint16_t = 1
for i := 0; i < sz; i++ { for i := 0; i < sz; i++ {
var suuid C.ble_uuid_t var suuid C.ble_uuid_t
@ -97,7 +97,7 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
// Start discovery of this service. // Start discovery of this service.
discoveringService.state.Set(1) discoveringService.state.Set(1)
var errCode uint32 var errCode C.uint32_t
if len(uuids) > 0 { if len(uuids) > 0 {
errCode = C.sd_ble_gattc_primary_services_discover(d.connectionHandle, startHandle, &suuid) errCode = C.sd_ble_gattc_primary_services_discover(d.connectionHandle, startHandle, &suuid)
} else { } else {
@ -160,14 +160,14 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
type DeviceCharacteristic struct { type DeviceCharacteristic struct {
uuid shortUUID uuid shortUUID
connectionHandle uint16 connectionHandle C.uint16_t
valueHandle uint16 valueHandle C.uint16_t
cccdHandle uint16 cccdHandle C.uint16_t
permissions CharacteristicPermissions permissions CharacteristicPermissions
} }
// UUID returns the UUID for this DeviceCharacteristic. // UUID returns the UUID for this DeviceCharacteristic.
func (c *DeviceCharacteristic) UUID() UUID { func (c DeviceCharacteristic) UUID() UUID {
return c.uuid.UUID() return c.uuid.UUID()
} }
@ -176,7 +176,7 @@ func (c *DeviceCharacteristic) UUID() UUID {
var discoveringCharacteristic struct { var discoveringCharacteristic struct {
uuid C.ble_uuid_t uuid C.ble_uuid_t
char_props C.ble_gatt_char_props_t char_props C.ble_gatt_char_props_t
handle_value volatile.Register16 handle_value volatileHandle
} }
// DiscoverCharacteristics discovers characteristics in this service. Pass a // DiscoverCharacteristics discovers characteristics in this service. Pass a
@ -188,7 +188,7 @@ var discoveringCharacteristic struct {
// //
// Passing a nil slice of UUIDs will return a complete // Passing a nil slice of UUIDs will return a complete
// list of characteristics. // 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 { if discoveringCharacteristic.handle_value.Get() != 0 {
return nil, errAlreadyDiscovering return nil, errAlreadyDiscovering
} }
@ -205,7 +205,7 @@ func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacter
if len(uuids) > 0 { if len(uuids) > 0 {
shortUUIDs = make([]C.ble_uuid_t, sz) shortUUIDs = make([]C.ble_uuid_t, sz)
for i, uuid := range uuids { for i, uuid := range uuids {
var errCode uint32 var errCode C.uint32_t
shortUUIDs[i], errCode = uuid.shortUUID() shortUUIDs[i], errCode = uuid.shortUUID()
if errCode != 0 { if errCode != 0 {
return nil, Error(errCode) return nil, Error(errCode)
@ -325,8 +325,8 @@ func (c DeviceCharacteristic) WriteWithoutResponse(p []byte) (n int, err error)
write_op: C.BLE_GATT_OP_WRITE_CMD, write_op: C.BLE_GATT_OP_WRITE_CMD,
handle: c.valueHandle, handle: c.valueHandle,
offset: 0, offset: 0,
len: uint16(len(p)), len: C.uint16_t(len(p)),
p_value: &p[0], p_value: (*C.uint8_t)(unsafe.Pointer(&p[0])),
}) })
if errCode != 0 { if errCode != 0 {
return 0, Error(errCode) return 0, Error(errCode)
@ -335,8 +335,8 @@ func (c DeviceCharacteristic) WriteWithoutResponse(p []byte) (n int, err error)
} }
type gattcNotificationCallback struct { type gattcNotificationCallback struct {
connectionHandle uint16 connectionHandle C.uint16_t
valueHandle uint16 // may be 0 if the slot is empty valueHandle C.uint16_t // may be 0 if the slot is empty
callback func([]byte) callback func([]byte)
} }
@ -401,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. // 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{ errCode := C.sd_ble_gattc_write(c.connectionHandle, &C.ble_gattc_write_params_t{
write_op: C.BLE_GATT_OP_WRITE_CMD, write_op: C.BLE_GATT_OP_WRITE_CMD,
handle: c.cccdHandle, handle: c.cccdHandle,
@ -415,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 // A global used to pass information from the event handler back to the
// Read function below. // Read function below.
var readingCharacteristic struct { var readingCharacteristic struct {
handle_value volatile.Register16 handle_value volatileHandle
offset uint16 offset C.uint16_t
length uint16 length C.uint16_t
value []byte value []byte
} }
// Read reads the current characteristic value up to MTU length. // Read reads the current characteristic value up to MTU length.
// A future enhancement would be to be able to retrieve a longer // A future enhancement would be to be able to retrieve a longer
// value by making multiple calls. // 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 // global will copy bytes from read operation into data slice
readingCharacteristic.value = data readingCharacteristic.value = data

View file

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

View file

@ -7,6 +7,8 @@ type Service struct {
Characteristics []CharacteristicConfig Characteristics []CharacteristicConfig
} }
type WriteEvent = func(client Connection, offset int, value []byte)
// CharacteristicConfig contains some parameters for the configuration of a // CharacteristicConfig contains some parameters for the configuration of a
// single characteristic. // single characteristic.
// //
@ -17,7 +19,7 @@ type CharacteristicConfig struct {
UUID UUID
Value []byte Value []byte
Flags CharacteristicPermissions Flags CharacteristicPermissions
WriteEvent func(client Connection, offset int, value []byte) WriteEvent WriteEvent
} }
// CharacteristicPermissions lists a number of basic permissions/capabilities // CharacteristicPermissions lists a number of basic permissions/capabilities
@ -56,3 +58,13 @@ func (p CharacteristicPermissions) Write() bool {
func (p CharacteristicPermissions) WriteWithoutResponse() bool { func (p CharacteristicPermissions) WriteWithoutResponse() bool {
return p&CharacteristicWriteWithoutResponsePermission != 0 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

@ -1,97 +1,156 @@
//go:build !baremetal //go:build !baremetal
// +build !baremetal
package bluetooth package bluetooth
import ( import (
"github.com/muka/go-bluetooth/api/service" "fmt"
"github.com/muka/go-bluetooth/bluez/profile/gatt" "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 // Characteristic is a single characteristic in a service. It has an UUID and a
// value. // value.
type Characteristic struct { type Characteristic struct {
handle *service.Char char *bluezChar
permissions CharacteristicPermissions 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 // AddService creates a new service with the characteristics listed in the
// Service struct. // Service struct.
func (a *Adapter) AddService(s *Service) error { func (a *Adapter) AddService(s *Service) error {
app, err := service.NewApp(service.AppOptions{ // Create a unique DBus path for this service.
AdapterID: a.id, id := atomic.AddUint64(&serviceID, 1)
}) path := dbus.ObjectPath(fmt.Sprintf("/org/tinygo/bluetooth/service%d", id))
if err != nil {
return err // 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. for i, char := range s.Characteristics {
// muka/go-bluetooth does some magic so you can use short UUIDs and it'll auto // Calculate Flags field.
// 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.
bluezCharFlags := []string{ bluezCharFlags := []string{
gatt.FlagCharacteristicBroadcast, // bit 0 "broadcast", // bit 0
gatt.FlagCharacteristicRead, // bit 1 "read", // bit 1
gatt.FlagCharacteristicWriteWithoutResponse, // bit 2 "write-without-response", // bit 2
gatt.FlagCharacteristicWrite, // bit 3 "write", // bit 3
gatt.FlagCharacteristicNotify, // bit 4 "notify", // bit 4
gatt.FlagCharacteristicIndicate, // bit 5 "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 { 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. // Export the properties of this characteristic.
if char.WriteEvent != nil { charPath := path + dbus.ObjectPath("/char"+strconv.Itoa(i))
callback := char.WriteEvent propsSpec := map[string]map[string]*prop.Prop{
bluezChar.OnWrite(func(c *service.Char, value []byte) ([]byte, error) { "org.bluez.GattCharacteristic1": {
// BlueZ doesn't seem to tell who did the write, so pass 0 "UUID": {Value: char.UUID.String()},
// always. "Service": {Value: path},
// It also doesn't provide which part of the value was written, "Flags": {Value: flags},
// so pretend the entire characteristic was updated (which might "Value": {Value: char.Value, Writable: true, Emit: prop.EmitTrue},
// not be the case). },
callback(0, 0, value)
return nil, nil
})
} }
objects[charPath] = propsSpec
// Add characteristic to the service, to activate it. props, err := prop.Export(a.bus, charPath, propsSpec)
err = bluezService.AddChar(bluezChar)
if err != nil { if err != nil {
return err 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. // Write replaces the characteristic value with a new value.
@ -100,7 +159,10 @@ func (c *Characteristic) Write(p []byte) (n int, err error) {
return 0, nil // nothing to do 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 { if gattError != nil {
return 0, gattError return 0, gattError
} }

View file

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

View file

@ -1,18 +1,33 @@
//go:build softdevice //go:build softdevice
// +build softdevice
package bluetooth package bluetooth
/* /*
#include "ble_gap.h" #include "ble_gap.h"
#include "ble_gatts.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 "C"
import "unsafe"
// Characteristic is a single characteristic in a service. It has an UUID and a // Characteristic is a single characteristic in a service. It has an UUID and a
// value. // value.
type Characteristic struct { type Characteristic struct {
handle uint16 handle C.uint16_t
permissions CharacteristicPermissions permissions CharacteristicPermissions
} }
@ -23,18 +38,18 @@ func (a *Adapter) AddService(service *Service) error {
if errCode != 0 { if errCode != 0 {
return Error(errCode) 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 { if errCode != 0 {
return Error(errCode) return Error(errCode)
} }
for _, char := range service.Characteristics { for _, char := range service.Characteristics {
metadata := C.ble_gatts_char_md_t{} metadata := C.ble_gatts_char_md_t{}
metadata.char_props.set_bitfield_broadcast(uint8(char.Flags>>0) & 1) metadata.char_props.set_bitfield_broadcast(C.uint8_t(char.Flags>>0) & 1)
metadata.char_props.set_bitfield_read(uint8(char.Flags>>1) & 1) metadata.char_props.set_bitfield_read(C.uint8_t(char.Flags>>1) & 1)
metadata.char_props.set_bitfield_write_wo_resp(uint8(char.Flags>>2) & 1) metadata.char_props.set_bitfield_write_wo_resp(C.uint8_t(char.Flags>>2) & 1)
metadata.char_props.set_bitfield_write(uint8(char.Flags>>3) & 1) metadata.char_props.set_bitfield_write(C.uint8_t(char.Flags>>3) & 1)
metadata.char_props.set_bitfield_notify(uint8(char.Flags>>4) & 1) metadata.char_props.set_bitfield_notify(C.uint8_t(char.Flags>>4) & 1)
metadata.char_props.set_bitfield_indicate(uint8(char.Flags>>5) & 1) metadata.char_props.set_bitfield_indicate(C.uint8_t(char.Flags>>5) & 1)
handles := C.ble_gatts_char_handles_t{} handles := C.ble_gatts_char_handles_t{}
charUUID, errCode := char.UUID.shortUUID() charUUID, errCode := char.UUID.shortUUID()
if errCode != 0 { if errCode != 0 {
@ -46,16 +61,16 @@ func (a *Adapter) AddService(service *Service) error {
read_perm: secModeOpen, read_perm: secModeOpen,
write_perm: secModeOpen, write_perm: secModeOpen,
}, },
init_len: uint16(len(char.Value)), init_len: C.uint16_t(len(char.Value)),
init_offs: 0, init_offs: 0,
max_len: 20, // This is a conservative maximum length. max_len: 20, // This is a conservative maximum length.
} }
if len(char.Value) != 0 { 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_vloc(C.BLE_GATTS_VLOC_STACK)
value.p_attr_md.set_bitfield_vlen(1) 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 { if errCode != 0 {
return Error(errCode) return Error(errCode)
} }
@ -79,13 +94,13 @@ func (a *Adapter) AddService(service *Service) error {
// charWriteHandler contains a handler->callback mapping for characteristic // charWriteHandler contains a handler->callback mapping for characteristic
// writes. // writes.
type charWriteHandler struct { type charWriteHandler struct {
handle uint16 handle C.uint16_t
callback func(connection Connection, offset int, value []byte) callback func(connection Connection, offset int, value []byte)
} }
// getCharWriteHandler returns a characteristic write handler if one matches the // getCharWriteHandler returns a characteristic write handler if one matches the
// handle, or nil otherwise. // 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. // Look through all handlers for a match.
// There is probably a way to do this more efficiently (with a hashmap for // 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 // example) but the number of event handlers is likely low and improving
@ -108,15 +123,16 @@ func (c *Characteristic) Write(p []byte) (n int, err error) {
} }
connHandle := currentConnection.Get() 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. // There is a connected central.
p_len := uint16(len(p)) p_len := uint16(len(p))
errCode := C.sd_ble_gatts_hvx(connHandle, &C.ble_gatts_hvx_params_t{ errCode := C.sd_ble_gatts_hvx_noescape(connHandle,
handle: c.handle, c.handle,
_type: C.BLE_GATT_HVX_NOTIFICATION, C.BLE_GATT_HVX_NOTIFICATION,
p_len: &p_len, 0,
p_data: &p[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 // Check for some expected errors. Don't report them as errors, but
// instead fall through and do a normal characteristic value update. // instead fall through and do a normal characteristic value update.
@ -134,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{ 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])))
len: uint16(len(p)),
p_value: &p[0],
})
if errCode != 0 { if errCode != 0 {
return 0, Error(errCode) return 0, Error(errCode)
} }

309
gatts_windows.go Normal file
View file

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

30
go.mod
View file

@ -1,17 +1,25 @@
module tinygo.org/x/bluetooth module gitrepo.ru/neonxp/bluetooth
go 1.15 go 1.20
require ( require (
github.com/go-ole/go-ole v1.2.6 github.com/go-ole/go-ole v1.2.6
github.com/godbus/dbus/v5 v5.0.3 github.com/godbus/dbus/v5 v5.1.0
github.com/muka/go-bluetooth v0.0.0-20220830075246-0746e3a1ea53 github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b
github.com/saltosystems/winrt-go v0.0.0-20230510070731-e096b9afa761 github.com/soypat/cyw43439 v0.0.0-20240609122733-da9153086796
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/tinygo-org/cbgo v0.0.4 github.com/tinygo-org/cbgo v0.0.4
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 golang.org/x/crypto v0.12.0
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect tinygo.org/x/drivers v0.26.1-0.20230922160320-ed51435c2ef6
golang.org/x/text v0.5.0 tinygo.org/x/tinyfont v0.4.0
tinygo.org/x/drivers v0.23.0 tinygo.org/x/tinyterm v0.3.0
tinygo.org/x/tinyterm v0.1.0 )
require (
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef // indirect
github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899 // indirect
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/term v0.11.0 // indirect
) )

141
go.sum
View file

@ -1,134 +1,49 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/glerchundi/subcommands v0.0.0-20181212083838-923a6ccb11f8/go.mod h1:r0g3O7Y5lrWXgDfcFBRgnAKzjmPgTzwoMC2ieB345FY=
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= 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/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 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/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
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.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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/muka/go-bluetooth v0.0.0-20220830075246-0746e3a1ea53 h1:zfLHhuGzmSbthZ00FfbEjgAHUOOj7NGiITojMTCFy6U=
github.com/muka/go-bluetooth v0.0.0-20220830075246-0746e3a1ea53/go.mod h1:dMCjicU6vRBk34dqOmIZm0aod6gUwZXOXzBROqGous0=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
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/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
github.com/peterbourgon/ff/v3 v3.1.2/go.mod h1:XNJLY8EIl6MjMVjBS4F0+G0LYoAqs0DTa4rmHHukKDE=
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sago35/go-bdf v0.0.0-20200313142241-6c17821c91c4/go.mod h1:rOebXGuMLsXhZAC6mF/TjxONsm45498ZyzVhel++6KM= github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b h1:du3zG5fd8snsFN6RBoLA7fpaYV9ZQIsyH9snlk2Zvik=
github.com/saltosystems/winrt-go v0.0.0-20230124093143-967a889c6c8f h1:sxsy5XkcxSzkiUkYgx38V9JviWLL8wthO2TURCi0Lcs= github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA=
github.com/saltosystems/winrt-go v0.0.0-20230124093143-967a889c6c8f/go.mod h1:UvKm1lyhg+8ehk99i8g5Q7AX1LXUJgks0lRyAkG/ahQ=
github.com/saltosystems/winrt-go v0.0.0-20230510070731-e096b9afa761 h1:xEscoMxTrGSpdho1mP9VnGsK0DGhXKwm0qP7kYcjgrI=
github.com/saltosystems/winrt-go v0.0.0-20230510070731-e096b9afa761/go.mod h1:UvKm1lyhg+8ehk99i8g5Q7AX1LXUJgks0lRyAkG/ahQ=
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/soypat/cyw43439 v0.0.0-20240609122733-da9153086796 h1:1/r2URInjjFtWqT61gU7YGVCq3BRyXt/C7z4oLRF9Lo=
github.com/soypat/cyw43439 v0.0.0-20240609122733-da9153086796/go.mod h1:1Otjk6PRhfzfcVHeWMEeku/VntFqWghUwuSQyivb2vE=
github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef h1:phH95I9wANjTYw6bSYLZDQfNvao+HqYDom8owbNa0P4=
github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef/go.mod h1:oCVCNGCHMKoBj97Zp9znLbQ1nHxpkmOY9X+UAGzOxc8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.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.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q= github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q=
github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/suapapa/go_eddystone v1.3.1/go.mod h1:bXC11TfJOS+3g3q/Uzd7FKd5g62STQEfeEIhcKe4Qy8=
github.com/tdakkota/win32metadata v0.1.0/go.mod h1:77e6YvX0LIVW+O81fhWLnXAxxcyu/wdZdG7iwed7Fyk=
github.com/tinygo-org/cbgo v0.0.4 h1:3D76CRYbH03Rudi8sEgs/YO0x3JIMdyq8jlQtk/44fU= github.com/tinygo-org/cbgo v0.0.4 h1:3D76CRYbH03Rudi8sEgs/YO0x3JIMdyq8jlQtk/44fU=
github.com/tinygo-org/cbgo v0.0.4/go.mod h1:7+HgWIHd4nbAz0ESjGlJ1/v9LDU1Ox8MGzP9mah/fLk= github.com/tinygo-org/cbgo v0.0.4/go.mod h1:7+HgWIHd4nbAz0ESjGlJ1/v9LDU1Ox8MGzP9mah/fLk=
github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899 h1:/DyaXDEWMqoVUVEJVJIlNk1bXTbFs8s3Q4GdPInSKTQ=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899/go.mod h1:LU7Dw00NJ+N86QkeTGjMLNkYcEYMor6wTDpTCu0EaH8=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 h1:/yRP+0AN7mf5DkD3BAI6TOFnd51gEoDEb8o35jIFtgw=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
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/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
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/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= tinygo.org/x/drivers v0.26.1-0.20230922160320-ed51435c2ef6 h1:w18u47MirULgAl+bP0piUGu5VUZDs7TvXwHASEVXqHk=
tinygo.org/x/drivers v0.14.0/go.mod h1:uT2svMq3EpBZpKkGO+NQHjxjGf1f42ra4OnMMwQL2aI= tinygo.org/x/drivers v0.26.1-0.20230922160320-ed51435c2ef6/go.mod h1:X7utcg3yfFUFuKLOMTZD56eztXMjpkcf8OHldfTBsjw=
tinygo.org/x/drivers v0.15.1/go.mod h1:uT2svMq3EpBZpKkGO+NQHjxjGf1f42ra4OnMMwQL2aI= tinygo.org/x/tinyfont v0.4.0 h1:XexPKEKiHInf6p4CMCJwsIheVPY0T46HUs6ictYyZfE=
tinygo.org/x/drivers v0.16.0/go.mod h1:uT2svMq3EpBZpKkGO+NQHjxjGf1f42ra4OnMMwQL2aI= tinygo.org/x/tinyfont v0.4.0/go.mod h1:7nVj3j3geqBoPDzpFukAhF1C8AP9YocMsZy0HSAcGCA=
tinygo.org/x/drivers v0.19.0/go.mod h1:uJD/l1qWzxzLx+vcxaW0eY464N5RAgFi1zTVzASFdqI= tinygo.org/x/tinyterm v0.3.0 h1:4MMZoMyrbWbjru1KP/Z2TGhaguy/Uh5Mdhf/niemM8c=
tinygo.org/x/drivers v0.23.0 h1:fUy4OmLOWWYCOzDp/83Qewej1Q+YgUpwkm11e7gxUc0= tinygo.org/x/tinyterm v0.3.0/go.mod h1:F1pQjxEwNZQIc5czeJSBtk57ucEvbR4u7vHaLhWhHtg=
tinygo.org/x/drivers v0.23.0/go.mod h1:J4+51Li1kcfL5F93kmnDWEEzQF3bLGz0Am3Q7E2a8/E=
tinygo.org/x/tinyfont v0.2.1/go.mod h1:eLqnYSrFRjt5STxWaMeOWJTzrKhXqpWw7nU3bPfKOAM=
tinygo.org/x/tinyfont v0.3.0 h1:HIRLQoI3oc+2CMhPcfv+Ig88EcTImE/5npjqOnMD4lM=
tinygo.org/x/tinyfont v0.3.0/go.mod h1:+TV5q0KpwSGRWnN+ITijsIhrWYJkoUCp9MYELjKpAXk=
tinygo.org/x/tinyfs v0.1.0/go.mod h1:ysc8Y92iHfhTXeyEM9+c7zviUQ4fN9UCFgSOFfMWv20=
tinygo.org/x/tinyfs v0.2.0/go.mod h1:6ZHYdvB3sFYeMB3ypmXZCNEnFwceKc61ADYTYHpep1E=
tinygo.org/x/tinyterm v0.1.0 h1:80i+j+KWoxCFa/Xfp6pWbh79x+8zUdMXC1vaKj2QhkY=
tinygo.org/x/tinyterm v0.1.0/go.mod h1:/DDhNnGwNF2/tNgHywvyZuCGnbH3ov49Z/6e8LPLRR4=

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,4 +1,4 @@
// +build linux,!baremetal darwin //go:build (linux && !baremetal) || darwin
// Package rawterm provides some sort of raw terminal interface, both on hosted // Package rawterm provides some sort of raw terminal interface, both on hosted
// systems and baremetal. It is intended only for use by examples. // systems and baremetal. It is intended only for use by examples.

View file

@ -1,4 +1,4 @@
// +build nrf //go:build nrf
package rawterm package rawterm

View file

@ -1,19 +1,18 @@
// Code generated by bin/gen-service-uuids; DO NOT EDIT. // 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 // 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 // https://github.com/NordicSemiconductor/bluetooth-numbers-database/blob/master/v1/service_uuids.json
//
package bluetooth package bluetooth
var ( var (
// ServiceUUIDExperimentalButtonlessDFU - Experimental Buttonless DFU Service // 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 - 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 - 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 - User Data
ServiceUUIDUserData = New16BitUUID(0x181C) ServiceUUIDUserData = New16BitUUID(0x181C)
@ -22,16 +21,16 @@ var (
ServiceUUIDAudioStreamControl = New16BitUUID(0x184E) ServiceUUIDAudioStreamControl = New16BitUUID(0x184E)
// ServiceUUIDSMP - SMP Service // 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 - 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 - Pulse Oximeter Service
ServiceUUIDPulseOximeter = New16BitUUID(0x1822) ServiceUUIDPulseOximeter = New16BitUUID(0x1822)
// ServiceUUIDEddystoneConfiguration - Eddystone Configuration Service // 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 - Location and Navigation
ServiceUUIDLocationAndNavigation = New16BitUUID(0x1819) ServiceUUIDLocationAndNavigation = New16BitUUID(0x1819)
@ -46,10 +45,10 @@ var (
ServiceUUIDSignifyNetherlandsBVFormerlyPhilipsLighting = New16BitUUID(0xFE0F) ServiceUUIDSignifyNetherlandsBVFormerlyPhilipsLighting = New16BitUUID(0xFE0F)
// ServiceUUIDPhilipsHueLightControl - Philips Hue Light Control Service // 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 - 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 - Alert Notification Service
ServiceUUIDAlertNotification = New16BitUUID(0x1811) ServiceUUIDAlertNotification = New16BitUUID(0x1811)
@ -64,16 +63,16 @@ var (
ServiceUUIDGenericAttribute = New16BitUUID(0x1801) ServiceUUIDGenericAttribute = New16BitUUID(0x1801)
// ServiceUUIDAdafruitMagnetometer - Adafruit Magnetometer Service // 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 - Microphone Control
ServiceUUIDMicrophoneControl = New16BitUUID(0x184D) ServiceUUIDMicrophoneControl = New16BitUUID(0x184D)
// ServiceUUIDLEGOWirelessProtocolV3Bootloader - LEGO® Wireless Protocol v3 Bootloader Service // 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 - 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 - Mesh Provisioning Service
ServiceUUIDMeshProvisioning = New16BitUUID(0x1827) ServiceUUIDMeshProvisioning = New16BitUUID(0x1827)
@ -82,22 +81,22 @@ var (
ServiceUUIDTransportDiscovery = New16BitUUID(0x1824) ServiceUUIDTransportDiscovery = New16BitUUID(0x1824)
// ServiceUUIDAppleReserved1 - Apple Reserved Service 1 // 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 - 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 - 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 - 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 - 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 - 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 - Scan Parameters
ServiceUUIDScanParameters = New16BitUUID(0x1813) ServiceUUIDScanParameters = New16BitUUID(0x1813)
@ -106,13 +105,13 @@ var (
ServiceUUIDCoordinatedSetIdentification = New16BitUUID(0x1846) ServiceUUIDCoordinatedSetIdentification = New16BitUUID(0x1846)
// ServiceUUIDMicrobitAccelerometer - micro:bit Accelerometer Service // 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 - 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 - 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 - Immediate Alert
ServiceUUIDImmediateAlert = New16BitUUID(0x1802) ServiceUUIDImmediateAlert = New16BitUUID(0x1802)
@ -127,10 +126,10 @@ var (
ServiceUUIDVolumeControl = New16BitUUID(0x1844) ServiceUUIDVolumeControl = New16BitUUID(0x1844)
// ServiceUUIDMicrobitMagnetometer - micro:bit Magnetometer Service // 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 - 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 - File Transfer Service by Adafruit
ServiceUUIDFileTransferByAdafruit = New16BitUUID(0xFEBB) ServiceUUIDFileTransferByAdafruit = New16BitUUID(0xFEBB)
@ -151,16 +150,16 @@ var (
ServiceUUIDFastPair = New16BitUUID(0xFE2C) ServiceUUIDFastPair = New16BitUUID(0xFE2C)
// ServiceUUIDAdafruitGyroscope - Adafruit Gyroscope Service // 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 - 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 - Telephone Bearer
ServiceUUIDTelephoneBearer = New16BitUUID(0x184B) ServiceUUIDTelephoneBearer = New16BitUUID(0x184B)
// ServiceUUIDNordicLEDAndButton - Nordic LED and Button Service // 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 - Internet Protocol Support Service
ServiceUUIDInternetProtocolSupport = New16BitUUID(0x1820) ServiceUUIDInternetProtocolSupport = New16BitUUID(0x1820)
@ -169,7 +168,7 @@ var (
ServiceUUIDPublishedAudioCapabilities = New16BitUUID(0x1850) ServiceUUIDPublishedAudioCapabilities = New16BitUUID(0x1850)
// ServiceUUIDNordicUART - Nordic UART Service // 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 - Exposure Notification Service
ServiceUUIDExposureNotification = New16BitUUID(0xFD6F) ServiceUUIDExposureNotification = New16BitUUID(0xFD6F)
@ -187,7 +186,7 @@ var (
ServiceUUIDConstantToneExtension = New16BitUUID(0x184A) ServiceUUIDConstantToneExtension = New16BitUUID(0x184A)
// ServiceUUIDThingyWeatherStation - Thingy Weather Station Service // 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 - Health Thermometer
ServiceUUIDHealthThermometer = New16BitUUID(0x1809) ServiceUUIDHealthThermometer = New16BitUUID(0x1809)
@ -202,19 +201,19 @@ var (
ServiceUUIDReconnectionConfiguration = New16BitUUID(0x1829) ServiceUUIDReconnectionConfiguration = New16BitUUID(0x1829)
// ServiceUUIDAdafruitQuaternion - Adafruit Quaternion Service // 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 - Mesh Proxy Service
ServiceUUIDMeshProxy = New16BitUUID(0x1828) ServiceUUIDMeshProxy = New16BitUUID(0x1828)
// ServiceUUIDThingyMotion - Thingy Motion Service // 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 - Indoor Positioning
ServiceUUIDIndoorPositioning = New16BitUUID(0x1821) ServiceUUIDIndoorPositioning = New16BitUUID(0x1821)
// ServiceUUIDAdafruitAddressable - Adafruit Addressable Service // 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 - Blood Pressure
ServiceUUIDBloodPressure = New16BitUUID(0x1810) ServiceUUIDBloodPressure = New16BitUUID(0x1810)
@ -238,7 +237,7 @@ var (
ServiceUUIDBroadcastAudioAnnouncement = New16BitUUID(0x1852) ServiceUUIDBroadcastAudioAnnouncement = New16BitUUID(0x1852)
// ServiceUUIDMicrobitDFUControl - micro:bit DFU Control Service // 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 - Bond Management Service
ServiceUUIDBondManagement = New16BitUUID(0x181E) ServiceUUIDBondManagement = New16BitUUID(0x181E)
@ -247,7 +246,7 @@ var (
ServiceUUIDLinkLoss = New16BitUUID(0x1803) ServiceUUIDLinkLoss = New16BitUUID(0x1803)
// ServiceUUIDAdafruitAccelerometer - Adafruit Accelerometer Service // 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 - Reference Time Update Service
ServiceUUIDReferenceTimeUpdate = New16BitUUID(0x1806) ServiceUUIDReferenceTimeUpdate = New16BitUUID(0x1806)
@ -256,7 +255,7 @@ var (
ServiceUUIDTxPower = New16BitUUID(0x1804) ServiceUUIDTxPower = New16BitUUID(0x1804)
// ServiceUUIDMicrobitTemperature - micro:bit Temperature Service // 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 - Cycling Speed and Cadence
ServiceUUIDCyclingSpeedAndCadence = New16BitUUID(0x1816) ServiceUUIDCyclingSpeedAndCadence = New16BitUUID(0x1816)
@ -268,10 +267,10 @@ var (
ServiceUUIDTMAS = New16BitUUID(0x1855) ServiceUUIDTMAS = New16BitUUID(0x1855)
// ServiceUUIDMicrobitButton - micro:bit Button Service // 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 - 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 - Automation IO
ServiceUUIDAutomationIO = New16BitUUID(0x1815) ServiceUUIDAutomationIO = New16BitUUID(0x1815)
@ -280,28 +279,28 @@ var (
ServiceUUIDGlucose = New16BitUUID(0x1808) ServiceUUIDGlucose = New16BitUUID(0x1808)
// ServiceUUIDAppleMedia - Apple Media Service // 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 - 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 - Heart Rate
ServiceUUIDHeartRate = New16BitUUID(0x180D) ServiceUUIDHeartRate = New16BitUUID(0x180D)
// ServiceUUIDPhilipsHueLightUpdate - Philips Hue Light Update Service // 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 - 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 - 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 - 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 - 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 - Object Transfer Service
ServiceUUIDObjectTransfer = New16BitUUID(0x1825) ServiceUUIDObjectTransfer = New16BitUUID(0x1825)
@ -316,16 +315,16 @@ var (
ServiceUUIDVolumeOffsetControl = New16BitUUID(0x1845) ServiceUUIDVolumeOffsetControl = New16BitUUID(0x1845)
// ServiceUUIDThingyConfiguration - Thingy Configuration Service // 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 - 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 - 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 - 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 - Battery Service
ServiceUUIDBattery = New16BitUUID(0x180F) ServiceUUIDBattery = New16BitUUID(0x180F)
@ -337,7 +336,7 @@ var (
ServiceUUIDAudioInputControl = New16BitUUID(0x1843) ServiceUUIDAudioInputControl = New16BitUUID(0x1843)
// ServiceUUIDAppleNotificationCenter - Apple Notification Center Service // 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 - Generic Access
ServiceUUIDGenericAccess = New16BitUUID(0x1800) ServiceUUIDGenericAccess = New16BitUUID(0x1800)
@ -350,5 +349,4 @@ var (
// ServiceUUIDHTTPProxy - HTTP Proxy // ServiceUUIDHTTPProxy - HTTP Proxy
ServiceUUIDHTTPProxy = New16BitUUID(0x1823) ServiceUUIDHTTPProxy = New16BitUUID(0x1823)
) )

View file

@ -1,5 +1,4 @@
//go:build ignore //go:build ignore
// +build ignore
package main package main
@ -17,7 +16,7 @@ import (
"golang.org/x/text/cases" "golang.org/x/text/cases"
"golang.org/x/text/language" "golang.org/x/text/language"
"tinygo.org/x/bluetooth" "gitrepo.ru/neonxp/bluetooth"
) )
type Characteristic struct { type Characteristic struct {

View file

@ -1,5 +1,4 @@
//go:build ignore //go:build ignore
// +build ignore
package main package main
@ -17,7 +16,7 @@ import (
"golang.org/x/text/cases" "golang.org/x/text/cases"
"golang.org/x/text/language" "golang.org/x/text/language"
"tinygo.org/x/bluetooth" "gitrepo.ru/neonxp/bluetooth"
) )
type Service struct { type Service struct {

22
uuid.go
View file

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

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

@ -1,5 +1,4 @@
//go:build softdevice //go:build softdevice
// +build softdevice
package bluetooth package bluetooth
@ -11,9 +10,9 @@ import "unsafe"
type shortUUID C.ble_uuid_t 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 var short C.ble_uuid_t
short.uuid = uint16(uuid[3]) short.uuid = C.uint16_t(uuid[3])
if uuid.Is16Bit() { if uuid.Is16Bit() {
short._type = C.BLE_UUID_TYPE_BLE short._type = C.BLE_UUID_TYPE_BLE
return short, 0 return short, 0
@ -25,7 +24,7 @@ func (uuid UUID) shortUUID() (C.ble_uuid_t, uint32) {
// UUID returns the full length UUID for this short UUID. // UUID returns the full length UUID for this short UUID.
func (s shortUUID) UUID() UUID { func (s shortUUID) UUID() UUID {
if s._type == C.BLE_UUID_TYPE_BLE { if s._type == C.BLE_UUID_TYPE_BLE {
return New16BitUUID(s.uuid) return New16BitUUID(uint16(s.uuid))
} }
var outLen C.uint8_t var outLen C.uint8_t
var outUUID UUID 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. // Version returns a user-readable string showing the version of the bluetooth package for support purposes.
// Update this value before release of new version of software. // Update this value before release of new version of software.
const Version = "0.6.0" const Version = "0.10.0"