From b10bd20e63a9030cccdf9c3bfe3e1039fc64eb4e Mon Sep 17 00:00:00 2001 From: Connor Rigby Date: Tue, 24 Dec 2024 11:21:04 -0800 Subject: [PATCH] Update README.md with examples --- README.md | 249 +++++++++++++++++++++++++++-- lib/blue_heron/advertising_data.ex | 8 +- 2 files changed, 244 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index c4c60f1..67e9820 100644 --- a/README.md +++ b/README.md @@ -56,17 +56,244 @@ Here's what's known: ## Getting started -See the [examples](https://github.com/blue-heron/blue_heron/tree/main/examples) for the time being. - -## Transports - -BlueHeron interacts with Bluetooth modules via transports. Transport -implementations are not part of this library since they are hardware-specific. -See -[BlueHeronTransportUART](https://github.com/blue-heron/blue_heron_transport_uart) -and -[BlueHeronTransportUSB](https://github.com/blue-heron/blue_heron_transport_usb) -for examples. +Below are examples of the roles supported by BlueHeron currently. + +### Broadcaster Role + +The simplest role to implement is a BLE Broadcaster. This is a device that doesn't *do* anything by itself, +but broadcasts it's information publically. A good example of this role is Apple's [iBeacon](https://en.wikipedia.org/wiki/IBeacon). + +```elixir +iex(1)> flags_ad = BlueHeron.AdvertisingData.flags([le_general_discoverable_mode: true, br_edr_not_supported: true]) +<<2, 1, 6>> +iex(2)> uuid = <<0xF018E00E0ECE45B09617B744833D89BA::128>> +<<240, 24, 224, 14, 14, 206, 69, 176, 150, 23, 183, 68, 131, 61, 137, 186>> +iex(3)> major = 1 +1 +iex(4)> minor = 0 +0 +iex(5)> tx_power = -60 +iex(6)> ibeacon = BlueHeron.AdvertisingData.IBeacon.new(uuid, major, minor, tx_power) +<<76, 0, 2, 21, 240, 24, 224, 14, 14, 206, 69, 176, 150, 23, 183, 68, 131, 61, + 137, 186, 0, 1, 0, 0, 196>> +iex(7)> ibeacon_ad = BlueHeron.AdvertisingData.manufacturer_specific_data(ibeacon) +<<26, 255, 76, 0, 2, 21, 240, 24, 224, 14, 14, 206, 69, 176, 150, 23, 183, 68, + 131, 61, 137, 186, 0, 1, 0, 0, 196>> +BlueHeron.Broadcaster.set_advertising_data(flags_ad <> BlueHeron.AdvertisingData.manufacturer_specific_data(ibeacon)) +:ok +iex(8)> BlueHeron.Broadcaster.start_advertising() +:ok +``` + +### Peripheral + +A Peripheral is a Broadcaster that allows a connection via the GATT and GAP Service Discovery protocols. This allows a device +to *do* something. To setup a Peripheral, first we need to enable the Broadcaster role so our device can be found. + +```elixir +# stop advertising while we change the payload. +iex(1)> BlueHeron.Broadcaster.stop_advertising() +:ok +# flags will be the same as before. +iex(2)> flags_ad = BlueHeron.AdvertisingData.flags([le_general_discoverable_mode: true, br_edr_not_supported: true]) +<<2, 1, 6>> +iex(3)> short_name = "nerves" +"nerves" +iex(4)> short_name_ad = BlueHeron.AdvertisingData.short_name(short_name) +"\a\bnerves" +iex(5)> incomplete_list_of_service_ids = BlueHeron.AdvertisingData.incomplete_list_of_service_uuids([0xF018E00E0ECE45B09617B744833D89BA]) +<<17, 6, 186, 137, 61, 131, 68, 183, 23, 150, 176, 69, 206, 14, 14, 224, 24, + 240>> +iex(6)> BlueHeron.Broadcaster.set_advertising_data(flags_ad <> short_name_ad <> incomplete_list_of_service_ids) +:ok +iex(7)> BlueHeron.Broadcaster.start_advertising() +:ok +``` + +This will set the scanned name to be `nerves`. + +Since the AdvertisingData payload is limited to only 31 bytes, if we want to set additional information, we +can put it in the `ScanResponseData`. This is an additional payload that is scanned and usually cached on +Central devices. For example setting the long name can be done with: + +```elixir +# stop advertising while we change the payload. +iex(1)> BlueHeron.Broadcaster.stop_advertising() +:ok +# flags will be the same as before. +iex(2)> flags_ad = BlueHeron.AdvertisingData.flags([le_general_discoverable_mode: true, br_edr_not_supported: true]) +<<2, 1, 6>> +iex(3)> long_name = "nerves-" <> Nerves.Runtime.serial_number() +"nerves-00000000b5f1bea0" +iex(4)> long_name_ad = BlueHeron.AdvertisingData.complete_name(long_name) +<<24, 9, 110, 101, 114, 118, 101, 115, 45, 48, 48, 48, 48, 48, 48, 48, 48, 98, + 53, 102, 49, 98, 101, 97, 48>> +iex(5)> BlueHeron.Broadcaster.set_scan_response_data(long_name_ad) +:ok +iex(6)> BlueHeron.Broadcaster.start_advertising() +:ok +``` + +Now that the device is advertising, we need to implement the service we listed: `0xF018E00E0ECE45B09617B744833D89BA`, as well as +implement the `GAP` and `GATT` profiles. + +```elixir +iex(7)> gap_service = BlueHeron.GATT.Service.new(%{ +...(7)> id: :gap, +...(7)> type: 0x1800, +...(7)> characteristics: [ +...(7)> BlueHeron.GATT.Characteristic.new(%{ +...(7)> id: {:gap, :device_name}, +...(7)> type: 0x2A00, +...(7)> properties: 0b0000010 +...(7)> }), +...(7)> BlueHeron.GATT.Characteristic.new(%{ +...(7)> id: {:gap, :appearance}, +...(7)> type: 0x2A01, +...(7)> properties: 0b0000010 +...(7)> }) +...(7)> ], +...(7)> read: fn +...(7)> {:gap, :device_name} -> +...(7)> "nerves-" <> Nerves.Runtime.serial_number() +...(7)> {:gap, :appearance} -> +...(7)> <<0x008D::little-16>> +...(7)> end +...(7)> }) +%BlueHeron.GATT.Service{ + id: :gap, + type: 6144, + characteristics: [ + %BlueHeron.GATT.Characteristic{ + id: {:gap, :device_name}, + type: 10752, + properties: 2, + permissions: nil, + descriptor: nil, + handle: nil, + value_handle: nil, + descriptor_handle: nil + }, + %BlueHeron.GATT.Characteristic{ + id: {:gap, :appearance}, + type: 10753, + properties: 2, + permissions: nil, + descriptor: nil, + handle: nil, + value_handle: nil, + descriptor_handle: nil + } + ], + handle: nil, + end_group_handle: nil, + read: #Function<42.39164016/1 in :erl_eval.expr/6>, + write: #Function<3.104805658/2 in BlueHeron.GATT.Service.default_write_callback>, + subscribe: #Function<5.104805658/1 in BlueHeron.GATT.Service.default_subscribe_callback>, + unsubscribe: #Function<7.104805658/1 in BlueHeron.GATT.Service.default_unsubscribe_callback> +} +iex(8)> gatt_service = BlueHeron.GATT.Service.new(%{ +...(8)> id: :gatt, +...(8)> type: 0x1801, +...(8)> characteristics: [ +...(8)> BlueHeron.GATT.Characteristic.new(%{ +...(8)> id: {:gatt, :service_changed}, +...(8)> type: 0x2A05, +...(8)> properties: 0b100000 +...(8)> }) +...(8)> ] +...(8)> }) +%BlueHeron.GATT.Service{ + id: :gatt, + type: 6145, + characteristics: [ + %BlueHeron.GATT.Characteristic{ + id: {:gatt, :service_changed}, + type: 10757, + properties: 32, + permissions: nil, + descriptor: nil, + handle: nil, + value_handle: nil, + descriptor_handle: nil + } + ], + handle: nil, + end_group_handle: nil, + read: #Function<1.104805658/1 in BlueHeron.GATT.Service.default_read_callback>, + write: #Function<3.104805658/2 in BlueHeron.GATT.Service.default_write_callback>, + subscribe: #Function<5.104805658/1 in BlueHeron.GATT.Service.default_subscribe_callback>, + unsubscribe: #Function<7.104805658/1 in BlueHeron.GATT.Service.default_unsubscribe_callback> +} +iex(9)> custom_service = BlueHeron.GATT.Service.new(%{ +...(9)> id: :test, +...(9)> type: 0xF018E00E0ECE45B09617B744833D89BA, +...(9)> characteristics: [ +...(9)> BlueHeron.GATT.Characteristic.new(%{ +...(9)> id: {:test, :char_1}, +...(9)> type: 0x2e0f8e717a7d4690998377626bc6b657, +...(9)> properties: 0b0000010, +...(9)> permissions: [:read_auth] +...(9)> }), +...(9)> BlueHeron.GATT.Characteristic.new(%{ +...(9)> id: {:test, :char_2}, +...(9)> type: 0x3e0f8e717a7d4690998377626bc6b657, +...(9)> properties: 0b0001000, +...(9)> permissions: [:write_auth] +...(9)> }), +...(9)> ], +...(9)> read: fn +...(9)> {:test, :char_1} -> +...(9)> "hello, world" +...(9)> end, +...(9)> write: fn +...(9)> {:test, :char_2}, value -> +...(9)> require Logger +...(9)> Logger.info("write #{inspect(value)}") +...(9)> end +...(9)> }) +%BlueHeron.GATT.Service{ + id: :test, + type: 319143878486512296490943150958665632186, + characteristics: [ + %BlueHeron.GATT.Characteristic{ + id: {:test, :char_1}, + type: 61225261351838855375776121692935861847, + properties: 2, + permissions: [:read_auth], + descriptor: nil, + handle: nil, + value_handle: nil, + descriptor_handle: nil + }, + %BlueHeron.GATT.Characteristic{ + id: {:test, :char_2}, + type: 82492909284397509342237034657421375063, + properties: 8, + permissions: [:write_auth], + descriptor: nil, + handle: nil, + value_handle: nil, + descriptor_handle: nil + } + ], + handle: nil, + end_group_handle: nil, + read: #Function<42.39164016/1 in :erl_eval.expr/6>, + write: #Function<41.39164016/2 in :erl_eval.expr/6>, + subscribe: #Function<5.104805658/1 in BlueHeron.GATT.Service.default_subscribe_callback>, + unsubscribe: #Function<7.104805658/1 in BlueHeron.GATT.Service.default_unsubscribe_callback> +} +iex(10)> BlueHeron.Peripheral.add_service(gap_service) +:ok +iex(11)> BlueHeron.Peripheral.add_service(gatt_service) +:ok +iex(12)> BlueHeron.Peripheral.add_service(encrypted_service) +:ok +``` + +Once completed, you should be able to connect to the `nerves` BLE device, it will do a encrypted `Bonding` procedure, and finally allow +you to `read` and `write` the implemented services. ## Helpful docs diff --git a/lib/blue_heron/advertising_data.ex b/lib/blue_heron/advertising_data.ex index 0bc28e2..87795b3 100644 --- a/lib/blue_heron/advertising_data.ex +++ b/lib/blue_heron/advertising_data.ex @@ -60,6 +60,10 @@ defmodule BlueHeron.AdvertisingData do iex()> BlueHeron.AdvertisingData.incomplete_list_of_service_uuids([0x12, 0xab]) <<5, 2, 171, 0, 18, 0>> + + iex()> BlueHeron.AdvertisingData.incomplete_list_of_service_uuids([0xF018E00E0ECE45B09617B744833D89BA]) + <<17, 6, 186, 137, 61, 131, 68, 183, 23, 150, 176, 69, 206, 14, 14, 224, 24, + 240>> """ @spec incomplete_list_of_service_uuids([0 | pos_integer]) :: ad() def incomplete_list_of_service_uuids([first | _] = list) when first <= 0xFF do @@ -80,7 +84,7 @@ defmodule BlueHeron.AdvertisingData do <> end - def incomplete_list_of_service_uuids([first | _] = list) when first <= 0xFFFF do + def incomplete_list_of_service_uuids(list) do list_binary = Enum.reduce(list, <<>>, fn uuid, acc -> <> <> acc @@ -113,7 +117,7 @@ defmodule BlueHeron.AdvertisingData do <> end - def complete_list_of_service_uuids([first | _] = list) when first <= 0xFFFF do + def complete_list_of_service_uuids(list) do list_binary = Enum.reduce(list, <<>>, fn uuid, acc -> <> <> acc