Compare commits

..

158 commits

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

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

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

* gap: expose ServiceData() in AdvertisementFields

* macos: include ServiceData in AdvertisementFields

* gap/linux: include ServiceData in AdvertisementFields

* gap: add unimplemented ServiceData() to raw advertisement

* added ServiceData advertising element also to the sending pieces

* more explicitly use the ad element type ids

* added a test case for ServiceData

* linux: added ServiceData advertising element

* sd: fix: handle no servicedata present

* linux: bluez uses string uuids for service data

* linux: fix: correct datatype for advertise with ServiceData

* uuid: add 32-Bit functions

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

* Revert unnessesary changes

* formatting

* remove extra check

---------

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

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

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

Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-23 21:38:07 -05:00
deadprogram
0a9bffe397 ninafw: should support muliple connections as a central
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-23 21:38:07 -05:00
deadprogram
00a475adf1 hci: add check for poll buffer overflow
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-23 21:38:07 -05:00
deadprogram
4c90cf4ab6 sd: replace unsafe.SliceData call with expression that is still supported in older Go versions
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-01-22 18:24:47 +01:00
deadprogram
bf647ecd57 ninafw: this PR contains several fixes and improvements for the NINAFW implementation including:
- correctly return from read requests instead of returning spurious error
- move some steps previously being done during Configure() into Start() where they more
correctly belonged.
- use advertising display name as the correct default value for the generic access characteristic.
- speed up the polling for new notifications for Centrals

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

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

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

With this rewrite, I fixed the following issues:

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

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

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

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

I ran the smoke tests and the binaries are exactly identical to what
they were before, so this change cannot have had an effect on these
smoke tests (which is expected, as this is mostly just changing some
types without changing the machine data type).
2023-10-05 19:11:46 +02:00
deadprogram
d0c7887b81 Update for release 0.8.0
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-09-21 13:45:40 +02:00
deadprogram
195d418876 build: remove CGo dependencies for Windows cross-compiler tests
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-09-21 12:54:07 +02:00
deadprogram
0cc860c018 docs: update README to remove CGo requirement for Windows
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-09-21 12:54:07 +02:00
deadprogram
1f58ec1fb4 windows: update github.com/saltosystems/winrt-go to no longer require CGo
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-09-21 12:54:07 +02:00
Filip Vranesevic
d34d15d830 Noescape workaround 2023-09-01 20:01:24 +02:00
deadprogram
20ccbeb113 build: add Windows to GH actions build jobs
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-08-29 22:57:42 +02:00
deadprogram
0403d51c8a build: add macOS 12 to GH actions build jobs
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-08-29 21:41:43 +02:00
Hari Bhaskaran
4da7f58124 Include WriteWithoutResponse permission, for examples, where Write exists 2023-08-11 14:26:37 +02:00
Erik de Vries
d9490f73ea Update dependencies - add documentation to heartrate-monitor 2023-08-11 12:49:03 +02:00
Binozo
d5276e5aed added manufacturer data support on SoftDevices 2023-08-11 11:52:12 +02:00
Binozo
da2032de42 Added option to add ManufacturerData to Advertisement 2023-08-11 11:52:12 +02:00
Ayke van Laethem
c3f9d593de sd: test creation of raw BLE advertisement packets
I realized we didn't have any tests for this yet, which we really should
have. So here they are.
2023-08-05 23:09:52 +02:00
Yurii Soldak
f9436906c1 all: go 1.18 and remove old-style build tags 2023-07-31 15:54:58 +02:00
Baden Parr
3c9cf83de2 macos: enable support for duplicate chars by moving from a map to a slice 2023-07-16 20:53:17 +02:00
Jagoba Gascón
4d067bc2b3 winrt-go: bump to latest
This version fixes critical bugs that caused random errors at runtime.
For more details see saltosystems/winrt-go/issues#71 and
saltosystems/winrt-go/issues#72. Diff:
e096b9a...c792451
2023-06-14 08:42:31 +02:00
deadprogram
1c44c024fd modules: update to latest TinyGo drivers and friends
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-06-12 01:46:46 +02:00
deadprogram
ba63457646 release: preparing for v0.7.0 release
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-06-12 01:46:46 +02:00
deadprogram
7f67fa0275 Update LICENSE year
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-06-12 01:46:46 +02:00
Baden Parr
eb782c5841 macos: remove unnecessary pointer indirection on service and char maps 2023-06-06 21:02:06 +02:00
Baden Parr
a341e8f543 macos: fixed reentrant service discovery 2023-06-06 21:02:06 +02:00
Ayke van Laethem
74e8f86261 nrf: clean up code after CGo improvements
Make use of a few new features in TinyGo:

  * Functions and globals in header files are supported.
  * Static functions are supported.

This allows us to remove workarounds specifically used for the lack of
support for static functions.
2023-06-06 16:59:23 +02:00
Ayke van Laethem
47d53464e4 sd: use constants from C instead of magic numbers 2023-06-03 08:21:59 +02:00
Sebastien Binet
f844306136 linux: properly close goroutines started by EnableNotifications
Fixes #168.

Signed-off-by: Sebastien Binet <binet@cern.ch>
2023-06-02 14:30:22 +02:00
Ayke van Laethem
87d24926c4 CONTRIBUTING: add note on new APIs 2023-05-15 13:04:23 +02:00
A.J. Lucas
cf51caa6ad gap/windows: Scan should set scanning mode to active to match other platforms 2023-05-11 13:54:22 +02:00
Ayke van Laethem
8260f2fb93 linux: do not randomize order of returned discovered services/chars
Previously the list of services and characteristics wasn't sorted, and
because of the way Go maps work they were in fact randomized.
This commit fixes the sorting for services and characteristics, which
both suffered from the same lack of sorting.

This fixes https://github.com/tinygo-org/bluetooth/issues/135.
2023-05-07 12:38:34 +02:00
Ayke van Laethem
5717af56e0 linux: fix characteristic scan order
This is very much like https://github.com/tinygo-org/bluetooth/pull/161,
but for Linux instead. In fact, I've copied the structure of the code
because it works for macOS.

I have tested that the scan order is now as requested. I also verified
that multiple characteristics with the same UUID are now supported, but
support is still a bit buggy: they appear in a random order (due to Go
map iteration order). I will fix this in the next commit.
2023-05-07 12:38:34 +02:00
Ayke van Laethem
7b36b3035c darwin: fix characteristic scan order
This fixes two bugs actually:

 1. The returned characteristics weren't in the order as given in the
    UUID slice.
 2. Duplicate characteristics weren't handled correctly.

I tested the first point, and it now works correctly.
I did not fully test the second point, it probably doesn't work
correctly yet because of the Device.characteristics map that is indexed
by UUID. This should be fixed in a separate change.
2023-05-07 12:32:40 +02:00
Baden Parr
51dff6faa7 linux: implement disconnect handeling
linux: remove unnecessary sending of nil prop change  

linux: first pass at watching connection events


linux: remove unneeded debug prints


linux: fix spelling

linux: fix pointer address references
2023-05-04 14:02:39 +02:00
Baden Parr
3523e14bfc macos: implement disconnect handeling
macos: add disconnect handler

macos: add default timeout and params.ConnectionTimeout handling


macos: add central manager CancelConnect call on connection timeout


macos: added address to connectHanndler call upon successful connection


macos: remove printing error on peripheral disconnection


macos: move and rename default connection timeout


macos: wait for disconnection upon timed out connection attempt
2023-05-04 14:02:39 +02:00
Yurii Soldak
e5b9a898ad nrf528xx: handle BLE_GAP_EVT_PHY_UPDATE_REQUEST
and explicitly ignore some other events
2023-05-02 12:04:01 +02:00
deadprogram
113f5fa503 build: switch to ghcr.io for docker container
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-04-30 01:06:44 +02:00
Ayke van Laethem
71f17598be gap: Set and SetRandom methods should have a pointer receiver
Without it, these calls are a no-op.

Fixes: https://github.com/tinygo-org/bluetooth/issues/144

In particular, this fixes a problem where IsRandom() would always return
false on Linux. With this fix, it correctly returns whether the address
is a random address.
2023-04-29 22:13:00 +02:00
Ayke van Laethem
d4bca3e97f README: add note on macOS Big Sur and iTerm2
Also see: https://github.com/tinygo-org/bluetooth/issues/48
2023-04-27 17:14:19 +02:00
Ayke van Laethem
b06d666dbf all: remove Addresser
Remove the Addresser type. It isn't really necessary (the Address type
can change between OSes) and makes it difficult to fix a heap allocation
in interrupts (on the Nordic SoftDevices).

This is a backwards incompatible change, but only programs that use
SetConnectHandler should notice this.
2023-04-26 23:37:36 +02:00
Ayke van Laethem
03d77ace1c Makefile: add examples/stop-advertisement
This example wasn't included in the smoke tests, and because of that I
initially missed the use of Addresser here.
2023-04-26 23:37:36 +02:00
Ayke van Laethem
e0e261e166 softdevice: avoid a heap allocation in the SoftDevice event handler
Events are delivered using interrupts. But there was an accidental heap
allocation in the interrupt.

Fix it by using a global instead. This is safe (and doesn't need
volatile accesses), as the global is only ever accessed from that
particular interrupt.
2023-04-26 23:37:36 +02:00
Alex
3f79b9e4e8 Added Indicate support to Windows driver 2023-02-26 22:29:27 +01:00
deadprogram
4c798a1e7d docs: correct badge link for GH actions
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-02-17 23:39:06 +01:00
deadprogram
570c0ce6e6 build: work around for CVE-2022-24765
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-02-12 08:40:03 +01:00
deadprogram
0bc6805828 build: update to actions/checkout@v3
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-02-12 08:40:03 +01:00
Bram van Neerven
50f176c7c8 delete old data folder 2023-01-29 23:54:12 +01:00
Bram van Neerven
34cb58db57 update uuid generation 2023-01-29 23:54:12 +01:00
Jagoba Gascón
be99863ef5 winrt-go: bump to latest
The latest winrt-go version fixes an access violation error caused by
some Go pointers being referenced in non-Go memory. More info:
https://github.com/saltosystems/winrt-go/issues/63
2023-01-25 13:02:48 +01:00
Jagoba Gascón
d0178d95f6 mtu-{darwin,linux,windows,sd}: add get mtu function 2022-10-19 20:02:55 +02:00
nv-h
c85b6cc9d3 windows: support empty manufacturer data
Fixed a runtime error when `manufacturerData` is empty.
2022-10-19 08:48:34 +02:00
deadprogram
e79ea1e4e9 release: updates for v0.6.0
Signed-off-by: deadprogram <ron@hybridgroup.com>
2022-10-02 10:55:42 +02:00
deadprogram
792f4f079e all: update dependencies before release
Signed-off-by: deadprogram <ron@hybridgroup.com>
2022-10-02 10:55:07 +02:00
deadprogram
79bbc2ed6d gap/linux: add helpful comment
Signed-off-by: deadprogram <ron@hybridgroup.com>
2022-09-29 10:49:08 +02:00
deadprogram
57f82b6241 gap/linux: workaround for https://github.com/muka/go-bluetooth/issues/163
Signed-off-by: deadprogram <ron@hybridgroup.com>
2022-09-29 10:49:08 +02:00
deadprogram
39e6a357ea linux: update to latest muka/go-bluetooth
Signed-off-by: deadprogram <ron@hybridgroup.com>
2022-09-29 10:49:08 +02:00
Jagoba Gascón Sánchez
e4e6976c64 windows: disable cache when reading characteristics 2022-09-26 09:17:55 +02:00
deadprogram
8fae597745 docs: update README with info on Windows support
Signed-off-by: deadprogram <ron@hybridgroup.com>
2022-09-25 15:23:24 +02:00
Jagoba Gascón
e843ce91a6 windows: add characteristic read, write and notify operations 2022-09-15 19:35:42 +02:00
Jagoba Gascón
1b30ec4619 windows: add characteristic discovery 2022-09-15 15:51:38 +02:00
Jagoba Gascón
2784a6b2d9 windows: add service discovery 2022-09-14 19:11:47 +02:00
Jagoba Gascón Sánchez
7113f8c021 windows: add device connection and disconnection 2022-09-12 11:07:57 +02:00
Jagoba Gascón Sánchez
8f13d06111 windows: add winrt-go dependency and remove manually generated code 2022-09-08 14:07:46 +02:00
deadprogram
7ec948bf3f macos: update to tinygo-org fork of cbgo v0.0.4
Signed-off-by: deadprogram <ron@hybridgroup.com>
2022-09-05 15:58:01 +02:00
deadprogram
79285321af all: unify UUID16 creation for all platforms
Signed-off-by: deadprogram <ron@hybridgroup.com>
2022-09-05 15:58:01 +02:00
deadprogram
0b701c55ca macos: use the same UUID format as expected by other standard
Signed-off-by: deadprogram <ron@hybridgroup.com>
2022-09-05 15:58:01 +02:00
deadprogram
cc5adf3789 macos: update to point to tinygo-org fork 2022-09-05 15:58:01 +02:00
Bart Meuris
948ea8c7ee
Improve UUID (#107)
Improvements to UUID:
* uuid now works with upper and lowercase
* sped up UUID.String() using stringbuilder, added benchmark
* UUID.String(): grow builder to prevent allocs
* make test in cicd happy
2022-08-25 14:22:52 +02:00
Ron Evans
25349d381e
build: add Github Action based CI build (#108)
* build: add Github Action based CI build
2022-07-12 17:16:21 +02:00
deadprogram
e7671110ad linux: update to latest go-bluetooth package
Signed-off-by: deadprogram <ron@hybridgroup.com>
2022-05-22 14:39:13 +02:00
Jagoba Gascón
9b9512fbc9 advertising: add manufacturer data field to advertisement payload 2022-05-22 14:36:27 +02:00
Yurii Soldak
20f0ce6119 gap: stop advertising 2022-05-22 08:59:22 +02:00
deadprogram
8dc1e155a0 all: prepare for release 0.5.0
Signed-off-by: deadprogram <ron@hybridgroup.com>
2022-04-29 13:47:49 +02:00
deadprogram
02fb2457d9 docs: update license year
Signed-off-by: deadprogram <ron@hybridgroup.com>
2022-04-29 13:47:18 +02:00
deadprogram
25d288fd10 all: update to drivers 0.20.0
Signed-off-by: deadprogram <ron@hybridgroup.com>
2022-04-29 11:53:23 +02:00
deadprogram
8cab553c59 all: update modules to use dev branch with TinyGo 0.23-pre compatible drivers
Signed-off-by: deadprogram <ron@hybridgroup.com>
2022-04-21 18:13:49 +02:00
Nicholas Page
7dee8d4d82 Fix ParseMAC bug 2022-04-19 13:44:36 +02:00
Ayke van Laethem
96a2be1571 Add //go:build lines for Go 1.18
This will be needed eventually, so add them now already.
2022-04-19 11:00:08 +02:00
Ayke van Laethem
4c0df2892d nrf: fix CGo errors after TinyGo update
For details, see: https://github.com/tinygo-org/tinygo/pull/2774
2022-04-13 18:59:12 +02:00
deadprogram
e75811786c release: update for v0.4.0
Signed-off-by: deadprogram <ron@hybridgroup.com>
2021-11-18 18:45:24 +01:00
David Barroso
bd75a42694 gattc/linux: DiscoverServices times out in 10s 2021-11-05 19:24:02 +01:00
Yurii Soldak
7f3b96cff9 adapter: address 2021-11-05 19:16:52 +01:00
Yurii Soldak
501b0aeecc nrf: update s140v7 SoftDevice version to latest, 7.3.0 2021-11-05 19:11:32 +01:00
deadprogram
899467bab3 modules: update muka/go-bluetooth to latest version
Signed-off-by: deadprogram <ron@hybridgroup.com>
2021-09-03 15:07:01 +02:00
James Lawrence
d985dcb55c fixes bluez 0.55 service registration.
muka/go-bluetooth does some magic so you can use short UUIDs and it'll auto
expand them to the full 128 bit uuid. setting these flags disables that behavior.

related issues:
- allows updating muka library to resolve https://github.com/tinygo-org/bluetooth/issues/35
- fixes the regression the upgrade caused https://github.com/tinygo-org/bluetooth/issues/46
- commit causing the regression in upstream 1c4c1c8613
2021-09-03 15:05:44 +02:00
deadprogram
ab40fc77fe examples: add scanner for Adafruit Clue
Signed-off-by: deadprogram <ron@hybridgroup.com>
2021-07-21 22:18:58 +02:00
deadprogram
d472af8a8e modules: add tinyterm package for clue example
Signed-off-by: deadprogram <ron@hybridgroup.com>
2021-07-21 22:18:58 +02:00
deadprogram
3cbf47cc61 macos: update xcode in use to 10.3.0
Signed-off-by: deadprogram <ron@hybridgroup.com>
2021-07-21 00:36:28 +02:00
Erik Price
cf63949412 darwin: make Adapter.Connect thread-safe
This change allows multiple concurrent goroutines to call
`Adapter.Connect` without racing.

Fixes #57
2021-07-21 00:09:35 +02:00
Ayke van Laethem
bb8767730c nrf: add support for S113 SoftDevice
This SoftDevice is used by default on the BBC micro:bit v2 so it's a
good idea to add support here.

Unfortunately this SoftDevice does not support scanning and connecting
to other devices. This means that I unfortunately had to duplicate the
event handler. I managed to refactor most other code to avoid
duplicating much more. (This is when macros would have been useful in
Go...)
2021-04-15 15:37:36 +02:00
Ayke van Laethem
340f6985ca Use TinyGo __app_ram_base
This makes sure the SoftDevice start address is configured in only one
place, and thus avoid potential issues when it is changed. It also will
allow setting a different SoftDevice RAM size with a linker flag
starting with LLVM 11 (when expressions are allowed in the MEMORY part
of a linker script).
2021-04-15 15:37:36 +02:00
Ayke van Laethem
10dcd116e8 nrf51: fix assertHandler function signature
Not using the right signature led to a compiler crash.
The compiler should probably be fixed in this case (to report an error
instead), but this at least fixes the issue.
2021-04-15 15:37:36 +02:00
deadprogram
28f9f4e69e release: prepare for v0.3.0
Signed-off-by: deadprogram <ron@hybridgroup.com>
2021-03-06 14:05:08 +01:00
deadprogram
172569b0a1 docs: few details on newly supported boards
Signed-off-by: deadprogram <ron@hybridgroup.com>
2021-03-05 00:22:43 +01:00
deadprogram
fa5736e183 docs: update license for 2021
Signed-off-by: deadprogram <ron@hybridgroup.com>
2021-03-05 00:22:11 +01:00
deadprogram
46364419f9 modules: update to latest TinyGo drivers package
Signed-off-by: deadprogram <ron@hybridgroup.com>
2021-02-14 13:21:12 +01:00
deadprogram
27cc35a60b examples: use standard service and characteristic UUIDs
Signed-off-by: deadprogram <ron@hybridgroup.com>
2021-02-02 00:17:38 +01:00
deadprogram
549cb4a3f7 gen: generate standard service and characteristic UUIDs from Nordic Semiconductor bluetooth numbers database
Signed-off-by: deadprogram <ron@hybridgroup.com>
2021-02-02 00:17:38 +01:00
Ron Evans
b4b125480a darwin: properly handle 16-bit UUIDs for service and characteristics in the unique format used by macOS
Signed-off-by: Ron Evans <ron@hybridgroup.com>
2021-02-02 00:17:14 +01:00
deadprogram
a355f254da linux: downgrade to older version of go-bluetooth that appears to work correctly with BlueZ 5.50
Signed-off-by: deadprogram <ron@hybridgroup.com>
2021-01-18 18:40:29 +01:00
Michael Mogenson
855d3c4b78
Update heart rate monitor data format
Print heart rate bpm from 2nd byte of payload.
2020-12-10 10:54:40 -05:00
Michael Mogenson
9dde7219a6
Fix up heart rate example
Fix up the heart rate example so that it conforms to the Heart Rate Service specification: https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=239866

This will let this example work with external clients like exercise equipment and fitness apps.

Changes:
- Advertise the HR service UUID instead of the HRM characteristic UUID.
- Change HRM characteristic to notify only.
- HRM payload needs to be two bytes:
  - 1st byte is flags specifying data type and sensor capabilities: this can be set to zero.
  - 2nd byte is HR measurement in bpm.

Tested on Raspberry Pi with nRF Connect app and exercise bike. Peripheral shows up as HR monitor and data is interpreted correctly.
2020-12-10 10:50:00 -05:00
140 changed files with 35327 additions and 11440 deletions

View file

@ -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
View 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
View 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
View file

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

3
.gitmodules vendored Normal file
View file

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

View file

@ -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
---
* **core**

View file

@ -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.
If your contribution includes a new API (one that does not exist yet for any BLE stack), please make sure it is portable:
* Ideally, add support for it to two different Bluetooth APIs at the same time, for example CoreBluetooth and BlueZ.
* If you are unable to do so, please explain (with links to documentation!) why this feature also fits a different API.
## How to use our Github repository
The `release` branch of this repo will always have the latest released version of the Go Bluetooth module. All of the active development work for the next release will take place in the `dev` branch. The Go Bluetooth module will use semantic versioning and will create a tag/release for each release.

View file

@ -1,4 +1,4 @@
Copyright (c) 2019 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
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
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
TinyGo Bluetooth includes data files from the Nordic Semiconductor Bluetooth
Numbers Database (https://github.com/NordicSemiconductor/bluetooth-numbers-database).
Copyright (c) 2019 - 2020, Nordic Semiconductor ASA. All rights reserved.

View file

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

101
README.md
View file

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

View file

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

116
adapter_cyw43439.go Normal file
View file

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

View file

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

205
adapter_hci.go Normal file
View file

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

122
adapter_hci_uart.go Normal file
View file

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

View file

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

134
adapter_ninafw.go Normal file
View file

@ -0,0 +1,134 @@
//go:build ninafw
package bluetooth
import (
"machine"
"time"
)
const maxConnections = 1
// Adapter represents the HCI connection to the NINA fw using the hardware UART.
type Adapter struct {
hciAdapter
}
// DefaultAdapter is the default adapter on the current system.
//
// Make sure to call Enable() before using it to initialize the adapter.
var DefaultAdapter = &Adapter{
hciAdapter: hciAdapter{
isDefault: true,
connectHandler: func(device Device, connected bool) {
return
},
connectedDevices: make([]Device, 0, maxConnections),
},
}
// Enable configures the BLE stack. It must be called before any
// Bluetooth-related calls (unless otherwise indicated).
func (a *Adapter) Enable() error {
// reset the NINA in BLE mode
machine.NINA_CS.Configure(machine.PinConfig{Mode: machine.PinOutput})
machine.NINA_CS.Low()
if machine.NINA_RESET_INVERTED {
resetNINAInverted()
} else {
resetNINA()
}
// serial port for nina chip
uart := machine.UART_NINA
cfg := machine.UARTConfig{
TX: machine.NINA_TX,
RX: machine.NINA_RX,
BaudRate: machine.NINA_BAUDRATE,
}
if !machine.NINA_SOFT_FLOWCONTROL {
cfg.CTS = machine.NINA_CTS
cfg.RTS = machine.NINA_RTS
}
uart.Configure(cfg)
transport := &hciUART{uart: uart}
if machine.NINA_SOFT_FLOWCONTROL {
machine.NINA_RTS.Configure(machine.PinConfig{Mode: machine.PinOutput})
machine.NINA_RTS.High()
machine.NINA_CTS.Configure(machine.PinConfig{Mode: machine.PinInput})
}
a.hci, a.att = newBLEStack(transport)
return a.enable()
}
func resetNINA() {
machine.NINA_RESETN.Configure(machine.PinConfig{Mode: machine.PinOutput})
machine.NINA_RESETN.High()
time.Sleep(100 * time.Millisecond)
machine.NINA_RESETN.Low()
time.Sleep(1000 * time.Millisecond)
}
func resetNINAInverted() {
machine.NINA_RESETN.Configure(machine.PinConfig{Mode: machine.PinOutput})
machine.NINA_RESETN.Low()
time.Sleep(100 * time.Millisecond)
machine.NINA_RESETN.High()
time.Sleep(1000 * time.Millisecond)
}
type hciUART struct {
uart *machine.UART
}
func (h *hciUART) startRead() {
if machine.NINA_SOFT_FLOWCONTROL {
machine.NINA_RTS.Low()
}
}
func (h *hciUART) endRead() {
if machine.NINA_SOFT_FLOWCONTROL {
machine.NINA_RTS.High()
}
}
func (h *hciUART) Buffered() int {
return h.uart.Buffered()
}
func (h *hciUART) ReadByte() (byte, error) {
return h.uart.ReadByte()
}
func (h *hciUART) Read(buf []byte) (int, error) {
return h.uart.Read(buf)
}
const writeAttempts = 200
func (h *hciUART) Write(buf []byte) (int, error) {
if machine.NINA_SOFT_FLOWCONTROL {
retries := writeAttempts
for machine.NINA_CTS.Get() {
retries--
if retries == 0 {
return 0, ErrHCITimeout
}
}
}
n, err := h.uart.Write(buf)
if err != nil {
return 0, err
}
return n, nil
}

View file

@ -1,17 +1,13 @@
// +build softdevice,s110v8
//go:build softdevice && s110v8
package bluetooth
/*
// Define SoftDevice functions as regular function declarations (not inline
// static functions).
#define SVCALL_AS_NORMAL_FUNCTION
#include "nrf_sdm.h"
#include "ble.h"
#include "ble_gap.h"
void assertHandler(void);
void assertHandler(uint32_t pc, uint16_t line_number, const uint8_t * p_file_name);
*/
import "C"
@ -46,8 +42,13 @@ func handleEvent() {
gapEvent := eventBuf.evt.unionfield_gap_evt()
switch id {
case C.BLE_GAP_EVT_CONNECTED:
currentConnection.Reg = gapEvent.conn_handle
DefaultAdapter.connectHandler(nil, true)
currentConnection.handle.Reg = uint16(gapEvent.conn_handle)
connectEvent := gapEvent.params.unionfield_connected()
device := Device{
Address: Address{makeMACAddress(connectEvent.peer_addr)},
connectionHandle: gapEvent.conn_handle,
}
DefaultAdapter.connectHandler(device, true)
case C.BLE_GAP_EVT_DISCONNECTED:
if defaultAdvertisement.isAdvertising.Get() != 0 {
// The advertisement was running but was automatically stopped
@ -58,8 +59,11 @@ func handleEvent() {
// necessary.
defaultAdvertisement.start()
}
currentConnection.Reg = C.BLE_CONN_HANDLE_INVALID
DefaultAdapter.connectHandler(nil, false)
currentConnection.handle.Reg = C.BLE_CONN_HANDLE_INVALID
device := Device{
connectionHandle: gapEvent.conn_handle,
}
DefaultAdapter.connectHandler(device, false)
case C.BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST:
// Respond with the default PPCP connection parameters by passing
// nil:
@ -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
View 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)
}
}
}

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

View file

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

View file

@ -1,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"

View file

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

13
adapter_s113v7.go Normal file
View 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"

View file

@ -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};

View file

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

View file

@ -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};

View file

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

View file

@ -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};

View file

@ -1,10 +1,13 @@
// +build softdevice,s140v7
//go:build softdevice && s140v7
package bluetooth
/*
// Add the correct SoftDevice include path to CFLAGS, so #include will work as
// 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"

View file

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

View file

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

1203
att_hci.go Normal file

File diff suppressed because it is too large Load diff

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

View file

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

1849
characteristic_uuids.go Normal file

File diff suppressed because it is too large Load diff

5
debug.go Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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
View file

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

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

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

View file

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

View file

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

229
gap.go
View file

@ -25,12 +25,12 @@ func (mac MACAddress) IsRandom() bool {
}
// SetRandom if is a random address.
func (mac MACAddress) SetRandom(val bool) {
func (mac *MACAddress) SetRandom(val bool) {
mac.isRandom = val
}
// Set the address
func (mac MACAddress) Set(val string) {
func (mac *MACAddress) Set(val string) {
m, err := ParseMAC(val)
if err != nil {
return
@ -53,6 +53,36 @@ type AdvertisementOptions struct {
// Interval in BLE-specific units. Create an interval by using NewDuration.
Interval Duration
// ManufacturerData stores Advertising Data.
ManufacturerData []ManufacturerDataElement
// ServiceData stores Advertising Data.
ServiceData []ServiceDataElement
}
// Manufacturer data that's part of an advertisement packet.
type ManufacturerDataElement struct {
// The company ID, which must be one of the assigned company IDs.
// The full list is in here:
// https://www.bluetooth.com/specifications/assigned-numbers/
// The list can also be viewed here:
// https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/company_identifiers/company_identifiers.yaml
// The value 0xffff can also be used for testing.
CompanyID uint16
// The value, which can be any value but can't be very large.
Data []byte
}
// ServiceDataElement strores a uuid/byte-array pair used as ServiceData advertisment elements
type ServiceDataElement struct {
// Service UUID.
// The list can also be viewed here:
// https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/uuids/service_uuids.yaml
UUID UUID
// the data byte array
Data []byte
}
// Duration is the unit of time used in BLE, in 0.625µs units. This unit of time
@ -69,34 +99,13 @@ func NewDuration(interval time.Duration) Duration {
// Connection is a numeric identifier that indicates a connection handle.
type Connection uint16
// Addresser contains a Bluetooth address, which is a MAC address plus some extra
// information.
type Addresser interface {
// String of the address
String() string
// Set the address
Set(val string)
// Is this address a random address?
// Bluetooth addresses are roughly split in two kinds: public
// (IEEE-assigned) addresses and random (not IEEE assigned) addresses.
// "Random" here doesn't mean it is exactly random but at least it looks
// random. Sometimes, it contains a hash.
// For more information:
// https://www.novelbits.io/bluetooth-address-privacy-ble/
// Set the address
SetRandom(bool)
IsRandom() bool
}
// ScanResult contains information from when an advertisement packet was
// received. It is passed as a parameter to the callback of the Scan method.
type ScanResult struct {
// Bluetooth address of the scanned device.
Address Addresser
Address Address
// RSSI the last time a packet from this device has been received.
// Signal strength of the advertisement packet.
RSSI int16
// The data obtained from the advertisement data, which may contain many
@ -126,6 +135,14 @@ type AdvertisementPayload interface {
// Bytes returns the raw advertisement packet, if available. It returns nil
// if this data is not available.
Bytes() []byte
// ManufacturerData returns a slice with all the manufacturer data present in the
// advertising. It may be empty.
ManufacturerData() []ManufacturerDataElement
// ServiceData returns a slice with all the service data present in the
// advertising. It may be empty.
ServiceData() []ServiceDataElement
}
// AdvertisementFields contains advertisement fields in structured form.
@ -138,6 +155,12 @@ type AdvertisementFields struct {
// part of the advertisement packet, in data types such as "complete list of
// 128-bit UUIDs".
ServiceUUIDs []UUID
// ManufacturerData is the manufacturer data of the advertisement.
ManufacturerData []ManufacturerDataElement
// ServiceData is the service data of the advertisement.
ServiceData []ServiceDataElement
}
// advertisementFields wraps AdvertisementFields to implement the
@ -170,6 +193,16 @@ func (p *advertisementFields) Bytes() []byte {
return nil
}
// ManufacturerData returns the underlying ManufacturerData field.
func (p *advertisementFields) ManufacturerData() []ManufacturerDataElement {
return p.AdvertisementFields.ManufacturerData
}
// ServiceData returns the underlying ServiceData field.
func (p *advertisementFields) ServiceData() []ServiceDataElement {
return p.AdvertisementFields.ServiceData
}
// rawAdvertisementPayload encapsulates a raw advertisement packet. Methods to
// get the data (such as LocalName()) will parse just the needed field. Scanning
// the data should be fast as most advertisement packets only have a very small
@ -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.
func (buf *rawAdvertisementPayload) reset() {
// 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
}
}
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
}
@ -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 {
// 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.
@ -358,4 +530,9 @@ type ConnectionParams struct {
// will be used.
MinInterval Duration
MaxInterval Duration
// Connection Supervision Timeout. After this time has passed with no
// communication, the connection is considered lost. If no timeout is
// specified, the timeout will be unchanged.
Timeout Duration
}

View file

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

422
gap_hci.go Normal file
View file

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

View file

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

View file

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

View file

@ -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, &params)
return makeError(errCode)
}
// Start advertisement. May only be called after it has been configured.
func (a *Advertisement) Start() error {
a.isAdvertising.Set(1)
errCode := C.sd_ble_gap_adv_start(a.handle, C.BLE_CONN_CFG_TAG_DEFAULT)
return makeError(errCode)
}
// Stop advertisement.
func (a *Advertisement) Stop() error {
a.isAdvertising.Set(0)
errCode := C.sd_ble_gap_adv_stop(a.handle)
return makeError(errCode)
}

View file

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

15
gap_sd.go Normal file
View file

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

133
gap_test.go Normal file
View file

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

View file

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

View file

@ -4,7 +4,7 @@ import (
"errors"
"time"
"github.com/JuulLabs-OSS/cbgo"
"github.com/tinygo-org/cbgo"
)
// 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
// services.
func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
cbuuids := []cbgo.UUID{}
for _, u := range uuids {
uuid, _ := cbgo.ParseUUID(u.String())
cbuuids = append(cbuuids, uuid)
}
d.prph.DiscoverServices(cbuuids)
func (d Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) {
d.prph.DiscoverServices([]cbgo.UUID{})
// clear cache of services
d.services = make(map[UUID]*DeviceService)
d.services = make(map[UUID]DeviceService)
// wait on channel for service discovery
select {
case <-d.servicesChan:
svcs := []DeviceService{}
for _, dsvc := range d.prph.Services() {
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{
uuidWrapper: uuid,
device: d,
service: dsvc,
deviceService: &deviceService{
uuidWrapper: dsvcuuid,
device: d,
service: dsvc,
},
}
svcs = append(svcs, svc)
d.services[svc.uuidWrapper] = &svc
d.services[svc.uuidWrapper] = svc
}
return svcs, nil
case <-time.NewTimer(10 * time.Second).C:
@ -52,15 +63,20 @@ type uuidWrapper = UUID
// DeviceService is a BLE service on a connected peripheral device.
type DeviceService struct {
*deviceService // embdedded as pointer to enable returning by []value in DiscoverServices
}
type deviceService struct {
uuidWrapper
device *Device
device Device
service cbgo.Service
service cbgo.Service
characteristics []DeviceCharacteristic
}
// UUID returns the UUID for this DeviceService.
func (s *DeviceService) UUID() UUID {
func (s DeviceService) UUID() UUID {
return s.uuidWrapper
}
@ -73,33 +89,51 @@ func (s *DeviceService) UUID() UUID {
//
// Passing a nil slice of UUIDs will return a complete list of
// characteristics.
func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteristic, error) {
func (s DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteristic, error) {
cbuuids := []cbgo.UUID{}
for _, u := range uuids {
uuid, _ := cbgo.ParseUUID(u.String())
cbuuids = append(cbuuids, uuid)
}
s.device.prph.DiscoverCharacteristics(cbuuids, s.service)
// clear cache of characteristics
s.device.characteristics = make(map[UUID]*DeviceCharacteristic)
s.characteristics = make([]DeviceCharacteristic, 0)
// wait on channel for characteristic discovery
select {
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() {
uuid, _ := ParseUUID(dchar.UUID().String())
char := DeviceCharacteristic{
deviceCharacteristic: &deviceCharacteristic{
uuidWrapper: uuid,
service: s,
characteristic: dchar,
},
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")
}
chars = append(chars, char)
s.device.characteristics[char.uuidWrapper] = &char
}
return chars, nil
case <-time.NewTimer(10 * time.Second).C:
@ -107,6 +141,19 @@ func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacter
}
}
// Small helper to create a DeviceCharacteristic object.
func (s DeviceService) makeCharacteristic(uuid UUID, dchar cbgo.Characteristic) DeviceCharacteristic {
char := DeviceCharacteristic{
deviceCharacteristic: &deviceCharacteristic{
uuidWrapper: uuid,
service: s,
characteristic: dchar,
},
}
s.characteristics = append(s.characteristics, char)
return char
}
// DeviceCharacteristic is a BLE characteristic on a connected peripheral
// device.
type DeviceCharacteristic struct {
@ -116,18 +163,40 @@ type DeviceCharacteristic struct {
type deviceCharacteristic struct {
uuidWrapper
service *DeviceService
service DeviceService
characteristic cbgo.Characteristic
callback func(buf []byte)
readChan chan error
writeChan chan error
}
// UUID returns the UUID for this DeviceCharacteristic.
func (c *DeviceCharacteristic) UUID() UUID {
func (c DeviceCharacteristic) UUID() UUID {
return c.uuidWrapper
}
// Write replaces the characteristic value with a new value. The
// call will return after all data has been written.
func (c DeviceCharacteristic) Write(p []byte) (n int, err error) {
c.writeChan = make(chan error)
c.service.device.prph.WriteCharacteristic(p, c.characteristic, true)
// wait for result
select {
case <-time.NewTimer(10 * time.Second).C:
err = errors.New("timeout on Write()")
case err = <-c.writeChan:
}
c.writeChan = nil
if err != nil {
return 0, err
}
return len(p), nil
}
// WriteWithoutResponse replaces the characteristic value with a new value. The
// call will return before all data has been written. A limited number of such
// writes can be in flight at any given time. This call is also known as a
@ -153,6 +222,11 @@ func (c DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) err
return nil
}
// GetMTU returns the MTU for the characteristic.
func (c DeviceCharacteristic) GetMTU() (uint16, error) {
return uint16(c.service.device.prph.MaximumWriteValueLength(false)), nil
}
// Read reads the current characteristic value.
func (c *deviceCharacteristic) Read(data []byte) (n int, err error) {
c.readChan = make(chan error)

330
gattc_hci.go Normal file
View file

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

View file

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

View file

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

434
gattc_windows.go Normal file
View 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
}

View file

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

128
gatts_hci.go Normal file
View file

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

View file

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

View file

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

View file

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

309
gatts_windows.go Normal file
View file

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

33
go.mod
View file

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

104
go.sum
View file

@ -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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/soypat/cyw43439 v0.0.0-20240609122733-da9153086796 h1:1/r2URInjjFtWqT61gU7YGVCq3BRyXt/C7z4oLRF9Lo=
github.com/soypat/cyw43439 v0.0.0-20240609122733-da9153086796/go.mod h1:1Otjk6PRhfzfcVHeWMEeku/VntFqWghUwuSQyivb2vE=
github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef h1:phH95I9wANjTYw6bSYLZDQfNvao+HqYDom8owbNa0P4=
github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef/go.mod h1:oCVCNGCHMKoBj97Zp9znLbQ1nHxpkmOY9X+UAGzOxc8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/suapapa/go_eddystone v1.3.1/go.mod h1:bXC11TfJOS+3g3q/Uzd7FKd5g62STQEfeEIhcKe4Qy8=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200602180216-279210d13fed h1:g4KENRiCMEx58Q7/ecwfT0N2o8z35Fnbsjig/Alf2T4=
golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q=
github.com/tinygo-org/cbgo v0.0.4 h1:3D76CRYbH03Rudi8sEgs/YO0x3JIMdyq8jlQtk/44fU=
github.com/tinygo-org/cbgo v0.0.4/go.mod h1:7+HgWIHd4nbAz0ESjGlJ1/v9LDU1Ox8MGzP9mah/fLk=
github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899 h1:/DyaXDEWMqoVUVEJVJIlNk1bXTbFs8s3Q4GdPInSKTQ=
github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899/go.mod h1:LU7Dw00NJ+N86QkeTGjMLNkYcEYMor6wTDpTCu0EaH8=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 h1:/yRP+0AN7mf5DkD3BAI6TOFnd51gEoDEb8o35jIFtgw=
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
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-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 h1:DvY3Zkh7KabQE/kfzMvYvKirSiguP9Q/veMtkYyf0o8=
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
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=
tinygo.org/x/drivers v0.13.0 h1:ohzhFiPb/5dcRop3X+Gdvsr6uswmnpfMX9KsAMtgtTM=
tinygo.org/x/drivers v0.13.0/go.mod h1:mShi1lpVtJFpApkZgwyrzDKHToeGfWIuB08utyHxZ7g=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
tinygo.org/x/drivers v0.26.1-0.20230922160320-ed51435c2ef6 h1:w18u47MirULgAl+bP0piUGu5VUZDs7TvXwHASEVXqHk=
tinygo.org/x/drivers v0.26.1-0.20230922160320-ed51435c2ef6/go.mod h1:X7utcg3yfFUFuKLOMTZD56eztXMjpkcf8OHldfTBsjw=
tinygo.org/x/tinyfont v0.4.0 h1:XexPKEKiHInf6p4CMCJwsIheVPY0T46HUs6ictYyZfE=
tinygo.org/x/tinyfont v0.4.0/go.mod h1:7nVj3j3geqBoPDzpFukAhF1C8AP9YocMsZy0HSAcGCA=
tinygo.org/x/tinyterm v0.3.0 h1:4MMZoMyrbWbjru1KP/Z2TGhaguy/Uh5Mdhf/niemM8c=
tinygo.org/x/tinyterm v0.3.0/go.mod h1:F1pQjxEwNZQIc5czeJSBtk57ucEvbR4u7vHaLhWhHtg=

787
hci.go Normal file
View file

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

156
l2cap_hci.go Normal file
View file

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

2
mac.go
View file

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

5
nodebug.go Normal file
View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View 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