Compare commits
158 commits
update-blu
...
release
Author | SHA1 | Date | |
---|---|---|---|
eae5a517c8 | |||
|
a668e1b0a0 | ||
|
b1081a9db1 | ||
|
0d0c149a20 | ||
|
457af7571a | ||
|
9905abd00e | ||
|
926aeb43f6 | ||
|
d46f2cc206 | ||
|
c26c9d5630 | ||
|
abbb565ef0 | ||
|
348de057f8 | ||
|
314ca89209 | ||
|
6b08161955 | ||
|
12b6f0bc25 | ||
|
52c3c068e2 | ||
|
852fa4ab6a | ||
|
e0d5fd4c3a | ||
|
0087e0549b | ||
|
d82232b16d | ||
|
9a53d2a327 | ||
|
3e90718eb8 | ||
|
553633e56a | ||
|
b6fde65fd6 | ||
|
8e8dd34fc2 | ||
|
07a9e1d02e | ||
|
0a9bffe397 | ||
|
00a475adf1 | ||
|
4c90cf4ab6 | ||
|
bf647ecd57 | ||
|
564b0ba58f | ||
|
dc7d1b4d4c | ||
|
5c62ee4645 | ||
|
10d1c71078 | ||
|
b8c79250c7 | ||
|
ecf09759ac | ||
|
3f8f8a6622 | ||
|
d74f6a1009 | ||
|
5d805a929c | ||
|
c9eafaff20 | ||
|
6e0df0ec3c | ||
|
735333aa1a | ||
|
56e56f3647 | ||
|
f639d80012 | ||
|
cd3c0c4835 | ||
|
c5ab6a9b65 | ||
|
eb30760e41 | ||
|
190c4be423 | ||
|
5746ccfb60 | ||
|
83fba1b809 | ||
|
b8a4a54d5f | ||
|
30138095e1 | ||
|
044320ea69 | ||
|
930a5c7a88 | ||
|
92c12af54f | ||
|
1860e505b9 | ||
|
d77521461d | ||
|
b278e2b932 | ||
|
8f92747a18 | ||
|
ec80e0111e | ||
|
b5d4e3f82a | ||
|
01243181c3 | ||
|
d0c7887b81 | ||
|
195d418876 | ||
|
0cc860c018 | ||
|
1f58ec1fb4 | ||
|
d34d15d830 | ||
|
20ccbeb113 | ||
|
0403d51c8a | ||
|
4da7f58124 | ||
|
d9490f73ea | ||
|
d5276e5aed | ||
|
da2032de42 | ||
|
c3f9d593de | ||
|
f9436906c1 | ||
|
3c9cf83de2 | ||
|
4d067bc2b3 | ||
|
1c44c024fd | ||
|
ba63457646 | ||
|
7f67fa0275 | ||
|
eb782c5841 | ||
|
a341e8f543 | ||
|
74e8f86261 | ||
|
47d53464e4 | ||
|
f844306136 | ||
|
87d24926c4 | ||
|
cf51caa6ad | ||
|
8260f2fb93 | ||
|
5717af56e0 | ||
|
7b36b3035c | ||
|
51dff6faa7 | ||
|
3523e14bfc | ||
|
e5b9a898ad | ||
|
113f5fa503 | ||
|
71f17598be | ||
|
d4bca3e97f | ||
|
b06d666dbf | ||
|
03d77ace1c | ||
|
e0e261e166 | ||
|
3f79b9e4e8 | ||
|
4c798a1e7d | ||
|
570c0ce6e6 | ||
|
0bc6805828 | ||
|
50f176c7c8 | ||
|
34cb58db57 | ||
|
be99863ef5 | ||
|
d0178d95f6 | ||
|
c85b6cc9d3 | ||
|
e79ea1e4e9 | ||
|
792f4f079e | ||
|
79bbc2ed6d | ||
|
57f82b6241 | ||
|
39e6a357ea | ||
|
e4e6976c64 | ||
|
8fae597745 | ||
|
e843ce91a6 | ||
|
1b30ec4619 | ||
|
2784a6b2d9 | ||
|
7113f8c021 | ||
|
8f13d06111 | ||
|
7ec948bf3f | ||
|
79285321af | ||
|
0b701c55ca | ||
|
cc5adf3789 | ||
|
948ea8c7ee | ||
|
25349d381e | ||
|
e7671110ad | ||
|
9b9512fbc9 | ||
|
20f0ce6119 | ||
|
8dc1e155a0 | ||
|
02fb2457d9 | ||
|
25d288fd10 | ||
|
8cab553c59 | ||
|
7dee8d4d82 | ||
|
96a2be1571 | ||
|
4c0df2892d | ||
|
e75811786c | ||
|
bd75a42694 | ||
|
7f3b96cff9 | ||
|
501b0aeecc | ||
|
899467bab3 | ||
|
d985dcb55c | ||
|
ab40fc77fe | ||
|
d472af8a8e | ||
|
3cbf47cc61 | ||
|
cf63949412 | ||
|
bb8767730c | ||
|
340f6985ca | ||
|
10dcd116e8 | ||
|
28f9f4e69e | ||
|
172569b0a1 | ||
|
fa5736e183 | ||
|
46364419f9 | ||
|
27cc35a60b | ||
|
549cb4a3f7 | ||
|
b4b125480a | ||
|
a355f254da | ||
|
855d3c4b78 | ||
|
9dde7219a6 |
140 changed files with 35327 additions and 11440 deletions
|
@ -1,48 +0,0 @@
|
||||||
version: 2.1
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
docker:
|
|
||||||
- image: tinygo/tinygo-dev
|
|
||||||
working_directory: /usr/local/go/src/github.com/tinygo-org/bluetooth
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- run: tinygo version
|
|
||||||
- run:
|
|
||||||
name: "Run unit tests"
|
|
||||||
command: go test
|
|
||||||
- run:
|
|
||||||
name: "Run TinyGo smoke tests"
|
|
||||||
command: make smoketest-tinygo
|
|
||||||
- run:
|
|
||||||
name: "Run Linux smoke tests"
|
|
||||||
command: make smoketest-linux
|
|
||||||
- run:
|
|
||||||
name: "Install Windows cross compiler"
|
|
||||||
command: |
|
|
||||||
# Install the tools themselves.
|
|
||||||
apt-get install -y gcc-mingw-w64-x86-64
|
|
||||||
- run:
|
|
||||||
name: "Run Windows smoke tests"
|
|
||||||
command: make smoketest-windows
|
|
||||||
build-macos:
|
|
||||||
macos:
|
|
||||||
xcode: "10.1.0"
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- run:
|
|
||||||
name: "Install dependencies"
|
|
||||||
command: |
|
|
||||||
curl https://dl.google.com/go/go1.14.darwin-amd64.tar.gz -o go1.14.darwin-amd64.tar.gz
|
|
||||||
sudo tar -C /usr/local -xzf go1.14.darwin-amd64.tar.gz
|
|
||||||
ln -s /usr/local/go/bin/go /usr/local/bin/go
|
|
||||||
- run: go version
|
|
||||||
- run:
|
|
||||||
name: "Run macOS smoke tests"
|
|
||||||
command: make smoketest-macos
|
|
||||||
|
|
||||||
workflows:
|
|
||||||
test-all:
|
|
||||||
jobs:
|
|
||||||
- build
|
|
||||||
- build-macos
|
|
30
.github/workflows/linux.yml
vendored
Normal file
30
.github/workflows/linux.yml
vendored
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
name: Linux
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
- release
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: ghcr.io/tinygo-org/tinygo-dev
|
||||||
|
steps:
|
||||||
|
- name: Work around CVE-2022-24765
|
||||||
|
# We're not on a multi-user machine, so this is safe.
|
||||||
|
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: TinyGo version check
|
||||||
|
run: tinygo version
|
||||||
|
- name: Run unit tests
|
||||||
|
run: go test
|
||||||
|
- name: Run TinyGo smoke tests
|
||||||
|
run: make smoketest-tinygo
|
||||||
|
- name: Run Linux smoke tests
|
||||||
|
run: make smoketest-linux
|
||||||
|
- name: "Run Windows cross-compiled smoke tests"
|
||||||
|
run: make smoketest-windows
|
58
.github/workflows/macos.yml
vendored
Normal file
58
.github/workflows/macos.yml
vendored
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
name: macOS
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
- release
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
macos-11:
|
||||||
|
name: macos-11
|
||||||
|
runs-on: macos-11
|
||||||
|
steps:
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v4.1.0
|
||||||
|
with:
|
||||||
|
go-version: '1.18.3'
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3.6.0
|
||||||
|
- name: Run unit tests
|
||||||
|
run: go test
|
||||||
|
- name: "Run macOS smoke tests"
|
||||||
|
run: make smoketest-macos
|
||||||
|
|
||||||
|
macos-12:
|
||||||
|
name: macos-12
|
||||||
|
runs-on: macos-12
|
||||||
|
steps:
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v4.1.0
|
||||||
|
with:
|
||||||
|
go-version: '1.21.0'
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3.6.0
|
||||||
|
- name: Run unit tests
|
||||||
|
run: go test
|
||||||
|
- name: "Run macOS smoke tests"
|
||||||
|
run: make smoketest-macos
|
||||||
|
|
||||||
|
macos-13:
|
||||||
|
name: macos-13
|
||||||
|
runs-on: macos-13
|
||||||
|
steps:
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v4.1.0
|
||||||
|
with:
|
||||||
|
go-version: '1.21.0'
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3.6.0
|
||||||
|
- name: Run unit tests
|
||||||
|
run: go test
|
||||||
|
- name: "Run macOS smoke tests"
|
||||||
|
run: make smoketest-macos
|
24
.github/workflows/windows.yml
vendored
Normal file
24
.github/workflows/windows.yml
vendored
Normal 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
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "bluetooth-numbers-database"]
|
||||||
|
path = bluetooth-numbers-database
|
||||||
|
url = https://github.com/NordicSemiconductor/bluetooth-numbers-database.git
|
215
CHANGELOG.md
215
CHANGELOG.md
|
@ -1,3 +1,218 @@
|
||||||
|
0.10.0
|
||||||
|
---
|
||||||
|
|
||||||
|
* **core**
|
||||||
|
- gap: fix ServiceDataElement.UUID comment
|
||||||
|
* **docs**
|
||||||
|
- add mention of support for rp2040-W to README
|
||||||
|
- Improve documentation of RSSI Fixes https://github.com/tinygo-org/bluetooth/issues/272
|
||||||
|
* **hci**
|
||||||
|
- cyw43439: HCI implementation
|
||||||
|
- refactor to separate HCI transport implementation from interface to not always assume UART.
|
||||||
|
- update for cyw43439 HCI functionality
|
||||||
|
* **windows**
|
||||||
|
- Add Address field to Windows Device struct
|
||||||
|
- Winrt full support (#266)
|
||||||
|
- winrt-go: bump to latest
|
||||||
|
- assign char handle write event (#274)
|
||||||
|
* **test**
|
||||||
|
- add hci_uart based implementation to smoke tests
|
||||||
|
|
||||||
|
0.9.0
|
||||||
|
---
|
||||||
|
|
||||||
|
* **build**
|
||||||
|
- add arduino-nano33 and pyportal to smoke tests
|
||||||
|
- add nina-fw smoketest as peripheral
|
||||||
|
- add some ninafw examples to smoketest
|
||||||
|
* **core**
|
||||||
|
- add ServiceData advertising element (#243)
|
||||||
|
- add RequestConnectionParams to request new connection parameters
|
||||||
|
- change ManufacturerData from a map to a slice
|
||||||
|
- don't use a pointer receiver for many method calls
|
||||||
|
- make Device a value instead of a pointer
|
||||||
|
- use 'debug' variable protected by build tags for debug logging
|
||||||
|
- use Device instead of Address in SetConnectHandler
|
||||||
|
* **docs**
|
||||||
|
- a small mention of the NINA BLE support
|
||||||
|
- complete README info about nina-fw support
|
||||||
|
* **linux**
|
||||||
|
- fix characteristic value
|
||||||
|
- rewrite everything to use DBus directly
|
||||||
|
* **macos**
|
||||||
|
- add Write command to the gattc implementation
|
||||||
|
* **examples**
|
||||||
|
- tinyscan to replace clue-scanner, also works on pyportal and pybadge+airlift
|
||||||
|
- update MCU central examples to use ldflags to pass the desired device to connect to
|
||||||
|
- discover: add MTU
|
||||||
|
* **hci**
|
||||||
|
- add check for poll buffer overflow
|
||||||
|
- allow for both ninafw and pure hci uart adapter implementations
|
||||||
|
- implement Characteristic WriteHandler
|
||||||
|
- multiple connections
|
||||||
|
- return service UUIDs with scan results
|
||||||
|
- add l2cap signaling support
|
||||||
|
- implement evtNumCompPkts to count in-flight packets
|
||||||
|
- correct implementation for WriteWithoutReponse
|
||||||
|
- speed up time waiting for hardware - corrections to MTU exchange
|
||||||
|
- add support for software RTS/CTS flow control for boards where hardware support is not available
|
||||||
|
- BLE central implementation on nina-fw co-processors
|
||||||
|
- fix connection timeout
|
||||||
|
- implement BLE peripheral support
|
||||||
|
- implement GetMTU()
|
||||||
|
- remove some pointer receivers from method calls
|
||||||
|
- should support muliple connections as a central
|
||||||
|
- correctly return from read requests instead of returning spurious error
|
||||||
|
- move some steps previously being done during Configure() into Start() where they more correctly belonged.
|
||||||
|
- use advertising display name as the correct default value for the generic access characteristic.
|
||||||
|
- speed up the polling for new notifications for Centrals
|
||||||
|
- use NINA settings from board file in main TinyGo repo
|
||||||
|
* **nordic semi**
|
||||||
|
- replace unsafe.SliceData call with expression that is still supported in older Go versions
|
||||||
|
- update to prepare for changes in the TinyGo CGo implementation
|
||||||
|
- add address of connecting device
|
||||||
|
- add support for connection timeout on connect
|
||||||
|
- don't send a notify/indicate without a CCCD
|
||||||
|
- fix connect timeout
|
||||||
|
- fix writing to a characteristic
|
||||||
|
- print connection parameters when debug is enabled
|
||||||
|
- return an error on a connection timeout
|
||||||
|
* **windows**
|
||||||
|
- Release AsyncOperationCompletedHandler (#208)
|
||||||
|
- check for error when scanning
|
||||||
|
- bump to latest winrt
|
||||||
|
|
||||||
|
|
||||||
|
0.8.0
|
||||||
|
---
|
||||||
|
|
||||||
|
* **build**
|
||||||
|
- remove CGo dependencies for Windows cross-compiler tests
|
||||||
|
- add Windows to GH actions build jobs
|
||||||
|
- add macOS 12 to GH actions build jobs
|
||||||
|
* **core**
|
||||||
|
- go 1.18 and remove old-style build tags
|
||||||
|
- Noescape workaround
|
||||||
|
* **docs**
|
||||||
|
- update README to remove CGo requirement for Windows
|
||||||
|
- add documentation to heartrate-monitor
|
||||||
|
* **linux**
|
||||||
|
- Added option to add ManufacturerData to Advertisement
|
||||||
|
* **macos**
|
||||||
|
- enable support for duplicate chars by moving from a map to a slice
|
||||||
|
* **examples**
|
||||||
|
- Include WriteWithoutResponse permission, for examples, where Write exists
|
||||||
|
* **nordic semi**
|
||||||
|
- softdevice: added manufacturer data support
|
||||||
|
- softdevice: test creation of raw BLE advertisement packets
|
||||||
|
* **windows**
|
||||||
|
- update github.com/saltosystems/winrt-go to no longer require CGo
|
||||||
|
|
||||||
|
|
||||||
|
0.7.0
|
||||||
|
---
|
||||||
|
|
||||||
|
* **build**
|
||||||
|
- switch to ghcr.io for docker container
|
||||||
|
- update to actions/checkout@v3
|
||||||
|
- work around for CVE-2022-24765
|
||||||
|
* **core**
|
||||||
|
- gap: Set and SetRandom methods should have a pointer receiver
|
||||||
|
- mtu-{darwin,linux,windows,sd}: add get mtu function
|
||||||
|
- remove Addresser
|
||||||
|
- update uuid generation
|
||||||
|
* **docs**
|
||||||
|
- CONTRIBUTING: add note on new APIs
|
||||||
|
- correct badge link for GH actions
|
||||||
|
- README: add note on macOS Big Sur and iTerm2
|
||||||
|
* **linux**
|
||||||
|
- do not randomize order of returned discovered services/chars
|
||||||
|
- fix characteristic scan order
|
||||||
|
- implement disconnect handling
|
||||||
|
* **macos**
|
||||||
|
- implement disconnect handling
|
||||||
|
- fix characteristic scan order
|
||||||
|
* **examples**
|
||||||
|
- add examples/stop-advertisement
|
||||||
|
* **nordic semi**
|
||||||
|
- nrf528xx: handle BLE_GAP_EVT_PHY_UPDATE_REQUEST and explicitly ignore some other events
|
||||||
|
- softdevice: avoid a heap allocation in the SoftDevice event handler
|
||||||
|
* **windows**
|
||||||
|
- Added Indicate support to Windows driver
|
||||||
|
- gap/windows: Scan should set scanning mode to active to match other platforms
|
||||||
|
- support empty manufacturer data
|
||||||
|
- winrt-go: bump to latest
|
||||||
|
|
||||||
|
|
||||||
|
0.6.0
|
||||||
|
---
|
||||||
|
* **core**
|
||||||
|
- unify UUID16 creation for all platforms
|
||||||
|
- Improve UUID (#107)
|
||||||
|
- gap: stop advertising
|
||||||
|
- advertising: add manufacturer data field to advertisement payload
|
||||||
|
* **linux**
|
||||||
|
- gap: workaround for https://github.com/muka/go-bluetooth/issues/163
|
||||||
|
- update to latest muka/go-bluetooth
|
||||||
|
* **windows**
|
||||||
|
- add characteristic read, write and notify operations
|
||||||
|
- add characteristic discovery
|
||||||
|
- add service discovery
|
||||||
|
- add device connection and disconnection
|
||||||
|
- add winrt-go dependency and remove manually generated code
|
||||||
|
- disable cache when reading characteristics
|
||||||
|
* **macos**
|
||||||
|
- update to tinygo-org fork of cbgo v0.0.4
|
||||||
|
- use the same UUID format as expected by other standard
|
||||||
|
* **docs**
|
||||||
|
- update README with info on Windows support
|
||||||
|
* **build**
|
||||||
|
- add Github Action based CI build (#108)
|
||||||
|
|
||||||
|
|
||||||
|
0.5.0
|
||||||
|
---
|
||||||
|
* **core**
|
||||||
|
- update to drivers 0.20.0
|
||||||
|
- Fix ParseMAC bug
|
||||||
|
- Add //go:build lines for Go 1.18
|
||||||
|
* **nordic semi**
|
||||||
|
- nrf: fix CGo errors after TinyGo update
|
||||||
|
|
||||||
|
0.4.0
|
||||||
|
---
|
||||||
|
* **core**
|
||||||
|
- adapter: add host address function
|
||||||
|
* **linux**
|
||||||
|
- fixes bluez 0.55 service registration
|
||||||
|
- update muka/go-bluetooth to latest version
|
||||||
|
- gattc/linux: DiscoverServices times out in 10s
|
||||||
|
* **macos**
|
||||||
|
- make Adapter.Connect thread-safe
|
||||||
|
* **nordic semi**
|
||||||
|
- nrf51: fix assertHandler function signature
|
||||||
|
- nrf: add support for S113 SoftDevice
|
||||||
|
- nrf: update s140v7 SoftDevice version to latest, 7.3.0
|
||||||
|
* **examples**
|
||||||
|
- add scanner for Adafruit Clue
|
||||||
|
* **build**
|
||||||
|
- circleci: update xcode in use to 10.3.0
|
||||||
|
- modules: add tinyterm package for clue example
|
||||||
|
|
||||||
|
0.3.0
|
||||||
|
---
|
||||||
|
* **core**
|
||||||
|
- generate standard service and characteristic UUIDs from Nordic Semiconductor bluetooth numbers database
|
||||||
|
* **linux**
|
||||||
|
- downgrade to older version of go-bluetooth that appears to work correctly with BlueZ 5.50
|
||||||
|
* **macos**
|
||||||
|
- properly handle 16-bit UUIDs for service and characteristics in the unique format used by macOS
|
||||||
|
* **docs**
|
||||||
|
- add a few details on some newly supported boards
|
||||||
|
* **examples**
|
||||||
|
- use standard service and characteristic UUIDs
|
||||||
|
- correct heart rate monitor data format
|
||||||
|
|
||||||
0.2.0
|
0.2.0
|
||||||
---
|
---
|
||||||
* **core**
|
* **core**
|
||||||
|
|
|
@ -18,6 +18,11 @@ We probably have not implemented it yet. Your pull request adding the functional
|
||||||
|
|
||||||
Please open a Github issue. We want to help, and also make sure that there is no duplications of efforts. Sometimes what you need is already being worked on by someone else.
|
Please open a Github issue. We want to help, and also make sure that there is no duplications of efforts. Sometimes what you need is already being worked on by someone else.
|
||||||
|
|
||||||
|
If your contribution includes a new API (one that does not exist yet for any BLE stack), please make sure it is portable:
|
||||||
|
|
||||||
|
* Ideally, add support for it to two different Bluetooth APIs at the same time, for example CoreBluetooth and BlueZ.
|
||||||
|
* If you are unable to do so, please explain (with links to documentation!) why this feature also fits a different API.
|
||||||
|
|
||||||
## How to use our Github repository
|
## How to use our Github repository
|
||||||
|
|
||||||
The `release` branch of this repo will always have the latest released version of the Go Bluetooth module. All of the active development work for the next release will take place in the `dev` branch. The Go Bluetooth module will use semantic versioning and will create a tag/release for each release.
|
The `release` branch of this repo will always have the latest released version of the Go Bluetooth module. All of the active development work for the next release will take place in the `dev` branch. The Go Bluetooth module will use semantic versioning and will create a tag/release for each release.
|
||||||
|
|
6
LICENSE
6
LICENSE
|
@ -1,4 +1,4 @@
|
||||||
Copyright (c) 2019 Ayke van Laethem. 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
|
||||||
|
@ -25,3 +25,7 @@ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
TinyGo Bluetooth includes data files from the Nordic Semiconductor Bluetooth
|
||||||
|
Numbers Database (https://github.com/NordicSemiconductor/bluetooth-numbers-database).
|
||||||
|
Copyright (c) 2019 - 2020, Nordic Semiconductor ASA. All rights reserved.
|
||||||
|
|
34
Makefile
34
Makefile
|
@ -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
|
||||||
|
@ -23,26 +25,56 @@ smoketest-tinygo:
|
||||||
@md5sum test.hex
|
@md5sum test.hex
|
||||||
$(TINYGO) build -o test.hex -size=short -target=pca10040-s132v6 ./examples/scanner
|
$(TINYGO) build -o test.hex -size=short -target=pca10040-s132v6 ./examples/scanner
|
||||||
@md5sum test.hex
|
@md5sum test.hex
|
||||||
|
$(TINYGO) build -o test.hex -size=short -target=pca10040-s132v6 ./examples/stop-advertisement
|
||||||
|
@md5sum test.hex
|
||||||
# Test some more boards that are not tested above.
|
# Test some more boards that are not tested above.
|
||||||
$(TINYGO) build -o test.hex -size=short -target=pca10056-s140v7 ./examples/advertisement
|
$(TINYGO) build -o test.hex -size=short -target=pca10056-s140v7 ./examples/advertisement
|
||||||
@md5sum test.hex
|
@md5sum test.hex
|
||||||
$(TINYGO) build -o test.hex -size=short -target=microbit-s110v8 ./examples/nusserver
|
$(TINYGO) build -o test.hex -size=short -target=microbit-s110v8 ./examples/nusserver
|
||||||
@md5sum test.hex
|
@md5sum test.hex
|
||||||
|
$(TINYGO) build -o test.hex -size=short -target=microbit-v2-s113v7 ./examples/nusserver
|
||||||
|
@md5sum test.hex
|
||||||
|
$(TINYGO) build -o test.uf2 -size=short -target=nano-rp2040 ./examples/discover
|
||||||
|
@md5sum test.hex
|
||||||
|
$(TINYGO) build -o test.uf2 -size=short -target=arduino-nano33 ./examples/discover
|
||||||
|
@md5sum test.hex
|
||||||
|
$(TINYGO) build -o test.uf2 -size=short -target=pyportal ./examples/discover
|
||||||
|
@md5sum test.hex
|
||||||
|
$(TINYGO) build -o test.uf2 -size=short -target=nano-rp2040 ./examples/advertisement
|
||||||
|
@md5sum test.hex
|
||||||
|
$(TINYGO) build -o test.uf2 -size=short -target=circuitplay-express -tags="hci hci_uart" ./examples/advertisement
|
||||||
|
@md5sum test.hex
|
||||||
|
$(TINYGO) build -o test.uf2 -size=short -target=pico-w ./examples/discover
|
||||||
|
@md5sum test.hex
|
||||||
|
$(TINYGO) build -o test.uf2 -size=short -target=badger2040-w ./examples/advertisement
|
||||||
|
@md5sum test.hex
|
||||||
|
|
||||||
smoketest-linux:
|
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/nusserver
|
GOOS=linux go build -o /tmp/go-build-discard ./examples/nusserver
|
||||||
GOOS=linux go build -o /tmp/go-build-discard ./examples/scanner
|
GOOS=linux go build -o /tmp/go-build-discard ./examples/scanner
|
||||||
GOOS=linux go build -o /tmp/go-build-discard ./examples/discover
|
GOOS=linux go build -o /tmp/go-build-discard ./examples/discover
|
||||||
|
|
||||||
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 go build -o /tmp/go-build-discard ./examples/discover
|
||||||
|
GOOS=windows go build -o /tmp/go-build-discard ./examples/heartrate-monitor
|
||||||
|
GOOS=windows go build -o /tmp/go-build-discard ./examples/advertisement
|
||||||
|
GOOS=windows go build -o /tmp/go-build-discard ./examples/heartrate
|
||||||
|
|
||||||
smoketest-macos:
|
smoketest-macos:
|
||||||
# Test on macos.
|
# Test on macos.
|
||||||
GOOS=darwin CGO_ENABLED=1 go build -o /tmp/go-build-discard ./examples/scanner
|
GOOS=darwin CGO_ENABLED=1 go build -o /tmp/go-build-discard ./examples/scanner
|
||||||
GOOS=darwin CGO_ENABLED=1 go build -o /tmp/go-build-discard ./examples/discover
|
GOOS=darwin CGO_ENABLED=1 go build -o /tmp/go-build-discard ./examples/discover
|
||||||
GOOS=darwin CGO_ENABLED=1 go build -o /tmp/go-build-discard ./examples/nusclient
|
GOOS=darwin CGO_ENABLED=1 go build -o /tmp/go-build-discard ./examples/nusclient
|
||||||
|
GOOS=darwin CGO_ENABLED=1 go build -o /tmp/go-build-discard ./examples/heartrate-monitor
|
||||||
|
|
||||||
|
gen-uuids:
|
||||||
|
# generate the standard service and characteristic UUIDs
|
||||||
|
go run ./tools/gen-service-uuids/main.go
|
||||||
|
go run ./tools/gen-characteristic-uuids/main.go
|
||||||
|
|
101
README.md
101
README.md
|
@ -2,14 +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)
|
[![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)
|
||||||
[![CircleCI](https://circleci.com/gh/tinygo-org/bluetooth/tree/dev.svg?style=svg)](https://circleci.com/gh/tinygo-org/bluetooth/tree/dev)
|
|
||||||
|
|
||||||
Go Bluetooth is a cross-platform package for using [Bluetooth Low Energy](https://en.wikipedia.org/wiki/Bluetooth_Low_Energy) hardware from the Go programming language.
|
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.
|
||||||
|
|
||||||
|
@ -23,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
|
||||||
|
@ -59,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
|
||||||
|
@ -93,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: | :x: | :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: | :x: | :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: | :x: | :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.
|
||||||
|
|
||||||
|
@ -131,7 +130,7 @@ After you have followed the installation, you should be able to compile/run the
|
||||||
|
|
||||||
## macOS
|
## macOS
|
||||||
|
|
||||||
Go Bluetooth support for macOS uses the [CoreBluetooth](https://developer.apple.com/documentation/corebluetooth?language=objc) libraries thanks to the https://github.com/JuulLabs-OSS/cbgo package.
|
Go Bluetooth support for macOS uses the [CoreBluetooth](https://developer.apple.com/documentation/corebluetooth?language=objc) libraries thanks to the https://github.com/tinygo-org/cbgo fork of the `cbgo` package.
|
||||||
|
|
||||||
As a result, it should work with most versions of macOS, although it will require compiling using whatever specific version of XCode is required by your version of the operating system.
|
As a result, it should work with most versions of macOS, although it will require compiling using whatever specific version of XCode is required by your version of the operating system.
|
||||||
|
|
||||||
|
@ -154,17 +153,21 @@ After you have followed the installation, you should be able to compile/run the
|
||||||
cd bluetooth
|
cd bluetooth
|
||||||
go run ./examples/scanner
|
go run ./examples/scanner
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
There is a known issue with iTerm2 and the Bluetooth package. If you are getting a message like `abort: trap`, try whitelisting iTerm2 manually through System Settings -> Privacy & Security -> Bluetooth.
|
||||||
|
|
||||||
## Windows
|
## Windows
|
||||||
|
|
||||||
Go Bluetooth support for Windows uses the [WinRT Bluetooth](https://docs.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothadapter?view=winrt-19041) interfaces by way of the https://github.com/tinygo-org/bluetooth/winbt package that is part of this package.
|
Go Bluetooth support for Windows uses the [WinRT Bluetooth](https://docs.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothadapter?view=winrt-19041) interfaces by way of the https://github.com/saltosystems/winrt-go package.
|
||||||
|
|
||||||
The Windows support is still experimental, and needs additional development to be useful. At this time, it can only be used to perform scanning operations as a BLE Central.
|
The Windows support only can only act as a BLE Central at this time, with some additional development work needed for full functionality.
|
||||||
|
|
||||||
For specifics please see https://github.com/tinygo-org/bluetooth/issues/13
|
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
Once you have done this, you can obtain the Go Bluetooth package using Git:
|
Only the Go compiler itself is needed to compile Go Bluetooth code targeting Windows.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
@ -210,8 +213,15 @@ For example, this command can be used to compile and flash an Adafruit Circuit P
|
||||||
|
|
||||||
tinygo flash -target circuitplay-bluefruit ./examples/circuitplay
|
tinygo flash -target circuitplay-bluefruit ./examples/circuitplay
|
||||||
|
|
||||||
|
There are other boards with TinyGo support that also use the same UF2 bootloader with pre-loaded SoftDevice. They include:
|
||||||
|
|
||||||
|
* [Nice Keyboards nice!nano](https://nicekeyboards.com/products/nice-nano-v1-0)
|
||||||
|
* [Makerdiary nRF52840 MDK USB Dongle](https://wiki.makerdiary.com/nrf52840-mdk-usb-dongle/)
|
||||||
|
|
||||||
### BBC micro:bit
|
### BBC micro:bit
|
||||||
|
|
||||||
|
#### Version 1
|
||||||
|
|
||||||
The [BBC micro:bit](https://microbit.org/) uses an nRF51 chip with a CMSIS-DAP interface.
|
The [BBC micro:bit](https://microbit.org/) uses an nRF51 chip with a CMSIS-DAP interface.
|
||||||
|
|
||||||
You will need to install OpenOCD (http://openocd.org/) to flash the board.
|
You will need to install OpenOCD (http://openocd.org/) to flash the board.
|
||||||
|
@ -225,6 +235,12 @@ Once you have copied the SoftDevice firmware to the BBC micro:bit, you can then
|
||||||
|
|
||||||
tinygo flash -target=microbit-s110v8 ./examples/heartrate
|
tinygo flash -target=microbit-s110v8 ./examples/heartrate
|
||||||
|
|
||||||
|
#### Version 2
|
||||||
|
|
||||||
|
The [BBC micro:bit v2](https://microbit.org/new-microbit/) uses an nRF52833 chip with a CMSIS-DAP interface.
|
||||||
|
|
||||||
|
Support for the v2 will be available soon.
|
||||||
|
|
||||||
### Supported Chips
|
### Supported Chips
|
||||||
|
|
||||||
The following Nordic Semiconductor chips are currently supported:
|
The following Nordic Semiconductor chips are currently supported:
|
||||||
|
@ -248,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.
|
||||||
|
|
|
@ -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 Addresser, connected bool)) {
|
func (a *Adapter) SetConnectHandler(c func(device Device, connected bool)) {
|
||||||
a.connectHandler = c
|
a.connectHandler = c
|
||||||
}
|
}
|
||||||
|
|
116
adapter_cyw43439.go
Normal file
116
adapter_cyw43439.go
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
//go:build cyw43439
|
||||||
|
|
||||||
|
package bluetooth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"machine"
|
||||||
|
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/soypat/cyw43439"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxConnections = 1
|
||||||
|
|
||||||
|
// Adapter represents a SPI connection to the HCI controller on an attached CYW4349 module.
|
||||||
|
type Adapter struct {
|
||||||
|
hciAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultAdapter is the default adapter on the current system.
|
||||||
|
//
|
||||||
|
// Make sure to call Enable() before using it to initialize the adapter.
|
||||||
|
var DefaultAdapter = &Adapter{
|
||||||
|
hciAdapter: hciAdapter{
|
||||||
|
isDefault: true,
|
||||||
|
connectHandler: func(device Device, connected bool) {
|
||||||
|
return
|
||||||
|
},
|
||||||
|
connectedDevices: make([]Device, 0, maxConnections),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable configures the BLE stack. It must be called before any
|
||||||
|
// Bluetooth-related calls (unless otherwise indicated).
|
||||||
|
func (a *Adapter) Enable() error {
|
||||||
|
if debug {
|
||||||
|
println("Initializing CYW43439 device")
|
||||||
|
}
|
||||||
|
|
||||||
|
dev := cyw43439.NewPicoWDevice()
|
||||||
|
cfg := cyw43439.DefaultBluetoothConfig()
|
||||||
|
if debug {
|
||||||
|
cfg.Logger = slog.New(slog.NewTextHandler(machine.USBCDC, &slog.HandlerOptions{
|
||||||
|
Level: slog.LevelDebug - 2,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
err := dev.Init(cfg)
|
||||||
|
if err != nil {
|
||||||
|
if debug {
|
||||||
|
println("Error initializing CYW43439 device", err.Error())
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
transport := &hciSPI{dev: dev}
|
||||||
|
|
||||||
|
a.hci, a.att = newBLEStack(transport)
|
||||||
|
if debug {
|
||||||
|
println("Enabling CYW43439 device")
|
||||||
|
}
|
||||||
|
|
||||||
|
a.enable()
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
println("Enabled CYW43439 device")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type hciSPI struct {
|
||||||
|
dev *cyw43439.Device
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hciSPI) startRead() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hciSPI) endRead() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hciSPI) Buffered() int {
|
||||||
|
return h.dev.BufferedHCI()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hciSPI) ReadByte() (byte, error) {
|
||||||
|
var buf [1]byte
|
||||||
|
|
||||||
|
r, err := h.dev.HCIReadWriter()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if _, err := r.Read(buf[:]); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hciSPI) Read(buf []byte) (int, error) {
|
||||||
|
r, err := h.dev.HCIReadWriter()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Read(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hciSPI) Write(buf []byte) (int, error) {
|
||||||
|
w, err := h.dev.HCIReadWriter()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return w.Write(buf)
|
||||||
|
}
|
|
@ -2,9 +2,10 @@ package bluetooth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/JuulLabs-OSS/cbgo"
|
"github.com/tinygo-org/cbgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Adapter is a connection to BLE devices.
|
// Adapter is a connection to BLE devices.
|
||||||
|
@ -18,9 +19,12 @@ type Adapter struct {
|
||||||
peripheralFoundHandler func(*Adapter, ScanResult)
|
peripheralFoundHandler func(*Adapter, ScanResult)
|
||||||
scanChan chan error
|
scanChan chan error
|
||||||
poweredChan chan error
|
poweredChan chan error
|
||||||
connectChan chan cbgo.Peripheral
|
|
||||||
|
|
||||||
connectHandler func(device Addresser, connected bool)
|
// connectMap is a mapping of peripheralId -> chan cbgo.Peripheral,
|
||||||
|
// used to allow multiple callers to call Connect concurrently.
|
||||||
|
connectMap sync.Map
|
||||||
|
|
||||||
|
connectHandler func(device Device, connected bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultAdapter is the default adapter on the system.
|
// DefaultAdapter is the default adapter on the system.
|
||||||
|
@ -29,8 +33,9 @@ type Adapter struct {
|
||||||
var DefaultAdapter = &Adapter{
|
var DefaultAdapter = &Adapter{
|
||||||
cm: cbgo.NewCentralManager(nil),
|
cm: cbgo.NewCentralManager(nil),
|
||||||
pm: cbgo.NewPeripheralManager(nil),
|
pm: cbgo.NewPeripheralManager(nil),
|
||||||
connectChan: make(chan cbgo.Peripheral),
|
connectMap: sync.Map{},
|
||||||
connectHandler: func(device Addresser, connected bool) {
|
|
||||||
|
connectHandler: func(device Device, connected bool) {
|
||||||
return
|
return
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -95,10 +100,35 @@ func (cmd *centralManagerDelegate) DidDiscoverPeripheral(cmgr cbgo.CentralManage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DidDisconnectPeripheral when peripheral is disconnected.
|
||||||
|
func (cmd *centralManagerDelegate) DidDisconnectPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peripheral, err error) {
|
||||||
|
id := prph.Identifier().String()
|
||||||
|
addr := Address{}
|
||||||
|
uuid, _ := ParseUUID(id)
|
||||||
|
addr.UUID = uuid
|
||||||
|
cmd.a.connectHandler(Device{Address: addr}, false)
|
||||||
|
|
||||||
|
// like with DidConnectPeripheral, check if we have a chan allocated for this and send through the peripheral
|
||||||
|
// this will only be true if the receiving side is still waiting for a connection to complete
|
||||||
|
if ch, ok := cmd.a.connectMap.LoadAndDelete(id); ok {
|
||||||
|
ch.(chan cbgo.Peripheral) <- prph
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// DidConnectPeripheral when peripheral is connected.
|
// DidConnectPeripheral when peripheral is connected.
|
||||||
func (cmd *centralManagerDelegate) DidConnectPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peripheral) {
|
func (cmd *centralManagerDelegate) DidConnectPeripheral(cmgr cbgo.CentralManager, prph cbgo.Peripheral) {
|
||||||
|
id := prph.Identifier().String()
|
||||||
|
|
||||||
|
// Check if we have a chan allocated for this peripheral, and remove it
|
||||||
|
// from the map if so (it's single-use, will be garbage collected after
|
||||||
|
// receiver receives the peripheral).
|
||||||
|
//
|
||||||
|
// If we don't have a chan allocated, the receiving side timed out, so
|
||||||
|
// ignore this connection.
|
||||||
|
if ch, ok := cmd.a.connectMap.LoadAndDelete(id); ok {
|
||||||
// Unblock now that we're connected.
|
// Unblock now that we're connected.
|
||||||
cmd.a.connectChan <- prph
|
ch.(chan cbgo.Peripheral) <- prph
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// makeScanResult creates a ScanResult when peripheral is found.
|
// makeScanResult creates a ScanResult when peripheral is found.
|
||||||
|
@ -111,6 +141,34 @@ func makeScanResult(prph cbgo.Peripheral, advFields cbgo.AdvFields, rssi int) Sc
|
||||||
serviceUUIDs = append(serviceUUIDs, parsedUUID)
|
serviceUUIDs = append(serviceUUIDs, parsedUUID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var manufacturerData []ManufacturerDataElement
|
||||||
|
if len(advFields.ManufacturerData) > 2 {
|
||||||
|
// Note: CoreBluetooth seems to assume there can be only one
|
||||||
|
// manufacturer data fields in an advertisement packet, while the
|
||||||
|
// specification allows multiple such fields. See the Bluetooth Core
|
||||||
|
// Specification Supplement, table 1.1:
|
||||||
|
// https://www.bluetooth.com/specifications/css-11/
|
||||||
|
manufacturerID := uint16(advFields.ManufacturerData[0])
|
||||||
|
manufacturerID += uint16(advFields.ManufacturerData[1]) << 8
|
||||||
|
manufacturerData = append(manufacturerData, ManufacturerDataElement{
|
||||||
|
CompanyID: manufacturerID,
|
||||||
|
Data: advFields.ManufacturerData[2:],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var serviceData []ServiceDataElement
|
||||||
|
for _, svcData := range advFields.ServiceData {
|
||||||
|
cbgoUUID := svcData.UUID
|
||||||
|
uuid, err := ParseUUID(cbgoUUID.String())
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
serviceData = append(serviceData, ServiceDataElement{
|
||||||
|
UUID: uuid,
|
||||||
|
Data: svcData.Data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Peripheral UUID is randomized on macOS, which means to
|
// Peripheral UUID is randomized on macOS, which means to
|
||||||
// different centrals it will appear to have a different UUID.
|
// different centrals it will appear to have a different UUID.
|
||||||
return ScanResult{
|
return ScanResult{
|
||||||
|
@ -122,6 +180,8 @@ func makeScanResult(prph cbgo.Peripheral, advFields cbgo.AdvFields, rssi int) Sc
|
||||||
AdvertisementFields{
|
AdvertisementFields{
|
||||||
LocalName: advFields.LocalName,
|
LocalName: advFields.LocalName,
|
||||||
ServiceUUIDs: serviceUUIDs,
|
ServiceUUIDs: serviceUUIDs,
|
||||||
|
ManufacturerData: manufacturerData,
|
||||||
|
ServiceData: serviceData,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
205
adapter_hci.go
Normal file
205
adapter_hci.go
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
//go:build hci || ninafw || cyw43439
|
||||||
|
|
||||||
|
package bluetooth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// hciAdapter represents the implementation for the connection to the HCI controller.
|
||||||
|
type hciAdapter struct {
|
||||||
|
hciport hciTransport
|
||||||
|
hci *hci
|
||||||
|
att *att
|
||||||
|
|
||||||
|
isDefault bool
|
||||||
|
scanning bool
|
||||||
|
|
||||||
|
connectHandler func(device Device, connected bool)
|
||||||
|
|
||||||
|
connectedDevices []Device
|
||||||
|
notificationsStarted bool
|
||||||
|
charWriteHandlers []charWriteHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *hciAdapter) enable() error {
|
||||||
|
if err := a.hci.start(); err != nil {
|
||||||
|
if debug {
|
||||||
|
println("error starting HCI:", err.Error())
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.hci.reset(); err != nil {
|
||||||
|
if debug {
|
||||||
|
println("error resetting HCI:", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(150 * time.Millisecond)
|
||||||
|
|
||||||
|
if err := a.hci.setEventMask(0x3FFFFFFFFFFFFFFF); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.hci.setLeEventMask(0x00000000000003FF)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *hciAdapter) Address() (MACAddress, error) {
|
||||||
|
if err := a.hci.readBdAddr(); err != nil {
|
||||||
|
return MACAddress{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return MACAddress{MAC: makeAddress(a.hci.address)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBLEStack(port hciTransport) (*hci, *att) {
|
||||||
|
h := newHCI(port)
|
||||||
|
a := newATT(h)
|
||||||
|
h.att = a
|
||||||
|
|
||||||
|
l := newL2CAP(h)
|
||||||
|
h.l2cap = l
|
||||||
|
|
||||||
|
return h, a
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert a NINA MAC address into a Go MAC address.
|
||||||
|
func makeAddress(mac [6]uint8) MAC {
|
||||||
|
return MAC{
|
||||||
|
uint8(mac[0]),
|
||||||
|
uint8(mac[1]),
|
||||||
|
uint8(mac[2]),
|
||||||
|
uint8(mac[3]),
|
||||||
|
uint8(mac[4]),
|
||||||
|
uint8(mac[5]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert a Go MAC address into a NINA MAC Address.
|
||||||
|
func makeNINAAddress(mac MAC) [6]uint8 {
|
||||||
|
return [6]uint8{
|
||||||
|
uint8(mac[0]),
|
||||||
|
uint8(mac[1]),
|
||||||
|
uint8(mac[2]),
|
||||||
|
uint8(mac[3]),
|
||||||
|
uint8(mac[4]),
|
||||||
|
uint8(mac[5]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *hciAdapter) startNotifications() {
|
||||||
|
if a.notificationsStarted {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
println("starting notifications...")
|
||||||
|
}
|
||||||
|
|
||||||
|
a.notificationsStarted = true
|
||||||
|
|
||||||
|
// go routine to poll for HCI events for ATT notifications
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
if err := a.att.poll(); err != nil {
|
||||||
|
// TODO: handle error
|
||||||
|
if debug {
|
||||||
|
println("error polling for notifications:", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// go routine to handle characteristic notifications
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case not := <-a.att.notifications:
|
||||||
|
if debug {
|
||||||
|
println("notification received", not.connectionHandle, not.handle, not.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
d := a.findConnection(not.connectionHandle)
|
||||||
|
if d.deviceInternal == nil {
|
||||||
|
if debug {
|
||||||
|
println("no device found for handle", not.connectionHandle)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
n := d.findNotificationRegistration(not.handle)
|
||||||
|
if n == nil {
|
||||||
|
if debug {
|
||||||
|
println("no notification registered for handle", not.handle)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.callback != nil {
|
||||||
|
n.callback(not.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.Gosched()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *hciAdapter) addConnection(d Device) {
|
||||||
|
a.connectedDevices = append(a.connectedDevices, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *hciAdapter) removeConnection(d Device) {
|
||||||
|
for i := range a.connectedDevices {
|
||||||
|
if d.handle == a.connectedDevices[i].handle {
|
||||||
|
a.connectedDevices[i] = a.connectedDevices[len(a.connectedDevices)-1]
|
||||||
|
a.connectedDevices[len(a.connectedDevices)-1] = Device{}
|
||||||
|
a.connectedDevices = a.connectedDevices[:len(a.connectedDevices)-1]
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *hciAdapter) findConnection(handle uint16) Device {
|
||||||
|
for _, d := range a.connectedDevices {
|
||||||
|
if d.handle == handle {
|
||||||
|
if debug {
|
||||||
|
println("found device", handle, d.Address.String(), "with notifications registered", len(d.notificationRegistrations))
|
||||||
|
}
|
||||||
|
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Device{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// charWriteHandler contains a handler->callback mapping for characteristic
|
||||||
|
// writes.
|
||||||
|
type charWriteHandler struct {
|
||||||
|
handle uint16
|
||||||
|
callback func(connection Connection, offset int, value []byte)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCharWriteHandler returns a characteristic write handler if one matches the
|
||||||
|
// handle, or nil otherwise.
|
||||||
|
func (a *Adapter) getCharWriteHandler(handle uint16) *charWriteHandler {
|
||||||
|
for i := range a.charWriteHandlers {
|
||||||
|
h := &a.charWriteHandlers[i]
|
||||||
|
if h.handle == handle {
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
122
adapter_hci_uart.go
Normal file
122
adapter_hci_uart.go
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
//go:build hci && hci_uart
|
||||||
|
|
||||||
|
package bluetooth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"machine"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxConnections = 1
|
||||||
|
|
||||||
|
// Adapter represents a "plain" UART connection to the HCI controller.
|
||||||
|
type Adapter struct {
|
||||||
|
hciAdapter
|
||||||
|
|
||||||
|
uart *machine.UART
|
||||||
|
|
||||||
|
// used for software flow control
|
||||||
|
cts, rts machine.Pin
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultAdapter is the default adapter on the current system.
|
||||||
|
//
|
||||||
|
// Make sure to call Enable() before using it to initialize the adapter.
|
||||||
|
var DefaultAdapter = &Adapter{
|
||||||
|
hciAdapter: hciAdapter{
|
||||||
|
isDefault: true,
|
||||||
|
connectHandler: func(device Device, connected bool) {
|
||||||
|
return
|
||||||
|
},
|
||||||
|
connectedDevices: make([]Device, 0, maxConnections),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUART sets the UART to use for the HCI connection.
|
||||||
|
// It must be called before calling Enable().
|
||||||
|
// Note that the UART must be configured with hardware flow control, or
|
||||||
|
// SetSoftwareFlowControl() must be called.
|
||||||
|
func (a *Adapter) SetUART(uart *machine.UART) error {
|
||||||
|
a.uart = uart
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSoftwareFlowControl sets the pins to use for software flow control,
|
||||||
|
// if hardware flow control is not available.
|
||||||
|
func (a *Adapter) SetSoftwareFlowControl(cts, rts machine.Pin) error {
|
||||||
|
a.cts = cts
|
||||||
|
a.rts = rts
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable configures the BLE stack. It must be called before any
|
||||||
|
// Bluetooth-related calls (unless otherwise indicated).
|
||||||
|
func (a *Adapter) Enable() error {
|
||||||
|
transport := &hciUART{uart: a.uart}
|
||||||
|
if a.cts != 0 && a.rts != 0 {
|
||||||
|
transport.rts = a.rts
|
||||||
|
a.rts.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||||
|
a.rts.High()
|
||||||
|
|
||||||
|
transport.cts = a.cts
|
||||||
|
a.cts.Configure(machine.PinConfig{Mode: machine.PinInput})
|
||||||
|
}
|
||||||
|
|
||||||
|
a.hci, a.att = newBLEStack(transport)
|
||||||
|
a.enable()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type hciUART struct {
|
||||||
|
uart *machine.UART
|
||||||
|
|
||||||
|
// used for software flow control
|
||||||
|
cts, rts machine.Pin
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hciUART) startRead() {
|
||||||
|
if h.rts != machine.NoPin {
|
||||||
|
h.rts.Low()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hciUART) endRead() {
|
||||||
|
if h.rts != machine.NoPin {
|
||||||
|
h.rts.High()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hciUART) Buffered() int {
|
||||||
|
return h.uart.Buffered()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hciUART) ReadByte() (byte, error) {
|
||||||
|
return h.uart.ReadByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hciUART) Read(buf []byte) (int, error) {
|
||||||
|
return h.uart.Read(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
const writeAttempts = 200
|
||||||
|
|
||||||
|
func (h *hciUART) Write(buf []byte) (int, error) {
|
||||||
|
if h.cts != machine.NoPin {
|
||||||
|
retries := writeAttempts
|
||||||
|
for h.cts.Get() {
|
||||||
|
retries--
|
||||||
|
if retries == 0 {
|
||||||
|
return 0, ErrHCITimeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := h.uart.Write(buf)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, nil
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
// +build !baremetal
|
//go: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
|
||||||
|
@ -6,17 +6,24 @@
|
||||||
package bluetooth
|
package bluetooth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/muka/go-bluetooth/api"
|
"errors"
|
||||||
"github.com/muka/go-bluetooth/bluez/profile/adapter"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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 Addresser, 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
|
||||||
|
@ -24,20 +31,40 @@ 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 Addresser, 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) {
|
||||||
|
if a.address == "" {
|
||||||
|
return MACAddress{}, errors.New("adapter not enabled")
|
||||||
|
}
|
||||||
|
mac, err := ParseMAC(a.address)
|
||||||
|
if err != nil {
|
||||||
|
return MACAddress{}, err
|
||||||
|
}
|
||||||
|
return MACAddress{MAC: mac}, nil
|
||||||
|
}
|
||||||
|
|
134
adapter_ninafw.go
Normal file
134
adapter_ninafw.go
Normal 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
|
||||||
|
}
|
|
@ -1,17 +1,13 @@
|
||||||
// +build softdevice,s110v8
|
//go:build softdevice && s110v8
|
||||||
|
|
||||||
package bluetooth
|
package bluetooth
|
||||||
|
|
||||||
/*
|
/*
|
||||||
// Define SoftDevice functions as regular function declarations (not inline
|
|
||||||
// static functions).
|
|
||||||
#define SVCALL_AS_NORMAL_FUNCTION
|
|
||||||
|
|
||||||
#include "nrf_sdm.h"
|
#include "nrf_sdm.h"
|
||||||
#include "ble.h"
|
#include "ble.h"
|
||||||
#include "ble_gap.h"
|
#include "ble_gap.h"
|
||||||
|
|
||||||
void assertHandler(void);
|
void assertHandler(uint32_t pc, uint16_t line_number, const uint8_t * p_file_name);
|
||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
|
@ -46,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(nil, 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
|
||||||
|
@ -58,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(nil, 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:
|
||||||
|
@ -106,3 +110,20 @@ func handleEvent() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Address() (MACAddress, error) {
|
||||||
|
var addr C.ble_gap_addr_t
|
||||||
|
errCode := C.sd_ble_gap_address_get(&addr)
|
||||||
|
if errCode != 0 {
|
||||||
|
return MACAddress{}, Error(errCode)
|
||||||
|
}
|
||||||
|
return MACAddress{MAC: makeAddress(addr.addr)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert a C.ble_gap_addr_t to a MACAddress struct.
|
||||||
|
func makeMACAddress(addr C.ble_gap_addr_t) MACAddress {
|
||||||
|
return MACAddress{
|
||||||
|
MAC: makeAddress(addr.addr),
|
||||||
|
isRandom: addr.addr_type != 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
268
adapter_nrf528xx-full.go
Normal file
268
adapter_nrf528xx-full.go
Normal file
|
@ -0,0 +1,268 @@
|
||||||
|
//go:build (softdevice && s132v6) || (softdevice && s140v6) || (softdevice && s140v7)
|
||||||
|
|
||||||
|
package bluetooth
|
||||||
|
|
||||||
|
// This file implements the event handler for SoftDevices with full support:
|
||||||
|
// both central and peripheral mode. This includes S132 and S140.
|
||||||
|
|
||||||
|
/*
|
||||||
|
#include "nrf_sdm.h"
|
||||||
|
#include "nrf_nvic.h"
|
||||||
|
#include "ble.h"
|
||||||
|
#include "ble_gap.h"
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleEvent() {
|
||||||
|
id := eventBuf.header.evt_id
|
||||||
|
switch {
|
||||||
|
case id >= C.BLE_GAP_EVT_BASE && id <= C.BLE_GAP_EVT_LAST:
|
||||||
|
gapEvent := eventBuf.evt.unionfield_gap_evt()
|
||||||
|
switch id {
|
||||||
|
case C.BLE_GAP_EVT_CONNECTED:
|
||||||
|
connectEvent := gapEvent.params.unionfield_connected()
|
||||||
|
device := Device{
|
||||||
|
Address: Address{makeMACAddress(connectEvent.peer_addr)},
|
||||||
|
connectionHandle: gapEvent.conn_handle,
|
||||||
|
}
|
||||||
|
switch connectEvent.role {
|
||||||
|
case C.BLE_GAP_ROLE_PERIPH:
|
||||||
|
if debug {
|
||||||
|
println("evt: connected in peripheral role")
|
||||||
|
}
|
||||||
|
currentConnection.handle.Reg = uint16(gapEvent.conn_handle)
|
||||||
|
DefaultAdapter.connectHandler(device, true)
|
||||||
|
case C.BLE_GAP_ROLE_CENTRAL:
|
||||||
|
if debug {
|
||||||
|
println("evt: connected in central role")
|
||||||
|
}
|
||||||
|
connectionAttempt.connectionHandle = gapEvent.conn_handle
|
||||||
|
connectionAttempt.state.Set(2) // connection was successful
|
||||||
|
DefaultAdapter.connectHandler(device, true)
|
||||||
|
}
|
||||||
|
case C.BLE_GAP_EVT_DISCONNECTED:
|
||||||
|
if debug {
|
||||||
|
println("evt: disconnected")
|
||||||
|
}
|
||||||
|
// Clean up state for this connection.
|
||||||
|
for i, cb := range gattcNotificationCallbacks {
|
||||||
|
if uint16(cb.connectionHandle) == currentConnection.handle.Reg {
|
||||||
|
gattcNotificationCallbacks[i].valueHandle = 0 // 0 means invalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentConnection.handle.Reg = C.BLE_CONN_HANDLE_INVALID
|
||||||
|
// Auto-restart advertisement if needed.
|
||||||
|
if defaultAdvertisement.isAdvertising.Get() != 0 {
|
||||||
|
// The advertisement was running but was automatically stopped
|
||||||
|
// by the connection event.
|
||||||
|
// Note that it cannot be restarted during connect like this,
|
||||||
|
// because it would need to be reconfigured as a non-connectable
|
||||||
|
// advertisement. That's left as a future addition, if
|
||||||
|
// necessary.
|
||||||
|
C.sd_ble_gap_adv_start(defaultAdvertisement.handle, C.BLE_CONN_CFG_TAG_DEFAULT)
|
||||||
|
}
|
||||||
|
device := Device{
|
||||||
|
connectionHandle: gapEvent.conn_handle,
|
||||||
|
}
|
||||||
|
DefaultAdapter.connectHandler(device, false)
|
||||||
|
case C.BLE_GAP_EVT_CONN_PARAM_UPDATE:
|
||||||
|
if debug {
|
||||||
|
// Print connection parameters for easy debugging.
|
||||||
|
params := gapEvent.params.unionfield_conn_param_update().conn_params
|
||||||
|
interval_ms := params.min_conn_interval * 125 / 100 // min and max are the same here
|
||||||
|
print("conn param update interval=", interval_ms, "ms latency=", params.slave_latency, " timeout=", params.conn_sup_timeout*10, "ms")
|
||||||
|
println()
|
||||||
|
}
|
||||||
|
case C.BLE_GAP_EVT_ADV_REPORT:
|
||||||
|
advReport := gapEvent.params.unionfield_adv_report()
|
||||||
|
if debug && &scanReportBuffer.data[0] != (*byte)(unsafe.Pointer(advReport.data.p_data)) {
|
||||||
|
// Sanity check.
|
||||||
|
panic("scanReportBuffer != advReport.p_data")
|
||||||
|
}
|
||||||
|
// Prepare the globalScanResult, which will be passed to the
|
||||||
|
// callback.
|
||||||
|
scanReportBuffer.len = byte(advReport.data.len)
|
||||||
|
globalScanResult.RSSI = int16(advReport.rssi)
|
||||||
|
globalScanResult.Address = Address{
|
||||||
|
makeMACAddress(advReport.peer_addr),
|
||||||
|
}
|
||||||
|
globalScanResult.AdvertisementPayload = &scanReportBuffer
|
||||||
|
// Signal to the main thread that there was a scan report.
|
||||||
|
// Scanning will be resumed (from the main thread) once the scan
|
||||||
|
// report has been processed.
|
||||||
|
gotScanReport.Set(1)
|
||||||
|
case C.BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST:
|
||||||
|
// Respond with the default PPCP connection parameters by passing
|
||||||
|
// nil:
|
||||||
|
// > If NULL is provided on a peripheral role, the parameters in the
|
||||||
|
// > PPCP characteristic of the GAP service will be used instead. If
|
||||||
|
// > NULL is provided on a central role and in response to a
|
||||||
|
// > BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST, the peripheral request
|
||||||
|
// > will be rejected
|
||||||
|
C.sd_ble_gap_conn_param_update(gapEvent.conn_handle, nil)
|
||||||
|
case C.BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST:
|
||||||
|
// We need to respond with sd_ble_gap_data_length_update. Setting
|
||||||
|
// both parameters to nil will make sure we send the default values.
|
||||||
|
C.sd_ble_gap_data_length_update(gapEvent.conn_handle, nil, nil)
|
||||||
|
case C.BLE_GAP_EVT_DATA_LENGTH_UPDATE:
|
||||||
|
// ignore confirmation of data length successfully updated
|
||||||
|
case C.BLE_GAP_EVT_PHY_UPDATE_REQUEST:
|
||||||
|
phyUpdateRequest := gapEvent.params.unionfield_phy_update_request()
|
||||||
|
C.sd_ble_gap_phy_update(gapEvent.conn_handle, &phyUpdateRequest.peer_preferred_phys)
|
||||||
|
case C.BLE_GAP_EVT_PHY_UPDATE:
|
||||||
|
// ignore confirmation of phy successfully updated
|
||||||
|
case C.BLE_GAP_EVT_TIMEOUT:
|
||||||
|
timeoutEvt := gapEvent.params.unionfield_timeout()
|
||||||
|
switch timeoutEvt.src {
|
||||||
|
case C.BLE_GAP_TIMEOUT_SRC_CONN:
|
||||||
|
// Failed to connect to a peripheral.
|
||||||
|
if debug {
|
||||||
|
println("gap timeout: conn")
|
||||||
|
}
|
||||||
|
connectionAttempt.state.Set(3) // connection timed out
|
||||||
|
default:
|
||||||
|
// For example a scan timeout.
|
||||||
|
if debug {
|
||||||
|
println("gap timeout: other")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if debug {
|
||||||
|
println("unknown GAP event:", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case id >= C.BLE_GATTS_EVT_BASE && id <= C.BLE_GATTS_EVT_LAST:
|
||||||
|
gattsEvent := eventBuf.evt.unionfield_gatts_evt()
|
||||||
|
switch id {
|
||||||
|
case C.BLE_GATTS_EVT_WRITE:
|
||||||
|
writeEvent := gattsEvent.params.unionfield_write()
|
||||||
|
len := writeEvent.len - writeEvent.offset
|
||||||
|
data := (*[255]byte)(unsafe.Pointer(&writeEvent.data[0]))[:len:len]
|
||||||
|
handler := DefaultAdapter.getCharWriteHandler(writeEvent.handle)
|
||||||
|
if handler != nil {
|
||||||
|
handler.callback(Connection(gattsEvent.conn_handle), int(writeEvent.offset), data)
|
||||||
|
}
|
||||||
|
case C.BLE_GATTS_EVT_SYS_ATTR_MISSING:
|
||||||
|
// This event is generated when reading the Generic Attribute
|
||||||
|
// service. It appears to be necessary for bonded devices.
|
||||||
|
// From the docs:
|
||||||
|
// > If the pointer is NULL, the system attribute info is
|
||||||
|
// > initialized, assuming that the application does not have any
|
||||||
|
// > previously saved system attribute data for this device.
|
||||||
|
// Maybe we should look at the error, but as there's not really a
|
||||||
|
// way to handle it, ignore it.
|
||||||
|
C.sd_ble_gatts_sys_attr_set(gattsEvent.conn_handle, nil, 0, 0)
|
||||||
|
case C.BLE_GATTS_EVT_EXCHANGE_MTU_REQUEST:
|
||||||
|
// This event is generated by some devices. While we could support
|
||||||
|
// larger MTUs, this default MTU is supported everywhere.
|
||||||
|
C.sd_ble_gatts_exchange_mtu_reply(gattsEvent.conn_handle, C.BLE_GATT_ATT_MTU_DEFAULT)
|
||||||
|
case C.BLE_GATTS_EVT_HVN_TX_COMPLETE:
|
||||||
|
// ignore confirmation of a notification successfully sent
|
||||||
|
default:
|
||||||
|
if debug {
|
||||||
|
println("unknown GATTS event:", id, id-C.BLE_GATTS_EVT_BASE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case id >= C.BLE_GATTC_EVT_BASE && id <= C.BLE_GATTC_EVT_LAST:
|
||||||
|
gattcEvent := eventBuf.evt.unionfield_gattc_evt()
|
||||||
|
switch id {
|
||||||
|
case C.BLE_GATTC_EVT_PRIM_SRVC_DISC_RSP:
|
||||||
|
discoveryEvent := gattcEvent.params.unionfield_prim_srvc_disc_rsp()
|
||||||
|
if debug {
|
||||||
|
println("evt: discovered primary service", discoveryEvent.count)
|
||||||
|
}
|
||||||
|
discoveringService.state.Set(2) // signal there is a result
|
||||||
|
if discoveryEvent.count >= 1 {
|
||||||
|
// Theoretically there may be more, but as we're only using
|
||||||
|
// sd_ble_gattc_primary_services_discover, there should only be
|
||||||
|
// one discovered service. Use the first as a sensible fallback.
|
||||||
|
discoveringService.startHandle.Set(discoveryEvent.services[0].handle_range.start_handle)
|
||||||
|
discoveringService.endHandle.Set(discoveryEvent.services[0].handle_range.end_handle)
|
||||||
|
discoveringService.uuid = discoveryEvent.services[0].uuid
|
||||||
|
} else {
|
||||||
|
// No service found.
|
||||||
|
discoveringService.startHandle.Set(0)
|
||||||
|
}
|
||||||
|
case C.BLE_GATTC_EVT_CHAR_DISC_RSP:
|
||||||
|
discoveryEvent := gattcEvent.params.unionfield_char_disc_rsp()
|
||||||
|
if debug {
|
||||||
|
println("evt: discovered characteristics", discoveryEvent.count)
|
||||||
|
}
|
||||||
|
if discoveryEvent.count >= 1 {
|
||||||
|
// There may be more, but for ease of implementing we only
|
||||||
|
// handle the first.
|
||||||
|
discoveringCharacteristic.handle_value.Set(discoveryEvent.chars[0].handle_value)
|
||||||
|
discoveringCharacteristic.char_props = discoveryEvent.chars[0].char_props
|
||||||
|
discoveringCharacteristic.uuid = discoveryEvent.chars[0].uuid
|
||||||
|
} else {
|
||||||
|
// zero indicates we received no characteristic, set handle_value to last
|
||||||
|
discoveringCharacteristic.handle_value.Set(0xffff)
|
||||||
|
}
|
||||||
|
case C.BLE_GATTC_EVT_DESC_DISC_RSP:
|
||||||
|
discoveryEvent := gattcEvent.params.unionfield_desc_disc_rsp()
|
||||||
|
if debug {
|
||||||
|
println("evt: discovered descriptors", discoveryEvent.count)
|
||||||
|
}
|
||||||
|
if discoveryEvent.count >= 1 {
|
||||||
|
// There may be more, but for ease of implementing we only
|
||||||
|
// handle the first.
|
||||||
|
uuid := discoveryEvent.descs[0].uuid
|
||||||
|
if uuid._type == C.BLE_UUID_TYPE_BLE && uuid.uuid == 0x2902 {
|
||||||
|
// Found a CCCD (Client Characteristic Configuration
|
||||||
|
// Descriptor), which has a 16-bit UUID with value 0x2902).
|
||||||
|
discoveringCharacteristic.handle_value.Set(discoveryEvent.descs[0].handle)
|
||||||
|
} else {
|
||||||
|
// Found something else?
|
||||||
|
// TODO: handle this properly by continuing the scan. For
|
||||||
|
// now, give up if we found something other than a CCCD.
|
||||||
|
if debug {
|
||||||
|
println(" found some other descriptor (unimplemented)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case C.BLE_GATTC_EVT_READ_RSP:
|
||||||
|
readEvent := gattcEvent.params.unionfield_read_rsp()
|
||||||
|
if debug {
|
||||||
|
println("evt: read response, data length", readEvent.len)
|
||||||
|
}
|
||||||
|
readingCharacteristic.handle_value.Set(readEvent.handle)
|
||||||
|
readingCharacteristic.offset = readEvent.offset
|
||||||
|
readingCharacteristic.length = readEvent.len
|
||||||
|
|
||||||
|
// copy read event data into Go slice
|
||||||
|
copy(readingCharacteristic.value, (*[255]byte)(unsafe.Pointer(&readEvent.data[0]))[:readEvent.len:readEvent.len])
|
||||||
|
case C.BLE_GATTC_EVT_HVX:
|
||||||
|
hvxEvent := gattcEvent.params.unionfield_hvx()
|
||||||
|
switch hvxEvent._type {
|
||||||
|
case C.BLE_GATT_HVX_NOTIFICATION:
|
||||||
|
if debug {
|
||||||
|
println("evt: notification", hvxEvent.handle)
|
||||||
|
}
|
||||||
|
// Find the callback and call it (if there is any).
|
||||||
|
for _, callbackInfo := range gattcNotificationCallbacks {
|
||||||
|
if callbackInfo.valueHandle == hvxEvent.handle && callbackInfo.connectionHandle == gattcEvent.conn_handle {
|
||||||
|
// Create a Go slice from the data, to pass to the
|
||||||
|
// callback.
|
||||||
|
data := (*[255]byte)(unsafe.Pointer(&hvxEvent.data[0]))[:hvxEvent.len:hvxEvent.len]
|
||||||
|
if callbackInfo.callback != nil {
|
||||||
|
callbackInfo.callback(data)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if debug {
|
||||||
|
println("unknown GATTC event:", id, id-C.BLE_GATTC_EVT_BASE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if debug {
|
||||||
|
println("unknown event:", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
109
adapter_nrf528xx-peripheral.go
Normal file
109
adapter_nrf528xx-peripheral.go
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
//go:build softdevice && s113v7
|
||||||
|
|
||||||
|
package bluetooth
|
||||||
|
|
||||||
|
// This file implements the event handler for SoftDevices with only peripheral
|
||||||
|
// mode support. This includes the S113.
|
||||||
|
|
||||||
|
/*
|
||||||
|
#include "nrf_sdm.h"
|
||||||
|
#include "nrf_nvic.h"
|
||||||
|
#include "ble.h"
|
||||||
|
#include "ble_gap.h"
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleEvent() {
|
||||||
|
id := eventBuf.header.evt_id
|
||||||
|
switch {
|
||||||
|
case id >= C.BLE_GAP_EVT_BASE && id <= C.BLE_GAP_EVT_LAST:
|
||||||
|
gapEvent := eventBuf.evt.unionfield_gap_evt()
|
||||||
|
switch id {
|
||||||
|
case C.BLE_GAP_EVT_CONNECTED:
|
||||||
|
if debug {
|
||||||
|
println("evt: connected in peripheral role")
|
||||||
|
}
|
||||||
|
currentConnection.handle.Reg = uint16(gapEvent.conn_handle)
|
||||||
|
connectEvent := gapEvent.params.unionfield_connected()
|
||||||
|
device := Device{
|
||||||
|
Address: Address{makeMACAddress(connectEvent.peer_addr)},
|
||||||
|
connectionHandle: gapEvent.conn_handle,
|
||||||
|
}
|
||||||
|
DefaultAdapter.connectHandler(device, true)
|
||||||
|
case C.BLE_GAP_EVT_DISCONNECTED:
|
||||||
|
if debug {
|
||||||
|
println("evt: disconnected")
|
||||||
|
}
|
||||||
|
currentConnection.handle.Reg = C.BLE_CONN_HANDLE_INVALID
|
||||||
|
// Auto-restart advertisement if needed.
|
||||||
|
if defaultAdvertisement.isAdvertising.Get() != 0 {
|
||||||
|
// The advertisement was running but was automatically stopped
|
||||||
|
// by the connection event.
|
||||||
|
// Note that it cannot be restarted during connect like this,
|
||||||
|
// because it would need to be reconfigured as a non-connectable
|
||||||
|
// advertisement. That's left as a future addition, if
|
||||||
|
// necessary.
|
||||||
|
C.sd_ble_gap_adv_start(defaultAdvertisement.handle, C.BLE_CONN_CFG_TAG_DEFAULT)
|
||||||
|
}
|
||||||
|
device := Device{
|
||||||
|
connectionHandle: gapEvent.conn_handle,
|
||||||
|
}
|
||||||
|
DefaultAdapter.connectHandler(device, false)
|
||||||
|
case C.BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST:
|
||||||
|
// We need to respond with sd_ble_gap_data_length_update. Setting
|
||||||
|
// both parameters to nil will make sure we send the default values.
|
||||||
|
C.sd_ble_gap_data_length_update(gapEvent.conn_handle, nil, nil)
|
||||||
|
case C.BLE_GAP_EVT_DATA_LENGTH_UPDATE:
|
||||||
|
// ignore confirmation of data length successfully updated
|
||||||
|
case C.BLE_GAP_EVT_PHY_UPDATE_REQUEST:
|
||||||
|
phyUpdateRequest := gapEvent.params.unionfield_phy_update_request()
|
||||||
|
C.sd_ble_gap_phy_update(gapEvent.conn_handle, &phyUpdateRequest.peer_preferred_phys)
|
||||||
|
case C.BLE_GAP_EVT_PHY_UPDATE:
|
||||||
|
// ignore confirmation of phy successfully updated
|
||||||
|
default:
|
||||||
|
if debug {
|
||||||
|
println("unknown GAP event:", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case id >= C.BLE_GATTS_EVT_BASE && id <= C.BLE_GATTS_EVT_LAST:
|
||||||
|
gattsEvent := eventBuf.evt.unionfield_gatts_evt()
|
||||||
|
switch id {
|
||||||
|
case C.BLE_GATTS_EVT_WRITE:
|
||||||
|
writeEvent := gattsEvent.params.unionfield_write()
|
||||||
|
len := writeEvent.len - writeEvent.offset
|
||||||
|
data := (*[255]byte)(unsafe.Pointer(&writeEvent.data[0]))[:len:len]
|
||||||
|
handler := DefaultAdapter.getCharWriteHandler(writeEvent.handle)
|
||||||
|
if handler != nil {
|
||||||
|
handler.callback(Connection(gattsEvent.conn_handle), int(writeEvent.offset), data)
|
||||||
|
}
|
||||||
|
case C.BLE_GATTS_EVT_SYS_ATTR_MISSING:
|
||||||
|
// This event is generated when reading the Generic Attribute
|
||||||
|
// service. It appears to be necessary for bonded devices.
|
||||||
|
// From the docs:
|
||||||
|
// > If the pointer is NULL, the system attribute info is
|
||||||
|
// > initialized, assuming that the application does not have any
|
||||||
|
// > previously saved system attribute data for this device.
|
||||||
|
// Maybe we should look at the error, but as there's not really a
|
||||||
|
// way to handle it, ignore it.
|
||||||
|
C.sd_ble_gatts_sys_attr_set(gattsEvent.conn_handle, nil, 0, 0)
|
||||||
|
case C.BLE_GATTS_EVT_EXCHANGE_MTU_REQUEST:
|
||||||
|
// This event is generated by some devices. While we could support
|
||||||
|
// larger MTUs, this default MTU is supported everywhere.
|
||||||
|
C.sd_ble_gatts_exchange_mtu_reply(gattsEvent.conn_handle, C.BLE_GATT_ATT_MTU_DEFAULT)
|
||||||
|
case C.BLE_GATTS_EVT_HVN_TX_COMPLETE:
|
||||||
|
// ignore confirmation of a notification successfully sent
|
||||||
|
default:
|
||||||
|
if debug {
|
||||||
|
println("unknown GATTS event:", id, id-C.BLE_GATTS_EVT_BASE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if debug {
|
||||||
|
println("unknown event:", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,10 @@
|
||||||
// +build softdevice,!s110v8
|
//go:build (softdevice && s113v7) || (softdevice && s132v6) || (softdevice && s140v6) || (softdevice && s140v7)
|
||||||
|
|
||||||
package bluetooth
|
package bluetooth
|
||||||
|
|
||||||
/*
|
// This file defines the SoftDevice adapter for all nrf52-series chips.
|
||||||
// Define SoftDevice functions as regular function declarations (not inline
|
|
||||||
// static functions).
|
|
||||||
#define SVCALL_AS_NORMAL_FUNCTION
|
|
||||||
|
|
||||||
|
/*
|
||||||
#include "nrf_sdm.h"
|
#include "nrf_sdm.h"
|
||||||
#include "nrf_nvic.h"
|
#include "nrf_nvic.h"
|
||||||
#include "ble.h"
|
#include "ble.h"
|
||||||
|
@ -33,6 +31,9 @@ var clockConfigXtal C.nrf_clock_lf_cfg_t = C.nrf_clock_lf_cfg_t{
|
||||||
accuracy: C.NRF_CLOCK_LF_ACCURACY_250_PPM,
|
accuracy: C.NRF_CLOCK_LF_ACCURACY_250_PPM,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:extern __app_ram_base
|
||||||
|
var appRAMBase [0]uint32
|
||||||
|
|
||||||
func (a *Adapter) enable() error {
|
func (a *Adapter) enable() error {
|
||||||
// Enable the SoftDevice.
|
// Enable the SoftDevice.
|
||||||
var clockConfig *C.nrf_clock_lf_cfg_t
|
var clockConfig *C.nrf_clock_lf_cfg_t
|
||||||
|
@ -45,219 +46,24 @@ func (a *Adapter) enable() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable the BLE stack.
|
// Enable the BLE stack.
|
||||||
appRAMBase := uint32(0x200039c0)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleEvent() {
|
func (a *Adapter) Address() (MACAddress, error) {
|
||||||
id := eventBuf.header.evt_id
|
var addr C.ble_gap_addr_t
|
||||||
switch {
|
errCode := C.sd_ble_gap_addr_get(&addr)
|
||||||
case id >= C.BLE_GAP_EVT_BASE && id <= C.BLE_GAP_EVT_LAST:
|
if errCode != 0 {
|
||||||
gapEvent := eventBuf.evt.unionfield_gap_evt()
|
return MACAddress{}, Error(errCode)
|
||||||
switch id {
|
|
||||||
case C.BLE_GAP_EVT_CONNECTED:
|
|
||||||
connectEvent := gapEvent.params.unionfield_connected()
|
|
||||||
switch connectEvent.role {
|
|
||||||
case C.BLE_GAP_ROLE_PERIPH:
|
|
||||||
if debug {
|
|
||||||
println("evt: connected in peripheral role")
|
|
||||||
}
|
}
|
||||||
currentConnection.Reg = gapEvent.conn_handle
|
return MACAddress{MAC: makeAddress(addr.addr)}, nil
|
||||||
DefaultAdapter.connectHandler(nil, true)
|
|
||||||
case C.BLE_GAP_ROLE_CENTRAL:
|
|
||||||
if debug {
|
|
||||||
println("evt: connected in central role")
|
|
||||||
}
|
}
|
||||||
connectionAttempt.connectionHandle = gapEvent.conn_handle
|
|
||||||
connectionAttempt.state.Set(2) // connection was successful
|
|
||||||
DefaultAdapter.connectHandler(nil, true)
|
|
||||||
}
|
|
||||||
case C.BLE_GAP_EVT_DISCONNECTED:
|
|
||||||
if debug {
|
|
||||||
println("evt: disconnected")
|
|
||||||
}
|
|
||||||
// Clean up state for this connection.
|
|
||||||
for i, cb := range gattcNotificationCallbacks {
|
|
||||||
if cb.connectionHandle == currentConnection.Reg {
|
|
||||||
gattcNotificationCallbacks[i].valueHandle = 0 // 0 means invalid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
currentConnection.Reg = C.BLE_CONN_HANDLE_INVALID
|
|
||||||
// Auto-restart advertisement if needed.
|
|
||||||
if defaultAdvertisement.isAdvertising.Get() != 0 {
|
|
||||||
// The advertisement was running but was automatically stopped
|
|
||||||
// by the connection event.
|
|
||||||
// Note that it cannot be restarted during connect like this,
|
|
||||||
// because it would need to be reconfigured as a non-connectable
|
|
||||||
// advertisement. That's left as a future addition, if
|
|
||||||
// necessary.
|
|
||||||
C.sd_ble_gap_adv_start(defaultAdvertisement.handle, C.BLE_CONN_CFG_TAG_DEFAULT)
|
|
||||||
}
|
|
||||||
DefaultAdapter.connectHandler(nil, false)
|
|
||||||
case C.BLE_GAP_EVT_ADV_REPORT:
|
|
||||||
advReport := gapEvent.params.unionfield_adv_report()
|
|
||||||
if debug && &scanReportBuffer.data[0] != advReport.data.p_data {
|
|
||||||
// Sanity check.
|
|
||||||
panic("scanReportBuffer != advReport.p_data")
|
|
||||||
}
|
|
||||||
// Prepare the globalScanResult, which will be passed to the
|
|
||||||
// callback.
|
|
||||||
scanReportBuffer.len = byte(advReport.data.len)
|
|
||||||
globalScanResult.RSSI = int16(advReport.rssi)
|
|
||||||
globalScanResult.Address = Address{
|
|
||||||
MACAddress{MAC: advReport.peer_addr.addr,
|
|
||||||
isRandom: advReport.peer_addr.bitfield_addr_type() != 0},
|
|
||||||
}
|
|
||||||
globalScanResult.AdvertisementPayload = &scanReportBuffer
|
|
||||||
// Signal to the main thread that there was a scan report.
|
|
||||||
// Scanning will be resumed (from the main thread) once the scan
|
|
||||||
// report has been processed.
|
|
||||||
gotScanReport.Set(1)
|
|
||||||
case C.BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST:
|
|
||||||
// Respond with the default PPCP connection parameters by passing
|
|
||||||
// nil:
|
|
||||||
// > If NULL is provided on a peripheral role, the parameters in the
|
|
||||||
// > PPCP characteristic of the GAP service will be used instead. If
|
|
||||||
// > NULL is provided on a central role and in response to a
|
|
||||||
// > BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST, the peripheral request
|
|
||||||
// > will be rejected
|
|
||||||
C.sd_ble_gap_conn_param_update(gapEvent.conn_handle, nil)
|
|
||||||
case C.BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST:
|
|
||||||
// We need to respond with sd_ble_gap_data_length_update. Setting
|
|
||||||
// both parameters to nil will make sure we send the default values.
|
|
||||||
C.sd_ble_gap_data_length_update(gapEvent.conn_handle, nil, nil)
|
|
||||||
default:
|
|
||||||
if debug {
|
|
||||||
println("unknown GAP event:", id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case id >= C.BLE_GATTS_EVT_BASE && id <= C.BLE_GATTS_EVT_LAST:
|
|
||||||
gattsEvent := eventBuf.evt.unionfield_gatts_evt()
|
|
||||||
switch id {
|
|
||||||
case C.BLE_GATTS_EVT_WRITE:
|
|
||||||
writeEvent := gattsEvent.params.unionfield_write()
|
|
||||||
len := writeEvent.len - writeEvent.offset
|
|
||||||
data := (*[255]byte)(unsafe.Pointer(&writeEvent.data[0]))[:len:len]
|
|
||||||
handler := DefaultAdapter.getCharWriteHandler(writeEvent.handle)
|
|
||||||
if handler != nil {
|
|
||||||
handler.callback(Connection(gattsEvent.conn_handle), int(writeEvent.offset), data)
|
|
||||||
}
|
|
||||||
case C.BLE_GATTS_EVT_SYS_ATTR_MISSING:
|
|
||||||
// This event is generated when reading the Generic Attribute
|
|
||||||
// service. It appears to be necessary for bonded devices.
|
|
||||||
// From the docs:
|
|
||||||
// > If the pointer is NULL, the system attribute info is
|
|
||||||
// > initialized, assuming that the application does not have any
|
|
||||||
// > previously saved system attribute data for this device.
|
|
||||||
// Maybe we should look at the error, but as there's not really a
|
|
||||||
// way to handle it, ignore it.
|
|
||||||
C.sd_ble_gatts_sys_attr_set(gattsEvent.conn_handle, nil, 0, 0)
|
|
||||||
case C.BLE_GATTS_EVT_EXCHANGE_MTU_REQUEST:
|
|
||||||
// This event is generated by some devices. While we could support
|
|
||||||
// larger MTUs, this default MTU is supported everywhere.
|
|
||||||
C.sd_ble_gatts_exchange_mtu_reply(gattsEvent.conn_handle, C.BLE_GATT_ATT_MTU_DEFAULT)
|
|
||||||
default:
|
|
||||||
if debug {
|
|
||||||
println("unknown GATTS event:", id, id-C.BLE_GATTS_EVT_BASE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case id >= C.BLE_GATTC_EVT_BASE && id <= C.BLE_GATTC_EVT_LAST:
|
|
||||||
gattcEvent := eventBuf.evt.unionfield_gattc_evt()
|
|
||||||
switch id {
|
|
||||||
case C.BLE_GATTC_EVT_PRIM_SRVC_DISC_RSP:
|
|
||||||
discoveryEvent := gattcEvent.params.unionfield_prim_srvc_disc_rsp()
|
|
||||||
if debug {
|
|
||||||
println("evt: discovered primary service", discoveryEvent.count)
|
|
||||||
}
|
|
||||||
discoveringService.state.Set(2) // signal there is a result
|
|
||||||
if discoveryEvent.count >= 1 {
|
|
||||||
// Theoretically there may be more, but as we're only using
|
|
||||||
// sd_ble_gattc_primary_services_discover, there should only be
|
|
||||||
// one discovered service. Use the first as a sensible fallback.
|
|
||||||
discoveringService.startHandle.Set(discoveryEvent.services[0].handle_range.start_handle)
|
|
||||||
discoveringService.endHandle.Set(discoveryEvent.services[0].handle_range.end_handle)
|
|
||||||
discoveringService.uuid = discoveryEvent.services[0].uuid
|
|
||||||
} else {
|
|
||||||
// No service found.
|
|
||||||
discoveringService.startHandle.Set(0)
|
|
||||||
}
|
|
||||||
case C.BLE_GATTC_EVT_CHAR_DISC_RSP:
|
|
||||||
discoveryEvent := gattcEvent.params.unionfield_char_disc_rsp()
|
|
||||||
if debug {
|
|
||||||
println("evt: discovered characteristics", discoveryEvent.count)
|
|
||||||
}
|
|
||||||
if discoveryEvent.count >= 1 {
|
|
||||||
// There may be more, but for ease of implementing we only
|
|
||||||
// handle the first.
|
|
||||||
discoveringCharacteristic.handle_value.Set(discoveryEvent.chars[0].handle_value)
|
|
||||||
discoveringCharacteristic.char_props = discoveryEvent.chars[0].char_props
|
|
||||||
discoveringCharacteristic.uuid = discoveryEvent.chars[0].uuid
|
|
||||||
} else {
|
|
||||||
// zero indicates we received no characteristic, set handle_value to last
|
|
||||||
discoveringCharacteristic.handle_value.Set(0xffff)
|
|
||||||
}
|
|
||||||
case C.BLE_GATTC_EVT_DESC_DISC_RSP:
|
|
||||||
discoveryEvent := gattcEvent.params.unionfield_desc_disc_rsp()
|
|
||||||
if debug {
|
|
||||||
println("evt: discovered descriptors", discoveryEvent.count)
|
|
||||||
}
|
|
||||||
if discoveryEvent.count >= 1 {
|
|
||||||
// There may be more, but for ease of implementing we only
|
|
||||||
// handle the first.
|
|
||||||
uuid := discoveryEvent.descs[0].uuid
|
|
||||||
if uuid._type == C.BLE_UUID_TYPE_BLE && uuid.uuid == 0x2902 {
|
|
||||||
// Found a CCCD (Client Characteristic Configuration
|
|
||||||
// Descriptor), which has a 16-bit UUID with value 0x2902).
|
|
||||||
discoveringCharacteristic.handle_value.Set(discoveryEvent.descs[0].handle)
|
|
||||||
} else {
|
|
||||||
// Found something else?
|
|
||||||
// TODO: handle this properly by continuing the scan. For
|
|
||||||
// now, give up if we found something other than a CCCD.
|
|
||||||
if debug {
|
|
||||||
println(" found some other descriptor (unimplemented)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case C.BLE_GATTC_EVT_READ_RSP:
|
|
||||||
readEvent := gattcEvent.params.unionfield_read_rsp()
|
|
||||||
if debug {
|
|
||||||
println("evt: read response, data length", readEvent.len)
|
|
||||||
}
|
|
||||||
readingCharacteristic.handle_value.Set(readEvent.handle)
|
|
||||||
readingCharacteristic.offset = readEvent.offset
|
|
||||||
readingCharacteristic.length = readEvent.len
|
|
||||||
|
|
||||||
// copy read event data into Go slice
|
// Convert a C.ble_gap_addr_t to a MACAddress struct.
|
||||||
copy(readingCharacteristic.value, (*[255]byte)(unsafe.Pointer(&readEvent.data[0]))[:readEvent.len:readEvent.len])
|
func makeMACAddress(addr C.ble_gap_addr_t) MACAddress {
|
||||||
case C.BLE_GATTC_EVT_HVX:
|
return MACAddress{
|
||||||
hvxEvent := gattcEvent.params.unionfield_hvx()
|
MAC: makeAddress(addr.addr),
|
||||||
switch hvxEvent._type {
|
isRandom: addr.bitfield_addr_type() != 0,
|
||||||
case C.BLE_GATT_HVX_NOTIFICATION:
|
|
||||||
if debug {
|
|
||||||
println("evt: notification", hvxEvent.handle)
|
|
||||||
}
|
|
||||||
// Find the callback and call it (if there is any).
|
|
||||||
for _, callbackInfo := range gattcNotificationCallbacks {
|
|
||||||
if callbackInfo.valueHandle == hvxEvent.handle && callbackInfo.connectionHandle == gattcEvent.conn_handle {
|
|
||||||
// Create a Go slice from the data, to pass to the
|
|
||||||
// callback.
|
|
||||||
data := (*[255]byte)(unsafe.Pointer(&hvxEvent.data[0]))[:hvxEvent.len:hvxEvent.len]
|
|
||||||
if callbackInfo.callback != nil {
|
|
||||||
callbackInfo.callback(data)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if debug {
|
|
||||||
println("unknown GATTC event:", id, id-C.BLE_GATTC_EVT_BASE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if debug {
|
|
||||||
println("unknown event:", id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
// +build softdevice,s110v8
|
|
||||||
|
|
||||||
// This file is necessary to define SVCall wrappers, because TinyGo does not yet
|
|
||||||
// support static functions in the preamble.
|
|
||||||
|
|
||||||
// Discard all 'static' attributes to define functions normally.
|
|
||||||
#define static
|
|
||||||
|
|
||||||
#include "s110_nrf51_8.0.0/s110_nrf51_8.0.0_API/include/nrf_sdm.h"
|
|
||||||
#include "s110_nrf51_8.0.0/s110_nrf51_8.0.0_API/include/nrf_soc.h"
|
|
||||||
#include "s110_nrf51_8.0.0/s110_nrf51_8.0.0_API/include/ble.h"
|
|
|
@ -1,4 +1,4 @@
|
||||||
// +build softdevice,s110v8
|
//go:build softdevice && s110v8
|
||||||
|
|
||||||
package bluetooth
|
package bluetooth
|
||||||
|
|
||||||
|
|
13
adapter_s113v7.go
Normal file
13
adapter_s113v7.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
//go:build softdevice && s113v7
|
||||||
|
|
||||||
|
package bluetooth
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Add the correct SoftDevice include path to CFLAGS, so #include will work as
|
||||||
|
// expected.
|
||||||
|
#cgo CFLAGS: -Is113_nrf52_7.0.1/s113_nrf52_7.0.1_API/include
|
||||||
|
|
||||||
|
#include "nrf_nvic.h"
|
||||||
|
nrf_nvic_state_t nrf_nvic_state = {0};
|
||||||
|
*/
|
||||||
|
import "C"
|
|
@ -1,22 +0,0 @@
|
||||||
// +build softdevice,s132v6
|
|
||||||
|
|
||||||
// This file is necessary to define SVCall wrappers, because TinyGo does not yet
|
|
||||||
// support static functions in the preamble.
|
|
||||||
|
|
||||||
// Discard all 'static' attributes to define functions normally.
|
|
||||||
#define static
|
|
||||||
|
|
||||||
// Get rid of all __STATIC_INLINE symbols.
|
|
||||||
// This is a bit less straightforward: we first need to include the header that
|
|
||||||
// defines it, and then redefine it.
|
|
||||||
#include "nrf.h"
|
|
||||||
#undef __STATIC_INLINE
|
|
||||||
#define __STATIC_INLINE
|
|
||||||
|
|
||||||
#include "s132_nrf52_6.1.1/s132_nrf52_6.1.1_API/include/nrf_sdm.h"
|
|
||||||
#include "s132_nrf52_6.1.1/s132_nrf52_6.1.1_API/include/nrf_nvic.h"
|
|
||||||
#include "s132_nrf52_6.1.1/s132_nrf52_6.1.1_API/include/ble.h"
|
|
||||||
|
|
||||||
// Define nrf_nvic_state, which is used by sd_nvic_critical_region_enter and
|
|
||||||
// sd_nvic_critical_region_exit.
|
|
||||||
nrf_nvic_state_t nrf_nvic_state = {0};
|
|
|
@ -1,4 +1,4 @@
|
||||||
// +build softdevice,s132v6
|
//go:build softdevice && s132v6
|
||||||
|
|
||||||
package bluetooth
|
package bluetooth
|
||||||
|
|
||||||
|
@ -6,5 +6,8 @@ package bluetooth
|
||||||
// Add the correct SoftDevice include path to CFLAGS, so #include will work as
|
// Add the correct SoftDevice include path to CFLAGS, so #include will work as
|
||||||
// expected.
|
// expected.
|
||||||
#cgo CFLAGS: -Is132_nrf52_6.1.1/s132_nrf52_6.1.1_API/include
|
#cgo CFLAGS: -Is132_nrf52_6.1.1/s132_nrf52_6.1.1_API/include
|
||||||
|
|
||||||
|
#include "nrf_nvic.h"
|
||||||
|
nrf_nvic_state_t nrf_nvic_state = {0};
|
||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
// +build softdevice,s140v6
|
|
||||||
|
|
||||||
// This file is necessary to define SVCall wrappers, because TinyGo does not yet
|
|
||||||
// support static functions in the preamble.
|
|
||||||
|
|
||||||
// Discard all 'static' attributes to define functions normally.
|
|
||||||
#define static
|
|
||||||
|
|
||||||
// Get rid of all __STATIC_INLINE symbols.
|
|
||||||
// This is a bit less straightforward: we first need to include the header that
|
|
||||||
// defines it, and then redefine it.
|
|
||||||
#include "nrf.h"
|
|
||||||
#undef __STATIC_INLINE
|
|
||||||
#define __STATIC_INLINE
|
|
||||||
|
|
||||||
#include "s140_nrf52_6.1.1/s140_nrf52_6.1.1_API/include/nrf_sdm.h"
|
|
||||||
#include "s140_nrf52_6.1.1/s140_nrf52_6.1.1_API/include/nrf_nvic.h"
|
|
||||||
#include "s140_nrf52_6.1.1/s140_nrf52_6.1.1_API/include/ble.h"
|
|
||||||
|
|
||||||
// Define nrf_nvic_state, which is used by sd_nvic_critical_region_enter and
|
|
||||||
// sd_nvic_critical_region_exit.
|
|
||||||
nrf_nvic_state_t nrf_nvic_state = {0};
|
|
|
@ -1,4 +1,4 @@
|
||||||
// +build softdevice,s140v6
|
//go:build softdevice && s140v6
|
||||||
|
|
||||||
package bluetooth
|
package bluetooth
|
||||||
|
|
||||||
|
@ -6,5 +6,8 @@ package bluetooth
|
||||||
// Add the correct SoftDevice include path to CFLAGS, so #include will work as
|
// Add the correct SoftDevice include path to CFLAGS, so #include will work as
|
||||||
// expected.
|
// expected.
|
||||||
#cgo CFLAGS: -Is140_nrf52_6.1.1/s140_nrf52_6.1.1_API/include
|
#cgo CFLAGS: -Is140_nrf52_6.1.1/s140_nrf52_6.1.1_API/include
|
||||||
|
|
||||||
|
#include "nrf_nvic.h"
|
||||||
|
nrf_nvic_state_t nrf_nvic_state = {0};
|
||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
// +build softdevice,s140v7
|
|
||||||
|
|
||||||
// This file is necessary to define SVCall wrappers, because TinyGo does not yet
|
|
||||||
// support static functions in the preamble.
|
|
||||||
|
|
||||||
// Discard all 'static' attributes to define functions normally.
|
|
||||||
#define static
|
|
||||||
|
|
||||||
// Get rid of all __STATIC_INLINE symbols.
|
|
||||||
// This is a bit less straightforward: we first need to include the header that
|
|
||||||
// defines it, and then redefine it.
|
|
||||||
#include "nrf.h"
|
|
||||||
#undef __STATIC_INLINE
|
|
||||||
#define __STATIC_INLINE
|
|
||||||
|
|
||||||
#include "s140_nrf52_7.0.1/s140_nrf52_7.0.1_API/include/nrf_sdm.h"
|
|
||||||
#include "s140_nrf52_7.0.1/s140_nrf52_7.0.1_API/include/nrf_nvic.h"
|
|
||||||
#include "s140_nrf52_7.0.1/s140_nrf52_7.0.1_API/include/ble.h"
|
|
||||||
|
|
||||||
// Define nrf_nvic_state, which is used by sd_nvic_critical_region_enter and
|
|
||||||
// sd_nvic_critical_region_exit.
|
|
||||||
nrf_nvic_state_t nrf_nvic_state = {0};
|
|
|
@ -1,10 +1,13 @@
|
||||||
// +build softdevice,s140v7
|
//go:build softdevice && s140v7
|
||||||
|
|
||||||
package bluetooth
|
package bluetooth
|
||||||
|
|
||||||
/*
|
/*
|
||||||
// Add the correct SoftDevice include path to CFLAGS, so #include will work as
|
// Add the correct SoftDevice include path to CFLAGS, so #include will work as
|
||||||
// expected.
|
// expected.
|
||||||
#cgo CFLAGS: -Is140_nrf52_7.0.1/s140_nrf52_7.0.1_API/include
|
#cgo CFLAGS: -Is140_nrf52_7.3.0/s140_nrf52_7.3.0_API/include
|
||||||
|
|
||||||
|
#include "nrf_nvic.h"
|
||||||
|
nrf_nvic_state_t nrf_nvic_state = {0};
|
||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// +build softdevice
|
//go:build softdevice
|
||||||
|
|
||||||
package bluetooth
|
package bluetooth
|
||||||
|
|
||||||
|
@ -10,6 +10,14 @@ import (
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// #include "ble.h"
|
||||||
|
// #ifdef NRF51
|
||||||
|
// #include "nrf_soc.h"
|
||||||
|
// #else
|
||||||
|
// #include "nrf_nvic.h"
|
||||||
|
// #endif
|
||||||
|
import "C"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrNotDefaultAdapter = errors.New("bluetooth: not the default adapter")
|
ErrNotDefaultAdapter = errors.New("bluetooth: not the default adapter")
|
||||||
)
|
)
|
||||||
|
@ -20,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 {
|
||||||
|
@ -40,7 +48,7 @@ type Adapter struct {
|
||||||
scanning bool
|
scanning bool
|
||||||
charWriteHandlers []charWriteHandler
|
charWriteHandlers []charWriteHandler
|
||||||
|
|
||||||
connectHandler func(device Addresser, 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,
|
||||||
|
@ -48,10 +56,12 @@ 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 Addresser, connected bool) {
|
connectHandler: func(device Device, connected bool) {
|
||||||
return
|
return
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
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).
|
||||||
func (a *Adapter) Enable() error {
|
func (a *Adapter) Enable() error {
|
||||||
|
@ -62,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
|
||||||
|
@ -87,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)
|
||||||
}
|
}
|
||||||
|
@ -106,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)
|
||||||
}
|
}
|
||||||
|
@ -115,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]),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,28 @@
|
||||||
package bluetooth
|
package bluetooth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/go-ole/go-ole"
|
"github.com/go-ole/go-ole"
|
||||||
"tinygo.org/x/bluetooth/winbt"
|
"github.com/saltosystems/winrt-go"
|
||||||
|
"github.com/saltosystems/winrt-go/windows/devices/bluetooth/advertisement"
|
||||||
|
"github.com/saltosystems/winrt-go/windows/foundation"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Adapter struct {
|
type Adapter struct {
|
||||||
watcher *winbt.IBluetoothLEAdvertisementWatcher
|
watcher *advertisement.BluetoothLEAdvertisementWatcher
|
||||||
|
|
||||||
connectHandler func(device Addresser, 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 Addresser, connected bool) {
|
connectHandler: func(device Device, connected bool) {
|
||||||
return
|
return
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -25,3 +32,35 @@ var DefaultAdapter = &Adapter{
|
||||||
func (a *Adapter) Enable() error {
|
func (a *Adapter) Enable() error {
|
||||||
return ole.RoInitialize(1) // initialize with multithreading enabled
|
return ole.RoInitialize(1) // initialize with multithreading enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func awaitAsyncOperation(asyncOperation *foundation.IAsyncOperation, genericParamSignature string) error {
|
||||||
|
var status foundation.AsyncStatus
|
||||||
|
|
||||||
|
// We need to obtain the GUID of the AsyncOperationCompletedHandler, but its a generic delegate
|
||||||
|
// so we also need the generic parameter type's signature:
|
||||||
|
// AsyncOperationCompletedHandler<genericParamSignature>
|
||||||
|
iid := winrt.ParameterizedInstanceGUID(foundation.GUIDAsyncOperationCompletedHandler, genericParamSignature)
|
||||||
|
|
||||||
|
// Wait until the async operation completes.
|
||||||
|
waitChan := make(chan struct{})
|
||||||
|
handler := foundation.NewAsyncOperationCompletedHandler(ole.NewGUID(iid), func(instance *foundation.AsyncOperationCompletedHandler, asyncInfo *foundation.IAsyncOperation, asyncStatus foundation.AsyncStatus) {
|
||||||
|
status = asyncStatus
|
||||||
|
close(waitChan)
|
||||||
|
})
|
||||||
|
defer handler.Release()
|
||||||
|
|
||||||
|
asyncOperation.SetCompleted(handler)
|
||||||
|
|
||||||
|
// Wait until async operation has stopped, and finish.
|
||||||
|
<-waitChan
|
||||||
|
|
||||||
|
if status != foundation.AsyncStatusCompleted {
|
||||||
|
return fmt.Errorf("async operation failed with status %d", status)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Address() (MACAddress, error) {
|
||||||
|
// TODO: get mac address
|
||||||
|
return MACAddress{}, errors.New("not implemented")
|
||||||
|
}
|
||||||
|
|
1203
att_hci.go
Normal file
1203
att_hci.go
Normal file
File diff suppressed because it is too large
Load diff
1
bluetooth-numbers-database
Submodule
1
bluetooth-numbers-database
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 3d0f452460237f76d7e11d8cd0de8c1cba46b62a
|
|
@ -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"
|
|
||||||
|
|
1849
characteristic_uuids.go
Normal file
1849
characteristic_uuids.go
Normal file
File diff suppressed because it is too large
Load diff
5
debug.go
Normal file
5
debug.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
//go:build bledebug
|
||||||
|
|
||||||
|
package bluetooth
|
||||||
|
|
||||||
|
var debug = true
|
63
error_sd.go
63
error_sd.go
|
@ -1,74 +1,79 @@
|
||||||
// +build softdevice
|
//go:build softdevice
|
||||||
|
|
||||||
package bluetooth
|
package bluetooth
|
||||||
|
|
||||||
|
// #include <stdint.h>
|
||||||
|
// #include "nrf_error.h"
|
||||||
|
// #include "nrf_error_sdm.h"
|
||||||
|
import "C"
|
||||||
|
|
||||||
// Error is an error from within the SoftDevice.
|
// Error is an error from within the SoftDevice.
|
||||||
type Error uint32
|
type Error uint32
|
||||||
|
|
||||||
func (e Error) Error() string {
|
func (e Error) Error() string {
|
||||||
switch {
|
switch {
|
||||||
case e < 0x1000:
|
case e >= C.NRF_ERROR_BASE_NUM && e < C.NRF_ERROR_SDM_BASE_NUM:
|
||||||
// Global errors.
|
// Global errors.
|
||||||
switch e {
|
switch e {
|
||||||
case 0:
|
case C.NRF_SUCCESS:
|
||||||
return "no error"
|
return "no error"
|
||||||
case 1:
|
case C.NRF_ERROR_SVC_HANDLER_MISSING:
|
||||||
return "SVC handler is missing"
|
return "SVC handler is missing"
|
||||||
case 2:
|
case C.NRF_ERROR_SOFTDEVICE_NOT_ENABLED:
|
||||||
return "SoftDevice has not been enabled"
|
return "SoftDevice has not been enabled"
|
||||||
case 3:
|
case C.NRF_ERROR_INTERNAL:
|
||||||
return "internal error"
|
return "internal error"
|
||||||
case 4:
|
case C.NRF_ERROR_NO_MEM:
|
||||||
return "no memory for operation"
|
return "no memory for operation"
|
||||||
case 5:
|
case C.NRF_ERROR_NOT_FOUND:
|
||||||
return "not found"
|
return "not found"
|
||||||
case 6:
|
case C.NRF_ERROR_NOT_SUPPORTED:
|
||||||
return "not supported"
|
return "not supported"
|
||||||
case 7:
|
case C.NRF_ERROR_INVALID_PARAM:
|
||||||
return "invalid parameter"
|
return "invalid parameter"
|
||||||
case 8:
|
case C.NRF_ERROR_INVALID_STATE:
|
||||||
return "invalid state, operation disallowed in this state"
|
return "invalid state, operation disallowed in this state"
|
||||||
case 9:
|
case C.NRF_ERROR_INVALID_LENGTH:
|
||||||
return "invalid Length"
|
return "invalid Length"
|
||||||
case 10:
|
case C.NRF_ERROR_INVALID_FLAGS:
|
||||||
return "invalid flags"
|
return "invalid flags"
|
||||||
case 11:
|
case C.NRF_ERROR_INVALID_DATA:
|
||||||
return "invalid data"
|
return "invalid data"
|
||||||
case 12:
|
case C.NRF_ERROR_DATA_SIZE:
|
||||||
return "invalid data size"
|
return "invalid data size"
|
||||||
case 13:
|
case C.NRF_ERROR_TIMEOUT:
|
||||||
return "operation timed out"
|
return "operation timed out"
|
||||||
case 14:
|
case C.NRF_ERROR_NULL:
|
||||||
return "null pointer"
|
return "null pointer"
|
||||||
case 15:
|
case C.NRF_ERROR_FORBIDDEN:
|
||||||
return "forbidden operation"
|
return "forbidden operation"
|
||||||
case 16:
|
case C.NRF_ERROR_INVALID_ADDR:
|
||||||
return "bad memory address"
|
return "bad memory address"
|
||||||
case 17:
|
case C.NRF_ERROR_BUSY:
|
||||||
return "busy"
|
return "busy"
|
||||||
case 18:
|
case 18: // C.NRF_ERROR_CONN_COUNT, not available on nrf51
|
||||||
return "maximum connection count exceeded"
|
return "maximum connection count exceeded"
|
||||||
case 19:
|
case 19: // C.NRF_ERROR_RESOURCES, not available on nrf51
|
||||||
return "not enough resources for operation"
|
return "not enough resources for operation"
|
||||||
default:
|
default:
|
||||||
return "other global error"
|
return "other global error"
|
||||||
}
|
}
|
||||||
case e < 0x2000:
|
case e >= C.NRF_ERROR_SDM_BASE_NUM && e < C.NRF_ERROR_SOC_BASE_NUM:
|
||||||
// SDM errors.
|
// SDM errors.
|
||||||
switch e {
|
switch e {
|
||||||
case 0x1000:
|
case C.NRF_ERROR_SDM_LFCLK_SOURCE_UNKNOWN:
|
||||||
return "unknown LFCLK source"
|
return "unknown LFCLK source"
|
||||||
case 0x1001:
|
case C.NRF_ERROR_SDM_INCORRECT_INTERRUPT_CONFIGURATION:
|
||||||
return "incorrect interrupt configuration"
|
return "incorrect interrupt configuration"
|
||||||
case 0x1002:
|
case C.NRF_ERROR_SDM_INCORRECT_CLENR0:
|
||||||
return "incorrect CLENR0"
|
return "incorrect CLENR0"
|
||||||
default:
|
default:
|
||||||
return "other SDM error"
|
return "other SDM error"
|
||||||
}
|
}
|
||||||
case e < 0x3000:
|
case e >= C.NRF_ERROR_SOC_BASE_NUM && e < C.NRF_ERROR_STK_BASE_NUM:
|
||||||
// SoC errors.
|
// SoC errors.
|
||||||
return "other SoC error"
|
return "other SoC error"
|
||||||
case e < 0x4000:
|
case e >= C.NRF_ERROR_STK_BASE_NUM && e < 0x4000:
|
||||||
// STK errors.
|
// STK errors.
|
||||||
return "other STK error"
|
return "other STK error"
|
||||||
default:
|
default:
|
||||||
|
@ -79,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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,13 +13,17 @@ 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())
|
||||||
|
|
||||||
println("advertising...")
|
println("advertising...")
|
||||||
|
address, _ := adapter.Address()
|
||||||
for {
|
for {
|
||||||
// Sleep forever.
|
println("Go Bluetooth /", address.MAC.String())
|
||||||
time.Sleep(time.Hour)
|
time.Sleep(time.Second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.Addresser, 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
|
||||||
|
|
84
examples/connparams/main.go
Normal file
84
examples/connparams/main.go
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -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())
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// +build !baremetal
|
//go:build !baremetal
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|
|
@ -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,18 +21,17 @@
|
||||||
//
|
//
|
||||||
// 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 (
|
||||||
adapter = bluetooth.DefaultAdapter
|
adapter = bluetooth.DefaultAdapter
|
||||||
|
|
||||||
heartRateServiceUUID = bluetooth.New16BitUUID(0x180D)
|
heartRateServiceUUID = bluetooth.ServiceUUIDHeartRate
|
||||||
heartRateCharacteristicUUID = bluetooth.New16BitUUID(0x2A37)
|
heartRateCharacteristicUUID = bluetooth.CharacteristicUUIDHeartRateMeasurement
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -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{})
|
||||||
|
@ -86,7 +90,7 @@ func main() {
|
||||||
println("found characteristic", char.UUID().String())
|
println("found characteristic", char.UUID().String())
|
||||||
|
|
||||||
char.EnableNotifications(func(buf []byte) {
|
char.EnableNotifications(func(buf []byte) {
|
||||||
println("data:", uint8(buf[0]))
|
println("data:", uint8(buf[1]))
|
||||||
})
|
})
|
||||||
|
|
||||||
select {}
|
select {}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// +build !baremetal
|
//go:build !baremetal
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
@ -18,29 +18,19 @@ func main() {
|
||||||
adv := adapter.DefaultAdvertisement()
|
adv := adapter.DefaultAdvertisement()
|
||||||
must("config adv", adv.Configure(bluetooth.AdvertisementOptions{
|
must("config adv", adv.Configure(bluetooth.AdvertisementOptions{
|
||||||
LocalName: "Go HRS",
|
LocalName: "Go HRS",
|
||||||
ServiceUUIDs: []bluetooth.UUID{bluetooth.New16BitUUID(0x2A37)},
|
ServiceUUIDs: []bluetooth.UUID{bluetooth.ServiceUUIDHeartRate},
|
||||||
}))
|
}))
|
||||||
must("start adv", adv.Start())
|
must("start adv", adv.Start())
|
||||||
|
|
||||||
var heartRateMeasurement bluetooth.Characteristic
|
var heartRateMeasurement bluetooth.Characteristic
|
||||||
must("add service", adapter.AddService(&bluetooth.Service{
|
must("add service", adapter.AddService(&bluetooth.Service{
|
||||||
UUID: bluetooth.New16BitUUID(0x180D), // Heart Rate
|
UUID: bluetooth.ServiceUUIDHeartRate,
|
||||||
Characteristics: []bluetooth.CharacteristicConfig{
|
Characteristics: []bluetooth.CharacteristicConfig{
|
||||||
{
|
{
|
||||||
Handle: &heartRateMeasurement,
|
Handle: &heartRateMeasurement,
|
||||||
UUID: bluetooth.New16BitUUID(0x2A37), // Heart Rate Measurement
|
UUID: bluetooth.CharacteristicUUIDHeartRateMeasurement,
|
||||||
Value: []byte{0, heartRate},
|
Value: []byte{0, heartRate},
|
||||||
Flags: bluetooth.CharacteristicReadPermission | bluetooth.CharacteristicWritePermission |
|
Flags: bluetooth.CharacteristicNotifyPermission,
|
||||||
bluetooth.CharacteristicNotifyPermission,
|
|
||||||
WriteEvent: func(client bluetooth.Connection, offset int, value []byte) {
|
|
||||||
if offset != 0 || len(value) < 2 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if value[1] != 0 { // avoid divide by zero
|
|
||||||
heartRate = value[1]
|
|
||||||
println("heart rate is now:", heartRate)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
@ -55,7 +45,7 @@ func main() {
|
||||||
heartRate = randomInt(65, 85)
|
heartRate = randomInt(65, 85)
|
||||||
|
|
||||||
// and push the next notification
|
// and push the next notification
|
||||||
heartRateMeasurement.Write([]byte{byte(heartRate)})
|
heartRateMeasurement.Write([]byte{0, heartRate})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -4,14 +4,14 @@ 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 (
|
||||||
serviceUUID = bluetooth.NewUUID([16]byte{0x6E, 0x40, 0x00, 0x01, 0xB5, 0xA3, 0xF3, 0x93, 0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E})
|
serviceUUID = bluetooth.ServiceUUIDNordicUART
|
||||||
rxUUID = bluetooth.NewUUID([16]byte{0x6E, 0x40, 0x00, 0x02, 0xB5, 0xA3, 0xF3, 0x93, 0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E})
|
rxUUID = bluetooth.CharacteristicUUIDUARTRX
|
||||||
txUUID = bluetooth.NewUUID([16]byte{0x6E, 0x40, 0x00, 0x03, 0xB5, 0xA3, 0xF3, 0x93, 0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E})
|
txUUID = bluetooth.CharacteristicUUIDUARTTX
|
||||||
)
|
)
|
||||||
|
|
||||||
var adapter = bluetooth.DefaultAdapter
|
var adapter = bluetooth.DefaultAdapter
|
||||||
|
|
|
@ -8,14 +8,14 @@ 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 (
|
||||||
serviceUUID = bluetooth.NewUUID([16]byte{0x6E, 0x40, 0x00, 0x01, 0xB5, 0xA3, 0xF3, 0x93, 0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E})
|
serviceUUID = bluetooth.ServiceUUIDNordicUART
|
||||||
rxUUID = bluetooth.NewUUID([16]byte{0x6E, 0x40, 0x00, 0x02, 0xB5, 0xA3, 0xF3, 0x93, 0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E})
|
rxUUID = bluetooth.CharacteristicUUIDUARTRX
|
||||||
txUUID = bluetooth.NewUUID([16]byte{0x6E, 0x40, 0x00, 0x03, 0xB5, 0xA3, 0xF3, 0x93, 0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E})
|
txUUID = bluetooth.CharacteristicUUIDUARTTX
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
|
@ -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
|
||||||
|
|
53
examples/stop-advertisement/main.go
Normal file
53
examples/stop-advertisement/main.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
// This example advertises for 5 minutes after
|
||||||
|
// - boot
|
||||||
|
// - disconnect
|
||||||
|
// and then stops advertising.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitrepo.ru/neonxp/bluetooth"
|
||||||
|
)
|
||||||
|
|
||||||
|
var adapter = bluetooth.DefaultAdapter
|
||||||
|
|
||||||
|
var advUntil = time.Now().Add(5 * time.Minute)
|
||||||
|
var advState = true
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
must("enable BLE stack", adapter.Enable())
|
||||||
|
adv := adapter.DefaultAdvertisement()
|
||||||
|
must("config adv", adv.Configure(bluetooth.AdvertisementOptions{
|
||||||
|
LocalName: "Go Bluetooth",
|
||||||
|
}))
|
||||||
|
adapter.SetConnectHandler(func(device bluetooth.Device, connected bool) {
|
||||||
|
if connected {
|
||||||
|
println("connected, not advertising...")
|
||||||
|
advState = false
|
||||||
|
} else {
|
||||||
|
println("disconnected, advertising...")
|
||||||
|
advState = true
|
||||||
|
advUntil = time.Now().Add(5 * time.Minute)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
must("start adv", adv.Start())
|
||||||
|
|
||||||
|
println("advertising...")
|
||||||
|
address, _ := adapter.Address()
|
||||||
|
for {
|
||||||
|
if advState && time.Now().After(advUntil) {
|
||||||
|
println("timeout, not advertising...")
|
||||||
|
advState = false
|
||||||
|
must("stop adv", adv.Stop())
|
||||||
|
}
|
||||||
|
println("Go Bluetooth /", address.MAC.String())
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func must(action string, err error) {
|
||||||
|
if err != nil {
|
||||||
|
panic("failed to " + action + ": " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
48
examples/tinyscan/clue.go
Normal file
48
examples/tinyscan/clue.go
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
//go:build clue
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"machine"
|
||||||
|
|
||||||
|
"tinygo.org/x/drivers/st7789"
|
||||||
|
"tinygo.org/x/tinyfont/proggy"
|
||||||
|
"tinygo.org/x/tinyterm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
font = &proggy.TinySZ8pt7b
|
||||||
|
)
|
||||||
|
|
||||||
|
func initTerminal() {
|
||||||
|
machine.SPI1.Configure(machine.SPIConfig{
|
||||||
|
Frequency: 8000000,
|
||||||
|
SCK: machine.TFT_SCK,
|
||||||
|
SDO: machine.TFT_SDO,
|
||||||
|
SDI: machine.TFT_SDO,
|
||||||
|
Mode: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
display := st7789.New(machine.SPI1,
|
||||||
|
machine.TFT_RESET,
|
||||||
|
machine.TFT_DC,
|
||||||
|
machine.TFT_CS,
|
||||||
|
machine.TFT_LITE)
|
||||||
|
|
||||||
|
display.Configure(st7789.Config{
|
||||||
|
Rotation: st7789.ROTATION_90,
|
||||||
|
//Height: 320,
|
||||||
|
FrameRate: st7789.FRAMERATE_111,
|
||||||
|
VSyncLines: st7789.MAX_VSYNC_SCANLINES,
|
||||||
|
})
|
||||||
|
|
||||||
|
display.FillScreen(black)
|
||||||
|
|
||||||
|
terminal = tinyterm.NewTerminal(&display)
|
||||||
|
terminal.Configure(&tinyterm.Config{
|
||||||
|
Font: font,
|
||||||
|
FontHeight: 10,
|
||||||
|
FontOffset: 6,
|
||||||
|
UseSoftwareScroll: true,
|
||||||
|
})
|
||||||
|
}
|
55
examples/tinyscan/main.go
Normal file
55
examples/tinyscan/main.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image/color"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitrepo.ru/neonxp/bluetooth"
|
||||||
|
"tinygo.org/x/tinyterm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
terminal *tinyterm.Terminal
|
||||||
|
|
||||||
|
black = color.RGBA{0, 0, 0, 255}
|
||||||
|
adapter = bluetooth.DefaultAdapter
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
initTerminal()
|
||||||
|
|
||||||
|
terminalOutput("enable interface...")
|
||||||
|
|
||||||
|
must("enable BLE interface", adapter.Enable())
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
terminalOutput("start scan...")
|
||||||
|
|
||||||
|
must("start scan", adapter.Scan(scanHandler))
|
||||||
|
|
||||||
|
for {
|
||||||
|
time.Sleep(time.Minute)
|
||||||
|
terminalOutput("scanning...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanHandler(adapter *bluetooth.Adapter, device bluetooth.ScanResult) {
|
||||||
|
msg := fmt.Sprintf("%s %d %s", device.Address.String(), device.RSSI, device.LocalName())
|
||||||
|
terminalOutput(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func must(action string, err error) {
|
||||||
|
if err != nil {
|
||||||
|
for {
|
||||||
|
terminalOutput("failed to " + action + ": " + err.Error())
|
||||||
|
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func terminalOutput(s string) {
|
||||||
|
println(s)
|
||||||
|
fmt.Fprintf(terminal, "\n%s", s)
|
||||||
|
}
|
39
examples/tinyscan/pybadge.go
Normal file
39
examples/tinyscan/pybadge.go
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
//go:build pybadge
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"machine"
|
||||||
|
|
||||||
|
"tinygo.org/x/drivers/st7735"
|
||||||
|
"tinygo.org/x/tinyfont"
|
||||||
|
"tinygo.org/x/tinyterm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
font = &tinyfont.Picopixel
|
||||||
|
)
|
||||||
|
|
||||||
|
func initTerminal() {
|
||||||
|
machine.SPI1.Configure(machine.SPIConfig{
|
||||||
|
SCK: machine.SPI1_SCK_PIN,
|
||||||
|
SDO: machine.SPI1_SDO_PIN,
|
||||||
|
SDI: machine.SPI1_SDI_PIN,
|
||||||
|
Frequency: 8000000,
|
||||||
|
})
|
||||||
|
|
||||||
|
display := st7735.New(machine.SPI1, machine.TFT_RST, machine.TFT_DC, machine.TFT_CS, machine.TFT_LITE)
|
||||||
|
display.Configure(st7735.Config{
|
||||||
|
Rotation: st7735.ROTATION_90,
|
||||||
|
})
|
||||||
|
|
||||||
|
display.FillScreen(black)
|
||||||
|
|
||||||
|
terminal = tinyterm.NewTerminal(&display)
|
||||||
|
terminal.Configure(&tinyterm.Config{
|
||||||
|
Font: font,
|
||||||
|
FontHeight: 8,
|
||||||
|
FontOffset: 4,
|
||||||
|
UseSoftwareScroll: true,
|
||||||
|
})
|
||||||
|
}
|
45
examples/tinyscan/pyportal.go
Normal file
45
examples/tinyscan/pyportal.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
//go:build pyportal
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"machine"
|
||||||
|
|
||||||
|
"tinygo.org/x/drivers/ili9341"
|
||||||
|
"tinygo.org/x/tinyfont/proggy"
|
||||||
|
"tinygo.org/x/tinyterm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
font = &proggy.TinySZ8pt7b
|
||||||
|
)
|
||||||
|
|
||||||
|
func initTerminal() {
|
||||||
|
display := ili9341.NewParallel(
|
||||||
|
machine.LCD_DATA0,
|
||||||
|
machine.TFT_WR,
|
||||||
|
machine.TFT_DC,
|
||||||
|
machine.TFT_CS,
|
||||||
|
machine.TFT_RESET,
|
||||||
|
machine.TFT_RD,
|
||||||
|
)
|
||||||
|
|
||||||
|
// configure backlight
|
||||||
|
backlight := machine.TFT_BACKLIGHT
|
||||||
|
backlight.Configure(machine.PinConfig{machine.PinOutput})
|
||||||
|
|
||||||
|
// configure display
|
||||||
|
display.Configure(ili9341.Config{})
|
||||||
|
display.SetRotation(ili9341.Rotation270)
|
||||||
|
display.FillScreen(black)
|
||||||
|
|
||||||
|
backlight.High()
|
||||||
|
|
||||||
|
terminal = tinyterm.NewTerminal(display)
|
||||||
|
terminal.Configure(&tinyterm.Config{
|
||||||
|
Font: font,
|
||||||
|
FontHeight: 10,
|
||||||
|
FontOffset: 6,
|
||||||
|
UseSoftwareScroll: true,
|
||||||
|
})
|
||||||
|
}
|
229
gap.go
229
gap.go
|
@ -25,12 +25,12 @@ func (mac MACAddress) IsRandom() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetRandom if is a random address.
|
// SetRandom if is a random address.
|
||||||
func (mac MACAddress) SetRandom(val bool) {
|
func (mac *MACAddress) SetRandom(val bool) {
|
||||||
mac.isRandom = val
|
mac.isRandom = val
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the address
|
// Set the address
|
||||||
func (mac MACAddress) Set(val string) {
|
func (mac *MACAddress) Set(val string) {
|
||||||
m, err := ParseMAC(val)
|
m, err := ParseMAC(val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@ -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
|
||||||
|
@ -69,34 +99,13 @@ func NewDuration(interval time.Duration) Duration {
|
||||||
// Connection is a numeric identifier that indicates a connection handle.
|
// Connection is a numeric identifier that indicates a connection handle.
|
||||||
type Connection uint16
|
type Connection uint16
|
||||||
|
|
||||||
// Addresser contains a Bluetooth address, which is a MAC address plus some extra
|
|
||||||
// information.
|
|
||||||
type Addresser interface {
|
|
||||||
// String of the address
|
|
||||||
String() string
|
|
||||||
|
|
||||||
// Set the address
|
|
||||||
Set(val string)
|
|
||||||
|
|
||||||
// Is this address a random address?
|
|
||||||
// Bluetooth addresses are roughly split in two kinds: public
|
|
||||||
// (IEEE-assigned) addresses and random (not IEEE assigned) addresses.
|
|
||||||
// "Random" here doesn't mean it is exactly random but at least it looks
|
|
||||||
// random. Sometimes, it contains a hash.
|
|
||||||
// For more information:
|
|
||||||
// https://www.novelbits.io/bluetooth-address-privacy-ble/
|
|
||||||
// Set the address
|
|
||||||
SetRandom(bool)
|
|
||||||
IsRandom() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// ScanResult contains information from when an advertisement packet was
|
// ScanResult contains information from when an advertisement packet was
|
||||||
// received. It is passed as a parameter to the callback of the Scan method.
|
// received. It is passed as a parameter to the callback of the Scan method.
|
||||||
type ScanResult struct {
|
type ScanResult struct {
|
||||||
// Bluetooth address of the scanned device.
|
// Bluetooth address of the scanned device.
|
||||||
Address Addresser
|
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
|
||||||
|
@ -126,6 +135,14 @@ type AdvertisementPayload interface {
|
||||||
// Bytes returns the raw advertisement packet, if available. It returns nil
|
// Bytes returns the raw advertisement packet, if available. It returns nil
|
||||||
// if this data is not available.
|
// if this data is not available.
|
||||||
Bytes() []byte
|
Bytes() []byte
|
||||||
|
|
||||||
|
// ManufacturerData returns a slice with all the manufacturer data present in the
|
||||||
|
// advertising. It may be empty.
|
||||||
|
ManufacturerData() []ManufacturerDataElement
|
||||||
|
|
||||||
|
// ServiceData returns a slice with all the service data present in the
|
||||||
|
// advertising. It may be empty.
|
||||||
|
ServiceData() []ServiceDataElement
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdvertisementFields contains advertisement fields in structured form.
|
// AdvertisementFields contains advertisement fields in structured form.
|
||||||
|
@ -138,6 +155,12 @@ type AdvertisementFields struct {
|
||||||
// part of the advertisement packet, in data types such as "complete list of
|
// part of the advertisement packet, in data types such as "complete list of
|
||||||
// 128-bit UUIDs".
|
// 128-bit UUIDs".
|
||||||
ServiceUUIDs []UUID
|
ServiceUUIDs []UUID
|
||||||
|
|
||||||
|
// ManufacturerData is the manufacturer data of the advertisement.
|
||||||
|
ManufacturerData []ManufacturerDataElement
|
||||||
|
|
||||||
|
// ServiceData is the service data of the advertisement.
|
||||||
|
ServiceData []ServiceDataElement
|
||||||
}
|
}
|
||||||
|
|
||||||
// advertisementFields wraps AdvertisementFields to implement the
|
// advertisementFields wraps AdvertisementFields to implement the
|
||||||
|
@ -170,6 +193,16 @@ func (p *advertisementFields) Bytes() []byte {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ManufacturerData returns the underlying ManufacturerData field.
|
||||||
|
func (p *advertisementFields) ManufacturerData() []ManufacturerDataElement {
|
||||||
|
return p.AdvertisementFields.ManufacturerData
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceData returns the underlying ServiceData field.
|
||||||
|
func (p *advertisementFields) ServiceData() []ServiceDataElement {
|
||||||
|
return p.AdvertisementFields.ServiceData
|
||||||
|
}
|
||||||
|
|
||||||
// rawAdvertisementPayload encapsulates a raw advertisement packet. Methods to
|
// 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
|
||||||
|
@ -258,6 +291,61 @@ func (buf *rawAdvertisementPayload) HasServiceUUID(uuid UUID) bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ManufacturerData returns the manufacturer data in the advertisement payload.
|
||||||
|
func (buf *rawAdvertisementPayload) ManufacturerData() []ManufacturerDataElement {
|
||||||
|
var manufacturerData []ManufacturerDataElement
|
||||||
|
for index := 0; index < int(buf.len)+4; index += int(buf.data[index]) + 1 {
|
||||||
|
fieldLength := int(buf.data[index+0])
|
||||||
|
if fieldLength < 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fieldType := buf.data[index+1]
|
||||||
|
if fieldType != 0xff {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := uint16(buf.data[index+2]) | uint16(buf.data[index+3])<<8
|
||||||
|
manufacturerData = append(manufacturerData, ManufacturerDataElement{
|
||||||
|
CompanyID: key,
|
||||||
|
Data: buf.data[index+4 : index+fieldLength+1],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return manufacturerData
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceData returns the service data in the advertisment payload
|
||||||
|
func (buf *rawAdvertisementPayload) ServiceData() []ServiceDataElement {
|
||||||
|
var serviceData []ServiceDataElement
|
||||||
|
for index := 0; index < int(buf.len)+4; index += int(buf.data[index]) + 1 {
|
||||||
|
fieldLength := int(buf.data[index+0])
|
||||||
|
if fieldLength < 3 { // field has only length and type and no data
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fieldType := buf.data[index+1]
|
||||||
|
switch fieldType {
|
||||||
|
case 0x16: // 16-bit uuid
|
||||||
|
serviceData = append(serviceData, ServiceDataElement{
|
||||||
|
UUID: New16BitUUID(uint16(buf.data[index+2]) + (uint16(buf.data[index+3]) << 8)),
|
||||||
|
Data: buf.data[index+4 : index+fieldLength+1],
|
||||||
|
})
|
||||||
|
case 0x20: // 32-bit uuid
|
||||||
|
serviceData = append(serviceData, ServiceDataElement{
|
||||||
|
UUID: New32BitUUID(uint32(buf.data[index+2]) + (uint32(buf.data[index+3]) << 8) + (uint32(buf.data[index+4]) << 16) + (uint32(buf.data[index+5]) << 24)),
|
||||||
|
Data: buf.data[index+6 : index+fieldLength+1],
|
||||||
|
})
|
||||||
|
case 0x21: // 128-bit uuid
|
||||||
|
var uuidArray [16]byte
|
||||||
|
copy(uuidArray[:], buf.data[index+2:index+18])
|
||||||
|
serviceData = append(serviceData, ServiceDataElement{
|
||||||
|
UUID: NewUUID(uuidArray),
|
||||||
|
Data: buf.data[index+18 : index+fieldLength+1],
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return serviceData
|
||||||
|
}
|
||||||
|
|
||||||
// reset restores this buffer to the original state.
|
// reset restores this buffer to the original state.
|
||||||
func (buf *rawAdvertisementPayload) reset() {
|
func (buf *rawAdvertisementPayload) reset() {
|
||||||
// The data is not reset (only the length), because with a zero length the
|
// The data is not reset (only the length), because with a zero length the
|
||||||
|
@ -285,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -346,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.
|
||||||
|
@ -358,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
|
||||||
}
|
}
|
||||||
|
|
117
gap_darwin.go
117
gap_darwin.go
|
@ -5,9 +5,12 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/JuulLabs-OSS/cbgo"
|
"github.com/tinygo-org/cbgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// default connection timeout
|
||||||
|
const defaultConnectionTimeout time.Duration = 10 * time.Second
|
||||||
|
|
||||||
// Address contains a Bluetooth address which on macOS is a UUID.
|
// Address contains a Bluetooth address which on macOS is a UUID.
|
||||||
type Address struct {
|
type Address struct {
|
||||||
// UUID since this is macOS.
|
// UUID since this is macOS.
|
||||||
|
@ -20,11 +23,11 @@ func (ad Address) IsRandom() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetRandom ignored on macOS.
|
// SetRandom ignored on macOS.
|
||||||
func (ad Address) SetRandom(val bool) {
|
func (ad *Address) SetRandom(val bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the address
|
// Set the address
|
||||||
func (ad Address) Set(val string) {
|
func (ad *Address) Set(val string) {
|
||||||
uuid, err := ParseUUID(val)
|
uuid, err := ParseUUID(val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@ -82,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
|
||||||
|
@ -90,57 +99,101 @@ type Device struct {
|
||||||
servicesChan chan error
|
servicesChan chan error
|
||||||
charsChan chan error
|
charsChan chan error
|
||||||
|
|
||||||
services map[UUID]*DeviceService
|
services map[UUID]DeviceService
|
||||||
characteristics map[UUID]*DeviceCharacteristic
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect starts a connection attempt to the given peripheral device address.
|
// Connect starts a connection attempt to the given peripheral device address.
|
||||||
func (a *Adapter) Connect(address Addresser, params ConnectionParams) (*Device, error) {
|
func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, error) {
|
||||||
adr := address.(Address)
|
uuid, err := cbgo.ParseUUID(address.UUID.String())
|
||||||
uuid, err := cbgo.ParseUUID(adr.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", adr.UUID.String())
|
return Device{}, fmt.Errorf("Connect failed: no peer with address: %s", address.UUID.String())
|
||||||
}
|
}
|
||||||
a.cm.Connect(prphs[0], nil)
|
|
||||||
|
|
||||||
|
timeout := defaultConnectionTimeout
|
||||||
|
if params.ConnectionTimeout != 0 {
|
||||||
|
timeout = time.Duration(int64(params.ConnectionTimeout)*625) * time.Microsecond
|
||||||
|
}
|
||||||
|
|
||||||
|
id := prphs[0].Identifier().String()
|
||||||
|
prphCh := make(chan cbgo.Peripheral)
|
||||||
|
|
||||||
|
a.connectMap.Store(id, prphCh)
|
||||||
|
defer a.connectMap.Delete(id)
|
||||||
|
|
||||||
|
a.cm.Connect(prphs[0], nil)
|
||||||
|
timeoutTimer := time.NewTimer(timeout)
|
||||||
|
var connectionError error
|
||||||
|
|
||||||
|
for {
|
||||||
// wait on channel for connect
|
// wait on channel for connect
|
||||||
select {
|
select {
|
||||||
case p := <-a.connectChan:
|
case p := <-prphCh:
|
||||||
d := &Device{
|
|
||||||
|
// check if we have received a disconnected peripheral
|
||||||
|
if p.State() == cbgo.PeripheralStateDisconnected {
|
||||||
|
return Device{}, connectionError
|
||||||
|
}
|
||||||
|
|
||||||
|
d := Device{
|
||||||
|
Address: address,
|
||||||
|
deviceInternal: &deviceInternal{
|
||||||
cm: a.cm,
|
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(nil, true)
|
a.connectHandler(d, true)
|
||||||
|
|
||||||
return d, nil
|
return d, nil
|
||||||
case <-time.NewTimer(10 * time.Second).C:
|
|
||||||
return nil, errors.New("timeout on Connect")
|
case <-timeoutTimer.C:
|
||||||
|
// we need to cancel the connection if we have timed out ourselves
|
||||||
|
a.cm.CancelConnect(prphs[0])
|
||||||
|
|
||||||
|
// record an error to use when the disconnect comes through later.
|
||||||
|
connectionError = errors.New("timeout on Connect")
|
||||||
|
|
||||||
|
// we are not ready to return yet, we need to wait for the disconnect event to come through
|
||||||
|
// so continue on from this case and wait for something to show up on prphCh
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -160,7 +213,12 @@ func (pd *peripheralDelegate) DidDiscoverCharacteristics(prph cbgo.Peripheral, s
|
||||||
// or receives a value for a read request.
|
// or receives a value for a read request.
|
||||||
func (pd *peripheralDelegate) DidUpdateValueForCharacteristic(prph cbgo.Peripheral, chr cbgo.Characteristic, err error) {
|
func (pd *peripheralDelegate) DidUpdateValueForCharacteristic(prph cbgo.Peripheral, chr cbgo.Characteristic, err error) {
|
||||||
uuid, _ := ParseUUID(chr.UUID().String())
|
uuid, _ := ParseUUID(chr.UUID().String())
|
||||||
if char, ok := pd.d.characteristics[uuid]; ok {
|
svcuuid, _ := ParseUUID(chr.Service().UUID().String())
|
||||||
|
|
||||||
|
if svc, ok := pd.d.services[svcuuid]; ok {
|
||||||
|
for _, char := range svc.characteristics {
|
||||||
|
|
||||||
|
if char.characteristic == chr && uuid == char.UUID() { // compare pointers
|
||||||
if err == nil && char.callback != nil {
|
if err == nil && char.callback != nil {
|
||||||
go char.callback(chr.Value())
|
go char.callback(chr.Value())
|
||||||
}
|
}
|
||||||
|
@ -169,4 +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
422
gap_hci.go
Normal 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)
|
||||||
|
}
|
308
gap_linux.go
308
gap_linux.go
|
@ -1,16 +1,23 @@
|
||||||
// +build !baremetal
|
//go:build !baremetal
|
||||||
|
|
||||||
package bluetooth
|
package bluetooth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"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/profile/advertising"
|
|
||||||
"github.com/muka/go-bluetooth/bluez/profile/device"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var errAdvertisementNotStarted = errors.New("bluetooth: stop advertisement that was not started")
|
||||||
|
var errAdvertisementAlreadyStarted = errors.New("bluetooth: start advertisement that was already started")
|
||||||
|
|
||||||
|
// Unique ID per advertisement (to generate a unique object path).
|
||||||
|
var advertisementID uint64
|
||||||
|
|
||||||
// Address contains a Bluetooth MAC address.
|
// Address contains a Bluetooth MAC address.
|
||||||
type Address struct {
|
type Address struct {
|
||||||
MACAddress
|
MACAddress
|
||||||
|
@ -19,8 +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
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultAdvertisement returns the default advertisement instance but does not
|
// DefaultAdvertisement returns the default advertisement instance but does not
|
||||||
|
@ -38,30 +45,82 @@ 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
|
||||||
}
|
|
||||||
_, 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)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop advertisement. May only be called after it has been started.
|
||||||
|
func (a *Advertisement) Stop() error {
|
||||||
|
err := a.adapter.adapter.Call("org.bluez.LEAdvertisingManager1.UnregisterAdvertisement", 0, a.path).Err
|
||||||
|
if err != nil {
|
||||||
|
if err, ok := err.(dbus.Error); ok && err.Name == "org.bluez.Error.DoesNotExist" {
|
||||||
|
return errAdvertisementNotStarted
|
||||||
|
}
|
||||||
|
return fmt.Errorf("bluetooth: could not stop advertisement: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -75,7 +134,7 @@ func (a *Advertisement) Start() 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,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
|
||||||
}
|
}
|
||||||
|
@ -146,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:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,28 +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)
|
|
||||||
}
|
}
|
||||||
|
for k, v := range changes {
|
||||||
|
device[k] = v
|
||||||
}
|
}
|
||||||
callback(a, makeScanResult(props))
|
callback(a, makeScanResult(device))
|
||||||
}
|
}
|
||||||
case <-cancelChan:
|
case <-cancelChan:
|
||||||
continue
|
continue
|
||||||
|
@ -198,37 +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")
|
||||||
|
|
||||||
|
var manufacturerData []ManufacturerDataElement
|
||||||
|
if mdata, ok := props["ManufacturerData"].Value().(map[uint16]dbus.Variant); ok {
|
||||||
|
for k, v := range mdata {
|
||||||
|
manufacturerData = append(manufacturerData, ManufacturerDataElement{
|
||||||
|
CompanyID: k,
|
||||||
|
Data: v.Value().([]byte),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get optional properties.
|
||||||
|
localName, _ := props["Name"].Value().(string)
|
||||||
|
rssi, _ := props["RSSI"].Value().(int16)
|
||||||
|
|
||||||
|
var serviceData []ServiceDataElement
|
||||||
|
if sdata, ok := props["ServiceData"].Value().(map[string]dbus.Variant); ok {
|
||||||
|
for k, v := range sdata {
|
||||||
|
uuid, err := ParseUUID(k)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
serviceData = append(serviceData, ServiceDataElement{
|
||||||
|
UUID: uuid,
|
||||||
|
Data: v.Value().([]byte),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ScanResult{
|
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: manufacturerData,
|
||||||
|
ServiceData: serviceData,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -236,39 +323,88 @@ 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
|
Address Address // the MAC address of the device
|
||||||
|
|
||||||
|
device dbus.BusObject // bluez device interface
|
||||||
|
adapter *Adapter // the adapter that was used to form this device connection
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 Addresser, params ConnectionParams) (*Device, error) {
|
func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, error) {
|
||||||
adr := address.(Address)
|
devicePath := dbus.ObjectPath(string(a.adapter.Path()) + "/dev_" + strings.Replace(address.MAC.String(), ":", "_", -1))
|
||||||
devicePath := dbus.ObjectPath(string(a.adapter.Path()) + "/dev_" + strings.Replace(adr.MAC.String(), ":", "_", -1))
|
device := Device{
|
||||||
dev, err := device.NewDevice1(devicePath)
|
Address: address,
|
||||||
|
device: a.bus.Object("org.bluez", devicePath),
|
||||||
|
adapter: a,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already start watching for property changes. We do this before reading
|
||||||
|
// the Connected property below to avoid a race condition: if the device
|
||||||
|
// were connected between the two calls the signal wouldn't be picked up.
|
||||||
|
signal := make(chan *dbus.Signal)
|
||||||
|
a.bus.Signal(signal)
|
||||||
|
defer a.bus.RemoveSignal(signal)
|
||||||
|
propertiesChangedMatchOptions := []dbus.MatchOption{dbus.WithMatchInterface("org.freedesktop.DBus.Properties")}
|
||||||
|
a.bus.AddMatchSignal(propertiesChangedMatchOptions...)
|
||||||
|
defer a.bus.RemoveMatchSignal(propertiesChangedMatchOptions...)
|
||||||
|
|
||||||
|
// Read whether this device is already connected.
|
||||||
|
connected, err := device.device.GetProperty("org.bluez.Device1.Connected")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return Device{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !dev.Properties.Connected {
|
// Connect to the device, if not already connected.
|
||||||
// Not yet connected, so do it now.
|
if !connected.Value().(bool) {
|
||||||
// The properties have just been read so this is fresh data.
|
// Start connecting (async).
|
||||||
err := dev.Connect()
|
err := device.device.Call("org.bluez.Device1.Connect", 0).Err
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return Device{}, fmt.Errorf("bluetooth: failed to connect: %w", err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: a proper async callback.
|
// Wait until the device has connected.
|
||||||
a.connectHandler(nil, true)
|
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{
|
return device, nil
|
||||||
device: dev,
|
|
||||||
}, 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 {
|
||||||
return d.device.Disconnect()
|
// we don't call our cancel function here, instead we wait for the
|
||||||
|
// property change in `watchForConnect` and cancel things then
|
||||||
|
return d.device.Call("org.bluez.Device1.Disconnect", 0).Err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestConnectionParams requests a different connection latency and timeout
|
||||||
|
// of the given device connection. Fields that are unset will be left alone.
|
||||||
|
// Whether or not the device will actually honor this, depends on the device and
|
||||||
|
// on the specific parameters.
|
||||||
|
//
|
||||||
|
// On Linux, this call doesn't do anything because BlueZ doesn't support
|
||||||
|
// changing the connection latency.
|
||||||
|
func (d Device) RequestConnectionParams(params ConnectionParams) error {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
27
gap_nrf51.go
27
gap_nrf51.go
|
@ -1,19 +1,21 @@
|
||||||
// +build softdevice,s110v8
|
//go:build softdevice && s110v8
|
||||||
|
|
||||||
package bluetooth
|
package bluetooth
|
||||||
|
|
||||||
/*
|
/*
|
||||||
// Define SoftDevice functions as regular function declarations (not inline
|
|
||||||
// static functions).
|
|
||||||
#define SVCALL_AS_NORMAL_FUNCTION
|
|
||||||
|
|
||||||
#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.
|
||||||
|
@ -51,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)
|
||||||
}
|
}
|
||||||
|
@ -63,14 +65,21 @@ func (a *Advertisement) Start() error {
|
||||||
return makeError(errCode)
|
return makeError(errCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop advertisement.
|
||||||
|
func (a *Advertisement) Stop() error {
|
||||||
|
a.isAdvertising.Set(0)
|
||||||
|
errCode := C.sd_ble_gap_adv_stop()
|
||||||
|
return makeError(errCode)
|
||||||
|
}
|
||||||
|
|
||||||
// Low-level version of Start. Used to restart advertisement when a connection
|
// 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(¶ms)
|
return C.sd_ble_gap_adv_start_noescape(params)
|
||||||
}
|
}
|
||||||
|
|
86
gap_nrf528xx-advertisement.go
Normal file
86
gap_nrf528xx-advertisement.go
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
//go:build (softdevice && s113v7) || (softdevice && s132v6) || (softdevice && s140v6) || (softdevice && s140v7)
|
||||||
|
|
||||||
|
package bluetooth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime/volatile"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
#include "ble_gap.h"
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
// Address contains a Bluetooth MAC address.
|
||||||
|
type Address struct {
|
||||||
|
MACAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advertisement encapsulates a single advertisement instance.
|
||||||
|
type Advertisement struct {
|
||||||
|
handle C.uint8_t
|
||||||
|
isAdvertising volatile.Register8
|
||||||
|
payload rawAdvertisementPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
// The nrf528xx devices only seem to support one advertisement instance. The way
|
||||||
|
// multiple advertisements are implemented is by changing the packet data
|
||||||
|
// frequently.
|
||||||
|
var defaultAdvertisement = Advertisement{
|
||||||
|
handle: C.BLE_GAP_ADV_SET_HANDLE_NOT_SET,
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultAdvertisement returns the default advertisement instance but does not
|
||||||
|
// configure it.
|
||||||
|
func (a *Adapter) DefaultAdvertisement() *Advertisement {
|
||||||
|
return &defaultAdvertisement
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure this advertisement.
|
||||||
|
func (a *Advertisement) Configure(options AdvertisementOptions) error {
|
||||||
|
// Fill empty options with reasonable defaults.
|
||||||
|
if options.Interval == 0 {
|
||||||
|
// Pick an advertisement interval recommended by Apple (section 35.5
|
||||||
|
// Advertising Interval):
|
||||||
|
// https://developer.apple.com/accessories/Accessory-Design-Guidelines.pdf
|
||||||
|
options.Interval = NewDuration(152500 * time.Microsecond) // 152.5ms
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct payload.
|
||||||
|
// Note that the payload needs to be part of the Advertisement object as the
|
||||||
|
// memory is still used after sd_ble_gap_adv_set_configure returns.
|
||||||
|
a.payload.reset()
|
||||||
|
if !a.payload.addFromOptions(options) {
|
||||||
|
return errAdvertisementPacketTooBig
|
||||||
|
}
|
||||||
|
|
||||||
|
data := C.ble_gap_adv_data_t{}
|
||||||
|
data.adv_data = C.ble_data_t{
|
||||||
|
p_data: (*C.uint8_t)(unsafe.Pointer(&a.payload.data[0])),
|
||||||
|
len: C.uint16_t(a.payload.len),
|
||||||
|
}
|
||||||
|
params := C.ble_gap_adv_params_t{
|
||||||
|
properties: C.ble_gap_adv_properties_t{
|
||||||
|
_type: C.BLE_GAP_ADV_TYPE_CONNECTABLE_SCANNABLE_UNDIRECTED,
|
||||||
|
},
|
||||||
|
interval: C.uint32_t(options.Interval),
|
||||||
|
}
|
||||||
|
errCode := C.sd_ble_gap_adv_set_configure(&a.handle, &data, ¶ms)
|
||||||
|
return makeError(errCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start advertisement. May only be called after it has been configured.
|
||||||
|
func (a *Advertisement) Start() error {
|
||||||
|
a.isAdvertising.Set(1)
|
||||||
|
errCode := C.sd_ble_gap_adv_start(a.handle, C.BLE_CONN_CFG_TAG_DEFAULT)
|
||||||
|
return makeError(errCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop advertisement.
|
||||||
|
func (a *Advertisement) Stop() error {
|
||||||
|
a.isAdvertising.Set(0)
|
||||||
|
errCode := C.sd_ble_gap_adv_stop(a.handle)
|
||||||
|
return makeError(errCode)
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
// +build softdevice,!s110v8
|
//go:build (softdevice && s132v6) || (softdevice && s140v6) || (softdevice && s140v7)
|
||||||
|
|
||||||
package bluetooth
|
package bluetooth
|
||||||
|
|
||||||
|
@ -7,18 +7,16 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"runtime/volatile"
|
"runtime/volatile"
|
||||||
"time"
|
"time"
|
||||||
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
// Define SoftDevice functions as regular function declarations (not inline
|
|
||||||
// static functions).
|
|
||||||
#define SVCALL_AS_NORMAL_FUNCTION
|
|
||||||
|
|
||||||
#include "ble_gap.h"
|
#include "ble_gap.h"
|
||||||
*/
|
*/
|
||||||
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 (
|
||||||
|
@ -27,71 +25,6 @@ var (
|
||||||
globalScanResult ScanResult
|
globalScanResult ScanResult
|
||||||
)
|
)
|
||||||
|
|
||||||
// Address contains a Bluetooth MAC address.
|
|
||||||
type Address struct {
|
|
||||||
MACAddress
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advertisement encapsulates a single advertisement instance.
|
|
||||||
type Advertisement struct {
|
|
||||||
handle uint8
|
|
||||||
isAdvertising volatile.Register8
|
|
||||||
payload rawAdvertisementPayload
|
|
||||||
}
|
|
||||||
|
|
||||||
// The nrf528xx devices only seem to support one advertisement instance. The way
|
|
||||||
// multiple advertisements are implemented is by changing the packet data
|
|
||||||
// frequently.
|
|
||||||
var defaultAdvertisement = Advertisement{
|
|
||||||
handle: C.BLE_GAP_ADV_SET_HANDLE_NOT_SET,
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultAdvertisement returns the default advertisement instance but does not
|
|
||||||
// configure it.
|
|
||||||
func (a *Adapter) DefaultAdvertisement() *Advertisement {
|
|
||||||
return &defaultAdvertisement
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure this advertisement.
|
|
||||||
func (a *Advertisement) Configure(options AdvertisementOptions) error {
|
|
||||||
// Fill empty options with reasonable defaults.
|
|
||||||
if options.Interval == 0 {
|
|
||||||
// Pick an advertisement interval recommended by Apple (section 35.5
|
|
||||||
// Advertising Interval):
|
|
||||||
// https://developer.apple.com/accessories/Accessory-Design-Guidelines.pdf
|
|
||||||
options.Interval = NewDuration(152500 * time.Microsecond) // 152.5ms
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct payload.
|
|
||||||
// Note that the payload needs to be part of the Advertisement object as the
|
|
||||||
// memory is still used after sd_ble_gap_adv_set_configure returns.
|
|
||||||
a.payload.reset()
|
|
||||||
if !a.payload.addFromOptions(options) {
|
|
||||||
return errAdvertisementPacketTooBig
|
|
||||||
}
|
|
||||||
|
|
||||||
data := C.ble_gap_adv_data_t{}
|
|
||||||
data.adv_data = C.ble_data_t{
|
|
||||||
p_data: &a.payload.data[0],
|
|
||||||
len: uint16(a.payload.len),
|
|
||||||
}
|
|
||||||
params := C.ble_gap_adv_params_t{
|
|
||||||
properties: C.ble_gap_adv_properties_t{
|
|
||||||
_type: C.BLE_GAP_ADV_TYPE_CONNECTABLE_SCANNABLE_UNDIRECTED,
|
|
||||||
},
|
|
||||||
interval: uint32(options.Interval),
|
|
||||||
}
|
|
||||||
errCode := C.sd_ble_gap_adv_set_configure(&a.handle, &data, ¶ms)
|
|
||||||
return makeError(errCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start advertisement. May only be called after it has been configured.
|
|
||||||
func (a *Advertisement) Start() error {
|
|
||||||
a.isAdvertising.Set(1)
|
|
||||||
errCode := C.sd_ble_gap_adv_start(a.handle, C.BLE_CONN_CFG_TAG_DEFAULT)
|
|
||||||
return makeError(errCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scan starts a BLE scan. It is stopped by a call to StopScan. A common pattern
|
// 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.
|
||||||
//
|
//
|
||||||
|
@ -109,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 {
|
||||||
|
@ -160,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.
|
||||||
|
@ -177,13 +105,12 @@ 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 Addresser, params ConnectionParams) (*Device, error) {
|
func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, error) {
|
||||||
adr := address.(Address)
|
|
||||||
// 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 = adr.MAC
|
addr.addr = makeSDAddress(address.MAC)
|
||||||
if address.IsRandom() {
|
if address.IsRandom() {
|
||||||
switch adr.MAC[5] >> 6 {
|
switch address.MAC[5] >> 6 {
|
||||||
case 0b11:
|
case 0b11:
|
||||||
addr.set_bitfield_addr_type(C.BLE_GAP_ADDR_TYPE_RANDOM_STATIC)
|
addr.set_bitfield_addr_type(C.BLE_GAP_ADDR_TYPE_RANDOM_STATIC)
|
||||||
case 0b01:
|
case 0b01:
|
||||||
|
@ -212,22 +139,25 @@ func (a *Adapter) Connect(address Addresser, params ConnectionParams) (*Device,
|
||||||
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)
|
||||||
|
|
||||||
|
@ -235,26 +165,33 @@ func (a *Adapter) Connect(address Addresser, params ConnectionParams) (*Device,
|
||||||
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)
|
||||||
|
@ -262,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
15
gap_sd.go
Normal 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
133
gap_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
344
gap_windows.go
344
gap_windows.go
|
@ -1,7 +1,16 @@
|
||||||
package bluetooth
|
package bluetooth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"tinygo.org/x/bluetooth/winbt"
|
"fmt"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/go-ole/go-ole"
|
||||||
|
"github.com/saltosystems/winrt-go"
|
||||||
|
"github.com/saltosystems/winrt-go/windows/devices/bluetooth"
|
||||||
|
"github.com/saltosystems/winrt-go/windows/devices/bluetooth/advertisement"
|
||||||
|
"github.com/saltosystems/winrt-go/windows/devices/bluetooth/genericattributeprofile"
|
||||||
|
"github.com/saltosystems/winrt-go/windows/foundation"
|
||||||
|
"github.com/saltosystems/winrt-go/windows/storage/streams"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Address contains a Bluetooth MAC address.
|
// Address contains a Bluetooth MAC address.
|
||||||
|
@ -9,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) {
|
||||||
|
@ -18,49 +129,75 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) {
|
||||||
return errScanning
|
return errScanning
|
||||||
}
|
}
|
||||||
|
|
||||||
a.watcher, err = winbt.NewBluetoothLEAdvertisementWatcher()
|
a.watcher, err = advertisement.NewBluetoothLEAdvertisementWatcher()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = a.watcher.Release()
|
||||||
|
a.watcher = nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Set scanning mode to active so we receive scan responses
|
||||||
|
// from devices in advertising mode
|
||||||
|
err = a.watcher.SetScanningMode(advertisement.BluetoothLEScanningModeActive)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer a.watcher.Release()
|
|
||||||
|
|
||||||
// Listen for incoming BLE advertisement packets.
|
// Listen for incoming BLE advertisement packets.
|
||||||
err = a.watcher.AddReceivedEvent(func(watcher *winbt.IBluetoothLEAdvertisementWatcher, args *winbt.IBluetoothLEAdvertisementReceivedEventArgs) {
|
// We need a TypedEventHandler<TSender, TResult> to listen to events, but since this is a parameterized delegate
|
||||||
var result ScanResult
|
// its GUID depends on the classes used as sender and result, so we need to compute it:
|
||||||
result.RSSI = args.RawSignalStrengthInDBm()
|
// TypedEventHandler<BluetoothLEAdvertisementWatcher, BluetoothLEAdvertisementReceivedEventArgs>
|
||||||
addr := args.BluetoothAddress()
|
eventReceivedGuid := winrt.ParameterizedInstanceGUID(
|
||||||
adr := result.Address.(Address)
|
foundation.GUIDTypedEventHandler,
|
||||||
for i := range adr.MAC {
|
advertisement.SignatureBluetoothLEAdvertisementWatcher,
|
||||||
adr.MAC[i] = byte(addr)
|
advertisement.SignatureBluetoothLEAdvertisementReceivedEventArgs,
|
||||||
addr >>= 8
|
)
|
||||||
}
|
handler := foundation.NewTypedEventHandler(ole.NewGUID(eventReceivedGuid), func(instance *foundation.TypedEventHandler, sender, arg unsafe.Pointer) {
|
||||||
// Note: the IsRandom bit is never set.
|
args := (*advertisement.BluetoothLEAdvertisementReceivedEventArgs)(arg)
|
||||||
advertisement := args.Advertisement()
|
result := getScanResultFromArgs(args)
|
||||||
result.AdvertisementPayload = &advertisementFields{
|
|
||||||
AdvertisementFields{
|
|
||||||
LocalName: advertisement.LocalName(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
callback(a, result)
|
callback(a, result)
|
||||||
})
|
})
|
||||||
|
defer handler.Release()
|
||||||
|
|
||||||
|
token, err := a.watcher.AddReceived(handler)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer a.watcher.RemoveReceived(token)
|
||||||
|
|
||||||
// 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)
|
||||||
err = a.watcher.AddStoppedEvent(func(watcher *winbt.IBluetoothLEAdvertisementWatcher, args *winbt.IBluetoothLEAdvertisementWatcherStoppedEventArgs) {
|
// TypedEventHandler<BluetoothLEAdvertisementWatcher, BluetoothLEAdvertisementWatcherStoppedEventArgs>
|
||||||
// Note: the args parameter has an Error property that should
|
eventStoppedGuid := winrt.ParameterizedInstanceGUID(
|
||||||
// probably be checked, but I'm not sure when stopping the
|
foundation.GUIDTypedEventHandler,
|
||||||
// advertisement watcher could ever result in an error (except
|
advertisement.SignatureBluetoothLEAdvertisementWatcher,
|
||||||
// for bugs).
|
advertisement.SignatureBluetoothLEAdvertisementWatcherStoppedEventArgs,
|
||||||
|
)
|
||||||
|
stoppedHandler := foundation.NewTypedEventHandler(ole.NewGUID(eventStoppedGuid), func(_ *foundation.TypedEventHandler, _, arg unsafe.Pointer) {
|
||||||
|
args := (*advertisement.BluetoothLEAdvertisementWatcherStoppedEventArgs)(arg)
|
||||||
|
errCode, err := args.GetError()
|
||||||
|
if err != nil {
|
||||||
|
// Got an error while getting the error value, that shouldn't
|
||||||
|
// happen.
|
||||||
|
stoppingChan <- fmt.Errorf("failed to get stopping error value: %w", err)
|
||||||
|
} else if errCode != bluetooth.BluetoothErrorSuccess {
|
||||||
|
// Could not stop the scan? I'm not sure when this would actually
|
||||||
|
// happen.
|
||||||
|
stoppingChan <- fmt.Errorf("failed to stop scanning (error code %d)", errCode)
|
||||||
|
}
|
||||||
close(stoppingChan)
|
close(stoppingChan)
|
||||||
})
|
})
|
||||||
|
defer stoppedHandler.Release()
|
||||||
|
|
||||||
|
token, err = a.watcher.AddStopped(stoppedHandler)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer a.watcher.RemoveStopped(token)
|
||||||
|
|
||||||
err = a.watcher.Start()
|
err = a.watcher.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -68,10 +205,62 @@ 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
|
||||||
a.watcher = nil
|
}
|
||||||
|
|
||||||
|
func getScanResultFromArgs(args *advertisement.BluetoothLEAdvertisementReceivedEventArgs) ScanResult {
|
||||||
|
// parse bluetooth address
|
||||||
|
addr, _ := args.GetBluetoothAddress()
|
||||||
|
adr := Address{}
|
||||||
|
for i := range adr.MAC {
|
||||||
|
adr.MAC[i] = byte(addr)
|
||||||
|
addr >>= 8
|
||||||
|
}
|
||||||
|
sigStrength, _ := args.GetRawSignalStrengthInDBm()
|
||||||
|
result := ScanResult{
|
||||||
|
RSSI: sigStrength,
|
||||||
|
Address: adr,
|
||||||
|
}
|
||||||
|
|
||||||
|
var manufacturerData []ManufacturerDataElement
|
||||||
|
if winAdv, err := args.GetAdvertisement(); err == nil && winAdv != nil {
|
||||||
|
vector, _ := winAdv.GetManufacturerData()
|
||||||
|
size, _ := vector.GetSize()
|
||||||
|
for i := uint32(0); i < size; i++ {
|
||||||
|
element, _ := vector.GetAt(i)
|
||||||
|
manData := (*advertisement.BluetoothLEManufacturerData)(element)
|
||||||
|
companyID, _ := manData.GetCompanyId()
|
||||||
|
buffer, _ := manData.GetData()
|
||||||
|
manufacturerData = append(manufacturerData, ManufacturerDataElement{
|
||||||
|
CompanyID: companyID,
|
||||||
|
Data: bufferToSlice(buffer),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: the IsRandom bit is never set.
|
||||||
|
advertisement, _ := args.GetAdvertisement()
|
||||||
|
localName, _ := advertisement.GetLocalName()
|
||||||
|
result.AdvertisementPayload = &advertisementFields{
|
||||||
|
AdvertisementFields{
|
||||||
|
LocalName: localName,
|
||||||
|
ManufacturerData: manufacturerData,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func bufferToSlice(buffer *streams.IBuffer) []byte {
|
||||||
|
dataReader, _ := streams.DataReaderFromBuffer(buffer)
|
||||||
|
defer dataReader.Release()
|
||||||
|
bufferSize, _ := buffer.GetLength()
|
||||||
|
if bufferSize == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
data, _ := dataReader.ReadBytes(bufferSize)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
// StopScan stops any in-progress scan. It can be called from within a Scan
|
// StopScan stops any in-progress scan. It can be called from within a Scan
|
||||||
// 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
|
||||||
|
@ -82,3 +271,104 @@ func (a *Adapter) StopScan() error {
|
||||||
}
|
}
|
||||||
return a.watcher.Stop()
|
return a.watcher.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Device is a connection to a remote peripheral.
|
||||||
|
type Device struct {
|
||||||
|
Address Address // the MAC address of the device
|
||||||
|
|
||||||
|
device *bluetooth.BluetoothLEDevice
|
||||||
|
session *genericattributeprofile.GattSession
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect starts a connection attempt to the given peripheral device address.
|
||||||
|
//
|
||||||
|
// On Linux and Windows, the IsRandom part of the address is ignored.
|
||||||
|
func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, error) {
|
||||||
|
var winAddr uint64
|
||||||
|
for i := range address.MAC {
|
||||||
|
winAddr += uint64(address.MAC[i]) << (8 * i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IAsyncOperation<BluetoothLEDevice>
|
||||||
|
bleDeviceOp, err := bluetooth.BluetoothLEDeviceFromBluetoothAddressAsync(winAddr)
|
||||||
|
if err != nil {
|
||||||
|
return Device{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to pass the signature of the parameter returned by the async operation:
|
||||||
|
// IAsyncOperation<BluetoothLEDevice>
|
||||||
|
if err := awaitAsyncOperation(bleDeviceOp, bluetooth.SignatureBluetoothLEDevice); err != nil {
|
||||||
|
return Device{}, fmt.Errorf("error connecting to device: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := bleDeviceOp.GetResults()
|
||||||
|
if err != nil {
|
||||||
|
return Device{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// The returned BluetoothLEDevice is set to null if FromBluetoothAddressAsync can't find the device identified by bluetoothAddress
|
||||||
|
if uintptr(res) == 0x0 {
|
||||||
|
return Device{}, fmt.Errorf("device with the given address was not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
bleDevice := (*bluetooth.BluetoothLEDevice)(res)
|
||||||
|
|
||||||
|
// Creating a BluetoothLEDevice object by calling this method alone doesn't (necessarily) initiate a connection.
|
||||||
|
// To initiate a connection, we need to set GattSession.MaintainConnection to true.
|
||||||
|
dID, err := bleDevice.GetBluetoothDeviceId()
|
||||||
|
if err != nil {
|
||||||
|
return Device{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows does not support explicitly connecting to a device.
|
||||||
|
// Instead it has the concept of a GATT session that is owned
|
||||||
|
// by the calling program.
|
||||||
|
gattSessionOp, err := genericattributeprofile.GattSessionFromDeviceIdAsync(dID) // IAsyncOperation<GattSession>
|
||||||
|
if err != nil {
|
||||||
|
return Device{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := awaitAsyncOperation(gattSessionOp, genericattributeprofile.SignatureGattSession); err != nil {
|
||||||
|
return Device{}, fmt.Errorf("error getting gatt session: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gattRes, err := gattSessionOp.GetResults()
|
||||||
|
if err != nil {
|
||||||
|
return Device{}, err
|
||||||
|
}
|
||||||
|
newSession := (*genericattributeprofile.GattSession)(gattRes)
|
||||||
|
// This keeps the device connected until we set maintain_connection = False.
|
||||||
|
if err := newSession.SetMaintainConnection(true); err != nil {
|
||||||
|
return Device{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return Device{address, bleDevice, newSession}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect from the BLE device. This method is non-blocking and does not
|
||||||
|
// wait until the connection is fully gone.
|
||||||
|
func (d Device) Disconnect() error {
|
||||||
|
defer d.device.Release()
|
||||||
|
defer d.session.Release()
|
||||||
|
|
||||||
|
if err := d.session.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.device.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestConnectionParams requests a different connection latency and timeout
|
||||||
|
// of the given device connection. Fields that are unset will be left alone.
|
||||||
|
// Whether or not the device will actually honor this, depends on the device and
|
||||||
|
// on the specific parameters.
|
||||||
|
//
|
||||||
|
// On Windows, this call doesn't do anything.
|
||||||
|
func (d Device) RequestConnectionParams(params ConnectionParams) error {
|
||||||
|
// TODO: implement this using
|
||||||
|
// BluetoothLEDevice.RequestPreferredConnectionParameters.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
138
gattc_darwin.go
138
gattc_darwin.go
|
@ -4,7 +4,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/JuulLabs-OSS/cbgo"
|
"github.com/tinygo-org/cbgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DiscoverServices starts a service discovery procedure. Pass a list of service
|
// DiscoverServices starts a service discovery procedure. Pass a list of service
|
||||||
|
@ -14,31 +14,42 @@ 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) {
|
||||||
cbuuids := []cbgo.UUID{}
|
d.prph.DiscoverServices([]cbgo.UUID{})
|
||||||
for _, u := range uuids {
|
|
||||||
uuid, _ := cbgo.ParseUUID(u.String())
|
|
||||||
cbuuids = append(cbuuids, uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.prph.DiscoverServices(cbuuids)
|
|
||||||
|
|
||||||
// clear cache of services
|
// clear cache of services
|
||||||
d.services = make(map[UUID]*DeviceService)
|
d.services = make(map[UUID]DeviceService)
|
||||||
|
|
||||||
// wait on channel for service discovery
|
// wait on channel for service discovery
|
||||||
select {
|
select {
|
||||||
case <-d.servicesChan:
|
case <-d.servicesChan:
|
||||||
svcs := []DeviceService{}
|
svcs := []DeviceService{}
|
||||||
for _, dsvc := range d.prph.Services() {
|
for _, dsvc := range d.prph.Services() {
|
||||||
uuid, _ := ParseUUID(dsvc.UUID().String())
|
dsvcuuid, _ := ParseUUID(dsvc.UUID().String())
|
||||||
|
// add if in our original list
|
||||||
|
if len(uuids) > 0 {
|
||||||
|
found := false
|
||||||
|
for _, uuid := range uuids {
|
||||||
|
if dsvcuuid.String() == uuid.String() {
|
||||||
|
// one of the services we're looking for.
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
svc := DeviceService{
|
svc := DeviceService{
|
||||||
uuidWrapper: uuid,
|
deviceService: &deviceService{
|
||||||
|
uuidWrapper: dsvcuuid,
|
||||||
device: d,
|
device: d,
|
||||||
service: dsvc,
|
service: dsvc,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
svcs = append(svcs, svc)
|
svcs = append(svcs, svc)
|
||||||
d.services[svc.uuidWrapper] = &svc
|
d.services[svc.uuidWrapper] = svc
|
||||||
}
|
}
|
||||||
return svcs, nil
|
return svcs, nil
|
||||||
case <-time.NewTimer(10 * time.Second).C:
|
case <-time.NewTimer(10 * time.Second).C:
|
||||||
|
@ -52,15 +63,20 @@ 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 {
|
||||||
|
*deviceService // embdedded as pointer to enable returning by []value in DiscoverServices
|
||||||
|
}
|
||||||
|
|
||||||
|
type deviceService struct {
|
||||||
uuidWrapper
|
uuidWrapper
|
||||||
|
|
||||||
device *Device
|
device Device
|
||||||
|
|
||||||
service cbgo.Service
|
service cbgo.Service
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,24 +89,60 @@ 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{}
|
||||||
for _, u := range uuids {
|
|
||||||
uuid, _ := cbgo.ParseUUID(u.String())
|
|
||||||
cbuuids = append(cbuuids, uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.device.prph.DiscoverCharacteristics(cbuuids, s.service)
|
s.device.prph.DiscoverCharacteristics(cbuuids, s.service)
|
||||||
|
|
||||||
// clear cache of characteristics
|
// clear cache of characteristics
|
||||||
s.device.characteristics = make(map[UUID]*DeviceCharacteristic)
|
s.characteristics = make([]DeviceCharacteristic, 0)
|
||||||
|
|
||||||
// wait on channel for characteristic discovery
|
// wait on channel for characteristic discovery
|
||||||
select {
|
select {
|
||||||
case <-s.device.charsChan:
|
case <-s.device.charsChan:
|
||||||
chars := []DeviceCharacteristic{}
|
var chars []DeviceCharacteristic
|
||||||
|
if len(uuids) > 0 {
|
||||||
|
// The caller wants to get a list of characteristics in a specific
|
||||||
|
// order.
|
||||||
|
chars = make([]DeviceCharacteristic, len(uuids))
|
||||||
|
}
|
||||||
for _, dchar := range s.service.Characteristics() {
|
for _, dchar := range s.service.Characteristics() {
|
||||||
uuid, _ := ParseUUID(dchar.UUID().String())
|
dcuuid, _ := ParseUUID(dchar.UUID().String())
|
||||||
|
if len(uuids) > 0 {
|
||||||
|
// The caller wants to get a list of characteristics in a
|
||||||
|
// specific order. Check whether this is one of those.
|
||||||
|
for i, uuid := range uuids {
|
||||||
|
if chars[i] != (DeviceCharacteristic{}) {
|
||||||
|
// To support multiple identical characteristics, we
|
||||||
|
// need to ignore the characteristics that are already
|
||||||
|
// found. See:
|
||||||
|
// https://github.com/tinygo-org/bluetooth/issues/131
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if dcuuid == uuid {
|
||||||
|
// one of the characteristics we're looking for.
|
||||||
|
chars[i] = s.makeCharacteristic(dcuuid, dchar)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The caller wants to get all characteristics, in any order.
|
||||||
|
chars = append(chars, s.makeCharacteristic(dcuuid, dchar))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, char := range chars {
|
||||||
|
if char == (DeviceCharacteristic{}) {
|
||||||
|
return nil, errors.New("bluetooth: did not find all requested characteristic")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chars, nil
|
||||||
|
case <-time.NewTimer(10 * time.Second).C:
|
||||||
|
return nil, errors.New("timeout on DiscoverCharacteristics")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small helper to create a DeviceCharacteristic object.
|
||||||
|
func (s DeviceService) makeCharacteristic(uuid UUID, dchar cbgo.Characteristic) DeviceCharacteristic {
|
||||||
char := DeviceCharacteristic{
|
char := DeviceCharacteristic{
|
||||||
deviceCharacteristic: &deviceCharacteristic{
|
deviceCharacteristic: &deviceCharacteristic{
|
||||||
uuidWrapper: uuid,
|
uuidWrapper: uuid,
|
||||||
|
@ -98,13 +150,8 @@ func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacter
|
||||||
characteristic: dchar,
|
characteristic: dchar,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
chars = append(chars, char)
|
s.characteristics = append(s.characteristics, char)
|
||||||
s.device.characteristics[char.uuidWrapper] = &char
|
return char
|
||||||
}
|
|
||||||
return chars, nil
|
|
||||||
case <-time.NewTimer(10 * time.Second).C:
|
|
||||||
return nil, errors.New("timeout on DiscoverCharacteristics")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeviceCharacteristic is a BLE characteristic on a connected peripheral
|
// DeviceCharacteristic is a BLE characteristic on a connected peripheral
|
||||||
|
@ -116,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
|
||||||
|
@ -153,6 +222,11 @@ func (c DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) err
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetMTU returns the MTU for the characteristic.
|
||||||
|
func (c DeviceCharacteristic) GetMTU() (uint16, error) {
|
||||||
|
return uint16(c.service.device.prph.MaximumWriteValueLength(false)), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Read reads the current characteristic value.
|
// Read reads the current characteristic value.
|
||||||
func (c *deviceCharacteristic) Read(data []byte) (n int, err error) {
|
func (c *deviceCharacteristic) Read(data []byte) (n int, err error) {
|
||||||
c.readChan = make(chan error)
|
c.readChan = make(chan error)
|
||||||
|
|
330
gattc_hci.go
Normal file
330
gattc_hci.go
Normal 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
|
||||||
|
}
|
219
gattc_linux.go
219
gattc_linux.go
|
@ -1,14 +1,18 @@
|
||||||
// +build !baremetal
|
//go:build !baremetal
|
||||||
|
|
||||||
package bluetooth
|
package bluetooth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/muka/go-bluetooth/bluez"
|
"github.com/godbus/dbus/v5"
|
||||||
"github.com/muka/go-bluetooth/bluez/profile/gatt"
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errDupNotif = errors.New("unclosed notifications")
|
||||||
)
|
)
|
||||||
|
|
||||||
// UUIDWrapper is a type alias for UUID so we ensure no conflicts with
|
// UUIDWrapper is a type alias for UUID so we ensure no conflicts with
|
||||||
|
@ -18,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,50 +41,57 @@ 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()
|
||||||
|
|
||||||
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 {
|
||||||
|
return nil, errors.New("timeout on DiscoverServices")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
objects := make([]string, 0, len(list))
|
||||||
for objectPath := range list {
|
for objectPath := range list {
|
||||||
if !strings.HasPrefix(string(objectPath), string(d.device.Path())+"/service") {
|
objects = append(objects, string(objectPath))
|
||||||
|
}
|
||||||
|
sort.Strings(objects)
|
||||||
|
for _, objectPath := range objects {
|
||||||
|
if !strings.HasPrefix(objectPath, string(d.device.Path())+"/service") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
suffix := string(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(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
|
||||||
|
@ -91,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) {
|
||||||
|
@ -118,12 +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 *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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,67 +150,69 @@ 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) {
|
||||||
chars := []DeviceCharacteristic{}
|
var chars []DeviceCharacteristic
|
||||||
uuidChars := make(map[string]string)
|
if len(uuids) > 0 {
|
||||||
characteristicsFound := 0
|
// The caller wants to get a list of characteristics in a specific
|
||||||
|
// order.
|
||||||
|
chars = make([]DeviceCharacteristic, len(uuids))
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
}
|
}
|
||||||
|
objects := make([]string, 0, len(list))
|
||||||
for objectPath := range list {
|
for objectPath := range list {
|
||||||
if !strings.HasPrefix(string(objectPath), string(s.service.Path())+"/char") {
|
objects = append(objects, string(objectPath))
|
||||||
|
}
|
||||||
|
sort.Strings(objects)
|
||||||
|
for _, objectPath := range objects {
|
||||||
|
if !strings.HasPrefix(objectPath, s.servicePath+"/char") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
suffix := string(objectPath)[len(s.service.Path()+"/"):]
|
properties, ok := list[dbus.ObjectPath(objectPath)]["org.bluez.GattCharacteristic1"]
|
||||||
if len(strings.Split(suffix, "/")) != 1 {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
char, err := gatt.NewGattCharacteristic1(objectPath)
|
cuuid, _ := ParseUUID(properties["UUID"].Value().(string))
|
||||||
if err != nil {
|
char := DeviceCharacteristic{
|
||||||
return nil, err
|
uuidWrapper: cuuid,
|
||||||
|
adapter: s.adapter,
|
||||||
|
characteristic: s.adapter.bus.Object("org.bluez", dbus.ObjectPath(objectPath)),
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(uuids) > 0 {
|
if len(uuids) > 0 {
|
||||||
found := false
|
// The caller wants to get a list of characteristics in a specific
|
||||||
for _, uuid := range uuids {
|
// order. Check whether this is one of those.
|
||||||
if char.Properties.UUID == uuid.String() {
|
for i, uuid := range uuids {
|
||||||
// One of the services we're looking for.
|
if chars[i] != (DeviceCharacteristic{}) {
|
||||||
found = true
|
// To support multiple identical characteristics, we need to
|
||||||
|
// ignore the characteristics that are already found. See:
|
||||||
|
// https://github.com/tinygo-org/bluetooth/issues/131
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if cuuid == uuid {
|
||||||
|
// one of the characteristics we're looking for.
|
||||||
|
chars[i] = char
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
} else {
|
||||||
continue
|
// The caller wants to get all characteristics, in any order.
|
||||||
|
chars = append(chars, char)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := uuidChars[char.Properties.UUID]; ok {
|
// Check that we have found all characteristics.
|
||||||
// There is more than one characteristic with the same UUID?
|
for _, char := range chars {
|
||||||
// Don't overwrite it, to keep the servicesFound count correct.
|
if char == (DeviceCharacteristic{}) {
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
uuid, _ := ParseUUID(char.Properties.UUID)
|
|
||||||
dc := DeviceCharacteristic{uuidWrapper: uuid,
|
|
||||||
characteristic: char,
|
|
||||||
}
|
|
||||||
|
|
||||||
chars = append(chars, dc)
|
|
||||||
characteristicsFound++
|
|
||||||
uuidChars[char.Properties.UUID] = char.Properties.UUID
|
|
||||||
}
|
|
||||||
|
|
||||||
if characteristicsFound < len(uuids) {
|
|
||||||
return nil, errors.New("bluetooth: could not find some characteristics")
|
return nil, errors.New("bluetooth: could not find some characteristics")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return chars, nil
|
return chars, nil
|
||||||
}
|
}
|
||||||
|
@ -206,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
|
||||||
}
|
}
|
||||||
|
@ -217,25 +233,72 @@ func (c DeviceCharacteristic) WriteWithoutResponse(p []byte) (n int, err error)
|
||||||
// Configuration Descriptor (CCCD). This means that most peripherals will send a
|
// Configuration Descriptor (CCCD). This means that most peripherals will send a
|
||||||
// notification with a new value every time the value of the characteristic
|
// notification with a new value every time the value of the characteristic
|
||||||
// changes.
|
// changes.
|
||||||
|
//
|
||||||
|
// Users may call EnableNotifications with a nil callback to disable notifications.
|
||||||
func (c DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) error {
|
func (c DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) error {
|
||||||
ch, err := c.characteristic.WatchProperties()
|
switch callback {
|
||||||
|
default:
|
||||||
|
if c.property != nil {
|
||||||
|
return errDupNotif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start watching for changes in the Value property.
|
||||||
|
c.property = make(chan *dbus.Signal)
|
||||||
|
c.adapter.bus.Signal(c.property)
|
||||||
|
c.propertiesChangedMatchOption = dbus.WithMatchInterface("org.freedesktop.DBus.Properties")
|
||||||
|
c.adapter.bus.AddMatchSignal(c.propertiesChangedMatchOption)
|
||||||
|
|
||||||
|
err := c.characteristic.Call("org.bluez.GattCharacteristic1.StartNotify", 0).Err
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for update := range ch {
|
for sig := range c.property {
|
||||||
if update.Interface == "org.bluez.GattCharacteristic1" && update.Name == "Value" {
|
if sig.Name == "org.freedesktop.DBus.Properties.PropertiesChanged" {
|
||||||
callback(update.Value.([]byte))
|
interfaceName := sig.Body[0].(string)
|
||||||
|
if interfaceName != "org.bluez.GattCharacteristic1" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if sig.Path != c.characteristic.Path() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
changes := sig.Body[1].(map[string]dbus.Variant)
|
||||||
|
if value, ok := changes["Value"].Value().([]byte); ok {
|
||||||
|
callback(value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
return c.characteristic.StartNotify()
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case nil:
|
||||||
|
if c.property == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.adapter.bus.RemoveMatchSignal(c.propertiesChangedMatchOption)
|
||||||
|
c.adapter.bus.RemoveSignal(c.property)
|
||||||
|
c.property = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMTU returns the MTU for the characteristic.
|
||||||
|
func (c DeviceCharacteristic) GetMTU() (uint16, error) {
|
||||||
|
mtu, err := c.characteristic.GetProperty("org.bluez.GattCharacteristic1.MTU")
|
||||||
|
if err != nil {
|
||||||
|
return uint16(0), err
|
||||||
|
}
|
||||||
|
return mtu.Value().(uint16), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
}
|
}
|
||||||
|
|
68
gattc_sd.go
68
gattc_sd.go
|
@ -1,12 +1,8 @@
|
||||||
// +build softdevice,!s110v8
|
//go:build (softdevice && s132v6) || (softdevice && s140v6) || (softdevice && s140v7)
|
||||||
|
|
||||||
package bluetooth
|
package bluetooth
|
||||||
|
|
||||||
/*
|
/*
|
||||||
// Define SoftDevice functions as regular function declarations (not inline
|
|
||||||
// static functions).
|
|
||||||
#define SVCALL_AS_NORMAL_FUNCTION
|
|
||||||
|
|
||||||
#include "ble_gattc.h"
|
#include "ble_gattc.h"
|
||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
|
@ -15,6 +11,7 @@ import (
|
||||||
"device/arm"
|
"device/arm"
|
||||||
"errors"
|
"errors"
|
||||||
"runtime/volatile"
|
"runtime/volatile"
|
||||||
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -32,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,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
|
||||||
|
@ -80,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)
|
||||||
|
@ -90,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
|
||||||
|
@ -100,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 {
|
||||||
|
@ -134,7 +131,7 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
|
||||||
|
|
||||||
// Store the discovered service.
|
// Store the discovered service.
|
||||||
svc := DeviceService{
|
svc := DeviceService{
|
||||||
uuid: suuid,
|
uuid: shortUUID(suuid),
|
||||||
connectionHandle: d.connectionHandle,
|
connectionHandle: d.connectionHandle,
|
||||||
startHandle: startHandle,
|
startHandle: startHandle,
|
||||||
endHandle: endHandle,
|
endHandle: endHandle,
|
||||||
|
@ -163,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,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
|
||||||
|
@ -191,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
|
||||||
}
|
}
|
||||||
|
@ -208,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)
|
||||||
|
@ -276,7 +273,7 @@ func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacter
|
||||||
permissions |= CharacteristicIndicatePermission
|
permissions |= CharacteristicIndicatePermission
|
||||||
}
|
}
|
||||||
|
|
||||||
dc := DeviceCharacteristic{uuid: discoveringCharacteristic.uuid}
|
dc := DeviceCharacteristic{uuid: shortUUID(discoveringCharacteristic.uuid)}
|
||||||
dc.permissions = permissions
|
dc.permissions = permissions
|
||||||
dc.valueHandle = foundCharacteristicHandle
|
dc.valueHandle = foundCharacteristicHandle
|
||||||
|
|
||||||
|
@ -328,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)
|
||||||
|
@ -338,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -404,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,
|
||||||
|
@ -418,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
|
||||||
|
|
||||||
|
@ -450,3 +447,8 @@ func (c *DeviceCharacteristic) Read(data []byte) (n int, err error) {
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetMTU returns the MTU for the characteristic.
|
||||||
|
func (c DeviceCharacteristic) GetMTU() (uint16, error) {
|
||||||
|
return uint16(C.BLE_GATT_ATT_MTU_DEFAULT), nil
|
||||||
|
}
|
||||||
|
|
434
gattc_windows.go
Normal file
434
gattc_windows.go
Normal file
|
@ -0,0 +1,434 @@
|
||||||
|
package bluetooth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/go-ole/go-ole"
|
||||||
|
"github.com/saltosystems/winrt-go"
|
||||||
|
"github.com/saltosystems/winrt-go/windows/devices/bluetooth"
|
||||||
|
"github.com/saltosystems/winrt-go/windows/devices/bluetooth/genericattributeprofile"
|
||||||
|
"github.com/saltosystems/winrt-go/windows/foundation"
|
||||||
|
"github.com/saltosystems/winrt-go/windows/storage/streams"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errNoWrite = errors.New("bluetooth: write not supported")
|
||||||
|
errNoWriteWithoutResponse = errors.New("bluetooth: write without response not supported")
|
||||||
|
errWriteFailed = errors.New("bluetooth: write failed")
|
||||||
|
errNoRead = errors.New("bluetooth: read not supported")
|
||||||
|
errNoNotify = errors.New("bluetooth: notify/indicate not supported")
|
||||||
|
errEnableNotificationsFailed = errors.New("bluetooth: enable notifications failed")
|
||||||
|
)
|
||||||
|
|
||||||
|
// DiscoverServices starts a service discovery procedure. Pass a list of service
|
||||||
|
// UUIDs you are interested in to this function. Either a slice of all services
|
||||||
|
// is returned (of the same length as the requested UUIDs and in the same
|
||||||
|
// order), or if some services could not be discovered an error is returned.
|
||||||
|
//
|
||||||
|
// Passing a nil slice of UUIDs will return a complete list of
|
||||||
|
// services.
|
||||||
|
func (d Device) DiscoverServices(filterUUIDs []UUID) ([]DeviceService, error) {
|
||||||
|
// IAsyncOperation<GattDeviceServicesResult>
|
||||||
|
getServicesOperation, err := d.device.GetGattServicesWithCacheModeAsync(bluetooth.BluetoothCacheModeUncached)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := awaitAsyncOperation(getServicesOperation, genericattributeprofile.SignatureGattDeviceServicesResult); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := getServicesOperation.GetResults()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
servicesResult := (*genericattributeprofile.GattDeviceServicesResult)(res)
|
||||||
|
|
||||||
|
status, err := servicesResult.GetStatus()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if status != genericattributeprofile.GattCommunicationStatusSuccess {
|
||||||
|
return nil, fmt.Errorf("could not retrieve device services, operation failed with code %d", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IVectorView<GattDeviceService>
|
||||||
|
servicesVector, err := servicesResult.GetServices()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert services vector to array
|
||||||
|
servicesSize, err := servicesVector.GetSize()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var services []DeviceService
|
||||||
|
for i := uint32(0); i < servicesSize; i++ {
|
||||||
|
s, err := servicesVector.GetAt(i)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := (*genericattributeprofile.GattDeviceService)(s)
|
||||||
|
guid, err := srv.GetUuid()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceUuid := winRTUuidToUuid(guid)
|
||||||
|
|
||||||
|
// only include services that are included in the input filter
|
||||||
|
if len(filterUUIDs) > 0 {
|
||||||
|
found := false
|
||||||
|
for _, uuid := range filterUUIDs {
|
||||||
|
if serviceUuid.String() == uuid.String() {
|
||||||
|
// One of the services we're looking for.
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
services = append(services, DeviceService{
|
||||||
|
uuidWrapper: serviceUuid,
|
||||||
|
service: srv,
|
||||||
|
device: d,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return services, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func winRTUuidToUuid(uuid syscall.GUID) UUID {
|
||||||
|
return NewUUID([16]byte{
|
||||||
|
byte(uuid.Data1 >> 24),
|
||||||
|
byte(uuid.Data1 >> 16),
|
||||||
|
byte(uuid.Data1 >> 8),
|
||||||
|
byte(uuid.Data1),
|
||||||
|
byte(uuid.Data2 >> 8),
|
||||||
|
byte(uuid.Data2),
|
||||||
|
byte(uuid.Data3 >> 8),
|
||||||
|
byte(uuid.Data3),
|
||||||
|
uuid.Data4[0], uuid.Data4[1],
|
||||||
|
uuid.Data4[2], uuid.Data4[3],
|
||||||
|
uuid.Data4[4], uuid.Data4[5],
|
||||||
|
uuid.Data4[6], uuid.Data4[7],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// uuidWrapper is a type alias for UUID so we ensure no conflicts with
|
||||||
|
// struct method of the same name.
|
||||||
|
type uuidWrapper = UUID
|
||||||
|
|
||||||
|
// DeviceService is a BLE service on a connected peripheral device.
|
||||||
|
type DeviceService struct {
|
||||||
|
uuidWrapper
|
||||||
|
|
||||||
|
service *genericattributeprofile.GattDeviceService
|
||||||
|
device Device
|
||||||
|
}
|
||||||
|
|
||||||
|
// UUID returns the UUID for this DeviceService.
|
||||||
|
func (s DeviceService) UUID() UUID {
|
||||||
|
return s.uuidWrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
// DiscoverCharacteristics discovers characteristics in this service. Pass a
|
||||||
|
// list of characteristic UUIDs you are interested in to this function. Either a
|
||||||
|
// list of all requested characteristics is returned, or if some characteristics could not be
|
||||||
|
// discovered an error is returned. If there is no error, the characteristics
|
||||||
|
// slice has the same length as the UUID slice with characteristics in the same
|
||||||
|
// order in the slice as in the requested UUID list.
|
||||||
|
//
|
||||||
|
// Passing a nil slice of UUIDs will return a complete
|
||||||
|
// list of characteristics.
|
||||||
|
func (s DeviceService) DiscoverCharacteristics(filterUUIDs []UUID) ([]DeviceCharacteristic, error) {
|
||||||
|
getCharacteristicsOp, err := s.service.GetCharacteristicsWithCacheModeAsync(bluetooth.BluetoothCacheModeUncached)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// IAsyncOperation<GattCharacteristicsResult>
|
||||||
|
if err := awaitAsyncOperation(getCharacteristicsOp, genericattributeprofile.SignatureGattCharacteristicsResult); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := getCharacteristicsOp.GetResults()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gattCharResult := (*genericattributeprofile.GattCharacteristicsResult)(res)
|
||||||
|
|
||||||
|
// IVectorView<GattCharacteristic>
|
||||||
|
charVector, err := gattCharResult.GetCharacteristics()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert characteristics vector to array
|
||||||
|
characteristicsSize, err := charVector.GetSize()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var characteristics []DeviceCharacteristic
|
||||||
|
for i := uint32(0); i < characteristicsSize; i++ {
|
||||||
|
c, err := charVector.GetAt(i)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
characteristic := (*genericattributeprofile.GattCharacteristic)(c)
|
||||||
|
guid, err := characteristic.GetUuid()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
characteristicUUID := winRTUuidToUuid(guid)
|
||||||
|
|
||||||
|
properties, err := characteristic.GetCharacteristicProperties()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// only include characteristics that are included in the input filter
|
||||||
|
if len(filterUUIDs) > 0 {
|
||||||
|
found := false
|
||||||
|
for _, uuid := range filterUUIDs {
|
||||||
|
if characteristicUUID.String() == uuid.String() {
|
||||||
|
// One of the characteristics we're looking for.
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
characteristics = append(characteristics, DeviceCharacteristic{
|
||||||
|
uuidWrapper: characteristicUUID,
|
||||||
|
service: s,
|
||||||
|
characteristic: characteristic,
|
||||||
|
properties: properties,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return characteristics, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceCharacteristic is a BLE characteristic on a connected peripheral
|
||||||
|
// device.
|
||||||
|
type DeviceCharacteristic struct {
|
||||||
|
uuidWrapper
|
||||||
|
|
||||||
|
characteristic *genericattributeprofile.GattCharacteristic
|
||||||
|
properties genericattributeprofile.GattCharacteristicProperties
|
||||||
|
|
||||||
|
service DeviceService
|
||||||
|
}
|
||||||
|
|
||||||
|
// UUID returns the UUID for this DeviceCharacteristic.
|
||||||
|
func (c DeviceCharacteristic) UUID() UUID {
|
||||||
|
return c.uuidWrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c DeviceCharacteristic) Properties() uint32 {
|
||||||
|
return uint32(c.properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMTU returns the MTU for the characteristic.
|
||||||
|
func (c DeviceCharacteristic) GetMTU() (uint16, error) {
|
||||||
|
return c.service.device.session.GetMaxPduSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write replaces the characteristic value with a new value. The
|
||||||
|
// call will return after all data has been written.
|
||||||
|
func (c DeviceCharacteristic) Write(p []byte) (n int, err error) {
|
||||||
|
if c.properties&genericattributeprofile.GattCharacteristicPropertiesWrite == 0 {
|
||||||
|
return 0, errNoWrite
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.write(p, genericattributeprofile.GattWriteOptionWriteWithResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteWithoutResponse replaces the characteristic value with a new value. The
|
||||||
|
// call will return before all data has been written. A limited number of such
|
||||||
|
// writes can be in flight at any given time. This call is also known as a
|
||||||
|
// "write command" (as opposed to a write request).
|
||||||
|
func (c DeviceCharacteristic) WriteWithoutResponse(p []byte) (n int, err error) {
|
||||||
|
if c.properties&genericattributeprofile.GattCharacteristicPropertiesWriteWithoutResponse == 0 {
|
||||||
|
return 0, errNoWriteWithoutResponse
|
||||||
|
}
|
||||||
|
return c.write(p, genericattributeprofile.GattWriteOptionWriteWithoutResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c DeviceCharacteristic) write(p []byte, mode genericattributeprofile.GattWriteOption) (n int, err error) {
|
||||||
|
// Convert data to buffer
|
||||||
|
writer, err := streams.NewDataWriter()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer writer.Release()
|
||||||
|
|
||||||
|
// Add bytes to writer
|
||||||
|
if err := writer.WriteBytes(uint32(len(p)), p); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := writer.DetachBuffer()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// IAsyncOperation<GattCommunicationStatus>
|
||||||
|
asyncOp, err := c.characteristic.WriteValueWithOptionAsync(value, mode)
|
||||||
|
|
||||||
|
if err := awaitAsyncOperation(asyncOp, genericattributeprofile.SignatureGattCommunicationStatus); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := asyncOp.GetResults()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
status := genericattributeprofile.GattCommunicationStatus(uintptr(res))
|
||||||
|
|
||||||
|
// Is the status success?
|
||||||
|
if status != genericattributeprofile.GattCommunicationStatusSuccess {
|
||||||
|
return 0, errWriteFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read reads the current characteristic value.
|
||||||
|
func (c DeviceCharacteristic) Read(data []byte) (int, error) {
|
||||||
|
if c.properties&genericattributeprofile.GattCharacteristicPropertiesRead == 0 {
|
||||||
|
return 0, errNoRead
|
||||||
|
}
|
||||||
|
|
||||||
|
readOp, err := c.characteristic.ReadValueWithCacheModeAsync(bluetooth.BluetoothCacheModeUncached)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// IAsyncOperation<GattReadResult>
|
||||||
|
if err := awaitAsyncOperation(readOp, genericattributeprofile.SignatureGattReadResult); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := readOp.GetResults()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := (*genericattributeprofile.GattReadResult)(res)
|
||||||
|
|
||||||
|
buffer, err := result.GetValue()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
datareader, err := streams.DataReaderFromBuffer(buffer)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bufferlen, err := buffer.GetLength()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
readBuffer, err := datareader.ReadBytes(bufferlen)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(data, readBuffer)
|
||||||
|
return len(readBuffer), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableNotifications enables notifications in the Client Characteristic
|
||||||
|
// Configuration Descriptor (CCCD). This means that most peripherals will send a
|
||||||
|
// notification with a new value every time the value of the characteristic
|
||||||
|
// changes.
|
||||||
|
func (c DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) error {
|
||||||
|
if (c.properties&genericattributeprofile.GattCharacteristicPropertiesNotify == 0) &&
|
||||||
|
(c.properties&genericattributeprofile.GattCharacteristicPropertiesIndicate == 0) {
|
||||||
|
return errNoNotify
|
||||||
|
}
|
||||||
|
|
||||||
|
// listen value changed event
|
||||||
|
// TypedEventHandler<GattCharacteristic,GattValueChangedEventArgs>
|
||||||
|
guid := winrt.ParameterizedInstanceGUID(foundation.GUIDTypedEventHandler, genericattributeprofile.SignatureGattCharacteristic, genericattributeprofile.SignatureGattValueChangedEventArgs)
|
||||||
|
valueChangedEventHandler := foundation.NewTypedEventHandler(ole.NewGUID(guid), func(instance *foundation.TypedEventHandler, sender, args unsafe.Pointer) {
|
||||||
|
valueChangedEvent := (*genericattributeprofile.GattValueChangedEventArgs)(args)
|
||||||
|
|
||||||
|
buf, err := valueChangedEvent.GetCharacteristicValue()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := streams.DataReaderFromBuffer(buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer reader.Release()
|
||||||
|
|
||||||
|
buflen, err := buf.GetLength()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := reader.ReadBytes(buflen)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(data)
|
||||||
|
})
|
||||||
|
_, err := c.characteristic.AddValueChanged(valueChangedEventHandler)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var writeOp *foundation.IAsyncOperation
|
||||||
|
if c.properties&genericattributeprofile.GattCharacteristicPropertiesNotify != 0 {
|
||||||
|
writeOp, err = c.characteristic.WriteClientCharacteristicConfigurationDescriptorAsync(genericattributeprofile.GattClientCharacteristicConfigurationDescriptorValueNotify)
|
||||||
|
} else {
|
||||||
|
writeOp, err = c.characteristic.WriteClientCharacteristicConfigurationDescriptorAsync(genericattributeprofile.GattClientCharacteristicConfigurationDescriptorValueIndicate)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// IAsyncOperation<GattCommunicationStatus>
|
||||||
|
if err := awaitAsyncOperation(writeOp, genericattributeprofile.SignatureGattCommunicationStatus); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := writeOp.GetResults()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := genericattributeprofile.GattCommunicationStatus(uintptr(res))
|
||||||
|
|
||||||
|
if result != genericattributeprofile.GattCommunicationStatusSuccess {
|
||||||
|
return errEnableNotificationsFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
14
gatts.go
14
gatts.go
|
@ -7,6 +7,8 @@ type Service struct {
|
||||||
Characteristics []CharacteristicConfig
|
Characteristics []CharacteristicConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WriteEvent = func(client Connection, offset int, value []byte)
|
||||||
|
|
||||||
// CharacteristicConfig contains some parameters for the configuration of a
|
// CharacteristicConfig contains some parameters for the configuration of a
|
||||||
// single characteristic.
|
// single characteristic.
|
||||||
//
|
//
|
||||||
|
@ -17,7 +19,7 @@ type CharacteristicConfig struct {
|
||||||
UUID
|
UUID
|
||||||
Value []byte
|
Value []byte
|
||||||
Flags CharacteristicPermissions
|
Flags CharacteristicPermissions
|
||||||
WriteEvent func(client Connection, offset int, value []byte)
|
WriteEvent WriteEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
// CharacteristicPermissions lists a number of basic permissions/capabilities
|
// CharacteristicPermissions lists a number of basic permissions/capabilities
|
||||||
|
@ -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
128
gatts_hci.go
Normal 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
|
||||||
|
}
|
182
gatts_linux.go
182
gatts_linux.go
|
@ -1,89 +1,156 @@
|
||||||
// +build !baremetal
|
//go: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
|
|
||||||
}
|
|
||||||
|
|
||||||
bluezService, err := app.NewService(s.UUID.String())
|
// All objects that will be part of the ObjectManager.
|
||||||
if err != nil {
|
objects := map[dbus.ObjectPath]map[string]map[string]*prop.Prop{}
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = app.AddService(bluezService)
|
// Define the service to be exported over DBus.
|
||||||
if err != nil {
|
serviceSpec := map[string]map[string]*prop.Prop{
|
||||||
return err
|
"org.bluez.GattService1": {
|
||||||
|
"UUID": {Value: s.UUID.String()},
|
||||||
|
"Primary": {Value: true},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
objects[path] = serviceSpec
|
||||||
|
|
||||||
for _, char := range s.Characteristics {
|
for i, char := range s.Characteristics {
|
||||||
// Create characteristic handle.
|
// Calculate Flags field.
|
||||||
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.
|
||||||
|
@ -92,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
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// +build !linux
|
//go:build !linux && !windows
|
||||||
|
|
||||||
package bluetooth
|
package bluetooth
|
||||||
|
|
||||||
|
|
69
gatts_sd.go
69
gatts_sd.go
|
@ -1,20 +1,33 @@
|
||||||
// +build softdevice
|
//go:build softdevice
|
||||||
|
|
||||||
package bluetooth
|
package bluetooth
|
||||||
|
|
||||||
/*
|
/*
|
||||||
// Define SoftDevice functions as regular function declarations (not inline
|
|
||||||
// static functions).
|
|
||||||
#define SVCALL_AS_NORMAL_FUNCTION
|
|
||||||
|
|
||||||
#include "ble_gap.h"
|
#include "ble_gap.h"
|
||||||
|
#include "ble_gatts.h"
|
||||||
|
|
||||||
|
// Workaround wrapper functions which prevent pointer arguments escaping to heap
|
||||||
|
static inline uint32_t sd_ble_gatts_hvx_noescape(uint16_t conn_handle, uint16_t handle, uint8_t type, uint16_t offset, uint16_t len, uint8_t *p_data) {
|
||||||
|
ble_gatts_hvx_params_t p_hvx_params = {handle, type, offset, &len, p_data};
|
||||||
|
return sd_ble_gatts_hvx(conn_handle, &p_hvx_params);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint32_t sd_ble_gatts_value_set_noescape(uint16_t conn_handle, uint16_t handle, uint16_t len, uint8_t *value) {
|
||||||
|
ble_gatts_value_t p_value = {
|
||||||
|
.len = len,
|
||||||
|
.offset = 0,
|
||||||
|
.p_value = value,
|
||||||
|
};
|
||||||
|
return sd_ble_gatts_value_set(conn_handle, handle, &p_value);
|
||||||
|
}
|
||||||
*/
|
*/
|
||||||
import "C"
|
import "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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,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 {
|
||||||
|
@ -48,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)
|
||||||
}
|
}
|
||||||
|
@ -81,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
|
||||||
|
@ -110,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.
|
||||||
|
@ -136,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
309
gatts_windows.go
Normal file
|
@ -0,0 +1,309 @@
|
||||||
|
package bluetooth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/go-ole/go-ole"
|
||||||
|
"github.com/saltosystems/winrt-go"
|
||||||
|
"github.com/saltosystems/winrt-go/windows/devices/bluetooth/genericattributeprofile"
|
||||||
|
"github.com/saltosystems/winrt-go/windows/foundation"
|
||||||
|
"github.com/saltosystems/winrt-go/windows/foundation/collections"
|
||||||
|
"github.com/saltosystems/winrt-go/windows/storage/streams"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Characteristic is a single characteristic in a service. It has an UUID and a
|
||||||
|
// value.
|
||||||
|
type Characteristic struct {
|
||||||
|
wintCharacteristic *genericattributeprofile.GattLocalCharacteristic
|
||||||
|
writeEvent WriteEvent
|
||||||
|
flags CharacteristicPermissions
|
||||||
|
|
||||||
|
valueMtx *sync.Mutex
|
||||||
|
value []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddService creates a new service with the characteristics listed in the
|
||||||
|
// Service struct.
|
||||||
|
func (a *Adapter) AddService(s *Service) error {
|
||||||
|
gattServiceOp, err := genericattributeprofile.GattServiceProviderCreateAsync(syscallUUIDFromUUID(s.UUID))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = awaitAsyncOperation(gattServiceOp, genericattributeprofile.SignatureGattServiceProviderResult); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := gattServiceOp.GetResults()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceProviderResult := (*genericattributeprofile.GattServiceProviderResult)(res)
|
||||||
|
serviceProvider, err := serviceProviderResult.GetServiceProvider()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
localService, err := serviceProvider.GetService()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: "ParameterizedInstanceGUID" + "foundation.NewTypedEventHandler"
|
||||||
|
// seems to always return the same instance, need to figure out how to get different instances each time...
|
||||||
|
// was following c# source for this flow: https://github.com/microsoft/Windows-universal-samples/blob/main/Samples/BluetoothLE/cs/Scenario3_ServerForeground.xaml.cs
|
||||||
|
// which relies on instanced event handlers. for now we'll manually setup our handlers with a map of golang characteristics
|
||||||
|
//
|
||||||
|
// TypedEventHandler<GattLocalCharacteristic,GattWriteRequestedEventArgs>
|
||||||
|
guid := winrt.ParameterizedInstanceGUID(
|
||||||
|
foundation.GUIDTypedEventHandler,
|
||||||
|
genericattributeprofile.SignatureGattLocalCharacteristic,
|
||||||
|
genericattributeprofile.SignatureGattWriteRequestedEventArgs)
|
||||||
|
|
||||||
|
goChars := map[syscall.GUID]*Characteristic{}
|
||||||
|
|
||||||
|
writeRequestedHandler := foundation.NewTypedEventHandler(ole.NewGUID(guid), func(instance *foundation.TypedEventHandler, sender, args unsafe.Pointer) {
|
||||||
|
writeReqArgs := (*genericattributeprofile.GattWriteRequestedEventArgs)(args)
|
||||||
|
reqAsyncOp, err := writeReqArgs.GetRequestAsync()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = awaitAsyncOperation(reqAsyncOp, genericattributeprofile.SignatureGattWriteRequest); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := reqAsyncOp.GetResults()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gattWriteRequest := (*genericattributeprofile.GattWriteRequest)(res)
|
||||||
|
|
||||||
|
buf, err := gattWriteRequest.GetValue()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
offset, err := gattWriteRequest.GetOffset()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
characteristic := (*genericattributeprofile.GattLocalCharacteristic)(sender)
|
||||||
|
uuid, err := characteristic.GetUuid()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
goChar, ok := goChars[uuid]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if goChar.writeEvent != nil {
|
||||||
|
// TODO: connection?
|
||||||
|
goChar.writeEvent(0, int(offset), bufferToSlice(buf))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
guid = winrt.ParameterizedInstanceGUID(
|
||||||
|
foundation.GUIDTypedEventHandler,
|
||||||
|
genericattributeprofile.SignatureGattLocalCharacteristic,
|
||||||
|
genericattributeprofile.SignatureGattReadRequestedEventArgs)
|
||||||
|
|
||||||
|
readRequestedHandler := foundation.NewTypedEventHandler(ole.NewGUID(guid), func(instance *foundation.TypedEventHandler, sender, args unsafe.Pointer) {
|
||||||
|
readReqArgs := (*genericattributeprofile.GattReadRequestedEventArgs)(args)
|
||||||
|
reqAsyncOp, err := readReqArgs.GetRequestAsync()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = awaitAsyncOperation(reqAsyncOp, genericattributeprofile.SignatureGattReadRequest); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := reqAsyncOp.GetResults()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gattReadRequest := (*genericattributeprofile.GattReadRequest)(res)
|
||||||
|
|
||||||
|
characteristic := (*genericattributeprofile.GattLocalCharacteristic)(sender)
|
||||||
|
uuid, err := characteristic.GetUuid()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
goChar, ok := goChars[uuid]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writer, err := streams.NewDataWriter()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer writer.Release()
|
||||||
|
|
||||||
|
goChar.valueMtx.Lock()
|
||||||
|
defer goChar.valueMtx.Unlock()
|
||||||
|
if len(goChar.value) > 0 {
|
||||||
|
if err = writer.WriteBytes(uint32(len(goChar.value)), goChar.value); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := writer.DetachBuffer()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gattReadRequest.RespondWithValue(buf)
|
||||||
|
buf.Release()
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, char := range s.Characteristics {
|
||||||
|
params, err := genericattributeprofile.NewGattLocalCharacteristicParameters()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = params.SetCharacteristicProperties(genericattributeprofile.GattCharacteristicProperties(char.Flags)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
uuid := syscallUUIDFromUUID(char.UUID)
|
||||||
|
createCharOp, err := localService.CreateCharacteristicAsync(uuid, params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = awaitAsyncOperation(createCharOp, genericattributeprofile.SignatureGattLocalCharacteristicResult); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := createCharOp.GetResults()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
characteristicResults := (*genericattributeprofile.GattLocalCharacteristicResult)(res)
|
||||||
|
characteristic, err := characteristicResults.GetCharacteristic()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = characteristic.AddWriteRequested(writeRequestedHandler)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = characteristic.AddReadRequested(readRequestedHandler)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the object around for Characteristic.Write.
|
||||||
|
if char.Handle != nil {
|
||||||
|
char.Handle.wintCharacteristic = characteristic
|
||||||
|
char.Handle.value = char.Value
|
||||||
|
char.Handle.valueMtx = &sync.Mutex{}
|
||||||
|
char.Handle.flags = char.Flags
|
||||||
|
char.Handle.writeEvent = char.WriteEvent
|
||||||
|
goChars[uuid] = char.Handle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params, err := genericattributeprofile.NewGattServiceProviderAdvertisingParameters()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = params.SetIsConnectable(true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = params.SetIsDiscoverable(true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return serviceProvider.StartAdvertisingWithParameters(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write replaces the characteristic value with a new value.
|
||||||
|
func (c *Characteristic) Write(p []byte) (n int, err error) {
|
||||||
|
length := len(p)
|
||||||
|
|
||||||
|
if length == 0 {
|
||||||
|
return 0, nil // nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.writeEvent != nil {
|
||||||
|
c.writeEvent(0, 0, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writes are only actually processed on read events from clients, we just set a variable here.
|
||||||
|
c.valueMtx.Lock()
|
||||||
|
defer c.valueMtx.Unlock()
|
||||||
|
c.value = p
|
||||||
|
|
||||||
|
// only notify if it's enabled, otherwise the below leads to an error
|
||||||
|
if c.flags&CharacteristicNotifyPermission != 0 {
|
||||||
|
writer, err := streams.NewDataWriter()
|
||||||
|
if err != nil {
|
||||||
|
return length, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer writer.Release()
|
||||||
|
err = writer.WriteBytes(uint32(len(p)), p)
|
||||||
|
if err != nil {
|
||||||
|
return length, err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := writer.DetachBuffer()
|
||||||
|
if err != nil {
|
||||||
|
return length, err
|
||||||
|
}
|
||||||
|
defer buf.Release()
|
||||||
|
|
||||||
|
op, err := c.wintCharacteristic.NotifyValueAsync(buf)
|
||||||
|
if err != nil {
|
||||||
|
return length, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// IVectorView<GattClientNotificationResult>
|
||||||
|
signature := fmt.Sprintf("pinterface({%s};%s)", collections.GUIDIVectorView, genericattributeprofile.SignatureGattClientNotificationResult)
|
||||||
|
if err = awaitAsyncOperation(op, signature); err != nil {
|
||||||
|
return length, err
|
||||||
|
}
|
||||||
|
defer op.Release()
|
||||||
|
|
||||||
|
res, err := op.GetResults()
|
||||||
|
if err != nil {
|
||||||
|
return length, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: process notification results, just getting this to release
|
||||||
|
vec := (*collections.IVectorView)(res)
|
||||||
|
vec.Release()
|
||||||
|
}
|
||||||
|
|
||||||
|
return length, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func syscallUUIDFromUUID(uuid UUID) syscall.GUID {
|
||||||
|
guid := ole.NewGUID(uuid.String())
|
||||||
|
return syscall.GUID{
|
||||||
|
Data1: guid.Data1,
|
||||||
|
Data2: guid.Data2,
|
||||||
|
Data3: guid.Data3,
|
||||||
|
Data4: guid.Data4,
|
||||||
|
}
|
||||||
|
}
|
33
go.mod
33
go.mod
|
@ -1,16 +1,25 @@
|
||||||
module tinygo.org/x/bluetooth
|
module gitrepo.ru/neonxp/bluetooth
|
||||||
|
|
||||||
go 1.15
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/JuulLabs-OSS/cbgo v0.0.2
|
github.com/go-ole/go-ole v1.2.6
|
||||||
github.com/go-ole/go-ole v1.2.4
|
github.com/godbus/dbus/v5 v5.1.0
|
||||||
github.com/godbus/dbus/v5 v5.0.3
|
github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b
|
||||||
github.com/kr/pretty v0.1.0 // indirect
|
github.com/soypat/cyw43439 v0.0.0-20240609122733-da9153086796
|
||||||
github.com/muka/go-bluetooth v0.0.0-20200928120822-44d49b402aee
|
github.com/tinygo-org/cbgo v0.0.4
|
||||||
github.com/sirupsen/logrus v1.6.0 // indirect
|
golang.org/x/crypto v0.12.0
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
tinygo.org/x/drivers v0.26.1-0.20230922160320-ed51435c2ef6
|
||||||
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 // indirect
|
tinygo.org/x/tinyfont v0.4.0
|
||||||
gopkg.in/yaml.v2 v2.2.8 // indirect
|
tinygo.org/x/tinyterm v0.3.0
|
||||||
tinygo.org/x/drivers v0.13.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
|
||||||
)
|
)
|
||||||
|
|
104
go.sum
104
go.sum
|
@ -1,75 +1,49 @@
|
||||||
github.com/JuulLabs-OSS/cbgo v0.0.2 h1:gCDyT0+EPuI8GOFyvAksFcVD2vF4CXBAVwT6uVnD9oo=
|
|
||||||
github.com/JuulLabs-OSS/cbgo v0.0.2/go.mod h1:L4YtGP+gnyD84w7+jN66ncspFRfOYB5aj9QSXaFHmBA=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/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/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||||
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||||
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||||
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
|
||||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/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.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
|
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/muka/go-bluetooth v0.0.0-20200619025933-f6113f7141c5 h1:xnTS/7y0g28W2SJeWNLMYTiTOmfW2P/YdPByoQnPvVo=
|
|
||||||
github.com/muka/go-bluetooth v0.0.0-20200619025933-f6113f7141c5/go.mod h1:yV39+EVOWdnoTe75NyKdo9iuyI3Slyh4t7eQvElUbWE=
|
|
||||||
github.com/muka/go-bluetooth v0.0.0-20200926181701-4ca7d8dd0ff5 h1:2n6xusPU4MxghLXhYNpGSYYov4AjnARQfA2xIPfqelA=
|
|
||||||
github.com/muka/go-bluetooth v0.0.0-20200926181701-4ca7d8dd0ff5/go.mod h1:dMCjicU6vRBk34dqOmIZm0aod6gUwZXOXzBROqGous0=
|
|
||||||
github.com/muka/go-bluetooth v0.0.0-20200928120822-44d49b402aee h1:viY7YR4j7hyLKFK9peIU+FysyOJ47ezp4oBwhiEW9Hk=
|
|
||||||
github.com/muka/go-bluetooth v0.0.0-20200928120822-44d49b402aee/go.mod h1:dMCjicU6vRBk34dqOmIZm0aod6gUwZXOXzBROqGous0=
|
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
|
||||||
github.com/paypal/gatt v0.0.0-20151011220935-4ae819d591cf/go.mod h1:+AwQL2mK3Pd3S+TUwg0tYQjid0q1txyNUJuuSmz8Kdk=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q=
|
github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b h1:du3zG5fd8snsFN6RBoLA7fpaYV9ZQIsyH9snlk2Zvik=
|
||||||
|
github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA=
|
||||||
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
|
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
|
||||||
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/soypat/cyw43439 v0.0.0-20240609122733-da9153086796 h1:1/r2URInjjFtWqT61gU7YGVCq3BRyXt/C7z4oLRF9Lo=
|
||||||
|
github.com/soypat/cyw43439 v0.0.0-20240609122733-da9153086796/go.mod h1:1Otjk6PRhfzfcVHeWMEeku/VntFqWghUwuSQyivb2vE=
|
||||||
|
github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef h1:phH95I9wANjTYw6bSYLZDQfNvao+HqYDom8owbNa0P4=
|
||||||
|
github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef/go.mod h1:oCVCNGCHMKoBj97Zp9znLbQ1nHxpkmOY9X+UAGzOxc8=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q=
|
||||||
github.com/suapapa/go_eddystone v1.3.1/go.mod h1:bXC11TfJOS+3g3q/Uzd7FKd5g62STQEfeEIhcKe4Qy8=
|
github.com/tinygo-org/cbgo v0.0.4 h1:3D76CRYbH03Rudi8sEgs/YO0x3JIMdyq8jlQtk/44fU=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/tinygo-org/cbgo v0.0.4/go.mod h1:7+HgWIHd4nbAz0ESjGlJ1/v9LDU1Ox8MGzP9mah/fLk=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899 h1:/DyaXDEWMqoVUVEJVJIlNk1bXTbFs8s3Q4GdPInSKTQ=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899/go.mod h1:LU7Dw00NJ+N86QkeTGjMLNkYcEYMor6wTDpTCu0EaH8=
|
||||||
golang.org/x/crypto v0.0.0-20200602180216-279210d13fed h1:g4KENRiCMEx58Q7/ecwfT0N2o8z35Fnbsjig/Alf2T4=
|
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
|
||||||
golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 h1:/yRP+0AN7mf5DkD3BAI6TOFnd51gEoDEb8o35jIFtgw=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||||
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 h1:DvY3Zkh7KabQE/kfzMvYvKirSiguP9Q/veMtkYyf0o8=
|
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
|
||||||
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
tinygo.org/x/drivers v0.13.0 h1:ohzhFiPb/5dcRop3X+Gdvsr6uswmnpfMX9KsAMtgtTM=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
tinygo.org/x/drivers v0.13.0/go.mod h1:mShi1lpVtJFpApkZgwyrzDKHToeGfWIuB08utyHxZ7g=
|
tinygo.org/x/drivers v0.26.1-0.20230922160320-ed51435c2ef6 h1:w18u47MirULgAl+bP0piUGu5VUZDs7TvXwHASEVXqHk=
|
||||||
|
tinygo.org/x/drivers v0.26.1-0.20230922160320-ed51435c2ef6/go.mod h1:X7utcg3yfFUFuKLOMTZD56eztXMjpkcf8OHldfTBsjw=
|
||||||
|
tinygo.org/x/tinyfont v0.4.0 h1:XexPKEKiHInf6p4CMCJwsIheVPY0T46HUs6ictYyZfE=
|
||||||
|
tinygo.org/x/tinyfont v0.4.0/go.mod h1:7nVj3j3geqBoPDzpFukAhF1C8AP9YocMsZy0HSAcGCA=
|
||||||
|
tinygo.org/x/tinyterm v0.3.0 h1:4MMZoMyrbWbjru1KP/Z2TGhaguy/Uh5Mdhf/niemM8c=
|
||||||
|
tinygo.org/x/tinyterm v0.3.0/go.mod h1:F1pQjxEwNZQIc5czeJSBtk57ucEvbR4u7vHaLhWhHtg=
|
||||||
|
|
787
hci.go
Normal file
787
hci.go
Normal 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
156
l2cap_hci.go
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
//go:build ninafw || hci || cyw43439
|
||||||
|
|
||||||
|
package bluetooth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
connectionParamUpdateRequest = 0x12
|
||||||
|
connectionParamUpdateResponse = 0x13
|
||||||
|
)
|
||||||
|
|
||||||
|
type l2capConnectionParamReqPkt struct {
|
||||||
|
minInterval uint16
|
||||||
|
maxInterval uint16
|
||||||
|
latency uint16
|
||||||
|
timeout uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *l2capConnectionParamReqPkt) Write(buf []byte) (int, error) {
|
||||||
|
l.minInterval = binary.LittleEndian.Uint16(buf[0:])
|
||||||
|
l.maxInterval = binary.LittleEndian.Uint16(buf[2:])
|
||||||
|
l.latency = binary.LittleEndian.Uint16(buf[4:])
|
||||||
|
l.timeout = binary.LittleEndian.Uint16(buf[6:])
|
||||||
|
|
||||||
|
return 8, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *l2capConnectionParamReqPkt) Read(p []byte) (int, error) {
|
||||||
|
binary.LittleEndian.PutUint16(p[0:], l.minInterval)
|
||||||
|
binary.LittleEndian.PutUint16(p[2:], l.maxInterval)
|
||||||
|
binary.LittleEndian.PutUint16(p[4:], l.latency)
|
||||||
|
binary.LittleEndian.PutUint16(p[6:], l.timeout)
|
||||||
|
|
||||||
|
return 8, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type l2capConnectionParamResponsePkt struct {
|
||||||
|
code uint8
|
||||||
|
identifier uint8
|
||||||
|
length uint16
|
||||||
|
value uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *l2capConnectionParamResponsePkt) Read(p []byte) (int, error) {
|
||||||
|
p[0] = l.code
|
||||||
|
p[1] = l.identifier
|
||||||
|
binary.LittleEndian.PutUint16(p[2:], l.length)
|
||||||
|
binary.LittleEndian.PutUint16(p[4:], l.value)
|
||||||
|
|
||||||
|
return 6, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type l2cap struct {
|
||||||
|
hci *hci
|
||||||
|
}
|
||||||
|
|
||||||
|
func newL2CAP(hci *hci) *l2cap {
|
||||||
|
return &l2cap{
|
||||||
|
hci: hci,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *l2cap) addConnection(handle uint16, role uint8, interval, timeout uint16) error {
|
||||||
|
if role != 0x01 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var b [12]byte
|
||||||
|
b[0] = connectionParamUpdateRequest
|
||||||
|
b[1] = 0x01
|
||||||
|
binary.LittleEndian.PutUint16(b[2:], 8)
|
||||||
|
binary.LittleEndian.PutUint16(b[4:], interval)
|
||||||
|
binary.LittleEndian.PutUint16(b[6:], interval)
|
||||||
|
binary.LittleEndian.PutUint16(b[8:], 0)
|
||||||
|
binary.LittleEndian.PutUint16(b[10:], timeout)
|
||||||
|
|
||||||
|
return l.sendReq(handle, b[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *l2cap) removeConnection(handle uint16) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *l2cap) handleData(handle uint16, buf []byte) error {
|
||||||
|
code := buf[0]
|
||||||
|
identifier := buf[1]
|
||||||
|
//length := binary.LittleEndian.Uint16(buf[2:4])
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
println("l2cap.handleData:", handle, "data:", hex.EncodeToString(buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: check length
|
||||||
|
|
||||||
|
switch code {
|
||||||
|
case connectionParamUpdateRequest:
|
||||||
|
return l.handleParameterUpdateRequest(handle, identifier, buf[4:])
|
||||||
|
|
||||||
|
case connectionParamUpdateResponse:
|
||||||
|
return l.handleParameterUpdateResponse(handle, identifier, buf[4:])
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *l2cap) handleParameterUpdateRequest(connectionHandle uint16, identifier uint8, data []byte) error {
|
||||||
|
if debug {
|
||||||
|
println("l2cap.handleParameterUpdateRequest:", connectionHandle, "data:", hex.EncodeToString(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
req := l2capConnectionParamReqPkt{}
|
||||||
|
req.Write(data)
|
||||||
|
|
||||||
|
// TODO: check against min/max
|
||||||
|
|
||||||
|
resp := l2capConnectionParamResponsePkt{
|
||||||
|
code: connectionParamUpdateResponse,
|
||||||
|
identifier: identifier,
|
||||||
|
length: 2,
|
||||||
|
value: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
var b [6]byte
|
||||||
|
resp.Read(b[:])
|
||||||
|
|
||||||
|
if err := l.sendReq(connectionHandle, b[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// valid so update connection parameters
|
||||||
|
if resp.value == 0 {
|
||||||
|
return l.hci.leConnUpdate(connectionHandle, req.minInterval, req.maxInterval, req.latency, req.timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *l2cap) handleParameterUpdateResponse(connectionHandle uint16, identifier uint8, data []byte) error {
|
||||||
|
if debug {
|
||||||
|
println("l2cap.handleParameterUpdateResponse:", connectionHandle, "data:", hex.EncodeToString(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// for now do nothing
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *l2cap) sendReq(handle uint16, data []byte) error {
|
||||||
|
if debug {
|
||||||
|
println("l2cap.sendReq:", handle, "data:", hex.EncodeToString(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.hci.sendAclPkt(handle, signalingCID, data)
|
||||||
|
}
|
2
mac.go
2
mac.go
|
@ -36,7 +36,7 @@ func ParseMAC(s string) (mac MAC, err error) {
|
||||||
}
|
}
|
||||||
macIndex--
|
macIndex--
|
||||||
}
|
}
|
||||||
if macIndex != 0 {
|
if macIndex != -1 {
|
||||||
err = errInvalidMAC
|
err = errInvalidMAC
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|
5
nodebug.go
Normal file
5
nodebug.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
//go:build !bledebug
|
||||||
|
|
||||||
|
package bluetooth
|
||||||
|
|
||||||
|
var debug = false
|
|
@ -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.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// +build nrf
|
//go:build nrf
|
||||||
|
|
||||||
package rawterm
|
package rawterm
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2012 - 2019, Nordic Semiconductor ASA
|
* Copyright (c) 2012 - 2020, Nordic Semiconductor ASA
|
||||||
* All rights reserved.
|
* All rights reserved.
|
||||||
*
|
*
|
||||||
* Redistribution and use in source and binary forms, with or without modification,
|
* Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
@ -89,8 +89,9 @@ enum BLE_COMMON_SVCS
|
||||||
*/
|
*/
|
||||||
enum BLE_COMMON_EVTS
|
enum BLE_COMMON_EVTS
|
||||||
{
|
{
|
||||||
BLE_EVT_USER_MEM_REQUEST = BLE_EVT_BASE + 0, /**< User Memory request. @ref ble_evt_user_mem_request_t */
|
BLE_EVT_USER_MEM_REQUEST = BLE_EVT_BASE + 0, /**< User Memory request. See @ref ble_evt_user_mem_request_t
|
||||||
BLE_EVT_USER_MEM_RELEASE = BLE_EVT_BASE + 1, /**< User Memory release. @ref ble_evt_user_mem_release_t */
|
\n Reply with @ref sd_ble_user_mem_reply. */
|
||||||
|
BLE_EVT_USER_MEM_RELEASE = BLE_EVT_BASE + 1, /**< User Memory release. See @ref ble_evt_user_mem_release_t */
|
||||||
};
|
};
|
||||||
|
|
||||||
/**@brief BLE Connection Configuration IDs.
|
/**@brief BLE Connection Configuration IDs.
|
||||||
|
@ -328,7 +329,6 @@ typedef union
|
||||||
* place of @ref ble_conn_cfg_t::conn_cfg_tag.
|
* place of @ref ble_conn_cfg_t::conn_cfg_tag.
|
||||||
*
|
*
|
||||||
* @sa sd_ble_gap_adv_start()
|
* @sa sd_ble_gap_adv_start()
|
||||||
* @sa sd_ble_gap_connect()
|
|
||||||
*
|
*
|
||||||
* @mscs
|
* @mscs
|
||||||
* @mmsc{@ref BLE_CONN_CFG}
|
* @mmsc{@ref BLE_CONN_CFG}
|
||||||
|
@ -338,7 +338,7 @@ typedef union
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
uint8_t conn_cfg_tag; /**< The application chosen tag it can use with the
|
uint8_t conn_cfg_tag; /**< The application chosen tag it can use with the
|
||||||
@ref sd_ble_gap_adv_start() and @ref sd_ble_gap_connect() calls
|
@ref sd_ble_gap_adv_start() call
|
||||||
to select this configuration when creating a connection.
|
to select this configuration when creating a connection.
|
||||||
Must be different for all connection configurations added and not @ref BLE_CONN_CFG_TAG_DEFAULT. */
|
Must be different for all connection configurations added and not @ref BLE_CONN_CFG_TAG_DEFAULT. */
|
||||||
union {
|
union {
|
||||||
|
@ -388,6 +388,19 @@ typedef union
|
||||||
* application RAM region (APP_RAM_BASE). On return, this will
|
* application RAM region (APP_RAM_BASE). On return, this will
|
||||||
* contain the minimum start address of the application RAM region
|
* contain the minimum start address of the application RAM region
|
||||||
* required by the SoftDevice for this configuration.
|
* required by the SoftDevice for this configuration.
|
||||||
|
* @warning After this call, the SoftDevice may generate several events. The list of events provided
|
||||||
|
* below require the application to initiate a SoftDevice API call. The corresponding API call
|
||||||
|
* is referenced in the event documentation.
|
||||||
|
* If the application fails to do so, the BLE connection may timeout, or the SoftDevice may stop
|
||||||
|
* communicating with the peer device.
|
||||||
|
* - @ref BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST
|
||||||
|
* - @ref BLE_GAP_EVT_PHY_UPDATE_REQUEST
|
||||||
|
* - @ref BLE_GAP_EVT_SEC_PARAMS_REQUEST
|
||||||
|
* - @ref BLE_GAP_EVT_SEC_INFO_REQUEST
|
||||||
|
* - @ref BLE_GAP_EVT_AUTH_KEY_REQUEST
|
||||||
|
* - @ref BLE_GATTS_EVT_EXCHANGE_MTU_REQUEST
|
||||||
|
* - @ref BLE_EVT_USER_MEM_REQUEST
|
||||||
|
* - @ref BLE_L2CAP_EVT_CH_SETUP_REQUEST
|
||||||
*
|
*
|
||||||
* @note The memory requirement for a specific configuration will not increase between SoftDevices
|
* @note The memory requirement for a specific configuration will not increase between SoftDevices
|
||||||
* with the same major version number.
|
* with the same major version number.
|
2269
s113_nrf52_7.0.1/s113_nrf52_7.0.1_API/include/ble_gap.h
Normal file
2269
s113_nrf52_7.0.1/s113_nrf52_7.0.1_API/include/ble_gap.h
Normal file
File diff suppressed because it is too large
Load diff
507
s113_nrf52_7.0.1/s113_nrf52_7.0.1_API/include/ble_l2cap.h
Normal file
507
s113_nrf52_7.0.1/s113_nrf52_7.0.1_API/include/ble_l2cap.h
Normal file
|
@ -0,0 +1,507 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2011 - 2020, Nordic Semiconductor ASA
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
* are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
* list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form, except as embedded into a Nordic
|
||||||
|
* Semiconductor ASA integrated circuit in a product or a software update for
|
||||||
|
* such product, must reproduce the above copyright notice, this list of
|
||||||
|
* conditions and the following disclaimer in the documentation and/or other
|
||||||
|
* materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. Neither the name of Nordic Semiconductor ASA nor the names of its
|
||||||
|
* contributors may be used to endorse or promote products derived from this
|
||||||
|
* software without specific prior written permission.
|
||||||
|
*
|
||||||
|
* 4. This software, with or without modification, must only be used with a
|
||||||
|
* Nordic Semiconductor ASA integrated circuit.
|
||||||
|
*
|
||||||
|
* 5. Any software provided in binary form under this license must not be reverse
|
||||||
|
* engineered, decompiled, modified and/or disassembled.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS
|
||||||
|
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||||
|
* OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
* DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||||
|
* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||||
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||||
|
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
|
||||||
|
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@addtogroup BLE_L2CAP Logical Link Control and Adaptation Protocol (L2CAP)
|
||||||
|
@{
|
||||||
|
@brief Definitions and prototypes for the L2CAP interface.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BLE_L2CAP_H__
|
||||||
|
#define BLE_L2CAP_H__
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "nrf_svc.h"
|
||||||
|
#include "nrf_error.h"
|
||||||
|
#include "ble_ranges.h"
|
||||||
|
#include "ble_types.h"
|
||||||
|
#include "ble_err.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**@addtogroup BLE_L2CAP_TERMINOLOGY Terminology
|
||||||
|
* @{
|
||||||
|
* @details
|
||||||
|
*
|
||||||
|
* L2CAP SDU
|
||||||
|
* - A data unit that the application can send/receive to/from a peer.
|
||||||
|
*
|
||||||
|
* L2CAP PDU
|
||||||
|
* - A data unit that is exchanged between local and remote L2CAP entities.
|
||||||
|
* It consists of L2CAP protocol control information and payload fields.
|
||||||
|
* The payload field can contain an L2CAP SDU or a part of an L2CAP SDU.
|
||||||
|
*
|
||||||
|
* L2CAP MTU
|
||||||
|
* - The maximum length of an L2CAP SDU.
|
||||||
|
*
|
||||||
|
* L2CAP MPS
|
||||||
|
* - The maximum length of an L2CAP PDU payload field.
|
||||||
|
*
|
||||||
|
* Credits
|
||||||
|
* - A value indicating the number of L2CAP PDUs that the receiver of the credit can send to the peer.
|
||||||
|
* @} */
|
||||||
|
|
||||||
|
/**@addtogroup BLE_L2CAP_ENUMERATIONS Enumerations
|
||||||
|
* @{ */
|
||||||
|
|
||||||
|
/**@brief L2CAP API SVC numbers. */
|
||||||
|
enum BLE_L2CAP_SVCS
|
||||||
|
{
|
||||||
|
SD_BLE_L2CAP_CH_SETUP = BLE_L2CAP_SVC_BASE + 0, /**< Set up an L2CAP channel. */
|
||||||
|
SD_BLE_L2CAP_CH_RELEASE = BLE_L2CAP_SVC_BASE + 1, /**< Release an L2CAP channel. */
|
||||||
|
SD_BLE_L2CAP_CH_RX = BLE_L2CAP_SVC_BASE + 2, /**< Receive an SDU on an L2CAP channel. */
|
||||||
|
SD_BLE_L2CAP_CH_TX = BLE_L2CAP_SVC_BASE + 3, /**< Transmit an SDU on an L2CAP channel. */
|
||||||
|
SD_BLE_L2CAP_CH_FLOW_CONTROL = BLE_L2CAP_SVC_BASE + 4, /**< Advanced SDU reception flow control. */
|
||||||
|
};
|
||||||
|
|
||||||
|
/**@brief L2CAP Event IDs. */
|
||||||
|
enum BLE_L2CAP_EVTS
|
||||||
|
{
|
||||||
|
BLE_L2CAP_EVT_CH_SETUP_REQUEST = BLE_L2CAP_EVT_BASE + 0, /**< L2CAP Channel Setup Request event.
|
||||||
|
\n Reply with @ref sd_ble_l2cap_ch_setup.
|
||||||
|
\n See @ref ble_l2cap_evt_ch_setup_request_t. */
|
||||||
|
BLE_L2CAP_EVT_CH_SETUP_REFUSED = BLE_L2CAP_EVT_BASE + 1, /**< L2CAP Channel Setup Refused event.
|
||||||
|
\n See @ref ble_l2cap_evt_ch_setup_refused_t. */
|
||||||
|
BLE_L2CAP_EVT_CH_SETUP = BLE_L2CAP_EVT_BASE + 2, /**< L2CAP Channel Setup Completed event.
|
||||||
|
\n See @ref ble_l2cap_evt_ch_setup_t. */
|
||||||
|
BLE_L2CAP_EVT_CH_RELEASED = BLE_L2CAP_EVT_BASE + 3, /**< L2CAP Channel Released event.
|
||||||
|
\n No additional event structure applies. */
|
||||||
|
BLE_L2CAP_EVT_CH_SDU_BUF_RELEASED = BLE_L2CAP_EVT_BASE + 4, /**< L2CAP Channel SDU data buffer released event.
|
||||||
|
\n See @ref ble_l2cap_evt_ch_sdu_buf_released_t. */
|
||||||
|
BLE_L2CAP_EVT_CH_CREDIT = BLE_L2CAP_EVT_BASE + 5, /**< L2CAP Channel Credit received.
|
||||||
|
\n See @ref ble_l2cap_evt_ch_credit_t. */
|
||||||
|
BLE_L2CAP_EVT_CH_RX = BLE_L2CAP_EVT_BASE + 6, /**< L2CAP Channel SDU received.
|
||||||
|
\n See @ref ble_l2cap_evt_ch_rx_t. */
|
||||||
|
BLE_L2CAP_EVT_CH_TX = BLE_L2CAP_EVT_BASE + 7, /**< L2CAP Channel SDU transmitted.
|
||||||
|
\n See @ref ble_l2cap_evt_ch_tx_t. */
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @} */
|
||||||
|
|
||||||
|
/**@addtogroup BLE_L2CAP_DEFINES Defines
|
||||||
|
* @{ */
|
||||||
|
|
||||||
|
/**@brief Maximum number of L2CAP channels per connection. */
|
||||||
|
#define BLE_L2CAP_CH_COUNT_MAX (64)
|
||||||
|
|
||||||
|
/**@brief Minimum L2CAP MTU, in bytes. */
|
||||||
|
#define BLE_L2CAP_MTU_MIN (23)
|
||||||
|
|
||||||
|
/**@brief Minimum L2CAP MPS, in bytes. */
|
||||||
|
#define BLE_L2CAP_MPS_MIN (23)
|
||||||
|
|
||||||
|
/**@brief Invalid CID. */
|
||||||
|
#define BLE_L2CAP_CID_INVALID (0x0000)
|
||||||
|
|
||||||
|
/**@brief Default number of credits for @ref sd_ble_l2cap_ch_flow_control. */
|
||||||
|
#define BLE_L2CAP_CREDITS_DEFAULT (1)
|
||||||
|
|
||||||
|
/**@defgroup BLE_L2CAP_CH_SETUP_REFUSED_SRCS L2CAP channel setup refused sources
|
||||||
|
* @{ */
|
||||||
|
#define BLE_L2CAP_CH_SETUP_REFUSED_SRC_LOCAL (0x01) /**< Local. */
|
||||||
|
#define BLE_L2CAP_CH_SETUP_REFUSED_SRC_REMOTE (0x02) /**< Remote. */
|
||||||
|
/** @} */
|
||||||
|
|
||||||
|
/** @defgroup BLE_L2CAP_CH_STATUS_CODES L2CAP channel status codes
|
||||||
|
* @{ */
|
||||||
|
#define BLE_L2CAP_CH_STATUS_CODE_SUCCESS (0x0000) /**< Success. */
|
||||||
|
#define BLE_L2CAP_CH_STATUS_CODE_LE_PSM_NOT_SUPPORTED (0x0002) /**< LE_PSM not supported. */
|
||||||
|
#define BLE_L2CAP_CH_STATUS_CODE_NO_RESOURCES (0x0004) /**< No resources available. */
|
||||||
|
#define BLE_L2CAP_CH_STATUS_CODE_INSUFF_AUTHENTICATION (0x0005) /**< Insufficient authentication. */
|
||||||
|
#define BLE_L2CAP_CH_STATUS_CODE_INSUFF_AUTHORIZATION (0x0006) /**< Insufficient authorization. */
|
||||||
|
#define BLE_L2CAP_CH_STATUS_CODE_INSUFF_ENC_KEY_SIZE (0x0007) /**< Insufficient encryption key size. */
|
||||||
|
#define BLE_L2CAP_CH_STATUS_CODE_INSUFF_ENC (0x0008) /**< Insufficient encryption. */
|
||||||
|
#define BLE_L2CAP_CH_STATUS_CODE_INVALID_SCID (0x0009) /**< Invalid Source CID. */
|
||||||
|
#define BLE_L2CAP_CH_STATUS_CODE_SCID_ALLOCATED (0x000A) /**< Source CID already allocated. */
|
||||||
|
#define BLE_L2CAP_CH_STATUS_CODE_UNACCEPTABLE_PARAMS (0x000B) /**< Unacceptable parameters. */
|
||||||
|
#define BLE_L2CAP_CH_STATUS_CODE_NOT_UNDERSTOOD (0x8000) /**< Command Reject received instead of LE Credit Based Connection Response. */
|
||||||
|
#define BLE_L2CAP_CH_STATUS_CODE_TIMEOUT (0xC000) /**< Operation timed out. */
|
||||||
|
/** @} */
|
||||||
|
|
||||||
|
/** @} */
|
||||||
|
|
||||||
|
/**@addtogroup BLE_L2CAP_STRUCTURES Structures
|
||||||
|
* @{ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief BLE L2CAP connection configuration parameters, set with @ref sd_ble_cfg_set.
|
||||||
|
*
|
||||||
|
* @note These parameters are set per connection, so all L2CAP channels created on this connection
|
||||||
|
* will have the same parameters.
|
||||||
|
*
|
||||||
|
* @retval ::NRF_ERROR_INVALID_PARAM One or more of the following is true:
|
||||||
|
* - rx_mps is smaller than @ref BLE_L2CAP_MPS_MIN.
|
||||||
|
* - tx_mps is smaller than @ref BLE_L2CAP_MPS_MIN.
|
||||||
|
* - ch_count is greater than @ref BLE_L2CAP_CH_COUNT_MAX.
|
||||||
|
* @retval ::NRF_ERROR_NO_MEM rx_mps or tx_mps is set too high.
|
||||||
|
*/
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
uint16_t rx_mps; /**< The maximum L2CAP PDU payload size, in bytes, that L2CAP shall
|
||||||
|
be able to receive on L2CAP channels on connections with this
|
||||||
|
configuration. The minimum value is @ref BLE_L2CAP_MPS_MIN. */
|
||||||
|
uint16_t tx_mps; /**< The maximum L2CAP PDU payload size, in bytes, that L2CAP shall
|
||||||
|
be able to transmit on L2CAP channels on connections with this
|
||||||
|
configuration. The minimum value is @ref BLE_L2CAP_MPS_MIN. */
|
||||||
|
uint8_t rx_queue_size; /**< Number of SDU data buffers that can be queued for reception per
|
||||||
|
L2CAP channel. The minimum value is one. */
|
||||||
|
uint8_t tx_queue_size; /**< Number of SDU data buffers that can be queued for transmission
|
||||||
|
per L2CAP channel. The minimum value is one. */
|
||||||
|
uint8_t ch_count; /**< Number of L2CAP channels the application can create per connection
|
||||||
|
with this configuration. The default value is zero, the maximum
|
||||||
|
value is @ref BLE_L2CAP_CH_COUNT_MAX.
|
||||||
|
@note if this parameter is set to zero, all other parameters in
|
||||||
|
@ref ble_l2cap_conn_cfg_t are ignored. */
|
||||||
|
} ble_l2cap_conn_cfg_t;
|
||||||
|
|
||||||
|
/**@brief L2CAP channel RX parameters. */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
uint16_t rx_mtu; /**< The maximum L2CAP SDU size, in bytes, that L2CAP shall be able to
|
||||||
|
receive on this L2CAP channel.
|
||||||
|
- Must be equal to or greater than @ref BLE_L2CAP_MTU_MIN. */
|
||||||
|
uint16_t rx_mps; /**< The maximum L2CAP PDU payload size, in bytes, that L2CAP shall be
|
||||||
|
able to receive on this L2CAP channel.
|
||||||
|
- Must be equal to or greater than @ref BLE_L2CAP_MPS_MIN.
|
||||||
|
- Must be equal to or less than @ref ble_l2cap_conn_cfg_t::rx_mps. */
|
||||||
|
ble_data_t sdu_buf; /**< SDU data buffer for reception.
|
||||||
|
- If @ref ble_data_t::p_data is non-NULL, initial credits are
|
||||||
|
issued to the peer.
|
||||||
|
- If @ref ble_data_t::p_data is NULL, no initial credits are
|
||||||
|
issued to the peer. */
|
||||||
|
} ble_l2cap_ch_rx_params_t;
|
||||||
|
|
||||||
|
/**@brief L2CAP channel setup parameters. */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
ble_l2cap_ch_rx_params_t rx_params; /**< L2CAP channel RX parameters. */
|
||||||
|
uint16_t le_psm; /**< LE Protocol/Service Multiplexer. Used when requesting
|
||||||
|
setup of an L2CAP channel, ignored otherwise. */
|
||||||
|
uint16_t status; /**< Status code, see @ref BLE_L2CAP_CH_STATUS_CODES.
|
||||||
|
Used when replying to a setup request of an L2CAP
|
||||||
|
channel, ignored otherwise. */
|
||||||
|
} ble_l2cap_ch_setup_params_t;
|
||||||
|
|
||||||
|
/**@brief L2CAP channel TX parameters. */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
uint16_t tx_mtu; /**< The maximum L2CAP SDU size, in bytes, that L2CAP is able to
|
||||||
|
transmit on this L2CAP channel. */
|
||||||
|
uint16_t peer_mps; /**< The maximum L2CAP PDU payload size, in bytes, that the peer is
|
||||||
|
able to receive on this L2CAP channel. */
|
||||||
|
uint16_t tx_mps; /**< The maximum L2CAP PDU payload size, in bytes, that L2CAP is able
|
||||||
|
to transmit on this L2CAP channel. This is effective tx_mps,
|
||||||
|
selected by the SoftDevice as
|
||||||
|
MIN( @ref ble_l2cap_ch_tx_params_t::peer_mps, @ref ble_l2cap_conn_cfg_t::tx_mps ) */
|
||||||
|
uint16_t credits; /**< Initial credits given by the peer. */
|
||||||
|
} ble_l2cap_ch_tx_params_t;
|
||||||
|
|
||||||
|
/**@brief L2CAP Channel Setup Request event. */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
ble_l2cap_ch_tx_params_t tx_params; /**< L2CAP channel TX parameters. */
|
||||||
|
uint16_t le_psm; /**< LE Protocol/Service Multiplexer. */
|
||||||
|
} ble_l2cap_evt_ch_setup_request_t;
|
||||||
|
|
||||||
|
/**@brief L2CAP Channel Setup Refused event. */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
uint8_t source; /**< Source, see @ref BLE_L2CAP_CH_SETUP_REFUSED_SRCS */
|
||||||
|
uint16_t status; /**< Status code, see @ref BLE_L2CAP_CH_STATUS_CODES */
|
||||||
|
} ble_l2cap_evt_ch_setup_refused_t;
|
||||||
|
|
||||||
|
/**@brief L2CAP Channel Setup Completed event. */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
ble_l2cap_ch_tx_params_t tx_params; /**< L2CAP channel TX parameters. */
|
||||||
|
} ble_l2cap_evt_ch_setup_t;
|
||||||
|
|
||||||
|
/**@brief L2CAP Channel SDU Data Buffer Released event. */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
ble_data_t sdu_buf; /**< Returned reception or transmission SDU data buffer. The SoftDevice
|
||||||
|
returns SDU data buffers supplied by the application, which have
|
||||||
|
not yet been returned previously via a @ref BLE_L2CAP_EVT_CH_RX or
|
||||||
|
@ref BLE_L2CAP_EVT_CH_TX event. */
|
||||||
|
} ble_l2cap_evt_ch_sdu_buf_released_t;
|
||||||
|
|
||||||
|
/**@brief L2CAP Channel Credit received event. */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
uint16_t credits; /**< Additional credits given by the peer. */
|
||||||
|
} ble_l2cap_evt_ch_credit_t;
|
||||||
|
|
||||||
|
/**@brief L2CAP Channel received SDU event. */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
uint16_t sdu_len; /**< Total SDU length, in bytes. */
|
||||||
|
ble_data_t sdu_buf; /**< SDU data buffer.
|
||||||
|
@note If there is not enough space in the buffer
|
||||||
|
(sdu_buf.len < sdu_len) then the rest of the SDU will be
|
||||||
|
silently discarded by the SoftDevice. */
|
||||||
|
} ble_l2cap_evt_ch_rx_t;
|
||||||
|
|
||||||
|
/**@brief L2CAP Channel transmitted SDU event. */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
ble_data_t sdu_buf; /**< SDU data buffer. */
|
||||||
|
} ble_l2cap_evt_ch_tx_t;
|
||||||
|
|
||||||
|
/**@brief L2CAP event structure. */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
uint16_t conn_handle; /**< Connection Handle on which the event occured. */
|
||||||
|
uint16_t local_cid; /**< Local Channel ID of the L2CAP channel, or
|
||||||
|
@ref BLE_L2CAP_CID_INVALID if not present. */
|
||||||
|
union
|
||||||
|
{
|
||||||
|
ble_l2cap_evt_ch_setup_request_t ch_setup_request; /**< L2CAP Channel Setup Request Event Parameters. */
|
||||||
|
ble_l2cap_evt_ch_setup_refused_t ch_setup_refused; /**< L2CAP Channel Setup Refused Event Parameters. */
|
||||||
|
ble_l2cap_evt_ch_setup_t ch_setup; /**< L2CAP Channel Setup Completed Event Parameters. */
|
||||||
|
ble_l2cap_evt_ch_sdu_buf_released_t ch_sdu_buf_released;/**< L2CAP Channel SDU Data Buffer Released Event Parameters. */
|
||||||
|
ble_l2cap_evt_ch_credit_t credit; /**< L2CAP Channel Credit Received Event Parameters. */
|
||||||
|
ble_l2cap_evt_ch_rx_t rx; /**< L2CAP Channel SDU Received Event Parameters. */
|
||||||
|
ble_l2cap_evt_ch_tx_t tx; /**< L2CAP Channel SDU Transmitted Event Parameters. */
|
||||||
|
} params; /**< Event Parameters. */
|
||||||
|
} ble_l2cap_evt_t;
|
||||||
|
|
||||||
|
/** @} */
|
||||||
|
|
||||||
|
/**@addtogroup BLE_L2CAP_FUNCTIONS Functions
|
||||||
|
* @{ */
|
||||||
|
|
||||||
|
/**@brief Set up an L2CAP channel.
|
||||||
|
*
|
||||||
|
* @details This function is used to:
|
||||||
|
* - Request setup of an L2CAP channel: sends an LE Credit Based Connection Request packet to a peer.
|
||||||
|
* - Reply to a setup request of an L2CAP channel (if called in response to a
|
||||||
|
* @ref BLE_L2CAP_EVT_CH_SETUP_REQUEST event): sends an LE Credit Based Connection
|
||||||
|
* Response packet to a peer.
|
||||||
|
*
|
||||||
|
* @note A call to this function will require the application to keep the SDU data buffer alive
|
||||||
|
* until the SDU data buffer is returned in @ref BLE_L2CAP_EVT_CH_RX or
|
||||||
|
* @ref BLE_L2CAP_EVT_CH_SDU_BUF_RELEASED event.
|
||||||
|
*
|
||||||
|
* @events
|
||||||
|
* @event{@ref BLE_L2CAP_EVT_CH_SETUP, Setup successful.}
|
||||||
|
* @event{@ref BLE_L2CAP_EVT_CH_SETUP_REFUSED, Setup failed.}
|
||||||
|
* @endevents
|
||||||
|
*
|
||||||
|
* @mscs
|
||||||
|
* @mmsc{@ref BLE_L2CAP_CH_SETUP_MSC}
|
||||||
|
* @endmscs
|
||||||
|
*
|
||||||
|
* @param[in] conn_handle Connection Handle.
|
||||||
|
* @param[in,out] p_local_cid Pointer to a uint16_t containing Local Channel ID of the L2CAP channel:
|
||||||
|
* - As input: @ref BLE_L2CAP_CID_INVALID when requesting setup of an L2CAP
|
||||||
|
* channel or local_cid provided in the @ref BLE_L2CAP_EVT_CH_SETUP_REQUEST
|
||||||
|
* event when replying to a setup request of an L2CAP channel.
|
||||||
|
* - As output: local_cid for this channel.
|
||||||
|
* @param[in] p_params L2CAP channel parameters.
|
||||||
|
*
|
||||||
|
* @retval ::NRF_SUCCESS Successfully queued request or response for transmission.
|
||||||
|
* @retval ::NRF_ERROR_BUSY The stack is busy, process pending events and retry.
|
||||||
|
* @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied.
|
||||||
|
* @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle.
|
||||||
|
* @retval ::NRF_ERROR_INVALID_PARAM Invalid parameter(s) supplied.
|
||||||
|
* @retval ::NRF_ERROR_INVALID_LENGTH Supplied higher rx_mps than has been configured on this link.
|
||||||
|
* @retval ::NRF_ERROR_INVALID_STATE Invalid State to perform operation (L2CAP channel already set up).
|
||||||
|
* @retval ::NRF_ERROR_NOT_FOUND CID not found.
|
||||||
|
* @retval ::NRF_ERROR_RESOURCES The limit has been reached for available L2CAP channels,
|
||||||
|
* see @ref ble_l2cap_conn_cfg_t::ch_count.
|
||||||
|
*/
|
||||||
|
SVCALL(SD_BLE_L2CAP_CH_SETUP, uint32_t, sd_ble_l2cap_ch_setup(uint16_t conn_handle, uint16_t *p_local_cid, ble_l2cap_ch_setup_params_t const *p_params));
|
||||||
|
|
||||||
|
/**@brief Release an L2CAP channel.
|
||||||
|
*
|
||||||
|
* @details This sends a Disconnection Request packet to a peer.
|
||||||
|
*
|
||||||
|
* @events
|
||||||
|
* @event{@ref BLE_L2CAP_EVT_CH_RELEASED, Release complete.}
|
||||||
|
* @endevents
|
||||||
|
*
|
||||||
|
* @mscs
|
||||||
|
* @mmsc{@ref BLE_L2CAP_CH_RELEASE_MSC}
|
||||||
|
* @endmscs
|
||||||
|
*
|
||||||
|
* @param[in] conn_handle Connection Handle.
|
||||||
|
* @param[in] local_cid Local Channel ID of the L2CAP channel.
|
||||||
|
*
|
||||||
|
* @retval ::NRF_SUCCESS Successfully queued request for transmission.
|
||||||
|
* @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle.
|
||||||
|
* @retval ::NRF_ERROR_INVALID_STATE Invalid State to perform operation (Setup or release is
|
||||||
|
* in progress for the L2CAP channel).
|
||||||
|
* @retval ::NRF_ERROR_NOT_FOUND CID not found.
|
||||||
|
*/
|
||||||
|
SVCALL(SD_BLE_L2CAP_CH_RELEASE, uint32_t, sd_ble_l2cap_ch_release(uint16_t conn_handle, uint16_t local_cid));
|
||||||
|
|
||||||
|
/**@brief Receive an SDU on an L2CAP channel.
|
||||||
|
*
|
||||||
|
* @details This may issue additional credits to the peer using an LE Flow Control Credit packet.
|
||||||
|
*
|
||||||
|
* @note A call to this function will require the application to keep the memory pointed by
|
||||||
|
* @ref ble_data_t::p_data alive until the SDU data buffer is returned in @ref BLE_L2CAP_EVT_CH_RX
|
||||||
|
* or @ref BLE_L2CAP_EVT_CH_SDU_BUF_RELEASED event.
|
||||||
|
*
|
||||||
|
* @note The SoftDevice can queue up to @ref ble_l2cap_conn_cfg_t::rx_queue_size SDU data buffers
|
||||||
|
* for reception per L2CAP channel.
|
||||||
|
*
|
||||||
|
* @events
|
||||||
|
* @event{@ref BLE_L2CAP_EVT_CH_RX, The SDU is received.}
|
||||||
|
* @endevents
|
||||||
|
*
|
||||||
|
* @mscs
|
||||||
|
* @mmsc{@ref BLE_L2CAP_CH_RX_MSC}
|
||||||
|
* @endmscs
|
||||||
|
*
|
||||||
|
* @param[in] conn_handle Connection Handle.
|
||||||
|
* @param[in] local_cid Local Channel ID of the L2CAP channel.
|
||||||
|
* @param[in] p_sdu_buf Pointer to the SDU data buffer.
|
||||||
|
*
|
||||||
|
* @retval ::NRF_SUCCESS Buffer accepted.
|
||||||
|
* @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied.
|
||||||
|
* @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle.
|
||||||
|
* @retval ::NRF_ERROR_INVALID_STATE Invalid State to perform operation (Setup or release is
|
||||||
|
* in progress for an L2CAP channel).
|
||||||
|
* @retval ::NRF_ERROR_NOT_FOUND CID not found.
|
||||||
|
* @retval ::NRF_ERROR_RESOURCES Too many SDU data buffers supplied. Wait for a
|
||||||
|
* @ref BLE_L2CAP_EVT_CH_RX event and retry.
|
||||||
|
*/
|
||||||
|
SVCALL(SD_BLE_L2CAP_CH_RX, uint32_t, sd_ble_l2cap_ch_rx(uint16_t conn_handle, uint16_t local_cid, ble_data_t const *p_sdu_buf));
|
||||||
|
|
||||||
|
/**@brief Transmit an SDU on an L2CAP channel.
|
||||||
|
*
|
||||||
|
* @note A call to this function will require the application to keep the memory pointed by
|
||||||
|
* @ref ble_data_t::p_data alive until the SDU data buffer is returned in @ref BLE_L2CAP_EVT_CH_TX
|
||||||
|
* or @ref BLE_L2CAP_EVT_CH_SDU_BUF_RELEASED event.
|
||||||
|
*
|
||||||
|
* @note The SoftDevice can queue up to @ref ble_l2cap_conn_cfg_t::tx_queue_size SDUs for
|
||||||
|
* transmission per L2CAP channel.
|
||||||
|
*
|
||||||
|
* @note The application can keep track of the available credits for transmission by following
|
||||||
|
* the procedure below:
|
||||||
|
* - Store initial credits given by the peer in a variable.
|
||||||
|
* (Initial credits are provided in a @ref BLE_L2CAP_EVT_CH_SETUP event.)
|
||||||
|
* - Decrement the variable, which stores the currently available credits, by
|
||||||
|
* ceiling((@ref ble_data_t::len + 2) / tx_mps) when a call to this function returns
|
||||||
|
* @ref NRF_SUCCESS. (tx_mps is provided in a @ref BLE_L2CAP_EVT_CH_SETUP event.)
|
||||||
|
* - Increment the variable, which stores the currently available credits, by additional
|
||||||
|
* credits given by the peer in a @ref BLE_L2CAP_EVT_CH_CREDIT event.
|
||||||
|
*
|
||||||
|
* @events
|
||||||
|
* @event{@ref BLE_L2CAP_EVT_CH_TX, The SDU is transmitted.}
|
||||||
|
* @endevents
|
||||||
|
*
|
||||||
|
* @mscs
|
||||||
|
* @mmsc{@ref BLE_L2CAP_CH_TX_MSC}
|
||||||
|
* @endmscs
|
||||||
|
*
|
||||||
|
* @param[in] conn_handle Connection Handle.
|
||||||
|
* @param[in] local_cid Local Channel ID of the L2CAP channel.
|
||||||
|
* @param[in] p_sdu_buf Pointer to the SDU data buffer.
|
||||||
|
*
|
||||||
|
* @retval ::NRF_SUCCESS Successfully queued L2CAP SDU for transmission.
|
||||||
|
* @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied.
|
||||||
|
* @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle.
|
||||||
|
* @retval ::NRF_ERROR_INVALID_STATE Invalid State to perform operation (Setup or release is
|
||||||
|
* in progress for the L2CAP channel).
|
||||||
|
* @retval ::NRF_ERROR_NOT_FOUND CID not found.
|
||||||
|
* @retval ::NRF_ERROR_DATA_SIZE Invalid SDU length supplied, must not be more than
|
||||||
|
* @ref ble_l2cap_ch_tx_params_t::tx_mtu provided in
|
||||||
|
* @ref BLE_L2CAP_EVT_CH_SETUP event.
|
||||||
|
* @retval ::NRF_ERROR_RESOURCES Too many SDUs queued for transmission. Wait for a
|
||||||
|
* @ref BLE_L2CAP_EVT_CH_TX event and retry.
|
||||||
|
*/
|
||||||
|
SVCALL(SD_BLE_L2CAP_CH_TX, uint32_t, sd_ble_l2cap_ch_tx(uint16_t conn_handle, uint16_t local_cid, ble_data_t const *p_sdu_buf));
|
||||||
|
|
||||||
|
/**@brief Advanced SDU reception flow control.
|
||||||
|
*
|
||||||
|
* @details Adjust the way the SoftDevice issues credits to the peer.
|
||||||
|
* This may issue additional credits to the peer using an LE Flow Control Credit packet.
|
||||||
|
*
|
||||||
|
* @mscs
|
||||||
|
* @mmsc{@ref BLE_L2CAP_CH_FLOW_CONTROL_MSC}
|
||||||
|
* @endmscs
|
||||||
|
*
|
||||||
|
* @param[in] conn_handle Connection Handle.
|
||||||
|
* @param[in] local_cid Local Channel ID of the L2CAP channel or @ref BLE_L2CAP_CID_INVALID to set
|
||||||
|
* the value that will be used for newly created channels.
|
||||||
|
* @param[in] credits Number of credits that the SoftDevice will make sure the peer has every
|
||||||
|
* time it starts using a new reception buffer.
|
||||||
|
* - @ref BLE_L2CAP_CREDITS_DEFAULT is the default value the SoftDevice will
|
||||||
|
* use if this function is not called.
|
||||||
|
* - If set to zero, the SoftDevice will stop issuing credits for new reception
|
||||||
|
* buffers the application provides or has provided. SDU reception that is
|
||||||
|
* currently ongoing will be allowed to complete.
|
||||||
|
* @param[out] p_credits NULL or pointer to a uint16_t. If a valid pointer is provided, it will be
|
||||||
|
* written by the SoftDevice with the number of credits that is or will be
|
||||||
|
* available to the peer. If the value written by the SoftDevice is 0 when
|
||||||
|
* credits parameter was set to 0, the peer will not be able to send more
|
||||||
|
* data until more credits are provided by calling this function again with
|
||||||
|
* credits > 0. This parameter is ignored when local_cid is set to
|
||||||
|
* @ref BLE_L2CAP_CID_INVALID.
|
||||||
|
*
|
||||||
|
* @note Application should take care when setting number of credits higher than default value. In
|
||||||
|
* this case the application must make sure that the SoftDevice always has reception buffers
|
||||||
|
* available (see @ref sd_ble_l2cap_ch_rx) for that channel. If the SoftDevice does not have
|
||||||
|
* such buffers available, packets may be NACKed on the Link Layer and all Bluetooth traffic
|
||||||
|
* on the connection handle may be stalled until the SoftDevice again has an available
|
||||||
|
* reception buffer. This applies even if the application has used this call to set the
|
||||||
|
* credits back to default, or zero.
|
||||||
|
*
|
||||||
|
* @retval ::NRF_SUCCESS Flow control parameters accepted.
|
||||||
|
* @retval ::NRF_ERROR_INVALID_ADDR Invalid pointer supplied.
|
||||||
|
* @retval ::BLE_ERROR_INVALID_CONN_HANDLE Invalid Connection Handle.
|
||||||
|
* @retval ::NRF_ERROR_INVALID_STATE Invalid State to perform operation (Setup or release is
|
||||||
|
* in progress for an L2CAP channel).
|
||||||
|
* @retval ::NRF_ERROR_NOT_FOUND CID not found.
|
||||||
|
*/
|
||||||
|
SVCALL(SD_BLE_L2CAP_CH_FLOW_CONTROL, uint32_t, sd_ble_l2cap_ch_flow_control(uint16_t conn_handle, uint16_t local_cid, uint16_t credits, uint16_t *p_credits));
|
||||||
|
|
||||||
|
/** @} */
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endif // BLE_L2CAP_H__
|
||||||
|
|
||||||
|
/**
|
||||||
|
@}
|
||||||
|
*/
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue