├── .formatter.exs ├── lib ├── blue_heron.ex └── blue_heron │ ├── hci.ex │ ├── hci │ ├── serializable.ex │ ├── events │ │ ├── command_complete │ │ │ └── return_parameters.ex │ │ ├── inquiry_complete.ex │ │ ├── number_of_completed_packets.ex │ │ ├── le_meta │ │ │ ├── connection_update_complete.ex │ │ │ ├── advertising_report.ex │ │ │ ├── long_term_key_request.ex │ │ │ ├── connection_complete.ex │ │ │ └── enhanced_connection_complete_v1.ex │ │ ├── le_meta.ex │ │ ├── encryption_change.ex │ │ ├── disconnection_complete.ex │ │ ├── command_status.ex │ │ └── command_complete.ex │ ├── commands │ │ ├── controller_and_baseband │ │ │ ├── write_class_of_device.ex │ │ │ ├── write_inquiry_mode.ex │ │ │ ├── write_page_timeout.ex │ │ │ ├── write_simple_pairing_mode.ex │ │ │ ├── write_scan_enable.ex │ │ │ ├── write_local_name.ex │ │ │ ├── reset.ex │ │ │ ├── write_default_erroneous_data_reporting.ex │ │ │ ├── write_secure_connections_host_support.ex │ │ │ ├── write_synchronous_flow_control_enable.ex │ │ │ ├── set_controller_to_host_flow_control.ex │ │ │ ├── write_le_host_support.ex │ │ │ ├── write_extended_inquiry_response.ex │ │ │ ├── read_local_name.ex │ │ │ └── host_buffer_size.ex │ │ ├── link_control │ │ │ ├── authentication_requested.ex │ │ │ ├── set_connection_encryption.ex │ │ │ └── disconnect.ex │ │ ├── le_controller │ │ │ ├── read_local_supported_features.ex │ │ │ ├── set_random_address.ex │ │ │ ├── set_event_mask.ex │ │ │ ├── set_scan_response_data.ex │ │ │ ├── create_connection_cancel.ex │ │ │ ├── set_advertising_data.ex │ │ │ ├── read_white_list_size.ex │ │ │ ├── set_advertising_enable.ex │ │ │ ├── long_term_key_request_negative_reply.ex │ │ │ ├── set_scan_enable.ex │ │ │ ├── long_term_key_request_reply.ex │ │ │ ├── read_buffer_size_v1.ex │ │ │ ├── set_scan_parameters.ex │ │ │ ├── set_advertising_parameters.ex │ │ │ └── create_connection.ex │ │ ├── informational_parameters │ │ │ ├── read_local_supported_commands.ex │ │ │ ├── read_bd_addr.ex │ │ │ ├── read_local_version.ex │ │ │ └── read_buffer_size.ex │ │ ├── le_controller.ex │ │ ├── link_control.ex │ │ ├── informational_parameters.ex │ │ ├── link_policy.ex │ │ ├── controller_and_baseband.ex │ │ └── link_policy │ │ │ └── write_default_link_policy_settings.ex │ ├── deserializable.ex │ ├── event.ex │ ├── transport │ │ ├── uart │ │ │ └── framing.ex │ │ └── uart.ex │ └── command.ex │ ├── data_type │ ├── manufacturer_behaviour.ex │ ├── service_data.ex │ ├── manufacturer_data.ex │ └── manufacturer_data │ │ └── apple.ex │ ├── att │ ├── responses │ │ ├── execute_write_response.ex │ │ ├── write_response.ex │ │ ├── read_response.ex │ │ ├── exchange_mtu_response.ex │ │ ├── read_blob_response.ex │ │ ├── prepare_write_response.ex │ │ ├── error_response.ex │ │ ├── find_by_type_value_response.ex │ │ ├── read_by_group_type_response.ex │ │ ├── find_information_response.ex │ │ └── read_by_type_response.ex │ ├── confirmations │ │ └── handle_value_confirmation.ex │ ├── requests │ │ ├── read_request.ex │ │ ├── write_request.ex │ │ ├── execute_write_request.ex │ │ ├── prepare_write_request.ex │ │ ├── exchange_mtu_request.ex │ │ ├── read_blob_request.ex │ │ ├── find_information_request.ex │ │ ├── find_by_type_value_request.ex │ │ ├── read_by_type_request.ex │ │ └── read_by_group_type_request.ex │ ├── indications │ │ └── handle_value_indication.ex │ ├── commands │ │ └── write_command.ex │ └── notifications │ │ └── handle_value_notification.ex │ ├── smp │ ├── default_io_handler.ex │ ├── io_handler.ex │ └── key_manager.ex │ ├── registry.ex │ ├── advertising_data │ └── ibeacon.ex │ ├── l2_cap.ex │ ├── assigned_numbers │ ├── company_identifiers.ex │ └── generic_access_profile.ex │ ├── gatt │ ├── characteristic │ │ └── descriptor.ex │ ├── characteristic.ex │ └── service.ex │ ├── acl.ex │ ├── application.ex │ ├── acl_buffer.ex │ └── address.ex ├── test ├── test_helper.exs ├── blue_heron_test.exs └── blue_heron │ ├── advertising_data_test.exs │ ├── gatt │ ├── service_test.exs │ └── characteristic_test.exs │ ├── transport_test.exs │ ├── hci │ └── command │ │ ├── le_controller │ │ ├── set_advertising_enable_test.exs │ │ ├── set_random_address_test.exs │ │ ├── set_advertising_data_test.exs │ │ ├── set_scan_parameters_test.exs │ │ └── set_advertising_parameters_test.exs │ │ └── controller_and_baseband │ │ ├── write_scan_enable_test.exs │ │ ├── write_le_host_support_test.exs │ │ ├── write_synchronous_flow_control_enable_test.exs │ │ ├── write_default_erroneous_data_reporting_test.exs │ │ └── set_event_mask_test.exs │ ├── acl_test.exs │ ├── transport │ └── uart │ │ └── framing_test.exs │ └── address_test.exs ├── NOTICE ├── .credo.exs ├── REUSE.toml ├── .gitignore ├── LICENSES └── MIT.txt ├── mix.exs ├── CHANGELOG.md ├── .github └── workflows │ └── elixir.yaml └── mix.lock /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test,examples}/**/*.{ex,exs}"], 4 | locals_without_parens: [defparameters: 1] 5 | ] 6 | -------------------------------------------------------------------------------- /lib/blue_heron.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron do 6 | @moduledoc """ 7 | BLE 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2021 Jon Carstens 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | ExUnit.start(capture_log: true) 7 | -------------------------------------------------------------------------------- /test/blue_heron_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeronTest do 6 | use ExUnit.Case 7 | doctest BlueHeron 8 | end 9 | -------------------------------------------------------------------------------- /lib/blue_heron/hci.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.HCI do 6 | @moduledoc """ 7 | Volume 4 of the Bluetooth Spec 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /test/blue_heron/advertising_data_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.AdvertisingDataTest do 6 | use ExUnit.Case 7 | doctest BlueHeron.AdvertisingData 8 | end 9 | -------------------------------------------------------------------------------- /test/blue_heron/gatt/service_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.GATT.ServiceTest do 6 | use ExUnit.Case 7 | 8 | doctest BlueHeron.GATT.Service 9 | end 10 | -------------------------------------------------------------------------------- /test/blue_heron/gatt/characteristic_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.GATT.CharacteristicTest do 6 | use ExUnit.Case 7 | 8 | doctest BlueHeron.GATT.Characteristic 9 | end 10 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | BlueHeron is open-source software licensed under the Apache License, Version 2.0. 2 | 3 | Copyright holders include Alexandre Cadeau, Alex McLain,Bruce Wong, Connor Rigby, Frank 4 | Hunleth, Jason Axelson, Jon Carstens Kevin Ansfield, Markus Hutzler, Peter 5 | Madsen-mygdal, Troels Brødsgaard 6 | 7 | Authoritative REUSE-compliant copyright and license metadata available at 8 | https://hex.pm/packages/blue-heron. -------------------------------------------------------------------------------- /.credo.exs: -------------------------------------------------------------------------------- 1 | # config/.credo.exs 2 | %{ 3 | configs: [ 4 | %{ 5 | name: "default", 6 | checks: [ 7 | {Credo.Check.Refactor.MapInto, false}, 8 | {Credo.Check.Warning.LazyLogging, false}, 9 | {Credo.Check.Readability.LargeNumbers, only_greater_than: 86400}, 10 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, parens: true} 11 | ] 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/serializable.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defprotocol BlueHeron.HCI.Serializable do 6 | @doc """ 7 | Serialize an HCI data structure as a binary 8 | """ 9 | def serialize(hci_struct) 10 | end 11 | 12 | defimpl BlueHeron.HCI.Serializable, for: BitString do 13 | # If its already serialized, pass it on 14 | def serialize(data), do: data 15 | end 16 | -------------------------------------------------------------------------------- /lib/blue_heron/data_type/manufacturer_behaviour.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Very 2 | # 3 | # SPDX-License-Identifier: MIT 4 | # 5 | defmodule BlueHeron.ManufacturerDataBehaviour do 6 | @moduledoc """ 7 | Defines a behaviour that manufacturer data modules should implement. 8 | """ 9 | 10 | @doc """ 11 | Returns the company associated with some manufacturer data. 12 | 13 | See: `BlueHeron.CompanyIdentifiers` 14 | """ 15 | @callback company :: String.t() 16 | end 17 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/events/command_complete/return_parameters.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defprotocol BlueHeron.HCI.CommandComplete.ReturnParameters do 6 | @doc """ 7 | Protocol for handling command return_parameters in CommandComplete event 8 | 9 | This is mainly to allow us to do function generation at compile time 10 | for handling this parsing for specific commands. 11 | """ 12 | def decode(cc_struct) 13 | 14 | def encode(cc_struct) 15 | end 16 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[annotations]] 4 | path = [ 5 | ".github/workflows/elixir.yaml", 6 | ".credo.exs", 7 | ".formatter.exs", 8 | ".gitignore", 9 | "CHANGELOG.md", 10 | "NOTICE", 11 | "REUSE.toml", 12 | "mix.exs", 13 | "mix.lock" 14 | ] 15 | precedence = "aggregate" 16 | SPDX-FileCopyrightText = "None" 17 | SPDX-License-Identifier = "CC0-1.0" 18 | 19 | [[annotations]] 20 | path = [ 21 | "README.md", 22 | ] 23 | precedence = "aggregate" 24 | SPDX-FileCopyrightText = "2024 Connor Rigby, Frank Hunleth, Jason Axelson, Jon Carstens" 25 | SPDX-License-Identifier = "CC-BY-4.0" 26 | -------------------------------------------------------------------------------- /lib/blue_heron/att/responses/execute_write_response.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.ATT.ExecuteWriteResponse do 7 | @moduledoc """ 8 | > The ATT_EXECUTE_WRITE_RSP PDU is sent in response to a received 9 | > ATT_EXECUTE_WRITE_REQ PDU. 10 | 11 | Bluetooth Spec v5.2, vol 3, Part F, 3.4.6.4 12 | """ 13 | 14 | defstruct [:opcode] 15 | 16 | def serialize(%{}) do 17 | <<0x19>> 18 | end 19 | 20 | def deserialize(<<0x19>>) do 21 | %__MODULE__{opcode: 0x19} 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/blue_heron/smp/default_io_handler.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.SMP.DefaultIOHandler do 6 | @moduledoc """ 7 | Default IO Handler for SMP 8 | """ 9 | 10 | require Logger 11 | 12 | @behaviour BlueHeron.SMP.IOHandler 13 | 14 | @impl true 15 | def keyfile, do: {:ok, "/data/blue_heron.term"} 16 | 17 | @impl true 18 | def passkey(data) do 19 | Logger.info("SMP Passkey: #{inspect(data)}") 20 | end 21 | 22 | @impl true 23 | def status_update(status) do 24 | Logger.info("SMP Status update: #{inspect(status)}") 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/blue_heron/att/responses/write_response.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.ATT.WriteResponse do 7 | @moduledoc """ 8 | > The ATT_WRITE_RSP PDU is sent in reply to a valid ATT_WRITE_REQ PDU and 9 | > acknowledges that the attribute has been successfully written. 10 | 11 | Bluetooth Spec v5.2, vol 3, Part F, 3.4.5.2 12 | """ 13 | 14 | defstruct [:opcode] 15 | 16 | def serialize(%{}) do 17 | <<0x13>> 18 | end 19 | 20 | def deserialize(<<0x13>>) do 21 | %__MODULE__{opcode: 0x13} 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/blue_heron/att/responses/read_response.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.ATT.ReadResponse do 7 | @moduledoc """ 8 | > The ATT_READ_RSP PDU is sent in reply to a received Read Request and contains 9 | > the value of the attribute that has been read. 10 | 11 | Bluetooth Spec v5.2, vol 3, Part F, 3.4.4.4 12 | """ 13 | 14 | defstruct [:opcode, :value] 15 | 16 | def serialize(%{value: value}) do 17 | <<0x0B, value::binary>> 18 | end 19 | 20 | def deserialize(<<0x0B, value::binary>>) do 21 | %__MODULE__{opcode: 0x0B, value: value} 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/blue_heron/att/confirmations/handle_value_confirmation.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.ATT.HandleValueConfirmation do 7 | @moduledoc """ 8 | > The ATT_HANDLE_VALUE_CFM PDU is sent in response to a received 9 | > ATT_HANDLE_VALUE_IND PDU and confirms that the client has received an indication 10 | > of the given attribute. 11 | 12 | Bluetooth Spec v5.2, vol 3, Part F, 3.4.7.3 13 | """ 14 | 15 | defstruct [:opcode] 16 | 17 | def serialize(%{}) do 18 | <<0x1E>> 19 | end 20 | 21 | def deserialize(<<0x1E>>) do 22 | %__MODULE__{opcode: 0x1E} 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/blue_heron/att/requests/read_request.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.ATT.ReadRequest do 7 | @moduledoc """ 8 | > The ATT_READ_REQ PDU is used to request the server to read the value of an 9 | > attribute and return its value in an ATT_READ_RSP PDU. 10 | 11 | Bluetooth Spec v5.2, vol 3, Part F, 3.4.4.3 12 | """ 13 | 14 | defstruct [:opcode, :handle] 15 | 16 | def serialize(%{handle: handle}) do 17 | <<0x0A, handle::little-16>> 18 | end 19 | 20 | def deserialize(<<0x0A, handle::little-16>>) do 21 | %__MODULE__{opcode: 0x0A, handle: handle} 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/blue_heron/att/responses/exchange_mtu_response.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.ATT.ExchangeMTUResponse do 7 | @moduledoc """ 8 | > The ATT_EXCHANGE_MTU_RSP PDU is sent in reply to a received 9 | > ATT_EXCHANGE_MTU_REQ PDU. 10 | 11 | Bluetooth Spec v5.2, vol 3, Part F, 3.4.2.2 12 | """ 13 | 14 | defstruct [:opcode, :server_rx_mtu] 15 | 16 | def serialize(emtu) do 17 | <<0x03, emtu.server_rx_mtu::little-16>> 18 | end 19 | 20 | def deserialize(<<0x03, server_rx_mtu::little-16>>) do 21 | %__MODULE__{opcode: 0x03, server_rx_mtu: server_rx_mtu} 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | blue_heron-*.tar 24 | 25 | .elixir_ls 26 | 27 | config/ 28 | !config/config.exs 29 | old 30 | -------------------------------------------------------------------------------- /lib/blue_heron/att/responses/read_blob_response.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.ATT.ReadBlobResponse do 7 | @moduledoc """ 8 | > The ATT_READ_BLOB_RSP PDU is sent in reply to a received 9 | > ATT_READ_BLOB_REQ PDU and contains part of the value of the attribute that has 10 | > been read 11 | 12 | Bluetooth Spec v5.2, vol 3, Part F, 3.4.4.6 13 | """ 14 | 15 | defstruct [:opcode, :value] 16 | 17 | def serialize(%{value: value}) do 18 | <<0x0D, value::binary>> 19 | end 20 | 21 | def deserialize(<<0x0D, value::binary>>) do 22 | %__MODULE__{opcode: 0x0D, value: value} 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/blue_heron/registry.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.Registry do 6 | @moduledoc """ 7 | Handles internal message passing of BlueHeron's components 8 | """ 9 | 10 | @pubsub "pubsub" 11 | 12 | @doc """ 13 | Subscribe to HCI events 14 | """ 15 | @spec subscribe() :: :ok 16 | def subscribe() do 17 | with {:ok, _} <- Registry.register(__MODULE__, @pubsub, nil) do 18 | :ok 19 | end 20 | end 21 | 22 | @doc false 23 | @spec broadcast(term()) :: :ok 24 | def broadcast(message) do 25 | Registry.dispatch(__MODULE__, @pubsub, fn entries -> 26 | for {pid, _data} <- entries, do: send(pid, message) 27 | end) 28 | 29 | :ok 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/blue_heron/att/indications/handle_value_indication.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.ATT.HandleValueIndication do 7 | @moduledoc """ 8 | > A server can send a notification of an attribute’s value at any time. 9 | 10 | Bluetooth Spec v5.2, vol 3, Part F, 3.4.7.2 11 | """ 12 | 13 | defstruct [:opcode, :handle, :value] 14 | 15 | def serialize(%{handle: handle, value: value}) do 16 | <<0x1D, handle::little-16, value::binary>> 17 | end 18 | 19 | def deserialize(<<0x1D, handle::little-16, value::binary>>) do 20 | %__MODULE__{ 21 | opcode: 0x1D, 22 | handle: handle, 23 | value: value 24 | } 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/blue_heron/smp/io_handler.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.SMP.IOHandler do 6 | @moduledoc """ 7 | Callback behavior for handling SMP IO requests 8 | """ 9 | 10 | @doc "returns the path to a file on the filesystem used to store encryption keys" 11 | @callback keyfile() :: {:ok, Path.t()} | {:error, any()} 12 | 13 | @doc "Should be used to display or otherwise print the passkey used for MITM mitigation" 14 | @callback passkey(data :: binary()) :: any() 15 | 16 | @typedoc "Type of status event" 17 | @type status :: :success | :passkey_mismatch | :fail 18 | 19 | @doc "Progress callback used to handle errors or successful pairing" 20 | @callback status_update(status :: status()) :: any 21 | end 22 | -------------------------------------------------------------------------------- /lib/blue_heron/att/requests/write_request.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.ATT.WriteRequest do 7 | @moduledoc """ 8 | > The ATT_WRITE_REQ PDU is used to request the server to write the value of an 9 | > attribute and acknowledge that this has been achieved in an ATT_WRITE_RSP PDU. 10 | 11 | Bluetooth Spec v5.2, vol 3, Part F, 3.4.5.1 12 | """ 13 | 14 | defstruct [:opcode, :handle, :value] 15 | 16 | def serialize(%{handle: handle, value: value}) do 17 | <<0x12, handle::little-16, value::binary>> 18 | end 19 | 20 | def deserialize(<<0x12, handle::little-16, value::binary>>) do 21 | %__MODULE__{opcode: 0x12, handle: handle, value: value} 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/blue_heron/att/requests/execute_write_request.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.ATT.ExecuteWriteRequest do 7 | @moduledoc """ 8 | > The ATT_EXECUTE_WRITE_REQ PDU is used to request the server to write or cancel 9 | > the write of all the prepared values currently held in the prepare queue from this client. 10 | > This request shall be handled by the server as an atomic operation 11 | 12 | Bluetooth Spec v5.2, vol 3, Part F, 3.4.6.3 13 | """ 14 | 15 | defstruct [:opcode, :flags] 16 | 17 | def serialize(%{flags: flags}) do 18 | <<0x18, flags>> 19 | end 20 | 21 | def deserialize(<<0x18, flags>>) do 22 | %__MODULE__{opcode: 0x18, flags: flags} 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/blue_heron/att/requests/prepare_write_request.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.ATT.PrepareWriteRequest do 7 | @moduledoc """ 8 | > The ATT_PREPARE_WRITE_REQ PDU is used to request the server to prepare 9 | > to write the value of an attribute. 10 | 11 | Bluetooth Spec v5.2, vol 3, Part F, 3.4.6.1 12 | """ 13 | 14 | defstruct [:opcode, :handle, :offset, :value] 15 | 16 | def serialize(%{handle: handle, offset: offset, value: value}) do 17 | <<0x16, handle::little-16, offset::little-16, value::binary>> 18 | end 19 | 20 | def deserialize(<<0x16, handle::little-16, offset::little-16, value::binary>>) do 21 | %__MODULE__{opcode: 0x16, handle: handle, offset: offset, value: value} 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/blue_heron/att/requests/exchange_mtu_request.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.ATT.ExchangeMTURequest do 7 | @moduledoc """ 8 | > The ATT_EXCHANGE_MTU_REQ PDU is used by the client to inform the server of the 9 | > client’s maximum receive MTU size and request the server to respond with its maximum 10 | > receive MTU size. 11 | 12 | Bluetooth Spec v5.2, vol 3, Part F, 3.4.2.1 13 | """ 14 | 15 | defstruct [:opcode, :client_rx_mtu] 16 | 17 | @type t() :: %__MODULE__{client_rx_mtu: non_neg_integer()} 18 | 19 | def serialize(emtu) do 20 | <<0x02, emtu.client_rx_mtu::little-16>> 21 | end 22 | 23 | def deserialize(<<0x02, client_rx_mtu::little-16>>) do 24 | %__MODULE__{opcode: 0x02, client_rx_mtu: client_rx_mtu} 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/blue_heron/att/requests/read_blob_request.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.ATT.ReadBlobRequest do 7 | @moduledoc """ 8 | > The ATT_READ_BLOB_REQ PDU is used to request the server to read part of the 9 | > value of an attribute at a given offset and return a specific part of the value in an 10 | > ATT_READ_BLOB_RSP PDU. 11 | 12 | Bluetooth Spec v5.2, vol 3, Part F, 3.4.4.5 13 | """ 14 | 15 | defstruct [:opcode, :handle, :offset] 16 | 17 | def serialize(%{handle: handle, offset: offset}) do 18 | <<0x0C, handle::little-16, offset::little-16>> 19 | end 20 | 21 | def deserialize(<<0x0C, handle::little-16, offset::little-16>>) do 22 | %__MODULE__{ 23 | opcode: 0x0C, 24 | handle: handle, 25 | offset: offset 26 | } 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/blue_heron/att/commands/write_command.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.ATT.WriteCommand do 7 | @moduledoc """ 8 | > The ATT_WRITE_CMD PDU is used to request the server to write the value of an 9 | > attribute, typically into a control-point attribute. 10 | 11 | Bluetooth Spec v5.2, vol 3, Part F, 3.4.5.3 12 | """ 13 | 14 | defstruct [:opcode, :handle, :data] 15 | 16 | def deserialize(<<0x52, handle::little-16, data::binary>>) do 17 | %__MODULE__{opcode: 0x52, handle: handle, data: data} 18 | end 19 | 20 | def serialize(%{data: %type{} = data} = write_command) do 21 | serialize(%{write_command | data: type.serialize(data)}) 22 | end 23 | 24 | def serialize(%{handle: handle, data: data}) do 25 | <<0x52, handle::little-16, data::binary>> 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/blue_heron/transport_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.TransportTest do 6 | use ExUnit.Case 7 | 8 | # @reset %BlueHeron.HCI.Command.ControllerAndBaseband.Reset{} 9 | # |> BlueHeron.HCI.Serializable.serialize() 10 | 11 | # test "basic init" do 12 | # config = %BlueHeron.HCI.Transport.NULL{ 13 | # init_commands: [ 14 | # @reset 15 | # ], 16 | # replies: %{ 17 | # @reset => "\x0e\x04\x03\x03\x0c\x00" 18 | # } 19 | # } 20 | 21 | # {:ok, pid} = BlueHeron.HCI.Transport.start_link(config) 22 | 23 | # assert {:ok, 24 | # %BlueHeron.HCI.Event.CommandComplete{ 25 | # num_hci_command_packets: 3, 26 | # opcode: <<3, 12>>, 27 | # return_parameters: <<0>> 28 | # }} = BlueHeron.HCI.Transport.command(pid, @reset) 29 | # end 30 | end 31 | -------------------------------------------------------------------------------- /lib/blue_heron/att/responses/prepare_write_response.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.ATT.PrepareWriteResponse do 7 | @moduledoc """ 8 | > The ATT_PREPARE_WRITE_RSP PDU is sent in response to a received 9 | > ATT_PREPARE_WRITE_REQ PDU and acknowledges that the value has been 10 | > successfully received and placed in the prepare write queue. 11 | 12 | Bluetooth Spec v5.2, vol 3, Part F, 3.4.6.2 13 | """ 14 | 15 | defstruct [:opcode, :handle, :offset, :value] 16 | 17 | def serialize(%{handle: handle, offset: offset, value: value}) do 18 | <<0x17, handle::little-16, offset::little-16, value::binary>> 19 | end 20 | 21 | def deserialize(<<0x17, handle::little-16, offset::little-16, value::binary>>) do 22 | %__MODULE__{opcode: 0x17, handle: handle, offset: offset, value: value} 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/blue_heron/advertising_data/ibeacon.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.AdvertisingData.IBeacon do 6 | @moduledoc """ 7 | Handles creating Apple IBeacon packets 8 | """ 9 | 10 | # Apple's ID 11 | @company_id <<0x4C, 0x00>> 12 | # iBeacon type 13 | @ibeacon_type <<0x02>> 14 | # Length of the payload (21 bytes) 15 | @payload_length <<21>> 16 | 17 | @doc """ 18 | Create a IBeacon packet. 19 | 20 | * UUID - 128 bit binary 21 | * major and minor - a 16 bit identifier 22 | * tx power - calibrated signal strength measured 1 meter away from the device. Helps estimate proximity. 23 | """ 24 | @spec new(binary(), 0..65535, 0..65535, -128..127) :: binary() 25 | def new(uuid, major, minor, tx_power) do 26 | payload = <> 27 | @company_id <> @ibeacon_type <> @payload_length <> uuid <> payload 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/blue_heron/att/notifications/handle_value_notification.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.ATT.HandleValueNotification do 7 | @moduledoc """ 8 | > A server can send a notification of an attribute’s value at any time. 9 | 10 | Bluetooth Spec v5.2, vol 3, Part F, 3.4.7.1 11 | """ 12 | 13 | defstruct [:opcode, :handle, :data] 14 | 15 | @type t() :: %__MODULE__{handle: non_neg_integer(), data: binary()} 16 | 17 | def deserialize(<<0x1B, handle::little-16, data::binary>>) do 18 | %__MODULE__{opcode: 0x1B, handle: handle, data: data} 19 | end 20 | 21 | def serialize(%{data: %type{} = data} = write_command) do 22 | serialize(%{write_command | data: type.serialize(data)}) 23 | end 24 | 25 | def serialize(%{handle: handle, data: data}) do 26 | <<0x1B, handle::little-16, data::binary>> 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/blue_heron/hci/command/le_controller/set_advertising_enable_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.HCI.Command.LEController.SetAdvertisingEnableTest do 6 | use ExUnit.Case 7 | 8 | alias BlueHeron.HCI.Command.LEController.SetAdvertisingEnable 9 | 10 | test "encodes parameters correctly" do 11 | serialized = 12 | %SetAdvertisingEnable{advertising_enable: true} 13 | |> BlueHeron.HCI.Serializable.serialize() 14 | 15 | assert <<0x0A, 0x20, 1, 0x01>> == serialized 16 | end 17 | 18 | test "serde is symmetric" do 19 | advertising_enable = Enum.random([true, false]) 20 | 21 | expected = %SetAdvertisingEnable{advertising_enable: advertising_enable} 22 | 23 | assert expected == 24 | expected 25 | |> BlueHeron.HCI.Serializable.serialize() 26 | |> SetAdvertisingEnable.deserialize() 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/blue_heron/hci/command/controller_and_baseband/write_scan_enable_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.HCI.Command.ControllerAndBaseband.WriteScanEnableTest do 6 | use ExUnit.Case 7 | 8 | alias BlueHeron.HCI.Command.ControllerAndBaseband.WriteScanEnable 9 | 10 | test "serializes scan_enable parameter correctly" do 11 | val = Enum.random(0x00..0x03) 12 | 13 | serialized = 14 | %WriteScanEnable{scan_enable: val} 15 | |> BlueHeron.HCI.Serializable.serialize() 16 | 17 | assert <<0x1A, 0x0C, 0x01, val>> == serialized 18 | end 19 | 20 | test "serde is symmetric" do 21 | for val <- 0x00..0x03 do 22 | assert %WriteScanEnable{scan_enable: val} == 23 | %WriteScanEnable{scan_enable: val} 24 | |> BlueHeron.HCI.Serializable.serialize() 25 | |> WriteScanEnable.deserialize() 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/blue_heron/hci/command/controller_and_baseband/write_le_host_support_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.HCI.Command.ControllerAndBaseband.WriteLEHostSupportTest do 6 | use ExUnit.Case 7 | 8 | alias BlueHeron.HCI.Command.ControllerAndBaseband.WriteLEHostSupport 9 | 10 | test "serializes parameters correctly" do 11 | serialized = 12 | %WriteLEHostSupport{le_supported_host_enabled: true} 13 | |> BlueHeron.HCI.Serializable.serialize() 14 | 15 | assert <<0x6D, 0x0C, 0x02, 0x01, 0x00>> == serialized 16 | end 17 | 18 | test "serde is symmetric" do 19 | for val <- [false, true] do 20 | assert %WriteLEHostSupport{le_supported_host_enabled: val} == 21 | %WriteLEHostSupport{le_supported_host_enabled: val} 22 | |> BlueHeron.HCI.Serializable.serialize() 23 | |> WriteLEHostSupport.deserialize() 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/events/inquiry_complete.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Event.InquiryComplete do 7 | use BlueHeron.HCI.Event, code: 0x01 8 | 9 | @moduledoc """ 10 | > The Inquiry Complete event indicates that the Inquiry is finished. This event contains a 11 | > Status parameter, which is used to indicate if the Inquiry completed successfully or if the 12 | > Inquiry was not completed. 13 | 14 | Reference: Version 5.2, Vol 4, Part E, 7.7.1 15 | """ 16 | 17 | defparameters [:status] 18 | 19 | defimpl BlueHeron.HCI.Serializable do 20 | def serialize(data) do 21 | <> 22 | end 23 | end 24 | 25 | @impl BlueHeron.HCI.Event 26 | def deserialize(<<@code, _size, status>>) do 27 | %__MODULE__{ 28 | status: status 29 | } 30 | end 31 | 32 | def deserialize(bin), do: {:error, bin} 33 | end 34 | -------------------------------------------------------------------------------- /lib/blue_heron/l2_cap.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.L2Cap do 6 | @moduledoc """ 7 | > Logical Link Control and Adaptation Protocol Specification. 8 | 9 | Bluetooth Spec v5.2, vol 4, Part A 10 | """ 11 | defstruct [:data, :cid] 12 | 13 | def deserialize(<>) do 14 | case cid do 15 | # att 16 | 0x4 -> 17 | BlueHeron.ATT.deserialize(%__MODULE__{cid: cid}, data) 18 | 19 | _ -> 20 | %__MODULE__{ 21 | cid: cid, 22 | data: data 23 | } 24 | end 25 | end 26 | 27 | def serialize(%__MODULE__{data: %type{} = data} = l2cap) do 28 | serialize(%{l2cap | data: type.serialize(data)}) 29 | end 30 | 31 | def serialize(%__MODULE__{cid: cid, data: data}) do 32 | length = byte_size(data) 33 | <> 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/blue_heron/att/requests/find_information_request.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.ATT.FindInformationRequest do 7 | @moduledoc """ 8 | > The ATT_FIND_INFORMATION_REQ PDU is used to obtain the mapping of attribute 9 | > handles with their associated types. This allows a client to discover the list of attributes 10 | > and their types on a server. 11 | 12 | Bluetooth Spec v5.2, vol 3, Part F, 3.4.3.1 13 | """ 14 | 15 | defstruct [:opcode, :starting_handle, :ending_handle] 16 | 17 | def serialize(%{starting_handle: starting_handle, ending_handle: ending_handle}) do 18 | <<0x04, starting_handle::little-16, ending_handle::little-16>> 19 | end 20 | 21 | def deserialize(<<0x04, starting_handle::little-16, ending_handle::little-16>>) do 22 | %__MODULE__{opcode: 0x04, starting_handle: starting_handle, ending_handle: ending_handle} 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/blue_heron/hci/command/le_controller/set_random_address_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.HCI.Command.LEController.SetRandomAddressTest do 6 | use ExUnit.Case 7 | 8 | alias BlueHeron.HCI.Command.LEController.SetRandomAddress 9 | 10 | test "encodes parameters correctly" do 11 | serialized = 12 | %SetRandomAddress{ 13 | random_address: 0x112233445566 14 | } 15 | |> BlueHeron.HCI.Serializable.serialize() 16 | 17 | assert <<0x05, 0x20, 0x06, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11>> == serialized 18 | end 19 | 20 | test "serde is symmetric" do 21 | random_address = Enum.random(0x000000000000..0xFFFFFFFFFFFF) 22 | 23 | expected = %SetRandomAddress{ 24 | random_address: random_address 25 | } 26 | 27 | assert expected == 28 | expected 29 | |> BlueHeron.HCI.Serializable.serialize() 30 | |> SetRandomAddress.deserialize() 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/blue_heron/hci/command/controller_and_baseband/write_synchronous_flow_control_enable_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.HCI.Command.ControllerAndBaseband.WriteSynchronousFlowControlEnableTest do 6 | use ExUnit.Case 7 | 8 | alias BlueHeron.HCI.Command.ControllerAndBaseband.WriteSynchronousFlowControlEnable 9 | 10 | test "serializes parameters correctly" do 11 | serialized = 12 | %WriteSynchronousFlowControlEnable{enabled: true} 13 | |> BlueHeron.HCI.Serializable.serialize() 14 | 15 | assert <<0x2F, 0x0C, 0x01, 0x01>> == serialized 16 | end 17 | 18 | test "serde is symmetric" do 19 | for val <- [false, true] do 20 | assert %WriteSynchronousFlowControlEnable{enabled: val} == 21 | %WriteSynchronousFlowControlEnable{enabled: val} 22 | |> BlueHeron.HCI.Serializable.serialize() 23 | |> WriteSynchronousFlowControlEnable.deserialize() 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/blue_heron/hci/command/controller_and_baseband/write_default_erroneous_data_reporting_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.HCI.Command.ControllerAndBaseband.WriteDefaultErroneousDataReportingTest do 6 | use ExUnit.Case 7 | 8 | alias BlueHeron.HCI.Command.ControllerAndBaseband.WriteDefaultErroneousDataReporting 9 | 10 | test "serializes parameters correctly" do 11 | serialized = 12 | %WriteDefaultErroneousDataReporting{enabled: true} 13 | |> BlueHeron.HCI.Serializable.serialize() 14 | 15 | assert <<0x5B, 0x0C, 0x01, 0x01>> == serialized 16 | end 17 | 18 | test "serde is symmetric" do 19 | for val <- [false, true] do 20 | assert %WriteDefaultErroneousDataReporting{enabled: val} == 21 | %WriteDefaultErroneousDataReporting{enabled: val} 22 | |> BlueHeron.HCI.Serializable.serialize() 23 | |> WriteDefaultErroneousDataReporting.deserialize() 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/blue_heron/assigned_numbers/company_identifiers.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Very 2 | # 3 | # SPDX-License-Identifier: MIT 4 | # 5 | defmodule BlueHeron.AssignedNumbers.CompanyIdentifiers do 6 | @moduledoc """ 7 | > Company identifiers are unique numbers assigned by the Bluetooth SIG to member companies 8 | > requesting one. 9 | 10 | Reference: https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers 11 | """ 12 | 13 | @definitions %{ 14 | 0x004C => "Apple, Inc." 15 | } 16 | 17 | @doc """ 18 | Returns the description associated with `id`. 19 | """ 20 | def description(id) 21 | 22 | @doc """ 23 | Returns the ID associated with `description`. 24 | """ 25 | def id(description) 26 | 27 | Enum.each(@definitions, fn 28 | {id, description} -> 29 | def description(unquote(id)), do: unquote(description) 30 | 31 | def id(unquote(description)), do: unquote(id) 32 | end) 33 | 34 | @doc """ 35 | Returns a list of all Company Identifier ids. 36 | """ 37 | defmacro ids, do: unquote(for {id, _} <- @definitions, do: id) 38 | end 39 | -------------------------------------------------------------------------------- /lib/blue_heron/gatt/characteristic/descriptor.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.GATT.Characteristic.Descriptor do 6 | @moduledoc """ 7 | Struct that represents a GATT characteristic descriptor. 8 | """ 9 | @opaque t() :: %__MODULE__{ 10 | permissions: integer(), 11 | value: binary() 12 | } 13 | 14 | defstruct [:permissions, value: <<0::16>>] 15 | 16 | @doc """ 17 | Create a characteristic with fields taken from the map `args`. 18 | 19 | The following fields are required: 20 | - `permissions`: The characteristic descriptor property flags. Integer. 21 | 22 | ## Example: 23 | 24 | iex> BlueHeron.GATT.Characteristic.Descriptor.new(%{ 25 | ...> value: 0x0001, 26 | ...> }) 27 | %BlueHeron.GATT.Characteristic.Descriptor{permissions: 2, value: <<0,0>>} 28 | """ 29 | @spec new(args :: map()) :: t() 30 | def new(args) do 31 | args = 32 | Map.take(args, [:permissions]) 33 | |> Map.put(:value, <<0, 0>>) 34 | 35 | struct!(__MODULE__, args) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | associated documentation files (the "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial 12 | portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 15 | LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 16 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 18 | USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /test/blue_heron/acl_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2023 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.ACLTest do 7 | use ExUnit.Case 8 | 9 | alias BlueHeron.{ACL, ATT, L2Cap} 10 | 11 | test "encodes packet correctly" do 12 | serialized = 13 | %ACL{ 14 | data: %L2Cap{ 15 | cid: 4, 16 | data: %ATT.ExchangeMTURequest{client_rx_mtu: 185, opcode: 2} 17 | }, 18 | flags: %{bc: 2, pb: 0}, 19 | handle: 64 20 | } 21 | |> ACL.serialize() 22 | 23 | assert <<64, 2, 7, 0, 3, 0, 4, 0, 2, 185, 0>> == serialized 24 | end 25 | 26 | test "serde is symmetric" do 27 | handle = Enum.random(0x001..0xEFF) 28 | pb = Enum.random(0b00..0b11) 29 | bc = Enum.random(0b00..0b11) 30 | 31 | expected = %ACL{ 32 | flags: %{bc: bc, pb: pb}, 33 | handle: handle, 34 | data: %L2Cap{ 35 | cid: 4, 36 | data: %ATT.ExchangeMTURequest{client_rx_mtu: 185, opcode: 2} 37 | } 38 | } 39 | 40 | assert expected == expected |> ACL.serialize() |> ACL.deserialize() 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/blue_heron/hci/command/le_controller/set_advertising_data_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.HCI.Command.LEController.SetAdvertisingDataTest do 6 | use ExUnit.Case 7 | 8 | alias BlueHeron.HCI.Command.LEController.SetAdvertisingData 9 | 10 | test "encodes parameters correctly" do 11 | serialized = 12 | %SetAdvertisingData{advertising_data: <<0x02, 0x01, 0b00000110>>} 13 | |> BlueHeron.HCI.Serializable.serialize() 14 | 15 | assert <<0x08, 0x20, 0x20, 0x03, 0x02, 0x01, 0b00000110, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 16 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 17 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00>> == serialized 18 | end 19 | 20 | test "serde is symmetric" do 21 | advertising_data = <<0x02, 0x01, Enum.random(0x00..0xFF)>> 22 | 23 | expected = %SetAdvertisingData{advertising_data: advertising_data} 24 | 25 | assert expected == 26 | expected 27 | |> BlueHeron.HCI.Serializable.serialize() 28 | |> SetAdvertisingData.deserialize() 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/blue_heron/acl.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.ACL do 7 | @moduledoc """ 8 | > HCI ACL Data packets are used to exchange data between the Host and Controller 9 | 10 | Bluetooth Spec v5.2, vol 4, Part E, 5.4.2 11 | """ 12 | alias BlueHeron.ACL 13 | require Logger 14 | 15 | defstruct [:handle, :flags, :data] 16 | 17 | def deserialize( 18 | <> 19 | ) do 20 | data = BlueHeron.L2Cap.deserialize(acl_data) 21 | 22 | %ACL{ 23 | handle: handle, 24 | flags: %{pb: pb, bc: bc}, 25 | data: data 26 | } 27 | end 28 | 29 | def serialize(%ACL{data: %type{} = data} = acl) do 30 | serialize(%{acl | data: type.serialize(data)}) 31 | end 32 | 33 | def serialize(%ACL{data: data, handle: handle, flags: %{pb: pb, bc: bc}}) do 34 | length = byte_size(data) 35 | <> 36 | end 37 | 38 | def serialize(binary) when is_binary(binary), do: binary 39 | end 40 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/events/number_of_completed_packets.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Markus Hutzler 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.HCI.Event.NumberOfCompletedPackets do 6 | use BlueHeron.HCI.Event, code: 0x13 7 | 8 | @moduledoc """ 9 | > The HCI_Number_Of_Completed_Packets event is used by the Controller to 10 | > indicate to the Host how many HCI Data packets have been completed for 11 | > each Connection_Handle since the previous HCI_Number_Of_Completed_- 12 | > Packets event was sent to the Host. 13 | 14 | Reference: Version 5.3, Vol 4, Part E, 7.7.19 15 | """ 16 | 17 | require Logger 18 | 19 | defparameters [:num_handles, :data] 20 | 21 | defimpl BlueHeron.HCI.Serializable do 22 | def serialize(data) do 23 | bin = <> 24 | size = byte_size(bin) 25 | <> 26 | end 27 | end 28 | 29 | @impl BlueHeron.HCI.Event 30 | def deserialize(<<@code, _size, num_handles, data::binary>>) do 31 | # TODO: deserialize data 32 | %__MODULE__{ 33 | num_handles: num_handles, 34 | data: data 35 | } 36 | end 37 | 38 | def deserialize(bin), do: {:error, bin} 39 | end 40 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/controller_and_baseband/write_class_of_device.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.ControllerAndBaseband.WriteClassOfDevice do 7 | use BlueHeron.HCI.Command.ControllerAndBaseband, ocf: 0x0024 8 | 9 | @moduledoc """ 10 | > This command writes the value for the Class_Of_Device parameter. 11 | 12 | * OGF: `#{inspect(@ogf, base: :hex)}` 13 | * OCF: `#{inspect(@ocf, base: :hex)}` 14 | * Opcode: `#{inspect(@opcode)}` 15 | 16 | Bluetooth Spec v5.2, Vol 4, Part E, section 7.3.26 17 | """ 18 | 19 | defparameters class: 0x00 20 | 21 | defimpl BlueHeron.HCI.Serializable do 22 | def serialize(%{opcode: opcode, class: class}) do 23 | <> 24 | end 25 | end 26 | 27 | @impl BlueHeron.HCI.Command 28 | def deserialize(<<@opcode::binary, 3, class::24>>) do 29 | new(class: class) 30 | end 31 | 32 | @impl BlueHeron.HCI.Command 33 | def deserialize_return_parameters(<>) do 34 | %{status: status} 35 | end 36 | 37 | @impl true 38 | def serialize_return_parameters(%{status: status}) do 39 | <> 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/blue_heron/application.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.Application do 6 | # See https://hexdocs.pm/elixir/Application.html 7 | # for more information on OTP Applications 8 | @moduledoc false 9 | 10 | use Application 11 | 12 | @impl true 13 | def start(_type, _args) do 14 | all_env = Application.get_all_env(:blue_heron) 15 | transport_args = Keyword.get(all_env, :transport, []) 16 | smp_args = Keyword.get(all_env, :smp, []) 17 | broadcaster_args = Keyword.get(all_env, :broadcaster, []) 18 | 19 | children = [ 20 | {PropertyTable, name: BlueHeron.GATT}, 21 | {Registry, 22 | [ 23 | keys: :duplicate, 24 | name: BlueHeron.Registry, 25 | partitions: System.schedulers_online() 26 | ]}, 27 | BlueHeron.ACLBuffer, 28 | {BlueHeron.Broadcaster, broadcaster_args}, 29 | {BlueHeron.SMP, smp_args}, 30 | BlueHeron.Peripheral, 31 | {BlueHeron.HCI.Transport, transport_args} 32 | ] 33 | 34 | # See https://hexdocs.pm/elixir/Supervisor.html 35 | # for other strategies and supported options 36 | opts = [strategy: :one_for_one, name: BlueHeron.Supervisor] 37 | Supervisor.start_link(children, opts) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/blue_heron/att/responses/error_response.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # SPDX-FileCopyrightText: 2023 Markus Hutzler 4 | # 5 | # SPDX-License-Identifier: Apache-2.0 6 | # 7 | defmodule BlueHeron.ATT.ErrorResponse do 8 | @moduledoc """ 9 | > The ATT_ERROR_RSP PDU is used to state that a given request cannot be performed, 10 | > and to provide the reason. 11 | 12 | Bluetooth Spec v5.2, vol 3, Part F, 3.4.1.1 13 | """ 14 | 15 | defstruct [:opcode, :request_opcode, :handle, :error] 16 | 17 | def serialize(%{request_opcode: request_opcode, handle: handle, error: error}) do 18 | <<0x01, request_opcode, handle::little-16, serialize_error(error)>> 19 | end 20 | 21 | defp serialize_error(:insufficient_authentication), do: 0x05 22 | defp serialize_error(:attribute_not_found), do: 0x0A 23 | 24 | def deserialize(<<0x01, request_opcode, handle::little-16, error>>) do 25 | %__MODULE__{ 26 | opcode: 0x01, 27 | request_opcode: request_opcode, 28 | handle: handle, 29 | error: deserialize_error(error) 30 | } 31 | end 32 | 33 | defp deserialize_error(0x05), do: :insufficient_authentication 34 | defp deserialize_error(0x0A), do: :attribute_not_found 35 | defp deserialize_error(code), do: code 36 | end 37 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/controller_and_baseband/write_inquiry_mode.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.ControllerAndBaseband.WriteInquiryMode do 7 | use BlueHeron.HCI.Command.ControllerAndBaseband, ocf: 0x0045 8 | 9 | @moduledoc """ 10 | > This command writes the Inquiry_Mode configuration parameter of the local BR/EDR 11 | > Controller 12 | 13 | * OGF: `#{inspect(@ogf, base: :hex)}` 14 | * OCF: `#{inspect(@ocf, base: :hex)}` 15 | * Opcode: `#{inspect(@opcode)}` 16 | 17 | Bluetooth Spec v5.2, Vol 4, Part E, section 7.3.50 18 | """ 19 | 20 | defparameters inquiry_mode: 0 21 | 22 | defimpl BlueHeron.HCI.Serializable do 23 | def serialize(data) do 24 | <> 25 | end 26 | end 27 | 28 | @impl BlueHeron.HCI.Command 29 | def deserialize(<<@opcode::binary, _size, mode>>) do 30 | %__MODULE__{inquiry_mode: mode} 31 | end 32 | 33 | @impl BlueHeron.HCI.Command 34 | def deserialize_return_parameters(<>) do 35 | %{status: status} 36 | end 37 | 38 | @impl BlueHeron.HCI.Command 39 | def serialize_return_parameters(%{status: status}) do 40 | <> 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/controller_and_baseband/write_page_timeout.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.ControllerAndBaseband.WritePageTimeout do 7 | use BlueHeron.HCI.Command.ControllerAndBaseband, ocf: 0x0018 8 | 9 | @moduledoc """ 10 | > This command writes the value for the Page_Timeout configuration parameter 11 | 12 | * OGF: `#{inspect(@ogf, base: :hex)}` 13 | * OCF: `#{inspect(@ocf, base: :hex)}` 14 | * Opcode: `#{inspect(@opcode)}` 15 | 16 | Bluetooth Spec v5.2, Vol 4, Part E, section 7.3.16 17 | """ 18 | 19 | defparameters timeout: 0x20 20 | 21 | defimpl BlueHeron.HCI.Serializable do 22 | def serialize(%{opcode: opcode, timeout: timeout}) do 23 | <> 24 | end 25 | end 26 | 27 | @impl BlueHeron.HCI.Command 28 | def deserialize(<<@opcode::binary, 2, timeout::16>>) do 29 | new(timeout: timeout) 30 | end 31 | 32 | @impl BlueHeron.HCI.Command 33 | def deserialize_return_parameters(<>) do 34 | %{status: status} 35 | end 36 | 37 | @impl true 38 | def serialize_return_parameters(%{status: status}) do 39 | <> 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/link_control/authentication_requested.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Markus Hutzler 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.LinkControl.AuthenticationRequested do 7 | use BlueHeron.HCI.Command.LinkControl, ocf: 0x0011 8 | 9 | @moduledoc """ 10 | > This command is used to try to authenticate the remote device associated with 11 | > the specified Connection_Handle. 12 | 13 | * OGF: `#{inspect(@ogf, base: :hex)}` 14 | * OCF: `#{inspect(@ocf, base: :hex)}` 15 | * Opcode: `#{inspect(@opcode)}` 16 | 17 | Bluetooth Spec v5.3, Vol 4, Part E, section 7.1.15 18 | """ 19 | 20 | defparameters handle: 0 21 | 22 | defimpl BlueHeron.HCI.Serializable do 23 | def serialize(%{opcode: opcode, handle: handle}) do 24 | bin = <> 25 | size = byte_size(bin) 26 | <> 27 | end 28 | end 29 | 30 | @impl BlueHeron.HCI.Command 31 | def deserialize(<<@opcode::binary, 0>>) do 32 | %__MODULE__{} 33 | end 34 | 35 | @impl BlueHeron.HCI.Command 36 | def deserialize_return_parameters(<<>>) do 37 | %{} 38 | end 39 | 40 | @impl true 41 | def serialize_return_parameters(%{}) do 42 | <<>> 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/le_controller/read_local_supported_features.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.HCI.Command.LEController.ReadLocalSupportedFeatures do 6 | use BlueHeron.HCI.Command.LEController, ocf: 0x0003 7 | 8 | @moduledoc """ 9 | > This command requests page 0 of the list of the supported LE features for the 10 | > Controller. 11 | 12 | 13 | Bluetooth Core Version 5.2 | Vol 4, Part E, section 7.8.3 14 | 15 | * OGF: `#{inspect(@ogf, base: :hex)}` 16 | * OCF: `#{inspect(@ocf, base: :hex)}` 17 | * Opcode: `#{inspect(@opcode)}` 18 | """ 19 | 20 | defparameters [] 21 | 22 | defimpl BlueHeron.HCI.Serializable do 23 | def serialize(command) do 24 | <> 25 | end 26 | end 27 | 28 | @impl BlueHeron.HCI.Command 29 | def deserialize(<<@opcode::binary, 0>>) do 30 | new() 31 | end 32 | 33 | @impl BlueHeron.HCI.Command 34 | def deserialize_return_parameters(<>) do 35 | %{status: status, le_features: le_features} 36 | end 37 | 38 | @impl BlueHeron.HCI.Command 39 | def serialize_return_parameters(%{status: status, le_features: features}) do 40 | <> 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/informational_parameters/read_local_supported_commands.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Markus Hutzler 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.InformationalParameters.ReadLocalSupportedCommands do 7 | use BlueHeron.HCI.Command.InformationalParameters, ocf: 0x0002 8 | 9 | @moduledoc """ 10 | > This command reads the list of HCI commands supported for the local Controller. 11 | 12 | * OGF: `#{inspect(@ogf, base: :hex)}` 13 | * OCF: `#{inspect(@ocf, base: :hex)}` 14 | * Opcode: `#{inspect(@opcode)}` 15 | 16 | Bluetooth Spec v5.3, Vol 4, Part E, section 7.4.2 17 | """ 18 | 19 | defparameters [] 20 | 21 | defimpl BlueHeron.HCI.Serializable do 22 | def serialize(rlv) do 23 | <> 24 | end 25 | end 26 | 27 | @impl BlueHeron.HCI.Command 28 | def deserialize(<<@opcode::binary, 0>>) do 29 | %__MODULE__{} 30 | end 31 | 32 | @impl BlueHeron.HCI.Command 33 | def deserialize_return_parameters(<>) do 34 | %{ 35 | status: status, 36 | supported_commands: bin 37 | } 38 | end 39 | 40 | @impl BlueHeron.HCI.Command 41 | def serialize_return_parameters(%{status: status} = params) do 42 | <> <> params.supported_commands 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/blue_heron/att/requests/find_by_type_value_request.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.ATT.FindByTypeValueRequest do 7 | @moduledoc """ 8 | > The ATT_FIND_BY_TYPE_VALUE_RSP PDU is sent in reply to a received 9 | > ATT_FIND_BY_TYPE_VALUE_REQ PDU and contains information about this server. 10 | 11 | Bluetooth Spec v5.2, vol 3, Part F, 3.4.3.4 12 | """ 13 | 14 | defstruct [:opcode, :starting_handle, :ending_handle, :attribute_type, :attribute_value] 15 | 16 | def serialize(%{ 17 | starting_handle: starting_handle, 18 | ending_handle: ending_handle, 19 | attribute_type: attribute_type, 20 | attribute_value: attribute_value 21 | }) do 22 | <<0x06, starting_handle::little-16, ending_handle::little-16, attribute_type::little-16, 23 | attribute_value::binary>> 24 | end 25 | 26 | def deserialize( 27 | <<0x06, starting_handle::little-16, ending_handle::little-16, attribute_type::little-16, 28 | attribute_value::binary>> 29 | ) do 30 | %__MODULE__{ 31 | opcode: 0x06, 32 | starting_handle: starting_handle, 33 | ending_handle: ending_handle, 34 | attribute_type: attribute_type, 35 | attribute_value: attribute_value 36 | } 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/informational_parameters/read_bd_addr.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Connor Rigby 2 | # SPDX-FileCopyrightText: 2023 Markus Hutzler 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.InformationalParameters.ReadBdAddr do 7 | use BlueHeron.HCI.Command.InformationalParameters, ocf: 0x0009 8 | 9 | @moduledoc """ 10 | > On a BR/EDR Controller, this command reads the Bluetooth Controller address 11 | > (BD_ADDR). 12 | 13 | * OGF: `#{inspect(@ogf, base: :hex)}` 14 | * OCF: `#{inspect(@ocf, base: :hex)}` 15 | * Opcode: `#{inspect(@opcode)}` 16 | 17 | Bluetooth Spec v5.3, Vol 4, Part E, section 7.4.6 18 | """ 19 | 20 | defparameters [] 21 | 22 | defimpl BlueHeron.HCI.Serializable do 23 | def serialize(rlv) do 24 | <> 25 | end 26 | end 27 | 28 | @impl BlueHeron.HCI.Command 29 | def deserialize(<<@opcode::binary, 0>>) do 30 | %__MODULE__{} 31 | end 32 | 33 | @impl BlueHeron.HCI.Command 34 | def deserialize_return_parameters(<>) do 35 | %{ 36 | status: status, 37 | bd_addr: BlueHeron.Address.parse(addr) 38 | } 39 | end 40 | 41 | @impl BlueHeron.HCI.Command 42 | def serialize_return_parameters(%{status: status} = params) do 43 | <> <> BlueHeron.Address.serialize(params.bd_addr) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/le_controller/set_random_address.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.LEController.SetRandomAddress do 7 | use BlueHeron.HCI.Command.LEController, ocf: 0x0005 8 | 9 | @moduledoc """ 10 | > The HCI_LE_Set_Random_Address command is used by the Host to set the LE 11 | > Random Device Address in the Controller 12 | 13 | Bluetooth Core Version 5.2 | Vol 4, Part E, section 7.8.4 14 | 15 | * OGF: `#{inspect(@ogf, base: :hex)}` 16 | * OCF: `#{inspect(@ocf, base: :hex)}` 17 | * Opcode: `#{inspect(@opcode)}` 18 | """ 19 | 20 | defparameters random_address: nil 21 | 22 | defimpl BlueHeron.HCI.Serializable do 23 | def serialize(command) do 24 | <> 25 | end 26 | end 27 | 28 | @impl BlueHeron.HCI.Command 29 | def deserialize(<<@opcode::binary, _fields_size, random_address::little-48>>) do 30 | new(random_address: random_address) 31 | end 32 | 33 | @impl BlueHeron.HCI.Command 34 | def deserialize_return_parameters(<>) do 35 | %{status: status} 36 | end 37 | 38 | @impl BlueHeron.HCI.Command 39 | def serialize_return_parameters(%{status: status}) do 40 | <> 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/events/le_meta/connection_update_complete.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.HCI.Event.LEMeta.ConnectionUpdateComplete do 6 | use BlueHeron.HCI.Event.LEMeta, subevent_code: 0x3 7 | 8 | @moduledoc """ 9 | > The HCI_LE_Connection_Update_Complete event is used to indicate that the 10 | > Connection Update procedure has completed. 11 | 12 | Reference: Version 5.2, Vol 4, Part E, 7.7.65.3 13 | """ 14 | 15 | defparameters [ 16 | :subevent_code, 17 | :connection_handle, 18 | :connection_interval, 19 | :peripheral_latency, 20 | :supervision_timeout, 21 | :status 22 | ] 23 | 24 | @impl BlueHeron.HCI.Event 25 | def deserialize(<<@code, _size, @subevent_code, bin::binary>>) do 26 | << 27 | status, 28 | connection_handle::little-12, 29 | _unused::4, 30 | connection_interval::little-16, 31 | peripheral_latency::little-16, 32 | supervision_timeout::little-16 33 | >> = bin 34 | 35 | %__MODULE__{ 36 | subevent_code: @subevent_code, 37 | connection_handle: connection_handle, 38 | connection_interval: connection_interval, 39 | peripheral_latency: peripheral_latency, 40 | supervision_timeout: supervision_timeout, 41 | status: status 42 | } 43 | end 44 | 45 | def deserialize(bin), do: {:error, bin} 46 | end 47 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/controller_and_baseband/write_simple_pairing_mode.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.ControllerAndBaseband.WriteSimplePairingMode do 7 | use BlueHeron.HCI.Command.ControllerAndBaseband, ocf: 0x0056 8 | 9 | @moduledoc """ 10 | > This command enables Simple Pairing mode in the BR/EDR Controller. 11 | 12 | * OGF: `#{inspect(@ogf, base: :hex)}` 13 | * OCF: `#{inspect(@ocf, base: :hex)}` 14 | * Opcode: `#{inspect(@opcode)}` 15 | 16 | Bluetooth Spec v5.2, Vol 4, Part E, section 7.3.59 17 | """ 18 | 19 | defparameters enabled: false 20 | 21 | defimpl BlueHeron.HCI.Serializable do 22 | def serialize(%{opcode: opcode, enabled: enabled?}) do 23 | val = if enabled?, do: <<1>>, else: <<0>> 24 | <> 25 | end 26 | end 27 | 28 | @impl BlueHeron.HCI.Command 29 | def deserialize(<<@opcode::binary, 1, enabled::binary>>) do 30 | val = if enabled == <<1>>, do: true, else: false 31 | %__MODULE__{enabled: val} 32 | end 33 | 34 | @impl BlueHeron.HCI.Command 35 | def deserialize_return_parameters(<>) do 36 | %{status: status} 37 | end 38 | 39 | @impl true 40 | def serialize_return_parameters(%{status: status}) do 41 | <> 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/controller_and_baseband/write_scan_enable.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.ControllerAndBaseband.WriteScanEnable do 7 | use BlueHeron.HCI.Command.ControllerAndBaseband, ocf: 0x001A 8 | 9 | @moduledoc """ 10 | > The Scan_Enable parameter controls whether or not the BR/EDR Controller will 11 | > periodically scan for page attempts and/or inquiry requests from other BR/EDR 12 | > Controllers. 13 | 14 | * OGF: `#{inspect(@ogf, base: :hex)}` 15 | * OCF: `#{inspect(@ocf, base: :hex)}` 16 | * Opcode: `#{inspect(@opcode)}` 17 | 18 | Bluetooth Spec v5.2, Vol 4, Part E, section 7.3.18 19 | """ 20 | 21 | defparameters scan_enable: 0x00 22 | 23 | defimpl BlueHeron.HCI.Serializable do 24 | def serialize(%{opcode: opcode, scan_enable: scan_enable}) do 25 | <> 26 | end 27 | end 28 | 29 | @impl BlueHeron.HCI.Command 30 | def deserialize(<<@opcode::binary, 1, scan_enable>>) do 31 | new(scan_enable: scan_enable) 32 | end 33 | 34 | @impl BlueHeron.HCI.Command 35 | def deserialize_return_parameters(<>) do 36 | %{status: status} 37 | end 38 | 39 | @impl true 40 | def serialize_return_parameters(%{status: status}) do 41 | <> 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/link_control/set_connection_encryption.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Markus Hutzler 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.LinkControl.SetConnectionEncryption do 7 | use BlueHeron.HCI.Command.LinkControl, ocf: 0x0013 8 | 9 | @moduledoc """ 10 | > This command is used to try to authenticate the remote device associated with 11 | > the specified Connection_Handle. 12 | 13 | * OGF: `#{inspect(@ogf, base: :hex)}` 14 | * OCF: `#{inspect(@ocf, base: :hex)}` 15 | * Opcode: `#{inspect(@opcode)}` 16 | 17 | Bluetooth Spec v5.3, Vol 4, Part E, section 7.1.16 18 | """ 19 | 20 | defparameters handle: 0, enable: true 21 | 22 | defimpl BlueHeron.HCI.Serializable do 23 | def serialize(%{opcode: opcode, handle: handle, enable: enable}) do 24 | bin = <> 25 | size = byte_size(bin) 26 | <> 27 | end 28 | 29 | defp as_uint8(true), do: 1 30 | defp as_uint8(false), do: 0 31 | end 32 | 33 | @impl BlueHeron.HCI.Command 34 | def deserialize(<<@opcode::binary, 0>>) do 35 | %__MODULE__{} 36 | end 37 | 38 | @impl BlueHeron.HCI.Command 39 | def deserialize_return_parameters(<<>>) do 40 | %{} 41 | end 42 | 43 | @impl true 44 | def serialize_return_parameters(%{}) do 45 | <<>> 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/controller_and_baseband/write_local_name.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.ControllerAndBaseband.WriteLocalName do 7 | use BlueHeron.HCI.Command.ControllerAndBaseband, ocf: 0x0013 8 | 9 | @moduledoc """ 10 | > he HCI_Write_Local_Name command provides the ability to modify the user-friendly 11 | > name for the BR/EDR Controller. 12 | 13 | * OGF: `#{inspect(@ogf, base: :hex)}` 14 | * OCF: `#{inspect(@ocf, base: :hex)}` 15 | * Opcode: `#{inspect(@opcode)}` 16 | 17 | Bluetooth Spec v5.2, Vol 4, Part E, section 7.3.11 18 | """ 19 | 20 | defparameters name: "Bluetooth" 21 | 22 | defimpl BlueHeron.HCI.Serializable do 23 | def serialize(%{opcode: opcode, name: name}) do 24 | padded = for _i <- 1..(248 - byte_size(name)), into: name, do: <<0>> 25 | <> 26 | end 27 | end 28 | 29 | @impl BlueHeron.HCI.Command 30 | def deserialize(<<@opcode::binary, 248, padded::binary>>) do 31 | new(name: String.trim(padded, <<0>>)) 32 | end 33 | 34 | @impl BlueHeron.HCI.Command 35 | def deserialize_return_parameters(<>) do 36 | %{status: status} 37 | end 38 | 39 | @impl true 40 | def serialize_return_parameters(%{status: status}) do 41 | <> 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/events/le_meta/advertising_report.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.HCI.Event.LEMeta.AdvertisingReport do 6 | use BlueHeron.HCI.Event.LEMeta, subevent_code: 0x02 7 | 8 | @moduledoc """ 9 | > The LE Advertising Report event indicates that one or more Bluetooth devices have responded to 10 | > an active scan or have broadcast advertisements that were received during a passive scan. The 11 | > Controller may queue these advertising reports and send information from multiple devices in 12 | > one LE Advertising Report event. 13 | 14 | Reference: Version 5.2, Vol 4, Part E, 7.7.65.2 15 | """ 16 | 17 | alias BlueHeron.HCI.{Event.LEMeta.AdvertisingReport.Device} 18 | 19 | defparameters devices: [], num_reports: 0 20 | 21 | defimpl BlueHeron.HCI.Serializable do 22 | def serialize(report) do 23 | {:ok, bin} = Device.serialize(report.devices) 24 | size = byte_size(bin) + 1 25 | 26 | <> 27 | end 28 | end 29 | 30 | @impl BlueHeron.HCI.Event 31 | def deserialize(<<@code, _size, @subevent_code, arrayed_bin::binary>>) do 32 | {_, devices} = Device.deserialize(arrayed_bin) 33 | <> = arrayed_bin 34 | 35 | %__MODULE__{devices: devices, num_reports: num_reports} 36 | end 37 | 38 | def deserialize(bin), do: {:error, bin} 39 | end 40 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/controller_and_baseband/reset.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.ControllerAndBaseband.Reset do 7 | use BlueHeron.HCI.Command.ControllerAndBaseband, ocf: 0x0003 8 | 9 | @moduledoc """ 10 | > The HCI_Reset command will reset the Controller and the Link Manager on the 11 | > BR/EDR Controller or the Link Layer on an LE Controller. 12 | 13 | * OGF: `#{inspect(@ogf, base: :hex)}` 14 | * OCF: `#{inspect(@ocf, base: :hex)}` 15 | * Opcode: `#{inspect(@opcode)}` 16 | 17 | Bluetooth Spec v5.2, Vol 4, Part E, section 7.3.2 18 | """ 19 | 20 | defparameters [] 21 | 22 | defimpl BlueHeron.HCI.Serializable do 23 | def serialize(%{opcode: opcode}) do 24 | <> 25 | end 26 | end 27 | 28 | @impl BlueHeron.HCI.Command 29 | def deserialize(<<@opcode::binary, 0>>) do 30 | # This is a pretty useless function because there aren't 31 | # any parameters to actually parse out of this, but we 32 | # can at least assert its correct with matching 33 | %__MODULE__{} 34 | end 35 | 36 | @impl BlueHeron.HCI.Command 37 | def deserialize_return_parameters(<>) do 38 | %{status: status} 39 | end 40 | 41 | @impl true 42 | def serialize_return_parameters(%{status: status}) do 43 | <> 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/controller_and_baseband/write_default_erroneous_data_reporting.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.ControllerAndBaseband.WriteDefaultErroneousDataReporting do 7 | use BlueHeron.HCI.Command.ControllerAndBaseband, ocf: 0x005B 8 | 9 | @moduledoc """ 10 | > This command writes the Erroneous_Data_Reporting parameter. 11 | 12 | * OGF: `#{inspect(@ogf, base: :hex)}` 13 | * OCF: `#{inspect(@ocf, base: :hex)}` 14 | * Opcode: `#{inspect(@opcode)}` 15 | 16 | Bluetooth Spec v5.2, Vol 4, Part E, section 7.3.65 17 | """ 18 | 19 | defparameters enabled: false 20 | 21 | defimpl BlueHeron.HCI.Serializable do 22 | def serialize(%{opcode: opcode, enabled: enabled?}) do 23 | val = if enabled?, do: <<0x01>>, else: <<0x00>> 24 | <> 25 | end 26 | end 27 | 28 | @impl BlueHeron.HCI.Command 29 | def deserialize(<<@opcode::binary, 1, enabled::binary>>) do 30 | val = if enabled == <<0x01>>, do: true, else: false 31 | new(enabled: val) 32 | end 33 | 34 | @impl BlueHeron.HCI.Command 35 | def deserialize_return_parameters(<>) do 36 | %{status: status} 37 | end 38 | 39 | @impl BlueHeron.HCI.Command 40 | def serialize_return_parameters(%{status: status}) do 41 | <> 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/controller_and_baseband/write_secure_connections_host_support.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.ControllerAndBaseband.WriteSecureConnectionsHostSupport do 7 | use BlueHeron.HCI.Command.ControllerAndBaseband, ocf: 0x007A 8 | 9 | @moduledoc """ 10 | > This command writes the Secure_Connections_Host_Support parameter in the BR/EDR 11 | > Controller. 12 | 13 | * OGF: `#{inspect(@ogf, base: :hex)}` 14 | * OCF: `#{inspect(@ocf, base: :hex)}` 15 | * Opcode: `#{inspect(@opcode)}` 16 | 17 | Bluetooth Spec v5.2, Vol 4, Part E, section 7.3.92 18 | """ 19 | 20 | defparameters enabled: false 21 | 22 | defimpl BlueHeron.HCI.Serializable do 23 | def serialize(%{opcode: opcode, enabled: enabled?}) do 24 | val = if enabled?, do: <<1>>, else: <<0>> 25 | <> 26 | end 27 | end 28 | 29 | @impl BlueHeron.HCI.Command 30 | def deserialize(<<@opcode::binary, 1, enabled::binary>>) do 31 | val = if enabled == <<1>>, do: true, else: false 32 | %__MODULE__{enabled: val} 33 | end 34 | 35 | @impl BlueHeron.HCI.Command 36 | def deserialize_return_parameters(<>) do 37 | %{status: status} 38 | end 39 | 40 | @impl BlueHeron.HCI.Command 41 | def serialize_return_parameters(%{status: status}) do 42 | <> 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/blue_heron/hci/command/le_controller/set_scan_parameters_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.HCI.Command.LEController.SetScanParametersTest do 6 | use ExUnit.Case 7 | 8 | alias BlueHeron.HCI.Command.LEController.SetScanParameters 9 | 10 | test "encodes parameters correctly" do 11 | serialized = 12 | %SetScanParameters{ 13 | le_scan_type: 0x01, 14 | le_scan_interval: 0x0030, 15 | le_scan_window: 0x0030 16 | } 17 | |> BlueHeron.HCI.Serializable.serialize() 18 | 19 | assert <<0x0B, 0x20, 0x07, 0x01, 0x30, 0x00, 0x30, 0x00, 0x00, 0x00>> == serialized 20 | end 21 | 22 | test "serde is symmetric" do 23 | le_scan_type = Enum.random(0x00..0x01) 24 | le_scan_interval = Enum.random(0x0004..0x4000) 25 | le_scan_window = Enum.random(0x0004..0x4000) 26 | own_address_type = Enum.random(0x00..0x03) 27 | scanning_filter_policy = Enum.random(0x00..0x03) 28 | 29 | expected = %SetScanParameters{ 30 | le_scan_type: le_scan_type, 31 | le_scan_interval: le_scan_interval, 32 | le_scan_window: le_scan_window, 33 | own_address_type: own_address_type, 34 | scanning_filter_policy: scanning_filter_policy 35 | } 36 | 37 | assert expected == 38 | expected 39 | |> BlueHeron.HCI.Serializable.serialize() 40 | |> SetScanParameters.deserialize() 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/controller_and_baseband/write_synchronous_flow_control_enable.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.ControllerAndBaseband.WriteSynchronousFlowControlEnable do 7 | use BlueHeron.HCI.Command.ControllerAndBaseband, ocf: 0x002F 8 | 9 | @moduledoc """ 10 | > This command provides the ability to write the Synchronous_Flow_Control_Enable 11 | > parameter. 12 | 13 | * OGF: `#{inspect(@ogf, base: :hex)}` 14 | * OCF: `#{inspect(@ocf, base: :hex)}` 15 | * Opcode: `#{inspect(@opcode)}` 16 | 17 | Bluetooth Spec v5.2, Vol 4, Part E, section 7.3.37 18 | """ 19 | 20 | defparameters enabled: false 21 | 22 | defimpl BlueHeron.HCI.Serializable do 23 | def serialize(%{opcode: opcode, enabled: enabled?}) do 24 | val = if enabled?, do: <<0x01>>, else: <<0x00>> 25 | <> 26 | end 27 | end 28 | 29 | @impl BlueHeron.HCI.Command 30 | def deserialize(<<@opcode::binary, 1, enabled::binary>>) do 31 | val = if enabled == <<0x01>>, do: true, else: false 32 | new(enabled: val) 33 | end 34 | 35 | @impl BlueHeron.HCI.Command 36 | def deserialize_return_parameters(<>) do 37 | %{status: status} 38 | end 39 | 40 | @impl BlueHeron.HCI.Command 41 | def serialize_return_parameters(%{status: status}) do 42 | <> 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/le_controller/set_event_mask.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2023 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.LEController.SetEventMask do 7 | use BlueHeron.HCI.Command.LEController, ocf: 0x08 8 | 9 | @moduledoc """ 10 | > The HCI_LE_Set_Event_Mask command is used to control which LE events are 11 | > generated by the HCI for the Host. If the bit in the LE_Event_Mask is set to a one, 12 | > then the event associated with that bit will be enabled. The event mask allows the Host 13 | > to control which events will interrupt it. 14 | 15 | Bluetooth Core Version 5.2 | Vol 4, Part E, section 7.8.1 16 | 17 | * OGF: `#{inspect(@ogf, base: :hex)}` 18 | * OCF: `#{inspect(@ocf, base: :hex)}` 19 | * Opcode: `#{inspect(@opcode)}` 20 | """ 21 | 22 | defparameters mask: 0x00 23 | 24 | defimpl BlueHeron.HCI.Serializable do 25 | def serialize(command) do 26 | <> 27 | end 28 | end 29 | 30 | @impl BlueHeron.HCI.Command 31 | def deserialize(<<@opcode::binary, _, mask::little-64>>) do 32 | new(mask: mask) 33 | end 34 | 35 | @impl BlueHeron.HCI.Command 36 | def deserialize_return_parameters(<>) do 37 | %{status: status} 38 | end 39 | 40 | @impl BlueHeron.HCI.Command 41 | def serialize_return_parameters(%{status: status}) do 42 | <> 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/link_control/disconnect.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.LinkControl.Disconnect do 7 | use BlueHeron.HCI.Command.LinkControl, ocf: 0x0006 8 | 9 | @moduledoc """ 10 | > The HCI_Disconnect command is used to terminate an existing connection. The 11 | > Connection_Handle parameter indicates which connection is to be disconnected 12 | 13 | * OGF: `#{inspect(@ogf, base: :hex)}` 14 | * OCF: `#{inspect(@ocf, base: :hex)}` 15 | * Opcode: `#{inspect(@opcode)}` 16 | 17 | Bluetooth Spec v5.3, Vol 4, Part E, section 7.1.6 18 | """ 19 | 20 | defparameters reason: 0x16, connection_handle: 0 21 | 22 | defimpl BlueHeron.HCI.Serializable do 23 | def serialize(%{opcode: opcode, connection_handle: handle, reason: reason}) do 24 | bin = <> 25 | size = byte_size(bin) 26 | <> 27 | end 28 | end 29 | 30 | @impl BlueHeron.HCI.Command 31 | def deserialize(<<@opcode::binary, 0>>) do 32 | # This is a pretty useless function because there aren't 33 | # any parameters to actually parse out of this, but we 34 | # can at least assert its correct with matching 35 | %__MODULE__{} 36 | end 37 | 38 | @impl BlueHeron.HCI.Command 39 | def deserialize_return_parameters(<<>>) do 40 | %{} 41 | end 42 | 43 | @impl true 44 | def serialize_return_parameters(%{}) do 45 | <<>> 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/events/le_meta.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.HCI.Event.LEMeta do 6 | @moduledoc """ 7 | The LE Meta Event is used to encapsulate all LE Controller specific events. 8 | 9 | Reference: Version 5.2, Vol 4, Part E, 7.7.65 10 | """ 11 | 12 | alias __MODULE__ 13 | 14 | @typedoc """ 15 | > An LE Controller uses subevent codes to transmit LE specific events from the Controller to the 16 | > Host. Note: The subevent code will always be the first Event Parameter (See Section 7.7.65). 17 | 18 | Reference: Version 5.2, Vol 4, Part E, 5.4.4 19 | """ 20 | @type subevent_code :: pos_integer() 21 | 22 | @code 0x3E 23 | 24 | def __code__(), do: @code 25 | 26 | @doc """ 27 | List all available controller and baseband command modules 28 | """ 29 | @spec list :: [module()] 30 | def list() do 31 | Application.spec(:blue_heron, :modules) 32 | |> Enum.filter(&match?(["BlueHeron", "HCI", "Event", "LEMeta", _mod], Module.split(&1))) 33 | end 34 | 35 | defmacro __using__(opts) do 36 | quote location: :keep, bind_quoted: [opts: opts] do 37 | subevent_code = 38 | Keyword.get_lazy(opts, :subevent_code, fn -> 39 | raise ":subevent_code key required when defining BlueHeron.HCI.Event.LEMeta.__using__/1" 40 | end) 41 | 42 | use BlueHeron.HCI.Event, code: LEMeta.__code__() 43 | 44 | @subevent_code subevent_code 45 | 46 | def __subevent_code__(), do: @subevent_code 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/le_controller.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.HCI.Command.LEController do 6 | alias __MODULE__, as: LEC 7 | @ogf 0x08 8 | 9 | @moduledoc """ 10 | HCI commands for working with the LE Controller. 11 | 12 | * OGF: `#{inspect(@ogf, base: :hex)}` 13 | 14 | > The LE Controller Commands provide access and control to various capabilities of the Bluetooth 15 | > hardware, as well as methods for the Host to affect how the Link Layer manages the piconet, 16 | > and controls connections. 17 | 18 | Reference: Version 5.2, Vol 4, Part E, 7.8 19 | """ 20 | 21 | @doc false 22 | def __ogf__(), do: @ogf 23 | 24 | @doc """ 25 | List all available LE Controller command modules 26 | """ 27 | @spec list :: [module()] 28 | def list() do 29 | Application.spec(:blue_heron, :modules) 30 | |> Enum.filter( 31 | &match?(["BlueHeron", "HCI", "Command", "LEController", _mod], Module.split(&1)) 32 | ) 33 | end 34 | 35 | defmacro __using__(opts) do 36 | quote location: :keep, bind_quoted: [opts: opts] do 37 | ocf = 38 | Keyword.get_lazy(opts, :ocf, fn -> 39 | raise ":ocf key required when defining HCI.Command.LEController.__using__/1" 40 | end) 41 | 42 | use BlueHeron.HCI.Command, Keyword.put(opts, :ogf, LEC.__ogf__()) 43 | 44 | @ocf ocf 45 | @opcode BlueHeron.HCI.Command.opcode(LEC.__ogf__(), @ocf) 46 | 47 | def __ocf__(), do: @ocf 48 | def __opcode__(), do: @opcode 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/le_controller/set_scan_response_data.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.HCI.Command.LEController.SetScanResponseData do 6 | use BlueHeron.HCI.Command.LEController, ocf: 0x0009 7 | 8 | @moduledoc """ 9 | > This command is used to provide data used in Scanning Packets that have a data field. 10 | 11 | Bluetooth Core Version 5.2 | Vol 4, Part E, section 7.8.10 12 | 13 | * OGF: `#{inspect(@ogf, base: :hex)}` 14 | * OCF: `#{inspect(@ocf, base: :hex)}` 15 | * Opcode: `#{inspect(@opcode)}` 16 | """ 17 | 18 | defparameters scan_response_data: <<>> 19 | 20 | defimpl BlueHeron.HCI.Serializable do 21 | def serialize(%{opcode: opcode, scan_response_data: scan_response_data}) 22 | when byte_size(scan_response_data) <= 31 do 23 | length = byte_size(scan_response_data) 24 | padding_size = (31 - length) * 8 25 | 26 | <> 27 | end 28 | end 29 | 30 | @impl BlueHeron.HCI.Command 31 | def deserialize( 32 | <<@opcode::binary, 32, length, scan_response_data::binary-size(length), _rest::binary>> 33 | ) do 34 | new(scan_response_data: scan_response_data) 35 | end 36 | 37 | @impl BlueHeron.HCI.Command 38 | def deserialize_return_parameters(<>) do 39 | %{status: status} 40 | end 41 | 42 | @impl BlueHeron.HCI.Command 43 | def serialize_return_parameters(%{status: status}) do 44 | <> 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/link_control.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.HCI.Command.LinkControl do 6 | alias __MODULE__, as: LC 7 | @ogf 0x01 8 | 9 | @moduledoc """ 10 | HCI commands for working with Link Control. 11 | 12 | * OGF: `#{inspect(@ogf, base: :hex)}` 13 | 14 | > The Link Control commands allow a Controller to control connections to other BR/EDR 15 | > Controllers. Some Link Control commands are used only with a BR/EDR Controller 16 | > whereas other Link Control commands are also used with an LE Controller. 17 | 18 | Reference: Version 5.2, Vol 4, Part E, 7.1 19 | """ 20 | 21 | @doc false 22 | def __ogf__(), do: @ogf 23 | 24 | @doc """ 25 | List all available LE Controller command modules 26 | """ 27 | @spec list :: [module()] 28 | def list() do 29 | Application.spec(:blue_heron, :modules) 30 | |> Enum.filter( 31 | &match?(["BlueHeron", "HCI", "Command", "LinkControl", _mod], Module.split(&1)) 32 | ) 33 | end 34 | 35 | defmacro __using__(opts) do 36 | quote location: :keep, bind_quoted: [opts: opts] do 37 | ocf = 38 | Keyword.get_lazy(opts, :ocf, fn -> 39 | raise ":ocf key required when defining HCI.Command.LinkControl.__using__/1" 40 | end) 41 | 42 | use BlueHeron.HCI.Command, Keyword.put(opts, :ogf, LC.__ogf__()) 43 | 44 | @ocf ocf 45 | @opcode BlueHeron.HCI.Command.opcode(LC.__ogf__(), @ocf) 46 | 47 | def __ocf__(), do: @ocf 48 | def __opcode__(), do: @opcode 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/controller_and_baseband/set_controller_to_host_flow_control.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.ControllerAndBaseband.SetControllerToHostFlowControl do 7 | use BlueHeron.HCI.Command.ControllerAndBaseband, ocf: 0x0031 8 | 9 | @moduledoc """ 10 | > This command is used by the Host to turn flow control on or off for data and/or 11 | > voice sent in the direction from the Controller to the Host. 12 | 13 | * OGF: `#{inspect(@ogf, base: :hex)}` 14 | * OCF: `#{inspect(@ocf, base: :hex)}` 15 | * Opcode: `#{inspect(@opcode)}` 16 | 17 | Bluetooth Spec v5.2, Vol 4, Part E, section 7.3.38 18 | """ 19 | 20 | defparameters flow_control_enable: 0 21 | 22 | defimpl BlueHeron.HCI.Serializable do 23 | def serialize(%{opcode: opcode, flow_control_enable: flow_control_enable}) do 24 | <> 25 | end 26 | end 27 | 28 | @impl BlueHeron.HCI.Command 29 | def deserialize(<<@opcode::binary, 0>>) do 30 | # This is a pretty useless function because there aren't 31 | # any parameters to actually parse out of this, but we 32 | # can at least assert its correct with matching 33 | %__MODULE__{} 34 | end 35 | 36 | @impl BlueHeron.HCI.Command 37 | def deserialize_return_parameters(<>) do 38 | %{status: status} 39 | end 40 | 41 | @impl true 42 | def serialize_return_parameters(%{status: status}) do 43 | <> 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/controller_and_baseband/write_le_host_support.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.ControllerAndBaseband.WriteLEHostSupport do 7 | use BlueHeron.HCI.Command.ControllerAndBaseband, ocf: 0x006D 8 | 9 | @moduledoc """ 10 | > The HCI_Write_LE_Host_Support command is used to set the LE Supported (Host) 11 | > Link Manager Protocol feature bi 12 | 13 | * OGF: `#{inspect(@ogf, base: :hex)}` 14 | * OCF: `#{inspect(@ocf, base: :hex)}` 15 | * Opcode: `#{inspect(@opcode)}` 16 | 17 | Bluetooth Spec v5.2, Vol 4, Part E, section 7.3.79 18 | """ 19 | 20 | defparameters le_supported_host_enabled: false 21 | 22 | defimpl BlueHeron.HCI.Serializable do 23 | def serialize(%{opcode: opcode, le_supported_host_enabled: le_supported_host_enabled?}) do 24 | val = if le_supported_host_enabled?, do: <<0x01>>, else: <<0x00>> 25 | <> 26 | end 27 | end 28 | 29 | @impl BlueHeron.HCI.Command 30 | def deserialize(<<@opcode::binary, 2, le_supported_host_enabled::binary-1, 0x00>>) do 31 | val = if le_supported_host_enabled == <<0x01>>, do: true, else: false 32 | new(le_supported_host_enabled: val) 33 | end 34 | 35 | @impl BlueHeron.HCI.Command 36 | def deserialize_return_parameters(<>) do 37 | %{status: status} 38 | end 39 | 40 | @impl true 41 | def serialize_return_parameters(%{status: status}) do 42 | <> 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/deserializable.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defprotocol BlueHeron.HCI.Deserializable do 6 | @doc """ 7 | Deserialize a binary into HCI data structures 8 | """ 9 | def deserialize(bin) 10 | end 11 | 12 | defimpl BlueHeron.HCI.Deserializable, for: BitString do 13 | # Define deserialize/1 for HCI.Command modules 14 | for mod <- BlueHeron.HCI.Command.__modules__(), opcode = mod.__opcode__() do 15 | def deserialize(unquote(opcode) <> _ = bin) do 16 | unquote(mod).deserialize(bin) 17 | end 18 | end 19 | 20 | # Define deserialize/1 for HCI.Event modules 21 | for mod <- BlueHeron.HCI.Event.__modules__(), code = mod.__code__() do 22 | if function_exported?(mod, :__subevent_code__, 0) do 23 | # These are LEMeta subevents 24 | def deserialize( 25 | <> = bin 26 | ) do 27 | unquote(mod).deserialize(bin) 28 | end 29 | else 30 | # Normal events 31 | def deserialize(<> = bin) do 32 | unquote(mod).deserialize(bin) 33 | end 34 | end 35 | end 36 | 37 | def deserialize(bin) do 38 | error = """ 39 | Unable to deserialize #{inspect(bin, base: :hex)} 40 | 41 | If this is unexpected, then be sure that the target deserialized module 42 | is defined in the @modules attribute of the appropiate type: 43 | 44 | * BlueHeron.HCI.Command 45 | * BlueHeron.HCI.Event 46 | """ 47 | 48 | {:error, error} 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/blue_heron/att/requests/read_by_type_request.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.ATT.ReadByTypeRequest do 7 | @moduledoc """ 8 | > The ATT_READ_BY_TYPE_REQ PDU is used to obtain the values of attributes where 9 | > the attribute type is known but the handle is not known. 10 | 11 | Bluetooth Spec v5.2, vol 3, Part F, 3.4.4.1 12 | """ 13 | 14 | defstruct [:opcode, :starting_handle, :ending_handle, :uuid] 15 | 16 | def serialize(%{ 17 | starting_handle: starting_handle, 18 | ending_handle: ending_handle, 19 | uuid: uuid 20 | }) 21 | when uuid > 65535 do 22 | <<0x8, starting_handle::little-16, ending_handle::little-16, uuid::little-128>> 23 | end 24 | 25 | def serialize(%{ 26 | starting_handle: starting_handle, 27 | ending_handle: ending_handle, 28 | uuid: uuid 29 | }) do 30 | <<0x8, starting_handle::little-16, ending_handle::little-16, uuid::little-16>> 31 | end 32 | 33 | def deserialize(<<0x8, starting_handle::little-16, ending_handle::little-16, uuid::little-16>>) do 34 | %__MODULE__{ 35 | opcode: 0x8, 36 | starting_handle: starting_handle, 37 | ending_handle: ending_handle, 38 | uuid: uuid 39 | } 40 | end 41 | 42 | def deserialize(<<0x8, starting_handle::little-16, ending_handle::little-16, uuid::little-128>>) do 43 | %__MODULE__{ 44 | opcode: 0x8, 45 | starting_handle: starting_handle, 46 | ending_handle: ending_handle, 47 | uuid: uuid 48 | } 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/le_controller/create_connection_cancel.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.LEController.CreateConnectionCancel do 7 | use BlueHeron.HCI.Command.LEController, ocf: 0x000E 8 | 9 | @moduledoc """ 10 | > The HCI_LE_Create_Connection_Cancel command is used to cancel the 11 | > HCI_LE_Create_Connection or HCI_LE_Extended_Create_Connection commands. 12 | > If no HCI_LE_Create_Connection or HCI_LE_Extended_Create_Connection command 13 | > is pending, then the Controller shall return the error code Command Disallowed (0x0C). 14 | 15 | Bluetooth Core Version 5.2 | Vol 4, Part E, section 7.8.13 16 | 17 | * OGF: `#{inspect(@ogf, base: :hex)}` 18 | * OCF: `#{inspect(@ocf, base: :hex)}` 19 | * Opcode: `#{inspect(@opcode)}` 20 | """ 21 | 22 | defparameters([]) 23 | 24 | defimpl BlueHeron.HCI.Serializable do 25 | def serialize(%{opcode: opcode}) do 26 | <> 27 | end 28 | end 29 | 30 | @impl BlueHeron.HCI.Command 31 | def deserialize(<<@opcode::binary, 0>>) do 32 | # This is a pretty useless function because there aren't 33 | # any parameters to actually parse out of this, but we 34 | # can at least assert its correct with matching 35 | %__MODULE__{} 36 | end 37 | 38 | @impl BlueHeron.HCI.Command 39 | def deserialize_return_parameters(<>) do 40 | %{status: status} 41 | end 42 | 43 | @impl true 44 | def serialize_return_parameters(%{status: status}) do 45 | <> 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/le_controller/set_advertising_data.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2023 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.LEController.SetAdvertisingData do 7 | use BlueHeron.HCI.Command.LEController, ocf: 0x0008 8 | 9 | @moduledoc """ 10 | > The HCI_LE_Set_Advertising_Data command is used to set the data used in 11 | > advertising packets that have a data field. 12 | 13 | Bluetooth Core Version 5.2 | Vol 4, Part E, section 7.8.7 14 | 15 | * OGF: `#{inspect(@ogf, base: :hex)}` 16 | * OCF: `#{inspect(@ocf, base: :hex)}` 17 | * Opcode: `#{inspect(@opcode)}` 18 | """ 19 | 20 | defparameters advertising_data: <<>> 21 | 22 | defimpl BlueHeron.HCI.Serializable do 23 | def serialize(%{opcode: opcode, advertising_data: advertising_data}) 24 | when byte_size(advertising_data) <= 31 do 25 | length = byte_size(advertising_data) 26 | padding_size = (31 - length) * 8 27 | 28 | <> 29 | end 30 | end 31 | 32 | @impl BlueHeron.HCI.Command 33 | def deserialize( 34 | <<@opcode::binary, 32, length, advertising_data::binary-size(length), _rest::binary>> 35 | ) do 36 | new(advertising_data: advertising_data) 37 | end 38 | 39 | @impl BlueHeron.HCI.Command 40 | def deserialize_return_parameters(<>) do 41 | %{status: status} 42 | end 43 | 44 | @impl BlueHeron.HCI.Command 45 | def serialize_return_parameters(%{status: status}) do 46 | <> 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/le_controller/read_white_list_size.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.LEController.ReadWhiteListSize do 7 | use BlueHeron.HCI.Command.LEController, ocf: 0x000F 8 | 9 | @moduledoc """ 10 | > The HCI_LE_Read_Filter_Accept_List_Size command is used to read the total number 11 | > of Filter Accept List entries that can be stored in the Controller. 12 | > Note: The number of entries that can be stored is not fixed and the Controller can 13 | > change it at any time (e.g. because the memory used to store the Filter Accept List can 14 | > also be used for other purposes). 15 | 16 | Bluetooth Core Version 5.2 | Vol 4, Part E, section 7.8.14 17 | 18 | * OGF: `#{inspect(@ogf, base: :hex)}` 19 | * OCF: `#{inspect(@ocf, base: :hex)}` 20 | * Opcode: `#{inspect(@opcode)}` 21 | """ 22 | 23 | defparameters [] 24 | 25 | defimpl BlueHeron.HCI.Serializable do 26 | def serialize(%{opcode: opcode}) do 27 | <> 28 | end 29 | end 30 | 31 | @impl BlueHeron.HCI.Command 32 | def deserialize(<<@opcode::binary, 0x00>>) do 33 | new() 34 | end 35 | 36 | @impl BlueHeron.HCI.Command 37 | def deserialize_return_parameters(<>) do 38 | %{ 39 | status: status, 40 | white_list_size: white_list_size 41 | } 42 | end 43 | 44 | @impl BlueHeron.HCI.Command 45 | def serialize_return_parameters(%{status: status, white_list_size: white_list_size}) do 46 | <> 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/controller_and_baseband/write_extended_inquiry_response.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.ControllerAndBaseband.WriteExtendedInquiryResponse do 7 | use BlueHeron.HCI.Command.ControllerAndBaseband, ocf: 0x0052 8 | 9 | @moduledoc """ 10 | > The HCI_Write_Extended_Inquiry_Response command writes the extended inquiry 11 | > response to be sent during the extended inquiry response procedure. 12 | 13 | * OGF: `#{inspect(@ogf, base: :hex)}` 14 | * OCF: `#{inspect(@ocf, base: :hex)}` 15 | * Opcode: `#{inspect(@opcode)}` 16 | 17 | Bluetooth Spec v5.2, Vol 4, Part E, section 7.3.56 18 | """ 19 | 20 | defparameters fec_required?: false, extended_inquiry_response: <<0>> 21 | 22 | defimpl BlueHeron.HCI.Serializable do 23 | def serialize(data) do 24 | val = if data.fec_required?, do: <<1>>, else: <<0>> 25 | rem = 240 - byte_size(data.extended_inquiry_response) 26 | 27 | padded = for _i <- 1..rem, into: data.extended_inquiry_response, do: <<0>> 28 | 29 | <> 30 | end 31 | end 32 | 33 | @impl BlueHeron.HCI.Command 34 | def deserialize(<<@opcode::binary, _size, fec_req, eir::binary>>) do 35 | val = if fec_req == 1, do: true, else: false 36 | %__MODULE__{fec_required?: val, extended_inquiry_response: String.trim(eir, <<0>>)} 37 | end 38 | 39 | @impl BlueHeron.HCI.Command 40 | def deserialize_return_parameters(<>) do 41 | %{status: status} 42 | end 43 | 44 | @impl true 45 | def serialize_return_parameters(%{status: status}) do 46 | <> 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/informational_parameters.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.HCI.Command.InformationalParameters do 6 | alias __MODULE__, as: IP 7 | @ogf 0x04 8 | 9 | @moduledoc """ 10 | HCI commands for working with the informational parameters. 11 | 12 | * OGF: `#{inspect(@ogf, base: :hex)}` 13 | 14 | The Informational Parameters are fixed by the manufacturer of the Bluetooth 15 | hardware. These parameters provide information about the BR/EDR Controller and 16 | the capabilities of the Link Manager and Baseband in the BR/EDR Controller and 17 | PAL in the AMP Controller. The host device cannot modify any of these 18 | parameters. 19 | 20 | Bluetooth Spec v5.2, vol 4, Part E, 7.2 21 | """ 22 | 23 | @doc false 24 | def __ogf__(), do: @ogf 25 | 26 | @doc """ 27 | List all available controller and baseband command modules 28 | """ 29 | @spec list :: [module()] 30 | def list() do 31 | Application.spec(:blue_heron, :modules) 32 | |> Enum.filter( 33 | &match?(["BlueHeron", "HCI", "Command", "InformationalParameters", _mod], Module.split(&1)) 34 | ) 35 | end 36 | 37 | defmacro __using__(opts) do 38 | quote location: :keep, bind_quoted: [opts: opts] do 39 | ocf = 40 | Keyword.get_lazy(opts, :ocf, fn -> 41 | raise ":ocf key required when defining HCI.Command.InformationalParameters.__using__/1" 42 | end) 43 | 44 | use BlueHeron.HCI.Command, Keyword.put(opts, :ogf, IP.__ogf__()) 45 | 46 | @ocf ocf 47 | @opcode BlueHeron.HCI.Command.opcode(IP.__ogf__(), @ocf) 48 | 49 | def __ocf__(), do: @ocf 50 | def __opcode__(), do: @opcode 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/blue_heron/att/requests/read_by_group_type_request.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.ATT.ReadByGroupTypeRequest do 7 | @moduledoc """ 8 | > The ATT_READ_BY_GROUP_TYPE_REQ PDU is used to obtain the values of 9 | > attributes where the attribute type is known, the type of a grouping attribute as defined 10 | > by a higher layer specification, but the handle is not known. 11 | 12 | Bluetooth Spec v5.2, vol 3, Part F, 3.4.4.9 13 | """ 14 | 15 | defstruct [:opcode, :starting_handle, :ending_handle, :uuid] 16 | 17 | def serialize(%{ 18 | starting_handle: starting_handle, 19 | ending_handle: ending_handle, 20 | uuid: uuid 21 | }) 22 | when uuid > 65535 do 23 | <<0x10, starting_handle::little-16, ending_handle::little-16, uuid::little-128>> 24 | end 25 | 26 | def serialize(%{ 27 | starting_handle: starting_handle, 28 | ending_handle: ending_handle, 29 | uuid: uuid 30 | }) do 31 | <<0x10, starting_handle::little-16, ending_handle::little-16, uuid::little-16>> 32 | end 33 | 34 | def deserialize(<<0x10, starting_handle::little-16, ending_handle::little-16, uuid::little-16>>) do 35 | %__MODULE__{ 36 | opcode: 0x10, 37 | starting_handle: starting_handle, 38 | ending_handle: ending_handle, 39 | uuid: uuid 40 | } 41 | end 42 | 43 | def deserialize( 44 | <<0x10, starting_handle::little-16, ending_handle::little-16, uuid::little-128>> 45 | ) do 46 | %__MODULE__{ 47 | opcode: 0x10, 48 | starting_handle: starting_handle, 49 | ending_handle: ending_handle, 50 | uuid: uuid 51 | } 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/events/encryption_change.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Markus Hutzler 2 | # SPDX-FileCopyrightText: 2025 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Event.EncryptionChange do 7 | use BlueHeron.HCI.Event, code: 0x08 8 | 9 | @moduledoc """ 10 | The HCI_Encryption_Change event is used to indicate that the change of the 11 | encryption mode has been completed. The Connection_Handle event 12 | parameter will be a Connection_Handle for an ACL connection and is used to 13 | identify the remote device. 14 | 15 | Reference: Version 5.4, Vol 4, Part E, 7.7.8 16 | """ 17 | 18 | defparameters [ 19 | :status, 20 | :connection_handle, 21 | :encryption_enabled 22 | ] 23 | 24 | defimpl BlueHeron.HCI.Serializable do 25 | def serialize(data) do 26 | <> = <> 27 | handle = <> 28 | 29 | bin = << 30 | data.status, 31 | handle::binary, 32 | data.encryption_enabled 33 | >> 34 | 35 | size = byte_size(bin) 36 | <> 37 | end 38 | end 39 | 40 | @impl BlueHeron.HCI.Event 41 | def deserialize( 42 | <<@code, _size, 43 | << 44 | status, 45 | lower_handle, 46 | _::4, 47 | upper_handle::4, 48 | encryption_enabled 49 | >>::binary>> 50 | ) do 51 | <> = <> 52 | 53 | %__MODULE__{ 54 | status: status, 55 | connection_handle: handle, 56 | encryption_enabled: encryption_enabled 57 | } 58 | end 59 | 60 | def deserialize(bin), do: {:error, bin} 61 | end 62 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/link_policy.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.HCI.Command.LinkPolicy do 6 | alias __MODULE__, as: LP 7 | @ogf 0x02 8 | 9 | @moduledoc """ 10 | HCI commands for working with the Link Policy. 11 | 12 | * OGF: `#{inspect(@ogf, base: :hex)}` 13 | 14 | > The Link Policy Commands provide methods for the Host to affect 15 | > how the Link Manager manages the piconet. When Link Policy Commands are 16 | > used, the LM still controls how Bluetooth piconets and scatternets are 17 | > established and maintained, depending on adjustable policy parameters. 18 | > These policy commands modify the Link Manager behavior that can result 19 | > in changes to the link layer connections with Bluetooth remote devices 20 | 21 | Reference: Version 5.2, Vol 4, Part E, 7.2 22 | """ 23 | 24 | @doc false 25 | def __ogf__(), do: @ogf 26 | 27 | @doc """ 28 | List all available LE Controller command modules 29 | """ 30 | @spec list :: [module()] 31 | def list() do 32 | Application.spec(:blue_heron, :modules) 33 | |> Enum.filter(&match?(["BlueHeron", "HCI", "Command", "LinkPolicy", _mod], Module.split(&1))) 34 | end 35 | 36 | defmacro __using__(opts) do 37 | quote location: :keep, bind_quoted: [opts: opts] do 38 | ocf = 39 | Keyword.get_lazy(opts, :ocf, fn -> 40 | raise ":ocf key required when defining HCI.Command.LinkPolicy.__using__/1" 41 | end) 42 | 43 | use BlueHeron.HCI.Command, Keyword.put(opts, :ogf, LP.__ogf__()) 44 | 45 | @ocf ocf 46 | @opcode BlueHeron.HCI.Command.opcode(LP.__ogf__(), @ocf) 47 | 48 | def __ocf__(), do: @ocf 49 | def __opcode__(), do: @opcode 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/blue_heron/smp/key_manager.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Connor Rigby 2 | # SPDX-FileCopyrightText: 2023 Markus Hutzler 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.SMP.KeyManager do 7 | @moduledoc "Handles storage of keys for SMP" 8 | use GenServer 9 | 10 | def start_link(path) do 11 | GenServer.start_link(__MODULE__, path) 12 | end 13 | 14 | def init(path) do 15 | {:ok, %{key_file: path}} 16 | end 17 | 18 | def new(manager) do 19 | GenServer.call(manager, :new) 20 | end 21 | 22 | def get(manager, index) do 23 | GenServer.call(manager, {:get, index}) 24 | end 25 | 26 | def handle_call(:new, _from, state) do 27 | data = read_or_create(state.key_file) 28 | {index, keys} = generate_keys(data) 29 | data = Map.put(data, index, keys) 30 | File.write!(state.key_file, :erlang.term_to_binary(data)) 31 | {:reply, {index, keys}, state} 32 | end 33 | 34 | def handle_call({:get, index}, _from, state) do 35 | data = read_or_create(state.key_file) 36 | keys = get_keys(data, index) 37 | {:reply, {index, keys}, state} 38 | end 39 | 40 | defp generate_keys(data) do 41 | <> = :crypto.strong_rand_bytes(2) 42 | val = :crypto.strong_rand_bytes(56) 43 | 44 | # Make sure key (index) is unique 45 | if Map.has_key?(data, key) do 46 | generate_keys(data) 47 | else 48 | {key, val} 49 | end 50 | end 51 | 52 | defp get_keys(data, index) do 53 | case Map.fetch(data, index) do 54 | {:ok, keys} -> keys 55 | :error -> nil 56 | end 57 | end 58 | 59 | defp read_or_create(file) do 60 | case File.read(file) do 61 | {:ok, content} -> :erlang.binary_to_term(content) 62 | {:error, _reason} -> %{} 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/le_controller/set_advertising_enable.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.LEController.SetAdvertisingEnable do 7 | use BlueHeron.HCI.Command.LEController, ocf: 0x000A 8 | 9 | @moduledoc """ 10 | > The HCI_LE_Set_Advertising_Enable command is used to request the Controller 11 | > to start or stop advertising. The Controller manages the timing of advertisements 12 | > as per the advertising parameters given in the HCI_LE_Set_Advertising_Parameters 13 | > command. 14 | 15 | Bluetooth Core Version 5.2 | Vol 4, Part E, section 7.8.9 16 | 17 | * OGF: `#{inspect(@ogf, base: :hex)}` 18 | * OCF: `#{inspect(@ocf, base: :hex)}` 19 | * Opcode: `#{inspect(@opcode)}` 20 | """ 21 | 22 | defparameters advertising_enable: false 23 | 24 | defimpl BlueHeron.HCI.Serializable do 25 | def serialize(command) do 26 | advertising_enable = as_uint8(command.advertising_enable) 27 | <> 28 | end 29 | 30 | defp as_uint8(true), do: 1 31 | defp as_uint8(false), do: 0 32 | end 33 | 34 | @impl BlueHeron.HCI.Command 35 | def deserialize(<<@opcode::binary, _fields_size, advertising_enable>>) do 36 | new(advertising_enable: as_boolean(advertising_enable)) 37 | end 38 | 39 | @impl BlueHeron.HCI.Command 40 | def deserialize_return_parameters(<>) do 41 | %{status: status} 42 | end 43 | 44 | @impl BlueHeron.HCI.Command 45 | def serialize_return_parameters(%{status: status}) do 46 | <> 47 | end 48 | 49 | defp as_boolean(val) when val in [1, "1", true, <<1>>], do: true 50 | defp as_boolean(_), do: false 51 | end 52 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/controller_and_baseband.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.HCI.Command.ControllerAndBaseband do 6 | alias __MODULE__, as: CaB 7 | @ogf 0x03 8 | 9 | @moduledoc """ 10 | HCI commands for working with the controller and baseband. 11 | 12 | * OGF: `#{inspect(@ogf, base: :hex)}` 13 | 14 | > The Controller & Baseband Commands provide access and control to various capabilities of the 15 | > Bluetooth hardware. These parameters provide control of BR/EDR Controllers and of the 16 | > capabilities of the Link Manager and Baseband in the BR/EDR Controller, the PAL in an AMP 17 | > Controller, and the Link Layer in an LE Controller. The Host can use these commands to modify 18 | > the behavior of the local Controller. 19 | Bluetooth Spec v5 20 | """ 21 | 22 | @doc false 23 | def __ogf__(), do: @ogf 24 | 25 | @doc """ 26 | List all available controller and baseband command modules 27 | """ 28 | @spec list :: [module()] 29 | def list() do 30 | Application.spec(:blue_heron, :modules) 31 | |> Enum.filter( 32 | &match?(["BlueHeron", "HCI", "Command", "ControllerAndBaseband", _mod], Module.split(&1)) 33 | ) 34 | end 35 | 36 | defmacro __using__(opts) do 37 | quote location: :keep, bind_quoted: [opts: opts] do 38 | ocf = 39 | Keyword.get_lazy(opts, :ocf, fn -> 40 | raise ":ocf key required when defining HCI.Command.ControllerAndBaseband.__using__/1" 41 | end) 42 | 43 | use BlueHeron.HCI.Command, Keyword.put(opts, :ogf, CaB.__ogf__()) 44 | 45 | @ocf ocf 46 | @opcode BlueHeron.HCI.Command.opcode(CaB.__ogf__(), @ocf) 47 | 48 | def __ocf__(), do: @ocf 49 | def __opcode__(), do: @opcode 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/controller_and_baseband/read_local_name.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.ControllerAndBaseband.ReadLocalName do 7 | use BlueHeron.HCI.Command.ControllerAndBaseband, ocf: 0x0014 8 | 9 | @moduledoc """ 10 | > The HCI_Read_Local_Name command provides the ability to read the stored user- 11 | > friendly name for the BR/EDR Controller. 12 | 13 | * OGF: `#{inspect(@ogf, base: :hex)}` 14 | * OCF: `#{inspect(@ocf, base: :hex)}` 15 | * Opcode: `#{inspect(@opcode)}` 16 | 17 | Bluetooth Spec v5.2, Vol 4, Part E, section 7.3.12 18 | """ 19 | 20 | defparameters [] 21 | 22 | defimpl BlueHeron.HCI.Serializable do 23 | def serialize(%{opcode: opcode}) do 24 | <> 25 | end 26 | end 27 | 28 | @impl true 29 | def deserialize(<<@opcode::binary, 0>>) do 30 | # This is a pretty useless function because there aren't 31 | # any parameters to actually parse out of this, but we 32 | # can at least assert its correct with matching 33 | %__MODULE__{} 34 | end 35 | 36 | @impl true 37 | def deserialize_return_parameters(<>) do 38 | %{ 39 | status: status, 40 | # The local name field will fill any remainder of the 41 | # 248 bytes with null bytes. So just trim those. 42 | local_name: String.trim(local_name, <<0>>) 43 | } 44 | end 45 | 46 | @impl true 47 | def serialize_return_parameters(%{status: status, local_name: local_name}) do 48 | name_length = byte_size(local_name) 49 | padding = 248 - name_length 50 | 51 | <> 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/informational_parameters/read_local_version.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.InformationalParameters.ReadLocalVersion do 7 | use BlueHeron.HCI.Command.InformationalParameters, ocf: 0x0001 8 | 9 | @moduledoc """ 10 | > This command reads the values for the version information for the local Controller. 11 | 12 | * OGF: `#{inspect(@ogf, base: :hex)}` 13 | * OCF: `#{inspect(@ocf, base: :hex)}` 14 | * Opcode: `#{inspect(@opcode)}` 15 | 16 | Bluetooth Spec v5.3, Vol 4, Part E, section 7.4.1 17 | """ 18 | 19 | defparameters [] 20 | 21 | defimpl BlueHeron.HCI.Serializable do 22 | def serialize(rlv) do 23 | <> 24 | end 25 | end 26 | 27 | @impl BlueHeron.HCI.Command 28 | def deserialize(<<@opcode::binary, 0>>) do 29 | %__MODULE__{} 30 | end 31 | 32 | @impl BlueHeron.HCI.Command 33 | def deserialize_return_parameters(<>) do 34 | << 35 | hci_version, 36 | hci_revision::little-16, 37 | lmp_pal_version, 38 | manufacturer_name::little-16, 39 | lmp_pal_subversion::little-16 40 | >> = bin 41 | 42 | %{ 43 | status: status, 44 | hci_version: hci_version, 45 | hci_revision: hci_revision, 46 | lmp_pal_version: lmp_pal_version, 47 | manufacturer_name: manufacturer_name, 48 | lmp_pal_subversion: lmp_pal_subversion 49 | } 50 | end 51 | 52 | @impl BlueHeron.HCI.Command 53 | def serialize_return_parameters(%{status: status} = params) do 54 | <> 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/blue_heron/data_type/service_data.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Very 2 | # 3 | # SPDX-License-Identifier: MIT 4 | # 5 | defmodule BlueHeron.DataType.ServiceData do 6 | @moduledoc """ 7 | > The Service Data data type consists of a service UUID with the data associated with that 8 | > service. 9 | 10 | Reference: Core Specification Supplement, Part A, section 1.11.1 11 | """ 12 | 13 | require BlueHeron.AssignedNumbers.GenericAccessProfile, as: GenericAccessProfile 14 | 15 | @description_32 "Service Data - 32-bit UUID" 16 | 17 | @doc """ 18 | Returns the three GAP descriptions encompassed by service data. 19 | """ 20 | def gap_descriptions, do: [@description_32] 21 | 22 | @doc """ 23 | iex> serialize({"Service Data - 32-bit UUID", %{data: <<5, 6>>, uuid: 67305985}}) 24 | {:ok, <<32, 1, 2, 3, 4, 5, 6>>} 25 | """ 26 | def serialize({@description_32, %{data: data, uuid: uuid}}) do 27 | binary = << 28 | GenericAccessProfile.id(unquote(@description_32)), 29 | uuid::little-size(32), 30 | data::binary 31 | >> 32 | 33 | {:ok, binary} 34 | end 35 | 36 | def serialize(_), do: :error 37 | 38 | @doc """ 39 | Deserialize a service data binary. 40 | 41 | iex> deserialize(<<32, 1, 2, 3, 4, 5, 6>>) 42 | {:ok, {"Service Data - 32-bit UUID", %{data: <<5, 6>>, uuid: 67305985}}} 43 | """ 44 | def deserialize(<>) do 45 | {status, data} = 46 | case bin do 47 | <> -> 48 | service_data_32 = %{ 49 | uuid: uuid, 50 | data: data 51 | } 52 | 53 | {:ok, service_data_32} 54 | 55 | _ -> 56 | {:error, bin} 57 | end 58 | 59 | {status, {@description_32, data}} 60 | end 61 | 62 | def deserialize(bin), do: {:error, bin} 63 | end 64 | -------------------------------------------------------------------------------- /test/blue_heron/transport/uart/framing_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.Transport.UART.FramingTest do 6 | use ExUnit.Case 7 | alias BlueHeron.HCI.Transport.UART.Framing 8 | 9 | test "hci frame" do 10 | frame = <<0x04, 0x0E, 0x0A, 0x01, 0x09, 0x10, 0x00, 0xB2, 0xE2, 0x66, 0x1C, 0xFB, 0xE8>> 11 | 12 | {:ok, state} = Framing.init([]) 13 | 14 | assert {:ok, 15 | [<<0x04, 0x0E, 0x0A, 0x01, 0x09, 0x10, 0x00, 0xB2, 0xE2, 0x66, 0x1C, 0xFB, 0xE8>>], 16 | %BlueHeron.HCI.Transport.UART.Framing.State{frame: "", type: nil, frames: []}} = 17 | Framing.remove_framing(frame, state) 18 | end 19 | 20 | test "acl frame" do 21 | frame = 22 | <<0x2, 0x80, 0x20, 0xB, 0x0, 0x7, 0x0, 0x4, 0x0, 0x10, 0x1, 0x0, 0xFF, 0xFF, 0x0, 0x28>> 23 | 24 | {:ok, state} = Framing.init([]) 25 | 26 | assert {:ok, 27 | [ 28 | <<0x2, 0x80, 0x20, 0xB, 0x0, 0x7, 0x0, 0x4, 0x0, 0x10, 0x1, 0x0, 0xFF, 0xFF, 0x0, 29 | 0x28>> 30 | ], 31 | %BlueHeron.HCI.Transport.UART.Framing.State{frame: "", type: nil, frames: []}} = 32 | Framing.remove_framing(frame, state) 33 | end 34 | 35 | test "doubled-up ACL frame" do 36 | frames = 37 | <<0x2, 0x80, 0x20, 0xB, 0x0, 0x7, 0x0, 0x4, 0x0, 0x8, 0xE, 0x0, 0xE, 0x0, 0x3, 0x28, 0x4, 38 | 0x13, 0x5, 0x1, 0x80, 0x0, 0x1, 0x0>> 39 | 40 | {:ok, state} = Framing.init([]) 41 | 42 | assert {:ok, 43 | [ 44 | <<0x2, 0x80, 0x20, 0xB, 0x0, 0x7, 0x0, 0x4, 0x0, 0x8, 0xE, 0x0, 0xE, 0x0, 0x3, 45 | 0x28>>, 46 | <<0x4, 0x13, 0x5, 0x1, 0x80, 0x0, 0x1, 0x0>> 47 | ], 48 | %BlueHeron.HCI.Transport.UART.Framing.State{frame: "", type: nil, frames: []}} = 49 | Framing.remove_framing(frames, state) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/link_policy/write_default_link_policy_settings.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.LinkPolicy.WriteDefaultLinkPolicySettings do 7 | use BlueHeron.HCI.Command.LinkPolicy, ocf: 0x000F 8 | 9 | @moduledoc """ 10 | This command writes the Default Link Policy configuration value. 11 | 12 | Bluetooth Core Version 5.2 | Vol 4, Part E, section 7.2.12 13 | 14 | The Default_Link_Policy_Settings parameter determines the initial value of the Link_Policy_Settings for all new BR/EDR connections. 15 | 16 | Note: See the Link Policy Settings configuration parameter for more information. See Section 6.18. 17 | 18 | * OGF: `#{inspect(@ogf, base: :hex)}` 19 | * OCF: `#{inspect(@ocf, base: :hex)}` 20 | * Opcode: `#{inspect(@opcode)}` 21 | """ 22 | 23 | defparameters enable_role_switch: 0, enable_hold_mode: 0, enable_sniff_mode: 0 24 | 25 | defimpl BlueHeron.HCI.Serializable do 26 | def serialize(data) do 27 | << 28 | data.opcode, 29 | 2, 30 | 0::13, 31 | data.enable_sniff_mode::1, 32 | data.enable_hold_mode::1, 33 | data.enable_role_switch::1 34 | >> 35 | end 36 | end 37 | 38 | @impl BlueHeron.HCI.Command 39 | def deserialize(<<@opcode, _size, 0::13, dlps::binary-3-unit(1)>>) do 40 | <> = dlps 41 | 42 | %__MODULE__{ 43 | enable_sniff_mode: enable_sniff_mode, 44 | enable_hold_mode: enable_hold_mode, 45 | enable_role_switch: enable_role_switch 46 | } 47 | end 48 | 49 | @impl BlueHeron.HCI.Command 50 | def deserialize_return_parameters(<>) do 51 | %{status: status} 52 | end 53 | 54 | @impl true 55 | def serialize_return_parameters(%{status: status}) do 56 | <> 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/le_controller/long_term_key_request_negative_reply.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Markus Hutzler 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.LEController.LongTermKeyRequestNegativeReply do 7 | use BlueHeron.HCI.Command.LEController, ocf: 0x001B 8 | 9 | @moduledoc """ 10 | > The HCI_LE_Long_Term_Key_Request_Negative_Reply command is used to reply to 11 | > an HCI_LE_Long_Term_Key_Request event from the Controller if the Host cannot 12 | > provide a Long Term Key for this Connection_Handle. 13 | > This command shall only be used when the local device’s role is Peripheral. 14 | 15 | Bluetooth Core Version 5.2 | Vol 4, Part E, section 7.8.26 16 | 17 | * OGF: `#{inspect(@ogf, base: :hex)}` 18 | * OCF: `#{inspect(@ocf, base: :hex)}` 19 | * Opcode: `#{inspect(@opcode)}` 20 | """ 21 | 22 | defparameters [ 23 | :status, 24 | :connection_handle 25 | ] 26 | 27 | defimpl BlueHeron.HCI.Serializable do 28 | def serialize(%{opcode: opcode, connection_handle: handle}) do 29 | <> 30 | end 31 | end 32 | 33 | @impl BlueHeron.HCI.Command 34 | def deserialize(<<@opcode::binary, 2, lower_handle, _::4, upper_handle::4>>) do 35 | <> = <> 36 | 37 | %__MODULE__{ 38 | opcode: @opcode, 39 | connection_handle: handle 40 | } 41 | end 42 | 43 | @impl BlueHeron.HCI.Command 44 | def deserialize_return_parameters(<>) do 45 | <> = <> 46 | %{status: status, connection_handle: handle} 47 | end 48 | 49 | @impl BlueHeron.HCI.Command 50 | def serialize_return_parameters(%{status: status, connection_handle: handle}) do 51 | <> 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/blue_heron/att/responses/find_by_type_value_response.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.ATT.FindByTypeValueResponse do 7 | @moduledoc """ 8 | > The ATT_FIND_BY_TYPE_VALUE_RSP PDU is sent in reply to a received 9 | > ATT_FIND_BY_TYPE_VALUE_REQ PDU and contains information about this server. 10 | 11 | Bluetooth Spec v5.2, vol 3, Part F, 3.4.3.4 12 | """ 13 | 14 | defstruct [:opcode, :handles_information_list] 15 | 16 | defmodule HandlesInformation do 17 | @moduledoc """ 18 | Strucutured HandleInformation encoder/decoder. 19 | """ 20 | 21 | defstruct [:found_attribute_handle, :group_end_handle] 22 | 23 | def serialize(%{ 24 | found_attribute_handle: found_attribute_handle, 25 | group_end_handle: group_end_handle 26 | }) do 27 | <> 28 | end 29 | 30 | def deserialize(<>) do 31 | %__MODULE__{ 32 | found_attribute_handle: found_attribute_handle, 33 | group_end_handle: group_end_handle 34 | } 35 | end 36 | end 37 | 38 | def serialize(%{handles_information_list: handles_information_list}) do 39 | handles_information_list = 40 | handles_information_list 41 | |> Enum.map(fn handles_info -> HandlesInformation.serialize(handles_info) end) 42 | |> IO.iodata_to_binary() 43 | 44 | <<0x07, handles_information_list::binary>> 45 | end 46 | 47 | def deserialize(<<0x07, handles_information_list::binary>>) do 48 | handles_information_list = 49 | for <> do 50 | HandlesInformation.deserialize(handles_info) 51 | end 52 | 53 | %__MODULE__{opcode: 0x07, handles_information_list: handles_information_list} 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/events/le_meta/long_term_key_request.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Markus Hutzler 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Event.LEMeta.LongTermKeyRequest do 7 | use BlueHeron.HCI.Event.LEMeta, subevent_code: 0x05 8 | 9 | @moduledoc """ 10 | > The HCI_LE_Long_Term_Key_Request event indicates that the peer device, in 11 | > the Central role, is attempting to encrypt or re-encrypt the link and is requesting 12 | > the Long Term Key from the Host. (See [Vol 6] Part B, Section 5.1.3). 13 | 14 | Reference: Version 5.3, Vol 4, Part E, 7.7.65.5 15 | """ 16 | 17 | defparameters [ 18 | :subevent_code, 19 | :connection_handle, 20 | :random_number, 21 | :encrypted_diversifier 22 | ] 23 | 24 | defimpl BlueHeron.HCI.Serializable do 25 | def serialize(data) do 26 | <> = <> 27 | handle = <> 28 | 29 | bin = << 30 | data.subevent_code, 31 | data.status, 32 | handle::binary, 33 | data.random_number::little-64, 34 | data.encrypted_diversifier::little-16 35 | >> 36 | 37 | size = byte_size(bin) 38 | 39 | <> 40 | end 41 | end 42 | 43 | @impl BlueHeron.HCI.Event 44 | def deserialize(<<@code, _size, @subevent_code, bin::binary>>) do 45 | << 46 | lower_handle, 47 | _::4, 48 | upper_handle::4, 49 | random_number::little-64, 50 | encrypted_diversifier::little-16 51 | >> = bin 52 | 53 | <> = <> 54 | 55 | %__MODULE__{ 56 | subevent_code: @subevent_code, 57 | connection_handle: handle, 58 | random_number: random_number, 59 | encrypted_diversifier: encrypted_diversifier 60 | } 61 | end 62 | 63 | def deserialize(bin), do: {:error, bin} 64 | end 65 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/le_controller/set_scan_enable.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.LEController.SetScanEnable do 7 | use BlueHeron.HCI.Command.LEController, ocf: 0x000C 8 | 9 | @moduledoc """ 10 | > The HCI_LE_Set_Scan_Enable command is used to start and stop scanning for legacy 11 | > PDUs (but not extended PDUs, even if the device supports extended advertising). 12 | > Scanning is used to discover advertising devices nearby. 13 | 14 | Bluetooth Core Version 5.2 | Vol 4, Part E, section 7.8.11 15 | 16 | * OGF: `#{inspect(@ogf, base: :hex)}` 17 | * OCF: `#{inspect(@ocf, base: :hex)}` 18 | * Opcode: `#{inspect(@opcode)}` 19 | """ 20 | 21 | defparameters le_scan_enable: false, 22 | filter_duplicates: false 23 | 24 | defimpl BlueHeron.HCI.Serializable do 25 | def serialize(cc) do 26 | fields = << 27 | as_uint8(cc.le_scan_enable), 28 | as_uint8(cc.filter_duplicates) 29 | >> 30 | 31 | fields_size = byte_size(fields) 32 | 33 | <> 34 | end 35 | 36 | defp as_uint8(true), do: 1 37 | defp as_uint8(false), do: 0 38 | end 39 | 40 | @impl BlueHeron.HCI.Command 41 | def deserialize(<<@opcode::binary, _fields_size, le_scan_enable, filter_duplicates>>) do 42 | {:ok, 43 | %__MODULE__{ 44 | le_scan_enable: as_boolean(le_scan_enable), 45 | filter_duplicates: as_boolean(filter_duplicates) 46 | }} 47 | end 48 | 49 | @impl BlueHeron.HCI.Command 50 | def deserialize_return_parameters(<>) do 51 | %{status: status} 52 | end 53 | 54 | @impl BlueHeron.HCI.Command 55 | def serialize_return_parameters(%{status: status}) do 56 | <> 57 | end 58 | 59 | defp as_boolean(val) when val in [1, "1", true, <<1>>], do: true 60 | defp as_boolean(_), do: false 61 | end 62 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/le_controller/long_term_key_request_reply.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Markus Hutzler 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.LEController.LongTermKeyRequestReply do 7 | use BlueHeron.HCI.Command.LEController, ocf: 0x001A 8 | 9 | @moduledoc """ 10 | > The HCI_LE_Long_Term_Key_Request_Reply command is used to reply to an 11 | > HCI_LE_Long_Term_Key_Request event from the Controller, and specifies the 12 | > Long_Term_Key parameter that shall be used for this Connection_Handle. 13 | > This command shall only be used when the local device’s role is Peripheral. 14 | 15 | Bluetooth Core Version 5.2 | Vol 4, Part E, section 7.8.25 16 | 17 | * OGF: `#{inspect(@ogf, base: :hex)}` 18 | * OCF: `#{inspect(@ocf, base: :hex)}` 19 | * Opcode: `#{inspect(@opcode)}` 20 | """ 21 | 22 | defparameters [ 23 | :connection_handle, 24 | :ltk 25 | ] 26 | 27 | defimpl BlueHeron.HCI.Serializable do 28 | def serialize(%{opcode: opcode, connection_handle: handle, ltk: ltk}) do 29 | bin = <> <> ltk 30 | <> 31 | end 32 | end 33 | 34 | @impl BlueHeron.HCI.Command 35 | def deserialize(<<@opcode::binary, 18, lower_handle, _::4, upper_handle::4, ltk::binary>>) do 36 | <> = <> 37 | 38 | %__MODULE__{ 39 | opcode: @opcode, 40 | connection_handle: handle, 41 | ltk: ltk 42 | } 43 | end 44 | 45 | @impl BlueHeron.HCI.Command 46 | def deserialize_return_parameters(<>) do 47 | <> = <> 48 | %{status: status, connection_handle: handle} 49 | end 50 | 51 | @impl BlueHeron.HCI.Command 52 | def serialize_return_parameters(%{status: status, connection_handle: handle}) do 53 | <> 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/blue_heron/address_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.AddressTest do 6 | use ExUnit.Case 7 | doctest BlueHeron.Address 8 | alias BlueHeron.Address 9 | 10 | test "from integer" do 11 | address = Address.parse(0xA4C138A0498B) 12 | assert address.integer == 0xA4C138A0498B 13 | assert address.binary == <<0xA4, 0xC1, 0x38, 0xA0, 0x49, 0x8B>> 14 | assert address.string == "A4:C1:38:A0:49:8B" 15 | end 16 | 17 | test "from string" do 18 | address = Address.parse("A4:C1:38:A0:49:8B") 19 | assert address.integer == 0xA4C138A0498B 20 | assert address.binary == <<0xA4, 0xC1, 0x38, 0xA0, 0x49, 0x8B>> 21 | assert address.string == "A4:C1:38:A0:49:8B" 22 | end 23 | 24 | test "from binary" do 25 | address = Address.parse(<<0xA4, 0xC1, 0x38, 0xA0, 0x49, 0x8B>>) 26 | assert address.integer == 0xA4C138A0498B 27 | assert address.binary == <<0xA4, 0xC1, 0x38, 0xA0, 0x49, 0x8B>> 28 | assert address.string == "A4:C1:38:A0:49:8B" 29 | end 30 | 31 | test "to_string" do 32 | address_from_integer = Address.parse(0xA4C138A0498B) 33 | address_from_string = Address.parse("A4:C1:38:A0:49:8B") 34 | address_from_binary = Address.parse(<<0xA4, 0xC1, 0x38, 0xA0, 0x49, 0x8B>>) 35 | 36 | assert to_string(address_from_integer) == "A4:C1:38:A0:49:8B" 37 | assert to_string(address_from_string) == "A4:C1:38:A0:49:8B" 38 | assert to_string(address_from_binary) == "A4:C1:38:A0:49:8B" 39 | end 40 | 41 | test "inspect" do 42 | address_from_integer = Address.parse(0xA4C138A0498B) 43 | address_from_string = Address.parse("A4:C1:38:A0:49:8B") 44 | address_from_binary = Address.parse(<<0xA4, 0xC1, 0x38, 0xA0, 0x49, 0x8B>>) 45 | 46 | inspect(to_string(address_from_integer) == "Address") 47 | inspect(to_string(address_from_string) == "Address") 48 | inspect(to_string(address_from_binary) == "Address") 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/blue_heron/gatt/characteristic.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Jon Carstens 2 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 3 | # SPDX-FileCopyrightText: 2022 Connor Rigby 4 | # SPDX-FileCopyrightText: 2023 Markus Hutzler 5 | # 6 | # SPDX-License-Identifier: Apache-2.0 7 | # 8 | defmodule BlueHeron.GATT.Characteristic do 9 | @moduledoc """ 10 | Struct that represents a GATT characteristic. 11 | """ 12 | 13 | @type id :: term() 14 | 15 | @opaque t() :: %__MODULE__{ 16 | id: id, 17 | type: non_neg_integer(), 18 | properties: non_neg_integer(), 19 | permissions: nil | list(), 20 | descriptor: nil | map(), 21 | handle: any(), 22 | value_handle: any(), 23 | descriptor_handle: any() 24 | } 25 | 26 | defstruct [ 27 | :id, 28 | :type, 29 | :properties, 30 | :permissions, 31 | :descriptor, 32 | :handle, 33 | :value_handle, 34 | :descriptor_handle 35 | ] 36 | 37 | @doc """ 38 | Create a characteristic with fields taken from the map `args`. 39 | 40 | The following fields are required: 41 | - `id`: A user-defined term to identify the characteristic. Must be unique within the device profile. 42 | Can be any Erlang term. 43 | - `type`: The characteristic type UUID. Can be a 2- or 16-byte byte UUID. Integer. 44 | - `properties`: The characteristic property flags. Integer. 45 | 46 | ## Example: 47 | 48 | iex> BlueHeron.GATT.Characteristic.new(%{ 49 | ...> id: :fancy_key, 50 | ...> type: 0x2e0f8e717a7d4690998377626bc6b657, 51 | ...> properties: 0b00000010, 52 | ...> permissions: [:write] 53 | ...> }) 54 | %BlueHeron.GATT.Characteristic{id: :fancy_key, type: 0x2e0f8e717a7d4690998377626bc6b657, properties: 2, permissions: [:write]} 55 | """ 56 | @spec new(args :: map()) :: t() 57 | def new(args) do 58 | args = Map.take(args, [:id, :type, :properties, :permissions, :descriptor]) 59 | struct!(__MODULE__, args) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/informational_parameters/read_buffer_size.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.HCI.Command.InformationalParameters.ReadBufferSize do 6 | use BlueHeron.HCI.Command.InformationalParameters, ocf: 0x0005 7 | 8 | @moduledoc """ 9 | > The HCI_Read_Buffer_Size command is used to read the maximum size of the data 10 | > portion of HCI ACL and Synchronous Data packets sent from the Host to the Controller. 11 | 12 | * OGF: `#{inspect(@ogf, base: :hex)}` 13 | * OCF: `#{inspect(@ocf, base: :hex)}` 14 | * Opcode: `#{inspect(@opcode)}` 15 | 16 | Bluetooth Spec v5.3, Vol 4, Part E, section 7.4.5 17 | """ 18 | 19 | defparameters [] 20 | 21 | defimpl BlueHeron.HCI.Serializable do 22 | def serialize(payload) do 23 | <> 24 | end 25 | end 26 | 27 | @impl BlueHeron.HCI.Command 28 | def deserialize(<<@opcode::binary, 0>>) do 29 | %__MODULE__{} 30 | end 31 | 32 | @impl BlueHeron.HCI.Command 33 | def deserialize_return_parameters( 34 | <> 36 | ) do 37 | %{ 38 | status: status, 39 | acl_packet_length: acl_packet_length, 40 | syn_packet_length: syn_packet_length, 41 | acl_packet_number: acl_packet_number, 42 | syn_packet_number: syn_packet_number 43 | } 44 | end 45 | 46 | @impl BlueHeron.HCI.Command 47 | def serialize_return_parameters(%{ 48 | status: status, 49 | acl_packet_length: acl_packet_length, 50 | syn_packet_length: syn_packet_length, 51 | acl_packet_number: acl_packet_number, 52 | syn_packet_number: syn_packet_number 53 | }) do 54 | <> <> 55 | <> <> 56 | <> <> 57 | <> <> 58 | <> 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/le_controller/read_buffer_size_v1.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.LEController.ReadBufferSizeV1 do 7 | use BlueHeron.HCI.Command.LEController, ocf: 0x0002 8 | 9 | @moduledoc """ 10 | > This command is used to read the maximum size of the data portion of ACL data 11 | > packets and isochronous data packets sent from the Host to the Controller. The Host 12 | > shall fragment the data transmitted to the Controller according to these values so that 13 | > the HCI ACL Data packets and HCI ISO Data packets will contain data up to this size 14 | > (“data” includes optional fields in the HCI ISO Data packet, such as ISO_SDU_Length). 15 | 16 | 17 | Bluetooth Core Version 5.2 | Vol 4, Part E, section 7.8.2 18 | 19 | * OGF: `#{inspect(@ogf, base: :hex)}` 20 | * OCF: `#{inspect(@ocf, base: :hex)}` 21 | * Opcode: `#{inspect(@opcode)}` 22 | """ 23 | 24 | defparameters [] 25 | 26 | defimpl BlueHeron.HCI.Serializable do 27 | def serialize(%{opcode: opcode}) do 28 | <> 29 | end 30 | end 31 | 32 | @impl BlueHeron.HCI.Command 33 | def deserialize(<<@opcode::binary, 0x00>>) do 34 | new() 35 | end 36 | 37 | @impl BlueHeron.HCI.Command 38 | def deserialize_return_parameters( 39 | <> 40 | ) do 41 | %{ 42 | status: status, 43 | acl_data_packet_length: acl_data_packet_length, 44 | total_num_acl_data_packets: total_num_acl_data_packets 45 | } 46 | end 47 | 48 | @impl BlueHeron.HCI.Command 49 | def serialize_return_parameters(%{ 50 | status: status, 51 | acl_data_packet_length: acl_data_packet_length, 52 | total_num_acl_data_packets: total_num_acl_data_packets 53 | }) do 54 | << 55 | BlueHeron.ErrorCode.to_code!(status), 56 | acl_data_packet_length::little-16, 57 | total_num_acl_data_packets 58 | >> 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/blue_heron/data_type/manufacturer_data.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Very 2 | # 3 | # SPDX-License-Identifier: MIT 4 | # 5 | defmodule BlueHeron.DataType.ManufacturerData do 6 | @moduledoc """ 7 | > The Manufacturer Specific data type is used for manufacturer specific data. 8 | 9 | Reference: Core Specification Supplement, Part A, section 1.4.1 10 | 11 | Modules under the `BlueHeron.ManufacturerData` scope should implement the 12 | `BlueHeron.ManufacturerDataBehaviour` and `BlueHeron.Serializable` behaviours. 13 | """ 14 | 15 | alias BlueHeron.DataType.ManufacturerData.Apple 16 | require BlueHeron.AssignedNumbers.CompanyIdentifiers, as: CompanyIdentifiers 17 | 18 | @modules [Apple] 19 | 20 | @doc """ 21 | Returns a list of implementation modules. 22 | """ 23 | def modules, do: @modules 24 | 25 | @doc """ 26 | Serializes manufacturer data. 27 | """ 28 | def serialize(data) 29 | 30 | Enum.each(@modules, fn 31 | module -> 32 | def serialize({unquote(module.company()), data}) do 33 | data 34 | |> unquote(module).serialize() 35 | |> case do 36 | {:ok, bin} -> 37 | {:ok, <>} 38 | 39 | :error -> 40 | error = %{ 41 | remaining: data, 42 | serialized: <> 43 | } 44 | 45 | {:error, error} 46 | end 47 | end 48 | end) 49 | 50 | def serialize({:error, _} = ret), do: ret 51 | 52 | def serialize(ret), do: {:error, ret} 53 | 54 | @doc """ 55 | Deserialize a manufacturer data binary. 56 | """ 57 | def deserialize(binary) 58 | 59 | Enum.each(@modules, fn 60 | module -> 61 | def deserialize( 62 | <> = bin 63 | ) do 64 | case unquote(module).deserialize(sub_bin) do 65 | {:ok, data} -> {:ok, {unquote(module).company, data}} 66 | {:error, _} -> {:error, bin} 67 | end 68 | end 69 | end) 70 | 71 | def deserialize(bin), do: {:error, bin} 72 | end 73 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/event.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2023 Markus Hutzler 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Event do 7 | @moduledoc """ 8 | Handles parsing of HCI Events (opcode 0x04). 9 | 10 | new event decoders should `use` this module, and be added to the 11 | `@modules` attribute. 12 | """ 13 | 14 | @callback deserialize(binary()) :: struct() 15 | 16 | alias BlueHeron.HCI.Event 17 | 18 | @modules [ 19 | Event.CommandComplete, 20 | Event.CommandStatus, 21 | Event.DisconnectionComplete, 22 | Event.EncryptionChange, 23 | Event.InquiryComplete, 24 | Event.LEMeta.AdvertisingReport, 25 | Event.LEMeta.ConnectionComplete, 26 | Event.LEMeta.ConnectionUpdateComplete, 27 | Event.LEMeta.EnhancedConnectionCompleteV1, 28 | Event.LEMeta.LongTermKeyRequest, 29 | Event.NumberOfCompletedPackets 30 | ] 31 | 32 | @doc "returns the list of parsable modules" 33 | def __modules__(), do: @modules 34 | 35 | @doc "returns the opcode for HCI Events" 36 | def __indicator__(), do: 0x04 37 | 38 | defmacro defparameters(fields) do 39 | quote location: :keep, bind_quoted: [fields: fields] do 40 | fields = 41 | if Keyword.keyword?(fields) do 42 | fields 43 | else 44 | for key <- fields, do: {key, nil} 45 | end 46 | 47 | # This is odd, but defparameters/1 is only intended to be used 48 | # in modules with BlueHeron.HCI.Event.__using__/1 macro which will 49 | # @code defined. If not, let it fail 50 | fields = Keyword.merge(fields, code: @code) 51 | defstruct fields 52 | end 53 | end 54 | 55 | defmacro __using__(opts) do 56 | quote location: :keep, bind_quoted: [opts: opts] do 57 | code = 58 | Keyword.get_lazy(opts, :code, fn -> 59 | raise ":code key required when defining BlueHeron.HCI.Event.__using__/1" 60 | end) 61 | 62 | @behaviour BlueHeron.HCI.Event 63 | 64 | import BlueHeron.HCI.Event, only: [defparameters: 1] 65 | 66 | @code code 67 | 68 | def __code__(), do: @code 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/le_controller/set_scan_parameters.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.LEController.SetScanParameters do 7 | use BlueHeron.HCI.Command.LEController, ocf: 0x000B 8 | 9 | @moduledoc """ 10 | > The HCI_LE_Set_Scan_Parameters command is used to set the scan parameters. 11 | 12 | Bluetooth Core Version 5.2 | Vol 4, Part E, section 7.8.10 13 | 14 | * OGF: `#{inspect(@ogf, base: :hex)}` 15 | * OCF: `#{inspect(@ocf, base: :hex)}` 16 | * Opcode: `#{inspect(@opcode)}` 17 | """ 18 | 19 | defparameters le_scan_type: 0x00, 20 | le_scan_interval: 0x0010, 21 | le_scan_window: 0x0010, 22 | own_address_type: 0x00, 23 | scanning_filter_policy: 0x00 24 | 25 | defimpl BlueHeron.HCI.Serializable do 26 | def serialize(%{ 27 | opcode: opcode, 28 | le_scan_type: le_scan_type, 29 | le_scan_interval: le_scan_interval, 30 | le_scan_window: le_scan_window, 31 | own_address_type: own_address_type, 32 | scanning_filter_policy: scanning_filter_policy 33 | }) do 34 | <> 36 | end 37 | end 38 | 39 | @impl BlueHeron.HCI.Command 40 | def deserialize( 41 | <<@opcode, 0x07, le_scan_type, le_scan_interval::little-16, le_scan_window::little-16, 42 | own_address_type, scanning_filter_policy>> 43 | ) do 44 | new( 45 | le_scan_type: le_scan_type, 46 | le_scan_interval: le_scan_interval, 47 | le_scan_window: le_scan_window, 48 | own_address_type: own_address_type, 49 | scanning_filter_policy: scanning_filter_policy 50 | ) 51 | end 52 | 53 | @impl BlueHeron.HCI.Command 54 | def deserialize_return_parameters(<>) do 55 | %{status: status} 56 | end 57 | 58 | @impl BlueHeron.HCI.Command 59 | def serialize_return_parameters(%{status: status}) do 60 | <> 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/blue_heron/hci/command/le_controller/set_advertising_parameters_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.HCI.Command.LEController.SetAdvertisingParametersTest do 6 | use ExUnit.Case 7 | 8 | alias BlueHeron.HCI.Command.LEController.SetAdvertisingParameters 9 | 10 | test "encodes parameters correctly" do 11 | serialized = 12 | %SetAdvertisingParameters{ 13 | advertising_interval_min: 0x0801, 14 | advertising_interval_max: 0x0802, 15 | advertising_type: 0x00, 16 | own_address_type: 0x00, 17 | peer_address_type: 0x00, 18 | peer_address: 0xAABBCCDDEEFF, 19 | advertising_channel_map: 0x07, 20 | advertising_filter_policy: 0x00 21 | } 22 | |> BlueHeron.HCI.Serializable.serialize() 23 | 24 | assert <<0x06, 0x20, 15, 0x0801::little-16, 0x0802::little-16, 0x00, 0x00, 0x00, 0xFF, 0xEE, 25 | 0xDD, 0xCC, 0xBB, 0xAA, 0x07, 0x00>> == serialized 26 | end 27 | 28 | test "serde is symmetric" do 29 | advertising_interval_min = Enum.random(0x0020..0x4000) 30 | advertising_interval_max = Enum.random(advertising_interval_min..0x4000) 31 | advertising_type = Enum.random(0x00..0x04) 32 | own_address_type = Enum.random(0x00..0x03) 33 | peer_address_type = Enum.random(0x00..0x01) 34 | peer_address = Enum.random(0x00..0xFFFFFFFFFFFF) 35 | advertising_channel_map = Enum.random(0b000..0b111) 36 | advertising_filter_policy = Enum.random(0x00..0x03) 37 | 38 | expected = %SetAdvertisingParameters{ 39 | advertising_interval_min: advertising_interval_min, 40 | advertising_interval_max: advertising_interval_max, 41 | advertising_type: advertising_type, 42 | own_address_type: own_address_type, 43 | peer_address_type: peer_address_type, 44 | peer_address: peer_address, 45 | advertising_channel_map: advertising_channel_map, 46 | advertising_filter_policy: advertising_filter_policy 47 | } 48 | 49 | assert expected == 50 | expected 51 | |> BlueHeron.HCI.Serializable.serialize() 52 | |> SetAdvertisingParameters.deserialize() 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule BlueHeron.MixProject do 2 | use Mix.Project 3 | 4 | @version "0.5.4" 5 | @source_url "https://github.com/blue-heron/blue_heron" 6 | 7 | def project do 8 | [ 9 | app: :blue_heron, 10 | version: @version, 11 | elixir: "~> 1.17", 12 | start_permanent: Mix.env() == :prod, 13 | deps: deps(), 14 | description: description(), 15 | dialyzer: dialyzer(), 16 | docs: docs(), 17 | package: package(), 18 | preferred_cli_env: [ 19 | credo: :test, 20 | docs: :docs, 21 | "hex.build": :docs, 22 | "hex.publish": :docs 23 | ] 24 | ] 25 | end 26 | 27 | def application() do 28 | [ 29 | extra_applications: [:logger, :crypto], 30 | mod: {BlueHeron.Application, []} 31 | ] 32 | end 33 | 34 | defp deps() do 35 | [ 36 | {:circuits_uart, "~> 1.5"}, 37 | {:property_table, "~> 0.3.0 or ~> 0.2.6"}, 38 | {:ex_doc, "~> 0.35", only: :docs, runtime: false}, 39 | {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, 40 | {:credo, "~> 1.7", only: :test, runtime: false} 41 | ] 42 | end 43 | 44 | defp description() do 45 | "Use Bluetooth LE in Elixir" 46 | end 47 | 48 | defp dialyzer() do 49 | [ 50 | flags: [:unmatched_returns, :error_handling, :underspecs], 51 | plt_add_apps: [:mix] 52 | ] 53 | end 54 | 55 | defp docs() do 56 | [ 57 | extras: [ 58 | "README.md", 59 | "CHANGELOG.md", 60 | NOTICE: [title: "Notice"] 61 | ], 62 | main: "readme", 63 | source_ref: "v#{@version}", 64 | source_url: @source_url, 65 | skip_undefined_reference_warnings_on: ["CHANGELOG.md"] 66 | ] 67 | end 68 | 69 | defp package() do 70 | [ 71 | files: [ 72 | "CHANGELOG.md", 73 | "lib", 74 | "LICENSES/*", 75 | "mix.exs", 76 | "NOTICE", 77 | "README.md", 78 | "REUSE.toml", 79 | "test" 80 | ], 81 | licenses: ["Apache-2.0"], 82 | links: %{ 83 | "GitHub" => @source_url, 84 | "REUSE Compliance" => "https://api.reuse.software/info/github.com/blue-heron/blue_heron" 85 | } 86 | ] 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/events/disconnection_complete.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # SPDX-FileCopyrightText: 2022 Troels Brødsgaard 4 | # 5 | # SPDX-License-Identifier: Apache-2.0 6 | # 7 | defmodule BlueHeron.HCI.Event.DisconnectionComplete do 8 | use BlueHeron.HCI.Event, code: 0x05 9 | 10 | @moduledoc """ 11 | The HCI_Disconnection_Complete event occurs when a connection is terminated. 12 | 13 | The status parameter indicates if the disconnection was successful or not. The 14 | reason parameter indicates the reason for the disconnection if the 15 | disconnection was successful. If the disconnection was not successful, the 16 | value of the reason parameter shall be ignored by the Host. For example, this 17 | can be the case if the Host has issued the HCI_Disconnect command and there 18 | was a parameter error, or the command was not presently allowed, or a 19 | Connection_Handle that didn’t correspond to a connection was given. 20 | 21 | Note: When a physical link fails, one HCI_Disconnection_Complete event will be 22 | returned for each logical channel on the physical link with the corresponding 23 | Connection_Handle as a parameter. 24 | 25 | Reference: Version 5.2, Vol 4, Part E, 7.7.5 26 | """ 27 | 28 | defparameters [ 29 | :connection_handle, 30 | :reason, 31 | :reason_name, 32 | :status 33 | ] 34 | 35 | defimpl BlueHeron.HCI.Serializable do 36 | def serialize(dc) do 37 | <> = <> 38 | connection_handle = <> 39 | 40 | bin = << 41 | dc.status, 42 | connection_handle::binary, 43 | dc.reason 44 | >> 45 | 46 | size = byte_size(bin) 47 | 48 | <> 49 | end 50 | end 51 | 52 | @impl BlueHeron.HCI.Event 53 | def deserialize(<<@code, _size, status, lower_handle, _::4, upper_handle::4, reason>>) do 54 | <> = <> 55 | 56 | %__MODULE__{ 57 | connection_handle: connection_handle, 58 | reason: reason, 59 | status: status 60 | } 61 | end 62 | 63 | def deserialize(bin), do: {:error, bin} 64 | end 65 | -------------------------------------------------------------------------------- /lib/blue_heron/acl_buffer.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.ACLBuffer do 6 | @moduledoc """ 7 | simple fifo buffer implementation that handles sending ACL packets synchronously without blocking 8 | """ 9 | use GenServer 10 | require Logger 11 | 12 | alias BlueHeron.HCI.Event.NumberOfCompletedPackets 13 | 14 | @doc "queue up a message for output" 15 | def buffer(acl) do 16 | GenServer.cast(__MODULE__, {:buffer, acl}) 17 | end 18 | 19 | def start_link(args) do 20 | GenServer.start_link(__MODULE__, args, name: __MODULE__) 21 | end 22 | 23 | @impl GenServer 24 | def init(_args) do 25 | :ok = BlueHeron.Registry.subscribe() 26 | {:ok, %{acls: :queue.new()}} 27 | end 28 | 29 | @impl GenServer 30 | def handle_cast({:buffer, acl}, state) do 31 | new_state = %{state | acls: :queue.in(acl, state.acls)} 32 | 33 | case :queue.out(state.acls) do 34 | {:empty, _do_not_use_this} -> 35 | # queue is empty, so send now 36 | send(self(), :out) 37 | {:noreply, new_state} 38 | 39 | {{:value, _acl_do_not_use}, _do_not_use_this} -> 40 | # Logger.warning(%{buffering_acl_message: inspect(acl, base: :hex)}) 41 | # there are already items in the queue, so don't send yet 42 | {:noreply, new_state} 43 | end 44 | end 45 | 46 | @impl GenServer 47 | def handle_info(:out, %{acls: acls} = state) do 48 | case :queue.out(acls) do 49 | {{:value, acl}, acls} -> 50 | :ok = BlueHeron.HCI.Transport.send_acl(acl) 51 | {:noreply, %{state | acls: acls}} 52 | 53 | {:empty, acls} -> 54 | {:noreply, %{state | acls: acls}} 55 | end 56 | end 57 | 58 | def handle_info({:BLUETOOTH_EVENT_STATE, :HCI_STATE_WORKING}, state) do 59 | {:noreply, state} 60 | end 61 | 62 | def handle_info({:HCI_EVENT_PACKET, %NumberOfCompletedPackets{} = _event}, state) do 63 | send(self(), :out) 64 | {:noreply, state} 65 | end 66 | 67 | def handle_info({:HCI_EVENT_PACKET, _}, state) do 68 | {:noreply, state} 69 | end 70 | 71 | def handle_info({:HCI_ACL_DATA_PACKET, _}, state) do 72 | {:noreply, state} 73 | end 74 | 75 | def handle_info(_, state) do 76 | {:noreply, state} 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/transport/uart/framing.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.HCI.Transport.UART.Framing do 6 | @moduledoc """ 7 | A framer module that defines a frame as a HCI packet. 8 | 9 | Reference: Version 5.0, Vol 2, Part E, 5.4 10 | """ 11 | 12 | alias Circuits.UART.Framing 13 | 14 | defmodule State do 15 | @moduledoc false 16 | 17 | defstruct frame: <<>>, type: nil, frames: [] 18 | end 19 | 20 | @behaviour Framing 21 | 22 | @impl Framing 23 | def init(_args), do: {:ok, %State{}} 24 | 25 | @impl Framing 26 | def add_framing(data, state), do: {:ok, data, state} 27 | 28 | @impl Framing 29 | def flush(:transmit, state), do: state 30 | 31 | def flush(:receive, _state), do: %State{} 32 | 33 | def flush(:both, _state), do: %State{} 34 | 35 | @impl Framing 36 | def frame_timeout(state), do: {:ok, [state], <<>>} 37 | 38 | @impl Framing 39 | def remove_framing(new_data, state) do 40 | process(state.frame <> new_data, %{state | frame: <<>>}) 41 | end 42 | 43 | def process(<<0x2, rest::binary>>, %{type: nil} = state) do 44 | process(rest, %{state | type: 0x2}) 45 | end 46 | 47 | def process(<<0x4, rest::binary>>, %{type: nil} = state) do 48 | process(rest, %{state | type: 0x4}) 49 | end 50 | 51 | def process( 52 | <>, 54 | %{type: 0x2} = state 55 | ) do 56 | frame = <<0x2, handle::little-12, flags::4, length::little-16, data::binary-size(length)>> 57 | process(rest, %{state | type: nil, frames: [frame | state.frames]}) 58 | end 59 | 60 | def process( 61 | <>, 63 | %{type: 0x4} = state 64 | ) do 65 | frame = 66 | <<0x4, event_code::size(8), parameter_total_length::size(8), 67 | event_parameters::binary-size(parameter_total_length)>> 68 | 69 | process(rest, %{state | type: nil, frames: [frame | state.frames]}) 70 | end 71 | 72 | def process(<<>>, state) do 73 | {:ok, Enum.reverse(state.frames), %{state | frames: []}} 74 | end 75 | 76 | def process(data, state), do: {:in_frame, [], %{state | frame: data}} 77 | end 78 | -------------------------------------------------------------------------------- /lib/blue_heron/att/responses/read_by_group_type_response.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.ATT.ReadByGroupTypeResponse do 7 | @moduledoc """ 8 | > The ATT_READ_BY_GROUP_TYPE_RSP PDU is sent in reply to a received 9 | > ATT_READ_BY_GROUP_TYPE_REQ PDU and contains the handles and values of the 10 | > attributes that have been read 11 | 12 | Bluetooth Spec v5.2, vol 3, Part F, 3.4.4.10 13 | """ 14 | 15 | defstruct [:opcode, :attribute_data] 16 | 17 | defmodule AttributeData do 18 | @moduledoc """ 19 | Strucutured AttributeData encoder/decoder. 20 | """ 21 | defstruct [:handle, :end_group_handle, :uuid] 22 | 23 | def deserialize(<>) do 24 | %__MODULE__{handle: handle, end_group_handle: end_group_handle, uuid: uuid} 25 | end 26 | 27 | def deserialize(<>) do 28 | %__MODULE__{handle: handle, end_group_handle: end_group_handle, uuid: uuid} 29 | end 30 | 31 | def serialize(%{handle: handle, end_group_handle: end_group_handle, uuid: uuid}) 32 | when uuid > 65535 do 33 | <> 34 | end 35 | 36 | def serialize(%{handle: handle, end_group_handle: end_group_handle, uuid: uuid}) do 37 | <> 38 | end 39 | end 40 | 41 | def deserialize(<<0x11, length, attribute_data::binary>>) do 42 | %__MODULE__{ 43 | opcode: 0x11, 44 | attribute_data: deserialize_attribute_data(length, attribute_data, []) 45 | } 46 | end 47 | 48 | def serialize(%{attribute_data: attribute_data}) do 49 | [single | _] = attribute_data = for attr <- attribute_data, do: AttributeData.serialize(attr) 50 | length = byte_size(single) 51 | <<0x11, length>> <> Enum.join(attribute_data) 52 | end 53 | 54 | defp deserialize_attribute_data(_, <<>>, attribute_data), do: Enum.reverse(attribute_data) 55 | 56 | defp deserialize_attribute_data(item_length, data, acc) do 57 | <> = data 58 | attribute_data = AttributeData.deserialize(attribute_data) 59 | deserialize_attribute_data(item_length, rest, [attribute_data | acc]) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/blue_heron/att/responses/find_information_response.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.ATT.FindInformationResponse do 7 | @moduledoc """ 8 | > The ATT_FIND_INFORMATION_RSP PDU is sent in reply to a received 9 | > ATT_FIND_INFORMATION_REQ PDU and contains information about this server. 10 | 11 | Bluetooth Spec v5.2, vol 3, Part F, 3.4.3.2 12 | """ 13 | 14 | defstruct [:opcode, :format, :information_data] 15 | 16 | defmodule InformationData do 17 | @moduledoc """ 18 | Strucutured InformationData encoder/decoder. 19 | """ 20 | 21 | defstruct [:handle, :uuid] 22 | 23 | def serialize(%{handle: handle, uuid: uuid}) when uuid > 65535 do 24 | <> 25 | end 26 | 27 | def serialize(%{handle: handle, uuid: uuid}) do 28 | <> 29 | end 30 | 31 | def deserialize(<>) do 32 | %__MODULE__{handle: handle, uuid: uuid} 33 | end 34 | 35 | def deserialize(<>) do 36 | %__MODULE__{handle: handle, uuid: uuid} 37 | end 38 | end 39 | 40 | def serialize(%{format: format, information_data: information_data}) do 41 | information_data = 42 | for data <- information_data, into: <<>> do 43 | InformationData.serialize(data) 44 | end 45 | 46 | <<0x05, format, information_data::binary>> 47 | end 48 | 49 | def deserialize(<<0x05, format, information_data::binary>>) do 50 | pair_size = 51 | case format do 52 | # 0x01 means pairs of 2 byte UUIDs 53 | 0x01 -> 4 54 | # 0x02 means pairs of 16 byte UUIDs 55 | 0x02 -> 18 56 | end 57 | 58 | information_data = chunk_information_data(pair_size, information_data) 59 | 60 | %__MODULE__{ 61 | opcode: 0x05, 62 | format: format, 63 | information_data: information_data 64 | } 65 | end 66 | 67 | defp chunk_information_data(size, data, acc \\ []) 68 | 69 | defp chunk_information_data(_size, <<>>, acc) do 70 | Enum.reverse(acc) 71 | end 72 | 73 | defp chunk_information_data(pair_size, data, acc) do 74 | <> = data 75 | acc = [InformationData.deserialize(chunk) | acc] 76 | chunk_information_data(pair_size, rest, acc) 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/events/le_meta/connection_complete.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # SPDX-FileCopyrightText: 2022 Troels Brødsgaard 4 | # 5 | # SPDX-License-Identifier: Apache-2.0 6 | # 7 | defmodule BlueHeron.HCI.Event.LEMeta.ConnectionComplete do 8 | use BlueHeron.HCI.Event.LEMeta, subevent_code: 0x01 9 | 10 | @moduledoc """ 11 | > The HCI_LE_Connection_Complete event indicates to both of the Hosts forming 12 | > the connection that a new connection has been created. 13 | 14 | Reference: Version 5.2, Vol 4, Part E, 7.7.65.1 15 | """ 16 | 17 | defparameters [ 18 | :status, 19 | :connection_handle, 20 | :role, 21 | :peer_address_type, 22 | :peer_address, 23 | :connection_interval, 24 | :connection_latency, 25 | :supervision_timeout, 26 | :master_clock_accuracy, 27 | :subevent_code 28 | ] 29 | 30 | defimpl BlueHeron.HCI.Serializable do 31 | def serialize(cc) do 32 | <> = <> 33 | connection_handle = <> 34 | 35 | bin = << 36 | cc.subevent_code, 37 | cc.status, 38 | connection_handle::binary, 39 | cc.role, 40 | cc.peer_address_type, 41 | cc.peer_address::little-48, 42 | cc.connection_interval::little-16, 43 | cc.connection_latency::little-16, 44 | cc.supervision_timeout::little-16, 45 | cc.master_clock_accuracy 46 | >> 47 | 48 | size = byte_size(bin) 49 | 50 | <> 51 | end 52 | end 53 | 54 | @impl BlueHeron.HCI.Event 55 | def deserialize(<<@code, _size, @subevent_code, bin::binary>>) do 56 | << 57 | status, 58 | connection_handle::little-12, 59 | 0x00::4, 60 | role, 61 | peer_address_type, 62 | peer_address::little-48, 63 | connection_interval::little-16, 64 | connection_latency::little-16, 65 | supervision_timeout::little-16, 66 | master_clock_accuracy 67 | >> = bin 68 | 69 | %__MODULE__{ 70 | subevent_code: @subevent_code, 71 | status: status, 72 | connection_handle: connection_handle, 73 | role: role, 74 | peer_address_type: peer_address_type, 75 | peer_address: peer_address, 76 | connection_interval: connection_interval, 77 | connection_latency: connection_latency, 78 | supervision_timeout: supervision_timeout, 79 | master_clock_accuracy: master_clock_accuracy 80 | } 81 | end 82 | 83 | def deserialize(bin), do: {:error, bin} 84 | end 85 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/events/command_status.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Event.CommandStatus do 7 | use BlueHeron.HCI.Event, code: 0x0F 8 | 9 | @moduledoc """ 10 | The HCI_Command_Status event is used to indicate that the command described by 11 | the Command_Opcode parameter has been received, and that the Controller is 12 | currently performing the task for this command. 13 | 14 | This event is needed to provide mechanisms for asynchronous operation, which 15 | makes it possible to prevent the Host from waiting for a command to finish. If 16 | the command cannot begin to execute (a parameter error may have occurred, or the 17 | command may currently not be allowed), the Status event parameter will contain 18 | the corresponding error code, and no complete event will follow since the 19 | command was not started. The Num_HCI_Command_Packets event parameter allows the 20 | Controller to indicate the number of HCI command packets the Host can send to 21 | the Controller. If the Controller requires the Host to stop sending commands, 22 | the Num_HCI_Command_Packets event parameter will be set to zero. To indicate to 23 | the Host that the Controller is ready to receive HCI command packets, the 24 | Controller generates an HCI_Command_Status event with Status 0x00 and 25 | Command_Opcode 0x0000 and the Num_HCI_Command_Packets event parameter set to 1 26 | or more. Command_Opcode 0x0000 is a special value indicating that this event is 27 | not associated with a command sent by the Host. The Controller can send an 28 | HCI_Command_Status event with Command Opcode 0x0000 at any time to change the 29 | number of outstanding HCI command packets that the Host can send before waiting. 30 | 31 | Reference: Version 5.2, Vol 4, Part E, 7.7.15 32 | """ 33 | 34 | require Logger 35 | 36 | defparameters [ 37 | :num_hci_command_packets, 38 | :opcode, 39 | :status 40 | ] 41 | 42 | defimpl BlueHeron.HCI.Serializable do 43 | def serialize(data) do 44 | bin = <> 45 | size = byte_size(bin) 46 | <> 47 | end 48 | end 49 | 50 | @impl BlueHeron.HCI.Event 51 | def deserialize(<<@code, _size, status, num_hci_command_packets, opcode::binary-2>>) do 52 | %__MODULE__{ 53 | num_hci_command_packets: num_hci_command_packets, 54 | opcode: opcode, 55 | status: status 56 | } 57 | end 58 | 59 | def deserialize(bin), do: {:error, bin} 60 | end 61 | -------------------------------------------------------------------------------- /lib/blue_heron/address.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.Address do 6 | @moduledoc """ 7 | Helper struct for working with BLE Addresses. 8 | """ 9 | 10 | alias BlueHeron.Address 11 | 12 | defstruct string: "00:00:00:00:00:00", 13 | binary: <<00, 00, 00, 00, 00, 00>>, 14 | integer: 0x000000000000 15 | 16 | defimpl Inspect do 17 | def inspect(address, _opts) do 18 | "#Address<#{address.string}>" 19 | end 20 | end 21 | 22 | defimpl String.Chars do 23 | def to_string(address) do 24 | address.string 25 | end 26 | end 27 | 28 | def serialize(address) do 29 | reverse(address.binary) 30 | end 31 | 32 | defp reverse(bin), do: bin |> :binary.decode_unsigned(:little) |> :binary.encode_unsigned(:big) 33 | 34 | @doc """ 35 | Parses an Address from a colon delimited BLE address string 36 | Examples: 37 | 38 | iex> BlueHeron.Address.parse("A4:C1:38:A0:49:8B") 39 | #Address 40 | 41 | iex> BlueHeron.Address.parse(0xA4C138A0498B) 42 | #Address 43 | 44 | iex> BlueHeron.Address.parse(181149785672075) 45 | #Address 46 | 47 | iex> BlueHeron.Address.parse(<<0xA4, 0xC1, 0x38, 0xA0, 0x49, 0x8B>>) 48 | #Address 49 | 50 | iex> BlueHeron.Address.parse(<<164, 193, 56, 160, 73, 139>>) 51 | #Address 52 | 53 | """ 54 | def parse( 55 | <> = string 57 | ) do 58 | addr = String.to_integer(IO.iodata_to_binary([oct1, oct2, oct3, oct4, oct5, oct6]), 16) 59 | 60 | %Address{ 61 | string: string, 62 | binary: <>, 63 | integer: addr 64 | } 65 | end 66 | 67 | def parse(addr) when addr <= 0xFFFFFFFFFFFF do 68 | <> = <> 69 | 70 | string = format_iodata([oct1, oct2, oct3, oct4, oct5, oct6]) 71 | 72 | %Address{ 73 | string: string, 74 | binary: <>, 75 | integer: addr 76 | } 77 | end 78 | 79 | def parse(<> = binary) do 80 | <> = binary 81 | string = format_iodata([oct1, oct2, oct3, oct4, oct5, oct6]) 82 | 83 | %Address{ 84 | string: string, 85 | binary: binary, 86 | integer: addr 87 | } 88 | end 89 | 90 | defp format_iodata(iodata) do 91 | to_string(:io_lib.format(~c"~2.16.0B:~2.16.0B:~2.16.0B:~2.16.0B:~2.16.0B:~2.16.0B", iodata)) 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/le_controller/set_advertising_parameters.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 2 | # SPDX-FileCopyrightText: 2024 Connor Rigby 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.LEController.SetAdvertisingParameters do 7 | use BlueHeron.HCI.Command.LEController, ocf: 0x0006 8 | 9 | @moduledoc """ 10 | > The HCI_LE_Set_Scan_Parameters command is used to set the scan parameters. 11 | 12 | Bluetooth Core Version 5.2 | Vol 4, Part E, section 7.8.10 13 | 14 | * OGF: `#{inspect(@ogf, base: :hex)}` 15 | * OCF: `#{inspect(@ocf, base: :hex)}` 16 | * Opcode: `#{inspect(@opcode)}` 17 | """ 18 | 19 | defparameters advertising_interval_min: 0x0800, 20 | advertising_interval_max: 0x0800, 21 | advertising_type: 0x00, 22 | own_address_type: 0x00, 23 | peer_address_type: 0x00, 24 | peer_address: 0x001122334455, 25 | advertising_channel_map: 0x07, 26 | advertising_filter_policy: 0x00 27 | 28 | defimpl BlueHeron.HCI.Serializable do 29 | def serialize(command) do 30 | << 31 | command.opcode::binary, 32 | 15, 33 | command.advertising_interval_min::little-16, 34 | command.advertising_interval_max::little-16, 35 | command.advertising_type, 36 | command.own_address_type, 37 | command.peer_address_type, 38 | command.peer_address::little-48, 39 | command.advertising_channel_map, 40 | command.advertising_filter_policy 41 | >> 42 | end 43 | end 44 | 45 | @impl BlueHeron.HCI.Command 46 | def deserialize(<< 47 | @opcode::binary, 48 | _fields_size, 49 | advertising_interval_min::little-16, 50 | advertising_interval_max::little-16, 51 | advertising_type, 52 | own_address_type, 53 | peer_address_type, 54 | peer_address::little-48, 55 | advertising_channel_map, 56 | advertising_filter_policy 57 | >>) do 58 | new( 59 | advertising_interval_min: advertising_interval_min, 60 | advertising_interval_max: advertising_interval_max, 61 | advertising_type: advertising_type, 62 | own_address_type: own_address_type, 63 | peer_address_type: peer_address_type, 64 | peer_address: peer_address, 65 | advertising_channel_map: advertising_channel_map, 66 | advertising_filter_policy: advertising_filter_policy 67 | ) 68 | end 69 | 70 | @impl BlueHeron.HCI.Command 71 | def deserialize_return_parameters(<>) do 72 | %{status: status} 73 | end 74 | 75 | @impl BlueHeron.HCI.Command 76 | def serialize_return_parameters(%{status: status}) do 77 | <> 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/controller_and_baseband/host_buffer_size.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.HCI.Command.ControllerAndBaseband.HostBufferSize do 6 | use BlueHeron.HCI.Command.ControllerAndBaseband, ocf: 0x0033 7 | 8 | @moduledoc """ 9 | > The HCI_Host_Buffer_Size command is used by the Host to notify the 10 | > Controller about the maximum size of the data portion of HCI ACL and 11 | > Synchronous Data packets sent from the Controller to the Host. 12 | 13 | * OGF: `#{inspect(@ogf, base: :hex)}` 14 | * OCF: `#{inspect(@ocf, base: :hex)}` 15 | * Opcode: `#{inspect(@opcode)}` 16 | 17 | Bluetooth Spec v5.2, Vol 4, Part E, section 7.3.39 18 | """ 19 | 20 | defparameters [ 21 | :host_acl_data_packet_length, 22 | :host_synchronous_data_packet_length, 23 | :host_total_num_acl_data_packets, 24 | :host_total_num_synchronous_data_packets 25 | ] 26 | 27 | defimpl BlueHeron.HCI.Serializable do 28 | def serialize(%{ 29 | opcode: opcode, 30 | host_acl_data_packet_length: host_acl_data_packet_length, 31 | host_synchronous_data_packet_length: host_synchronous_data_packet_length, 32 | host_total_num_acl_data_packets: host_total_num_acl_data_packets, 33 | host_total_num_synchronous_data_packets: host_total_num_synchronous_data_packets 34 | }) do 35 | <> 38 | end 39 | end 40 | 41 | @impl BlueHeron.HCI.Command 42 | def deserialize( 43 | <<@opcode::binary, host_acl_data_packet_length::little-size(16), 44 | host_synchronous_data_packet_length, host_total_num_acl_data_packets::little-size(16), 45 | host_total_num_synchronous_data_packets::little-size(16)>> 46 | ) do 47 | # This is a pretty useless function because there aren't 48 | # any parameters to actually parse out of this, but we 49 | # can at least assert its correct with matching 50 | %__MODULE__{ 51 | host_acl_data_packet_length: host_acl_data_packet_length, 52 | host_synchronous_data_packet_length: host_synchronous_data_packet_length, 53 | host_total_num_acl_data_packets: host_total_num_acl_data_packets, 54 | host_total_num_synchronous_data_packets: host_total_num_synchronous_data_packets 55 | } 56 | end 57 | 58 | @impl BlueHeron.HCI.Command 59 | def deserialize_return_parameters(<>) do 60 | %{status: status} 61 | end 62 | 63 | @impl true 64 | def serialize_return_parameters(%{status: status}) do 65 | <> 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/transport/uart.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Connor Rigby 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | # 5 | defmodule BlueHeron.HCI.Transport.UART do 6 | @moduledoc """ 7 | > The objective of this HCI UART Transport Layer is to make it possible to use the Bluetooth HCI 8 | > over a serial interface between two UARTs on the same PCB. The HCI UART Transport Layer 9 | > assumes that the UART communication is free from line errors. 10 | 11 | Reference: Version 5.0, Vol 4, Part A, 1 12 | """ 13 | 14 | use GenServer 15 | require Logger 16 | alias Circuits.UART 17 | alias BlueHeron.HCI.Transport.UART.Framing 18 | 19 | @hci_command_packet 0x01 20 | @hci_acl_packet 0x02 21 | 22 | def start_link(args) do 23 | GenServer.start_link(__MODULE__, args) 24 | end 25 | 26 | @doc "Send binary HCI data" 27 | @spec send_command(GenServer.server(), binary()) :: :ok | {:error, term()} 28 | def send_command(pid, command) when is_binary(command) do 29 | GenServer.call(pid, {:send, [<<@hci_command_packet::8>>, command]}) 30 | end 31 | 32 | @doc "Send binary ACL data" 33 | @spec send_acl(GenServer.server(), binary()) :: :ok | {:error, term()} 34 | def send_acl(pid, acl) when is_binary(acl) do 35 | GenServer.call(pid, {:send, [<<@hci_acl_packet::8>>, acl]}) 36 | end 37 | 38 | @doc "Flush buffers" 39 | @spec flush(GenServer.server()) :: :ok 40 | def flush(pid) do 41 | GenServer.call(pid, :flush) 42 | end 43 | 44 | ## Server Callbacks 45 | 46 | @impl GenServer 47 | def init(args) do 48 | uart_opts = Keyword.merge(args, active: true, framing: {Framing, []}) 49 | device = Keyword.get(uart_opts, :device) 50 | {:ok, pid} = UART.start_link() 51 | send(self(), {:open, device, uart_opts}) 52 | state = %{uart_pid: pid} 53 | {:ok, state} 54 | end 55 | 56 | @impl GenServer 57 | def handle_call({:send, command}, _from, %{uart_pid: uart_pid} = state) do 58 | {:reply, UART.write(uart_pid, command), state} 59 | end 60 | 61 | def handle_call(:flush, _from, %{uart_pid: uart_pid} = state) do 62 | {:reply, UART.flush(uart_pid), state} 63 | end 64 | 65 | @impl GenServer 66 | def handle_info({:open, device, opts}, state) when is_binary(device) and is_list(opts) do 67 | case UART.open(state.uart_pid, device, opts) do 68 | :ok -> 69 | Logger.info("Opened UART for HCI transport: #{device} #{inspect(opts)}") 70 | :ok 71 | 72 | error -> 73 | Logger.error("Failed to open UART for HCI transport: #{inspect(error)}") 74 | end 75 | 76 | {:noreply, state} 77 | end 78 | 79 | def handle_info({:open, _, _}, state) do 80 | Logger.error("Failed to open UART for HCI transport: no device configured") 81 | {:noreply, state} 82 | end 83 | 84 | def handle_info({:circuits_uart, _dev, msg}, state) do 85 | _ = BlueHeron.HCI.Transport.transport_data(msg) 86 | {:noreply, state} 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/events/command_complete.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # SPDX-FileCopyrightText: 2021 Jon Carstens 4 | # 5 | # SPDX-License-Identifier: Apache-2.0 6 | # 7 | defmodule BlueHeron.HCI.Event.CommandComplete do 8 | use BlueHeron.HCI.Event, code: 0x0E 9 | 10 | @moduledoc """ 11 | > The Command Complete event is used by the Controller for most commands to 12 | > transmit return status of a command and the other event parameters that are 13 | > specified for the issued HCI command. 14 | 15 | Reference: Version 5.2, Vol 4, Part E, 7.7.14 16 | """ 17 | 18 | require BlueHeron.HCI.CommandComplete.ReturnParameters 19 | require Logger 20 | 21 | defparameters [:num_hci_command_packets, :opcode, :return_parameters] 22 | 23 | defimpl BlueHeron.HCI.Serializable do 24 | def serialize(data) do 25 | data = BlueHeron.HCI.CommandComplete.ReturnParameters.encode(data) 26 | bin = <> 27 | size = byte_size(bin) 28 | <> 29 | end 30 | end 31 | 32 | defimpl BlueHeron.HCI.CommandComplete.ReturnParameters do 33 | def decode(cc) do 34 | %{cc | return_parameters: do_decode(cc.opcode, cc.return_parameters)} 35 | end 36 | 37 | def encode(cc) do 38 | %{cc | return_parameters: do_encode(cc.opcode, cc.return_parameters)} 39 | end 40 | 41 | # Generate return_parameter parsing function for all available command 42 | # modules based on the requirements in BlueHeron.HCI.Command behaviour 43 | for mod <- BlueHeron.HCI.Command.__modules__(), opcode = mod.__opcode__() do 44 | defp do_decode(unquote(opcode), rp_bin) do 45 | unquote(mod).deserialize_return_parameters(rp_bin) 46 | end 47 | 48 | defp do_encode(unquote(opcode), rp_map) do 49 | unquote(mod).serialize_return_parameters(rp_map) 50 | end 51 | end 52 | 53 | defp do_encode(_unknown_opcode, data) when is_binary(data), do: data 54 | end 55 | 56 | @impl BlueHeron.HCI.Event 57 | def deserialize(<<@code, _size, num_hci_command_packets, opcode::binary-2, rp_bin::binary>>) do 58 | command_complete = %__MODULE__{ 59 | num_hci_command_packets: num_hci_command_packets, 60 | opcode: opcode, 61 | return_parameters: rp_bin 62 | } 63 | 64 | maybe_decode_return_parameters(command_complete) 65 | end 66 | 67 | def deserialize(bin), do: {:error, bin} 68 | 69 | def maybe_decode_return_parameters(cc) do 70 | BlueHeron.HCI.CommandComplete.ReturnParameters.decode(cc) 71 | catch 72 | kind, value -> 73 | Logger.warning(""" 74 | (#{inspect(kind)}, #{inspect(value)}) Unable to decode return_parameters for opcode #{inspect(cc.opcode, base: :hex)} 75 | return_parameters: #{inspect(cc.return_parameters)} 76 | #{inspect(__STACKTRACE__, limit: :infinity, pretty: true)} 77 | """) 78 | 79 | cc 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/blue_heron/data_type/manufacturer_data/apple.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Very 2 | # 3 | # SPDX-License-Identifier: MIT 4 | # 5 | defmodule BlueHeron.DataType.ManufacturerData.Apple do 6 | @moduledoc """ 7 | Serialization module for Apple. 8 | 9 | ## iBeacon 10 | 11 | Reference: https://en.wikipedia.org/wiki/IBeacon#Packet_Structure_Byte_Map 12 | """ 13 | 14 | alias BlueHeron.ManufacturerDataBehaviour 15 | 16 | @behaviour ManufacturerDataBehaviour 17 | 18 | @ibeacon_name "iBeacon" 19 | 20 | @ibeacon_identifier 0x02 21 | 22 | @ibeacon_length 0x15 23 | 24 | @doc """ 25 | Returns the Company Identifier description associated with this module. 26 | 27 | iex> company() 28 | "Apple, Inc." 29 | """ 30 | @impl ManufacturerDataBehaviour 31 | def company, do: "Apple, Inc." 32 | 33 | @doc """ 34 | Returns the iBeacon identifier. 35 | 36 | iex> ibeacon_identifier() 37 | 0x02 38 | """ 39 | def ibeacon_identifier, do: @ibeacon_identifier 40 | 41 | @doc """ 42 | Returns the length of the data following the length byte. 43 | 44 | iex> ibeacon_length() 45 | 0x15 46 | """ 47 | def ibeacon_length, do: @ibeacon_length 48 | 49 | @doc """ 50 | iex> serialize({"iBeacon", %{major: 1, minor: 2, tx_power: 3, uuid: 4}}) 51 | {:ok, <<2, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 1, 0, 2, 3>>} 52 | 53 | iex> serialize({"iBeacon", %{major: 1, minor: 2, tx_power: 3}}) 54 | :error 55 | 56 | iex> serialize(false) 57 | :error 58 | """ 59 | def serialize( 60 | {@ibeacon_name, 61 | %{ 62 | major: major, 63 | minor: minor, 64 | tx_power: tx_power, 65 | uuid: uuid 66 | }} 67 | ) do 68 | binary = << 69 | @ibeacon_identifier, 70 | @ibeacon_length, 71 | uuid::size(128), 72 | major::size(16), 73 | minor::size(16), 74 | tx_power 75 | >> 76 | 77 | {:ok, binary} 78 | end 79 | 80 | def serialize(_), do: :error 81 | 82 | @doc """ 83 | iex> deserialize(<<2, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 1, 0, 2, 3>>) 84 | {:ok, {"iBeacon", %{major: 1, minor: 2, tx_power: 3, uuid: 4}}} 85 | 86 | iex> deserialize(<<2, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 1, 0, 2>>) 87 | {:error, <<2, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 1, 0, 2>>} 88 | """ 89 | def deserialize(<<@ibeacon_identifier, @ibeacon_length, binary::binary-size(21)>>) do 90 | << 91 | uuid::size(128), 92 | major::size(16), 93 | minor::size(16), 94 | tx_power 95 | >> = binary 96 | 97 | data = %{ 98 | major: major, 99 | minor: minor, 100 | tx_power: tx_power, 101 | uuid: uuid 102 | } 103 | 104 | {:ok, {"iBeacon", data}} 105 | end 106 | 107 | def deserialize(bin) when is_binary(bin), do: {:error, bin} 108 | end 109 | -------------------------------------------------------------------------------- /test/blue_heron/hci/command/controller_and_baseband/set_event_mask_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 4 | # 5 | # SPDX-License-Identifier: Apache-2.0 6 | # 7 | defmodule BlueHeron.HCI.Command.ControllerAndBaseband.SetEventMaskTest do 8 | use ExUnit.Case 9 | alias BlueHeron.HCI.Command.ControllerAndBaseband.SetEventMask 10 | 11 | test "unmask_events/1" do 12 | assert SetEventMask.unmask_events(0x00001FFFFFFFFFFF) == 13 | [ 14 | inquiry_complete: true, 15 | inquiry_result: true, 16 | connection_complete: true, 17 | connection_request: true, 18 | disconnection_complete: true, 19 | authentication_complete: true, 20 | remote_name_request_complete: true, 21 | encryption_change: true, 22 | change_connection_link_key_complete: true, 23 | master_link_key_complete: true, 24 | read_remote_supported_features_complete: true, 25 | read_remote_version_information_complete: true, 26 | qos_setup_complete: true, 27 | hardware_error: true, 28 | flush_occurred: true, 29 | role_change: true, 30 | mode_change: true, 31 | return_link_keys: true, 32 | pin_code_request: true, 33 | link_key_request: true, 34 | link_key_notification: true, 35 | loopback_command: true, 36 | data_buffer_overflow: true, 37 | max_slots_change: true, 38 | read_clock_offset_complete: true, 39 | connection_packet_type_changed: true, 40 | qos_violation: true, 41 | page_scan_mode_change: true, 42 | page_scan_repetition_mode_change: true, 43 | flow_specification_complete: true, 44 | inquiry_resultwith_rssi: true, 45 | read_remote_extended_features_complete: true, 46 | synchronous_connection_complete: true, 47 | synchronous_connection_changed: true, 48 | sniff_subrating: false, 49 | extended_inquiry_result: false, 50 | encryption_key_refresh_complete: false, 51 | io_capability_request: false, 52 | io_capability_response: false, 53 | user_confirmation_request: false, 54 | user_passkey_request: false, 55 | remote_oob_data_request: false, 56 | simple_pairing_complete: false, 57 | link_supervision_timeout_changed: false, 58 | enhanced_flush_complete: false, 59 | user_passkey_notification: false, 60 | keypress_notification: false, 61 | remote_host_supported_features_notification: false, 62 | le_meta: false 63 | ] 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/blue_heron/assigned_numbers/generic_access_profile.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Very 2 | # 3 | # SPDX-License-Identifier: MIT 4 | # 5 | defmodule BlueHeron.AssignedNumbers.GenericAccessProfile do 6 | @moduledoc """ 7 | > Assigned numbers are used in GAP for inquiry response, EIR data type values, 8 | > manufacturer-specific data, advertising data, low energy UUIDs and appearance characteristics, 9 | > and class of device. 10 | 11 | Reference: https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile 12 | """ 13 | 14 | @definitions %{ 15 | 0x01 => "Flags", 16 | 0x02 => "Incomplete List of 16-bit Service Class UUIDs", 17 | 0x03 => "Complete List of 16-bit Service Class UUIDs", 18 | 0x04 => "Incomplete List of 32-bit Service Class UUIDs", 19 | 0x05 => "Complete List of 32-bit Service Class UUIDs", 20 | 0x06 => "Incomplete List of 128-bit Service Class UUIDs", 21 | 0x07 => "Complete List of 128-bit Service Class UUIDs", 22 | 0x08 => "Shortened Local Name", 23 | 0x09 => "Complete Local Name", 24 | 0x0A => "Tx Power Level", 25 | 0x0D => "Class of Device", 26 | 0x0E => "Simple Pairing Hash C-192", 27 | 0x0F => "Simple Pairing Randomizer R-192", 28 | 0x10 => "Device ID", 29 | 0x11 => "Security Manager Out of Band Flags", 30 | 0x12 => "Slave Connection Interval Range", 31 | 0x14 => "List of 16-bit Service Solicitation UUIDs", 32 | 0x15 => "List of 128-bit Service Solicitation UUIDs", 33 | 0x16 => "Service Data - 16-bit UUID", 34 | 0x17 => "Public Target Address", 35 | 0x18 => "Random Target Address", 36 | 0x19 => "Appearance", 37 | 0x1A => "Advertising Interval", 38 | 0x1B => "LE Bluetooth Device Address", 39 | 0x1C => "LE Role", 40 | 0x1D => "Simple Pairing Hash C-256", 41 | 0x1E => "Simple Pairing Randomizer R-256", 42 | 0x1F => "List of 32-bit Service Solicitation UUIDs", 43 | 0x20 => "Service Data - 32-bit UUID", 44 | 0x21 => "Service Data - 128-bit UUID", 45 | 0x22 => "LE Secure Connections Confirmation Value", 46 | 0x23 => "LE Secure Connections Random Value", 47 | 0x24 => "URI", 48 | 0x25 => "Indoor Positioning", 49 | 0x26 => "Transport Discovery Data", 50 | 0x27 => "LE Supported Features", 51 | 0x28 => "Channel Map Update Indication", 52 | 0x29 => "PB-ADV", 53 | 0x2A => "Mesh Message", 54 | 0x2B => "Mesh Beacon", 55 | 0x3D => "3D Information Data", 56 | 0xFF => "Manufacturer Specific Data" 57 | } 58 | 59 | @doc """ 60 | Returns the description associated with `id`. 61 | """ 62 | defmacro description(id) 63 | 64 | @doc """ 65 | Returns the ID associated with `description`. 66 | """ 67 | defmacro id(description) 68 | 69 | # handle a redundant GAP definition 70 | defmacro id("Simple Pairing Hash C"), do: 0x0E 71 | 72 | Enum.each(@definitions, fn 73 | {id, description} -> 74 | defmacro description(unquote(id)), do: unquote(description) 75 | 76 | defmacro id(unquote(description)), do: unquote(id) 77 | end) 78 | 79 | @doc """ 80 | Returns a list of all Generic Access Profile Data Type Values. 81 | """ 82 | defmacro ids, do: unquote(for {id, _} <- @definitions, do: id) 83 | end 84 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/events/le_meta/enhanced_connection_complete_v1.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # SPDX-FileCopyrightText: 2022 Troels Brødsgaard 4 | # 5 | # SPDX-License-Identifier: Apache-2.0 6 | # 7 | defmodule BlueHeron.HCI.Event.LEMeta.EnhancedConnectionCompleteV1 do 8 | use BlueHeron.HCI.Event.LEMeta, subevent_code: 0xA 9 | 10 | @moduledoc """ 11 | > The HCI_LE_Enhanced_Connection_Complete event indicates to both of the Hosts 12 | > forming the connection that a new connection has been created. Upon the creation 13 | > of the connection a Connection_Handle shall be assigned by the Controller, and 14 | > passed to the Host in this event. 15 | 16 | Reference: Version 5.2, Vol 4, Part E, 7.7.65.10 17 | """ 18 | 19 | defparameters [ 20 | :subevent_code, 21 | :status, 22 | :connection_handle, 23 | :role, 24 | :peer_address_type, 25 | :peer_address, 26 | :local_resolvable_private_address, 27 | :peer_resolvable_private_address, 28 | :connection_interval, 29 | :peripheral_latency, 30 | :supervision_timeout, 31 | :central_clock_accuracy 32 | ] 33 | 34 | defimpl BlueHeron.HCI.Serializable do 35 | def serialize(cc) do 36 | <> = <> 37 | connection_handle = <> 38 | 39 | bin = << 40 | cc.subevent_code, 41 | cc.status, 42 | connection_handle::binary, 43 | cc.role, 44 | cc.peer_address_type, 45 | cc.peer_address::little-48, 46 | cc.connection_interval::little-16, 47 | cc.connection_latency::little-16, 48 | cc.supervision_timeout::little-16, 49 | cc.master_clock_accuracy 50 | >> 51 | 52 | size = byte_size(bin) 53 | 54 | <> 55 | end 56 | end 57 | 58 | @impl BlueHeron.HCI.Event 59 | def deserialize(<<@code, _size, @subevent_code, bin::binary>>) do 60 | << 61 | status, 62 | lower_handle, 63 | _::4, 64 | upper_handle::4, 65 | role, 66 | peer_address_type, 67 | peer_address::little-48, 68 | local_resolvable_private_address::little-48, 69 | peer_resolvable_private_address::little-48, 70 | connection_interval::little-16, 71 | peripheral_latency::little-16, 72 | supervision_timeout::little-16, 73 | central_clock_accuracy 74 | >> = bin 75 | 76 | <> = <> 77 | 78 | %__MODULE__{ 79 | subevent_code: @subevent_code, 80 | status: status, 81 | connection_handle: connection_handle, 82 | role: role, 83 | peer_address_type: peer_address_type, 84 | peer_address: peer_address, 85 | connection_interval: connection_interval, 86 | peripheral_latency: peripheral_latency, 87 | supervision_timeout: supervision_timeout, 88 | central_clock_accuracy: central_clock_accuracy, 89 | local_resolvable_private_address: local_resolvable_private_address, 90 | peer_resolvable_private_address: peer_resolvable_private_address 91 | } 92 | end 93 | 94 | def deserialize(bin), do: {:error, bin} 95 | end 96 | -------------------------------------------------------------------------------- /lib/blue_heron/hci/commands/le_controller/create_connection.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | defmodule BlueHeron.HCI.Command.LEController.CreateConnection do 7 | use BlueHeron.HCI.Command.LEController, ocf: 0x000D 8 | 9 | @moduledoc """ 10 | > The HCI_LE_Create_Connection command is used to create an ACL connection to a 11 | > connectable advertiser 12 | 13 | Bluetooth Core Version 5.2 | Vol 4, Part E, section 7.8.12 14 | 15 | * OGF: `#{inspect(@ogf, base: :hex)}` 16 | * OCF: `#{inspect(@ocf, base: :hex)}` 17 | * Opcode: `#{inspect(@opcode)}` 18 | """ 19 | 20 | defparameters le_scan_interval: 0x0C80, 21 | le_scan_window: 0x0640, 22 | initiator_filter_policy: 0, 23 | peer_address_type: 0, 24 | peer_address: nil, 25 | own_address_type: 0, 26 | connection_interval_min: 0x0024, 27 | connection_interval_max: 0x0C80, 28 | connection_latency: 0x0012, 29 | supervision_timeout: 0x0640, 30 | min_ce_length: 0x0006, 31 | max_ce_length: 0x0054 32 | 33 | defimpl BlueHeron.HCI.Serializable do 34 | def serialize(cc) do 35 | fields = << 36 | cc.le_scan_interval::16-little, 37 | cc.le_scan_window::16-little, 38 | cc.initiator_filter_policy, 39 | cc.peer_address_type, 40 | cc.peer_address::little-48, 41 | cc.own_address_type, 42 | cc.connection_interval_min::16-little, 43 | cc.connection_interval_max::16-little, 44 | cc.connection_latency::16-little, 45 | cc.supervision_timeout::16-little, 46 | cc.min_ce_length::16-little, 47 | cc.max_ce_length::16-little 48 | >> 49 | 50 | fields_size = byte_size(fields) 51 | 52 | <> 53 | end 54 | end 55 | 56 | @impl BlueHeron.HCI.Command 57 | def deserialize(<<@opcode::binary, _fields_size, fields::binary>>) do 58 | << 59 | le_scan_interval::16-little, 60 | le_scan_window::16-little, 61 | initiator_filter_policy, 62 | peer_address_type, 63 | peer_address::48, 64 | own_address_type, 65 | connection_interval_min::16-little, 66 | connection_interval_max::16-little, 67 | connection_latency::16-little, 68 | supervision_timeout::16-little, 69 | min_ce_length::16-little, 70 | max_ce_length::16-little 71 | >> = fields 72 | 73 | %__MODULE__{ 74 | le_scan_interval: le_scan_interval, 75 | le_scan_window: le_scan_window, 76 | initiator_filter_policy: initiator_filter_policy, 77 | peer_address_type: peer_address_type, 78 | peer_address: peer_address, 79 | own_address_type: own_address_type, 80 | connection_interval_min: connection_interval_min, 81 | connection_interval_max: connection_interval_max, 82 | connection_latency: connection_latency, 83 | supervision_timeout: supervision_timeout, 84 | min_ce_length: min_ce_length, 85 | max_ce_length: max_ce_length 86 | } 87 | end 88 | 89 | @impl BlueHeron.HCI.Command 90 | def serialize_return_parameters(binary), do: binary 91 | 92 | @impl BlueHeron.HCI.Command 93 | def deserialize_return_parameters(binary) when is_binary(binary) do 94 | binary 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.5.4 4 | 5 | * Bugfixes 6 | * Fix potential MatchError in `BlueHeron.HCI.Event.EncryptionChange` 7 | * Changes 8 | * Update licensing and copyright for REUSE compliance 9 | 10 | ## v0.5.3 11 | 12 | * Enhancements 13 | * Updated ex_doc and property_table (Thanks @fhunleth ❤️) 14 | * Updated CI to build for Elixir 1.18 and 1.17 (Thanks @fhunleth ❤️) 15 | 16 | ## v0.5.2 17 | 18 | * Enhancements 19 | * Changed the Elixir version requirement to match Nerves. 20 | This fixes an unneeded warning on Elixir versions older than 1.18. 21 | 22 | ## v0.5.1 23 | 24 | * Enhancements 25 | * Added `BlueHeron.HCI.Transport.transport_ready?/0` 26 | * Bugfixes 27 | * Fixed issue where transport would fail to initialize due to garbage data needing 28 | to be flushed 29 | 30 | ## v0.5.0 31 | 32 | * Enhancements 33 | * `:blue_heron` is now it's own application 34 | * Rewrote `BlueHeron.HCI.Transport` to support the new supervision structure 35 | * Enable SMP by default 36 | * Persist GATT into a `PropertyTable` to allow for better error handling 37 | * Creating a Peripheral is now a little simpler and supervised 38 | * Added Broadcaster role 39 | * Depreciations 40 | * Removed `BlueHeron.Context` 41 | * Removed `BlueHeronTransportUART` and `BlueHeronTransportUSB` 42 | 43 | ## v0.4.2 44 | 45 | * Bugfixes 46 | * Fixed ACL messages not being delivered to parent process (Thanks @acadeau ❤️) 47 | 48 | ## v0.4.1 49 | 50 | * Enhancements 51 | * Added `set_scan_response_data` HCI command and Peripheral function 52 | * This allows for an additional 31 bytes of advertising data to be used 53 | 54 | ## v0.4.0 55 | 56 | * Enhancements 57 | * Added initial implementation of SMP (Thanks @markushutzler ❤️) 58 | * Added flow control for ACL data 59 | * Added new HCI commands required for SMP 60 | * Bugfixes 61 | * Fixed errors in GATT 62 | 63 | ## v0.3.0 64 | 65 | * Enhancements 66 | * Added initial implementation of GATT (Thanks @trarbr ❤️) 67 | 68 | ## v0.2.1 69 | 70 | * Enhancements 71 | * Added HCI commands for GATT (Thanks @trarbr ❤️) 72 | 73 | ## v0.2.0 74 | 75 | * Potential breaking changes 76 | There was quite a bit of internal adjustments and refactoring to cleanup 77 | implementation, although no core functions were changed. You should see 78 | no difference when updating but it was worth watching your implementation 79 | after updating in case something was missed in the cleanup 80 | 81 | * Enhancements 82 | * Add new Address module to simplify the different address interpretations 83 | * Allow disabling logging to /tmp/hcidump.pklg file (Thanks @axelson!) 84 | * Lots of HCI Commands added to better support default behavior (Thanks @trarbr!) 85 | 86 | * Fixes 87 | * fix/workaround for the rpi3 (Thanks @axelson!) 88 | * Fixed dmesg output display in govee example readme (Thanks @kevinansfield!) 89 | 90 | ## v0.1.1 91 | 92 | * Bugfixes 93 | * Disconnecting from an ATT client works now 94 | * Reconnect now works 95 | * Sending a `connection_create` cmd when a device is unavailable 96 | retries now 97 | * `connection_complete` event now uses the correct address 98 | * Multiple ATT clients can now be started simultaneously 99 | * HCI and ACL calls will now timeout rather than hang forever 100 | endianness 101 | * Transport now supports having multiple commands in flight 102 | * Enhancements 103 | * Added `btsnoop` parser 104 | 105 | ## v0.1.0 106 | 107 | Initial release 108 | -------------------------------------------------------------------------------- /lib/blue_heron/att/responses/read_by_type_response.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2020 Frank Hunleth 3 | # SPDX-FileCopyrightText: 2023 Markus Hutzler 4 | # 5 | # SPDX-License-Identifier: Apache-2.0 6 | # 7 | defmodule BlueHeron.ATT.ReadByTypeResponse do 8 | @moduledoc """ 9 | > The ATT_READ_BY_TYPE_RSP PDU is sent in reply to a received 10 | > ATT_READ_BY_TYPE_REQ PDU and contains the handles and values of the attributes 11 | > that have been read. 12 | 13 | Bluetooth Spec v5.2, vol 3, Part F, 3.4.4.2 14 | """ 15 | 16 | defmodule AttributeData do 17 | @moduledoc """ 18 | Structure containing decoders and encoders for the Attribute Data 19 | """ 20 | 21 | defstruct [:handle, :characteristic_properties, :characteristic_value_handle, :value, :uuid] 22 | 23 | def deserialize(<>) do 24 | %__MODULE__{ 25 | handle: handle, 26 | characteristic_properties: properties, 27 | characteristic_value_handle: value_handle, 28 | uuid: uuid 29 | } 30 | end 31 | 32 | def deserialize(<>) do 33 | %__MODULE__{ 34 | handle: handle, 35 | characteristic_properties: properties, 36 | characteristic_value_handle: value_handle, 37 | uuid: uuid 38 | } 39 | end 40 | 41 | def serialize(%{ 42 | handle: handle, 43 | characteristic_properties: characteristic_properties, 44 | characteristic_value_handle: characteristic_value_handle, 45 | uuid: uuid, 46 | value: nil 47 | }) 48 | when uuid < 0xFFFF do 49 | <> 51 | end 52 | 53 | def serialize(%{ 54 | handle: handle, 55 | characteristic_properties: characteristic_properties, 56 | characteristic_value_handle: characteristic_value_handle, 57 | uuid: uuid, 58 | value: nil 59 | }) 60 | when uuid > 0xFFFF do 61 | <> 63 | end 64 | 65 | def serialize(%{ 66 | handle: handle, 67 | characteristic_properties: _characteristic_properties, 68 | characteristic_value_handle: _characteristic_value_handle, 69 | uuid: _uuid, 70 | value: value 71 | }) do 72 | <> 73 | end 74 | end 75 | 76 | defstruct [:opcode, :attribute_data] 77 | 78 | def serialize(%{attribute_data: attribute_data}) do 79 | [single | _] = attribute_data = for attr <- attribute_data, do: AttributeData.serialize(attr) 80 | length = byte_size(single) 81 | <<0x9, length>> <> Enum.join(attribute_data) 82 | end 83 | 84 | def deserialize(<<0x9, attribute_data_length, attribute_data::binary>>) do 85 | %__MODULE__{ 86 | opcode: 0x9, 87 | attribute_data: deserialize_attribute_data(attribute_data_length, attribute_data, []) 88 | } 89 | end 90 | 91 | defp deserialize_attribute_data(_, <<>>, acc), do: Enum.reverse(acc) 92 | 93 | defp deserialize_attribute_data(item_length, attribute_data, acc) do 94 | <> = attribute_data 95 | attribute_data = AttributeData.deserialize(attribute_data) 96 | deserialize_attribute_data(item_length, rest, [attribute_data | acc]) 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/blue_heron/gatt/service.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Jon Carstens 2 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 3 | # SPDX-FileCopyrightText: 2022 Connor Rigby 4 | # 5 | # SPDX-License-Identifier: Apache-2.0 6 | # 7 | defmodule BlueHeron.GATT.Service do 8 | @moduledoc """ 9 | Struct that represents a GATT service. 10 | """ 11 | require Logger 12 | 13 | @type id :: term() 14 | 15 | @type read_fn :: (BlueHeron.GATT.Characteristic.id() -> binary()) 16 | @type write_fn :: (BlueHeron.GATT.Characteristic.id(), binary() -> any()) 17 | @type subscribe_fn :: (BlueHeron.GATT.Characteristic.id() -> any()) 18 | 19 | @opaque t() :: %__MODULE__{ 20 | id: id, 21 | type: non_neg_integer(), 22 | characteristics: [BlueHeron.GATT.Characteristic.t()], 23 | handle: any(), 24 | end_group_handle: any(), 25 | read: read_fn, 26 | write: write_fn, 27 | subscribe: subscribe_fn 28 | } 29 | 30 | defstruct [ 31 | :id, 32 | :type, 33 | :characteristics, 34 | :handle, 35 | :end_group_handle, 36 | :read, 37 | :write, 38 | :subscribe, 39 | :unsubscribe 40 | ] 41 | 42 | @doc """ 43 | Create a service with fields taken from the map `args`. 44 | 45 | The following fields are required: 46 | - `id`: A user-defined term to identify the service. Must be unique within the device profile. 47 | Can be any Erlang term. 48 | - `type`: The service type UUID. Can be a 2- or 16-byte byte UUID. Integer. 49 | - `characteristics`: A list of characteristics. 50 | - `read`: a 1 arity function called when the value of a characteristic should be read. 51 | - `write`: a 2 arity function called when the value of a characteristic should be written. 52 | - `subscribe`: a 1 arity function called when the value of a characteristic's value should be indicated. 53 | - `unsubscribe`: a 1 arity function called when the value of a characteristic's value should stop indicating. 54 | 55 | """ 56 | @spec new(args :: map()) :: t() 57 | def new(args) do 58 | args = Map.take(args, [:id, :type, :characteristics, :read, :write, :subscribe, :unsubscribe]) 59 | 60 | __MODULE__ 61 | |> struct!(args) 62 | |> validate_callbacks() 63 | end 64 | 65 | defp validate_callbacks(service) do 66 | service 67 | |> Map.update(:read, &default_read_callback/1, fn 68 | fun when is_function(fun, 1) -> fun 69 | _ -> &default_read_callback/1 70 | end) 71 | |> Map.update(:write, &default_write_callback/2, fn 72 | fun when is_function(fun, 2) -> fun 73 | _ -> &default_write_callback/2 74 | end) 75 | |> Map.update(:subscribe, &default_subscribe_callback/1, fn 76 | fun when is_function(fun, 1) -> fun 77 | _ -> &default_subscribe_callback/1 78 | end) 79 | |> Map.update(:unsubscribe, &default_unsubscribe_callback/1, fn 80 | fun when is_function(fun, 1) -> fun 81 | _ -> &default_unsubscribe_callback/1 82 | end) 83 | end 84 | 85 | defp default_read_callback(id) do 86 | Logger.error("Service Read #{inspect(id)}") 87 | <<0>> 88 | end 89 | 90 | defp default_write_callback(id, value) do 91 | Logger.error("Service Write #{inspect(id)} #{inspect(value)}") 92 | :ok 93 | end 94 | 95 | defp default_subscribe_callback(id) do 96 | Logger.error("Service Subscribe #{inspect(id)}") 97 | :ok 98 | end 99 | 100 | defp default_unsubscribe_callback(id) do 101 | Logger.error("Service Unsubscribe #{inspect(id)}") 102 | :ok 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /.github/workflows/elixir.yaml: -------------------------------------------------------------------------------- 1 | name: mix test 2 | 3 | # Define workflow that runs when changes are pushed to the 4 | # `main` branch or pushed to a PR branch that targets the `main` 5 | # branch. Change the branch name if your project uses a 6 | # different name for the main branch like "master" or "production". 7 | on: 8 | push: 9 | branches: [ "main" ] # adapt branch for project 10 | pull_request: 11 | branches: [ "main" ] # adapt branch for project 12 | 13 | # Sets the ENV `MIX_ENV` to `test` for running tests 14 | env: 15 | MIX_ENV: test 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | test: 22 | runs-on: ubuntu-latest 23 | name: Test on OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} 24 | strategy: 25 | # Specify the OTP and Elixir versions to use when building 26 | # and running the workflow steps. 27 | matrix: 28 | otp: ['27.2'] # Define the OTP version [required] 29 | elixir: ['1.17.3','1.18.2'] # Define the elixir version [required] 30 | steps: 31 | # Step: Setup Elixir + Erlang image as the base. 32 | - name: Set up Elixir 33 | uses: erlef/setup-beam@v1 34 | with: 35 | otp-version: ${{matrix.otp}} 36 | elixir-version: ${{matrix.elixir}} 37 | 38 | # Step: Check out the code. 39 | - name: Checkout code 40 | uses: actions/checkout@v3 41 | 42 | # Step: Define how to cache deps. Restores existing cache if present. 43 | - name: Cache deps 44 | id: cache-deps 45 | uses: actions/cache@v3 46 | env: 47 | cache-name: cache-elixir-deps 48 | with: 49 | path: deps 50 | key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} 51 | restore-keys: | 52 | ${{ runner.os }}-mix-${{ env.cache-name }}- 53 | 54 | # Step: Define how to cache the `_build` directory. After the first run, 55 | # this speeds up tests runs a lot. This includes not re-compiling our 56 | # project's downloaded deps every run. 57 | - name: Cache compiled build 58 | id: cache-build 59 | uses: actions/cache@v3 60 | env: 61 | cache-name: cache-compiled-build 62 | with: 63 | path: _build 64 | key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} 65 | restore-keys: | 66 | ${{ runner.os }}-mix-${{ env.cache-name }}- 67 | ${{ runner.os }}-mix- 68 | 69 | # Step: Download project dependencies. If unchanged, uses 70 | # the cached version. 71 | - name: Install dependencies 72 | run: mix deps.get 73 | 74 | # Step: Compile the project treating any warnings as errors. 75 | # Customize this step if a different behavior is desired. 76 | - name: Compiles without warnings 77 | run: mix compile --warnings-as-errors 78 | 79 | # Step: Check that the checked in code has already been formatted. 80 | # This step fails if something was found unformatted. 81 | # Customize this step as desired. 82 | - name: Check Formatting 83 | run: mix format --check-formatted 84 | 85 | # Step: Check that the checked in code has been linted. 86 | # This step fails if something was found not matching our credo spec. 87 | # Customize this step as desired. 88 | - name: Check Formatting 89 | run: mix credo --ignore todo refactor 90 | 91 | # Step: Execute the tests. 92 | - name: Run tests 93 | run: mix test --no-start 94 | 95 | check-license: 96 | runs-on: ubuntu-latest 97 | name: Check REUSE License Compliance 98 | steps: 99 | - name: Checkout code 100 | uses: actions/checkout@v3 101 | 102 | - name: Run REUSE lint 103 | uses: docker://fsfe/reuse:latest 104 | with: 105 | entrypoint: reuse 106 | args: lint -------------------------------------------------------------------------------- /lib/blue_heron/hci/command.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Connor Rigby 2 | # SPDX-FileCopyrightText: 2021 Troels Brødsgaard 3 | # SPDX-FileCopyrightText: 2021 Jon Carstens 4 | # SPDX-FileCopyrightText: 2023 Markus Hutzler 5 | # 6 | # SPDX-License-Identifier: Apache-2.0 7 | # 8 | defmodule BlueHeron.HCI.Command do 9 | @moduledoc """ 10 | > The Link Control commands allow a Controller to control connections to other BR/EDR 11 | > Controllers. Some Link Control commands are used only with a BR/EDR Controller 12 | > whereas other Link Control commands are also used with an LE Controller. 13 | 14 | Bluetooth Spec v5.2, vol 4, Part E, 7 15 | """ 16 | 17 | @callback deserialize_return_parameters(binary()) :: map() | binary() 18 | @callback serialize_return_parameters(map() | binary()) :: binary() 19 | @callback deserialize(binary()) :: term() 20 | 21 | alias __MODULE__.{ControllerAndBaseband, LEController, InformationalParameters, LinkPolicy} 22 | 23 | @modules [ 24 | ControllerAndBaseband.ReadLocalName, 25 | ControllerAndBaseband.Reset, 26 | ControllerAndBaseband.SetEventMask, 27 | ControllerAndBaseband.WriteClassOfDevice, 28 | ControllerAndBaseband.WriteDefaultErroneousDataReporting, 29 | ControllerAndBaseband.WriteExtendedInquiryResponse, 30 | ControllerAndBaseband.WriteInquiryMode, 31 | ControllerAndBaseband.WriteLEHostSupport, 32 | ControllerAndBaseband.WriteLocalName, 33 | ControllerAndBaseband.WritePageTimeout, 34 | ControllerAndBaseband.WriteScanEnable, 35 | ControllerAndBaseband.WriteSecureConnectionsHostSupport, 36 | ControllerAndBaseband.WriteSimplePairingMode, 37 | ControllerAndBaseband.WriteSynchronousFlowControlEnable, 38 | InformationalParameters.ReadBdAddr, 39 | InformationalParameters.ReadBufferSize, 40 | InformationalParameters.ReadLocalSupportedCommands, 41 | InformationalParameters.ReadLocalVersion, 42 | LEController.CreateConnection, 43 | LEController.CreateConnectionCancel, 44 | LEController.ReadBufferSizeV1, 45 | LEController.ReadWhiteListSize, 46 | LEController.SetAdvertisingData, 47 | LEController.SetAdvertisingEnable, 48 | LEController.SetAdvertisingParameters, 49 | LEController.SetRandomAddress, 50 | LEController.SetScanEnable, 51 | LEController.SetScanResponseData, 52 | LEController.SetScanParameters, 53 | LEController.LongTermKeyRequestReply, 54 | LEController.LongTermKeyRequestNegativeReply, 55 | LinkPolicy.WriteDefaultLinkPolicySettings 56 | ] 57 | 58 | def __modules__(), do: @modules 59 | 60 | @doc """ 61 | Helper to create Command opcode from OCF and OGF values 62 | """ 63 | def opcode(ogf, ocf) when ogf < 64 and ocf < 1024 do 64 | <> = <> 65 | <> 66 | end 67 | 68 | defmacro defparameters(fields) do 69 | quote location: :keep, bind_quoted: [fields: fields] do 70 | fields = 71 | if Keyword.keyword?(fields) do 72 | fields 73 | else 74 | for key <- fields, do: {key, nil} 75 | end 76 | 77 | # This is odd, but defparameters/1 is only intended to be used 78 | # in modules with BlueHeron.HCI.Command.__using__/1 macro which will 79 | # have these attributes defined. If not, let it fail 80 | fields = Keyword.merge(fields, ogf: @ogf, ocf: @ocf, opcode: @opcode) 81 | defstruct fields 82 | end 83 | end 84 | 85 | defmacro __using__(opts) do 86 | quote location: :keep, bind_quoted: [opts: opts] do 87 | ogf = 88 | Keyword.get_lazy(opts, :ogf, fn -> 89 | raise ":ogf key required when defining HCI.Command.__using__/1" 90 | end) 91 | 92 | @behaviour BlueHeron.HCI.Command 93 | import BlueHeron.HCI.Command, only: [defparameters: 1] 94 | 95 | @ogf ogf 96 | 97 | def __ogf__(), do: @ogf 98 | 99 | def new(args \\ []), do: struct(__MODULE__, args) 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 3 | "circuits_uart": {:hex, :circuits_uart, "1.5.3", "fb8e9cb8dcdcb987497b889d9bb51ee2932486afdf9961da2c3c699598da02d3", [:mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "9bd14660cc9f2c29500012f1772130e00b7edca27e2052d20360750185fc5d0f"}, 4 | "credo": {:hex, :credo, "1.7.11", "d3e805f7ddf6c9c854fd36f089649d7cf6ba74c42bc3795d587814e3c9847102", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "56826b4306843253a66e47ae45e98e7d284ee1f95d53d1612bb483f88a8cf219"}, 5 | "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, 6 | "earmark_parser": {:hex, :earmark_parser, "1.4.42", "f23d856f41919f17cd06a493923a722d87a2d684f143a1e663c04a2b93100682", [:mix], [], "hexpm", "6915b6ca369b5f7346636a2f41c6a6d78b5af419d61a611079189233358b8b8b"}, 7 | "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, 8 | "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, 9 | "ex_doc": {:hex, :ex_doc, "0.36.1", "4197d034f93e0b89ec79fac56e226107824adcce8d2dd0a26f5ed3a95efc36b1", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d7d26a7cf965dacadcd48f9fa7b5953d7d0cfa3b44fa7a65514427da44eafd89"}, 10 | "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, 11 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 12 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 13 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 14 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, 15 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 16 | "property_table": {:hex, :property_table, "0.3.0", "aa51e0eb5e9789edb45e1048223f7f30ffd4e4dd0e3bd4924d8fa22d7800f9f6", [:mix], [], "hexpm", "696289fe01a2d685eb460e5440e64736ec8a07d8e4748e2d573b12b590b931e3"}, 17 | } 18 | --------------------------------------------------------------------------------