Skip to content

Commit

Permalink
Update README.md with examples
Browse files Browse the repository at this point in the history
  • Loading branch information
ConnorRigby committed Dec 24, 2024
1 parent 5a131bc commit 148fc2f
Show file tree
Hide file tree
Showing 2 changed files with 244 additions and 13 deletions.
249 changes: 238 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 6 additions & 2 deletions lib/blue_heron/advertising_data.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -80,7 +84,7 @@ defmodule BlueHeron.AdvertisingData do
<<byte_size(list_binary) + 1, @incomplete_list_of_32_bit_service_uuids, list_binary::binary>>
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 -> <<uuid::little-128>> <> acc
Expand Down Expand Up @@ -113,7 +117,7 @@ defmodule BlueHeron.AdvertisingData do
<<byte_size(list_binary) + 1, @complete_list_of_32_bit_service_uuids, list_binary::binary>>
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 -> <<uuid::little-128>> <> acc
Expand Down

0 comments on commit 148fc2f

Please sign in to comment.