├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── architecture.md ├── build-and-deploy.md ├── contributing.md ├── purpose.md ├── readme.md ├── source-automation.md ├── testing │ └── testing.md └── usage.md └── src ├── .formatter.exs ├── .gitignore ├── changelog.md ├── config └── config.exs ├── coveralls.json ├── lib ├── harald.ex └── harald │ ├── assigned_numbers │ ├── company_identifiers.ex │ └── generic_access_profile.ex │ ├── controller │ └── error_codes.ex │ ├── data_type.ex │ ├── data_type │ ├── manufacturer_behaviour.ex │ ├── manufacturer_data.ex │ ├── manufacturer_data │ │ └── apple.ex │ └── service_data.ex │ ├── hci.ex │ ├── hci │ ├── acl_data.ex │ ├── arrayed_data.ex │ ├── commands.ex │ ├── commands │ │ ├── command.ex │ │ ├── controller_and_baseband.ex │ │ ├── controller_and_baseband │ │ │ ├── flush.ex │ │ │ ├── host_buffer_size.ex │ │ │ ├── host_number_of_completed_packets.ex │ │ │ ├── read_flow_control_mode.ex │ │ │ ├── read_local_name.ex │ │ │ ├── reset.ex │ │ │ ├── set_controller_to_host_flow_control_command.ex │ │ │ ├── set_event_mask.ex │ │ │ ├── set_event_mask_page_2.ex │ │ │ ├── write_flow_control_mode.ex │ │ │ ├── write_local_name.ex │ │ │ ├── write_pin_type.ex │ │ │ ├── write_simple_pairing_mode.ex │ │ │ └── write_synchronous_flow_control_enable.ex │ │ ├── informational_parameters.ex │ │ ├── informational_parameters │ │ │ ├── read_bd_addr.ex │ │ │ ├── read_buffer_size.ex │ │ │ └── read_local_supported_features.ex │ │ ├── le_controller.ex │ │ └── le_controller │ │ │ ├── read_buffer_size_v1.ex │ │ │ ├── read_local_supported_features.ex │ │ │ ├── set_advertising_data.ex │ │ │ ├── set_advertising_enable.ex │ │ │ ├── set_advertising_parameters.ex │ │ │ └── set_event_mask.ex │ ├── error_codes.ex │ ├── events.ex │ ├── events │ │ ├── command_complete.ex │ │ ├── disconnection_complete.ex │ │ ├── event.ex │ │ ├── hardware_error.ex │ │ ├── le_meta.ex │ │ ├── le_meta │ │ │ ├── advertising_report.ex │ │ │ ├── advertising_report │ │ │ │ └── device.ex │ │ │ ├── connection_complete.ex │ │ │ └── connection_update_complete.ex │ │ └── number_of_completed_packets.ex │ ├── packet.ex │ ├── synchronous_data.ex │ ├── transport.ex │ └── transport │ │ ├── adapter.ex │ │ ├── handler.ex │ │ ├── uart.ex │ │ └── uart │ │ └── framing.ex │ ├── host │ ├── advertising_data.ex │ ├── advertising_data │ │ ├── complete_list_of_128_bit_service_class_uuids.ex │ │ └── complete_local_name.ex │ ├── att.ex │ ├── att │ │ ├── error_codes.ex │ │ ├── error_rsp.ex │ │ ├── exchange_mtu_req.ex │ │ ├── exchange_mtu_rsp.ex │ │ ├── execute_write_req.ex │ │ ├── execute_write_rsp.ex │ │ ├── find_information_req.ex │ │ ├── find_information_rsp.ex │ │ ├── prepare_write_req.ex │ │ ├── prepare_write_rsp.ex │ │ ├── read_blob_req.ex │ │ ├── read_blob_rsp.ex │ │ ├── read_by_group_type_req.ex │ │ ├── read_by_group_type_rsp.ex │ │ ├── read_by_type.req.ex │ │ ├── read_by_type_rsp.ex │ │ ├── read_req.ex │ │ ├── read_rsp.ex │ │ ├── write_cmd.ex │ │ ├── write_req.ex │ │ └── write_rsp.ex │ └── l2cap.ex │ ├── le.ex │ └── serializable.ex ├── mix.exs ├── mix.lock └── test ├── harald ├── hci │ ├── acl_data_test.exs │ ├── commands │ │ ├── controller_and_baseband │ │ │ ├── read_local_name_test.exs │ │ │ ├── set_event_mask_test.exs │ │ │ ├── write_local_name_test.exs │ │ │ ├── write_pin_type_test.exs │ │ │ └── write_simple_pairing_mode_test.exs │ │ ├── controller_and_baseband_test.exs │ │ ├── le_controller │ │ │ ├── set_advertising_data_test.exs │ │ │ ├── set_advertising_enable_test.exs │ │ │ ├── set_advertising_parameters_test.exs │ │ │ └── set_event_mask_test.exs │ │ └── le_controller_test.exs │ ├── events │ │ ├── command_complete_test.exs │ │ ├── disconnection_complete_test.exs │ │ ├── le_meta │ │ │ ├── connection_complete_test.exs │ │ │ └── connection_update_complete_test.exs │ │ └── le_meta_test.exs │ └── synchronous_data_test.exs ├── hci_test.exs ├── host │ ├── att │ │ ├── error_codes_test.exs │ │ ├── error_rsp_test.exs │ │ ├── exchange_mtu_req_test.exs │ │ ├── execute_write_req_test.exs │ │ ├── execute_write_rsp_test.exs │ │ ├── find_information_req_test.exs │ │ ├── find_information_rsp_test.exs │ │ ├── prepare_write_req_test.exs │ │ ├── prepare_write_rsp_test.exs │ │ ├── read_blob_req_test.exs │ │ ├── read_blob_rsp_test.exs │ │ ├── read_by_group_type_req_test.exs │ │ ├── read_by_group_type_rsp_test.exs │ │ ├── read_req_test.exs │ │ ├── read_rsp_test.exs │ │ ├── write_cmd_test.exs │ │ ├── write_req_test.exs │ │ └── write_rsp_test.exs │ ├── att_test.exs │ └── l2cap_test.exs └── transport │ └── uart │ └── framing_test.exs ├── harald_test.exs ├── support └── harald_case.ex └── test_helper.exs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | env: 10 | MIX_ENV: test 11 | ELIXIR_VERSION: 1.10.4 12 | OTP_VERSION: 23.0 13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 14 | 15 | jobs: 16 | test_coverage: 17 | runs-on: ubuntu-20.04 18 | defaults: 19 | run: 20 | working-directory: src 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - uses: erlef/setup-beam@v1 25 | name: Setup Elixir 26 | with: 27 | elixir-version: ${{ env.ELIXIR_VERSION }} 28 | otp-version: ${{ env.OTP_VERSION }} 29 | 30 | - uses: actions/cache@v3 31 | with: 32 | path: | 33 | src/deps 34 | src/_build 35 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 36 | restore-keys: | 37 | ${{ runner.os }}-mix- 38 | 39 | - run: mix deps.get 40 | - run: mix compile --warnings-as-errors 41 | - run: mix format --check-formatted 42 | 43 | - name: Run Tests 44 | run: mix coveralls.github 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Very 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Harald 2 | 3 | This project is part of [Very Labs](https://github.com/verypossible-labs/docs/blob/master/README.md). Maintenance of this project is subject to contributor availability. 4 | 5 | --- 6 | 7 | ![CI](https://github.com/verypossible-labs/harald/actions/workflows/ci.yml/badge.svg)[![Hex version badge](https://img.shields.io/hexpm/v/harald.svg)](https://hex.pm/packages/harald)[![Coverage Status](https://coveralls.io/repos/github/verypossible-labs/harald/badge.svg)](https://coveralls.io/github/verypossible-labs/harald) 8 | 9 | [Documentation](docs). 10 | 11 | This repository contains the documentation and implementation of Harald; an Elixir Bluetooth HCI 12 | data binding. 13 | -------------------------------------------------------------------------------- /docs/architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | Elixir module hierarchy. 4 | 5 | - Harald: Top level interface. 6 | - AssignedNumbers 7 | - CompanyIdentifiers 8 | - GenericAccessProfile 9 | - HCI 10 | - ACLData 11 | - ArrayedData 12 | - Commands 13 | - ErrorCodes 14 | - Events 15 | - Packet 16 | - SynchronousData 17 | - Host 18 | - ATT 19 | - ExchangeMTUReq 20 | - ExecuteWriteReq 21 | - ExecuteWriteRsp 22 | - FindInformationReq 23 | - FindInformationRsp 24 | - PrepareWriteReq 25 | - PrepareWriteRsp 26 | - ReadBlobReq 27 | - ReadBlobRsp 28 | - ReadByGroupTypeReq 29 | - ReadByGroupTypeRsp 30 | - ReadByTypeReq 31 | - ReadByTypeRsp 32 | - ReadReq 33 | - ReadRsp 34 | - WriteCmd 35 | - WriteReq 36 | - WriteRsp 37 | - L2CAP 38 | - AdvertisingData 39 | - CompleteListOf128BitServiceClassUUIDs 40 | - CompleteLocalName 41 | -------------------------------------------------------------------------------- /docs/build-and-deploy.md: -------------------------------------------------------------------------------- 1 | # Build and deploy 2 | 3 | To build and deploy a release, use the following steps: 4 | 5 | 1. Ensure `src/changelog.md` is updated and merged into the default branch. 6 | 7 | 2. Once the changelog is already up to date on the default branch, update the Elixir application version in `src/mix.exs`. 8 | 9 | 3. Commit the above changes with a title like "Release v0.1.0". Where the version number is the one to be deployed. 10 | 11 | 4. Open a pull request for release commit. 12 | 13 | 5. Once merged and CI passes for the default branch, check it out locally and create and push a git tag for the release. 14 | 15 | 6. Once CI passed for the tag, `mix hex.publish`. 16 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Development and feedback loop 4 | 5 | ```bash 6 | # clone the repository 7 | git clone github.com/verypossible-labs/harald 8 | 9 | # change directory into it 10 | cd harald 11 | 12 | # get dependencies 13 | mix deps.get 14 | 15 | # watch tests 16 | mix test.watch 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/purpose.md: -------------------------------------------------------------------------------- 1 | # Purpose 2 | 3 | Harald provides the foundation for building Bluetooth libraries and applications in Elixir by 4 | defining Elixir data structures and functions to decode and encode to and from binary HCI packets. 5 | 6 | Harald does not provide any stateful or process based functionality. The expectation is that 7 | higher level libraries will leverage Harald to provide higher level interfaces that can be made 8 | stateful and leveraged more efficiently by applications. 9 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | ## Table of contents 4 | 5 | 1. [Purpose](purpose.md). 6 | 2. [Usage](usage.md). 7 | 3. [Architecture](architecture.md). 8 | 4. [Contributing](contributing.md). 9 | 5. [Source automation](source-automation.md). 10 | 6. [Build and deploy](build-and-deploy.md). 11 | 12 | ## Project structure 13 | 14 | - [docs](../docs): Project documentation. 15 | - [src](../src): Elixir library application. 16 | -------------------------------------------------------------------------------- /docs/source-automation.md: -------------------------------------------------------------------------------- 1 | # Source automation 2 | -------------------------------------------------------------------------------- /docs/testing/testing.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | Run `mix test.watch` to run all the test / qa tools everytime a file changes. 4 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | If the Bluetooth controller is reachable and ready over UART, then Harald is ready to work. 4 | 5 | Getting a controller to be reachable and ready over UART is not always straightforward. Oftentimes controllers must have GPIOs toggled in precise sequences to be successfully awoken. Following that, some controllers require custom manufacturer HCI commands to be sent before anything else to initialize correctly. 6 | 7 | ```bash 8 | # lookup the latest version to add to your Mix file 9 | mix hex.info harald 10 | > An Elixir Bluetooth Host library. 11 | > 12 | > Config: {:harald, "~> 0.3.0"} 13 | > Releases: 0.3.0, 0.2.0, 0.1.1 14 | > 15 | > Licenses: MIT 16 | > Links: 17 | > GitHub: https://github.com/verypossible-labs/harald 18 | ``` 19 | 20 | ## Beaglebone Black Wireless 21 | 22 | TODO 23 | 24 | ## Raspberry Pi 0W, 3B 25 | 26 | 1. add a custom `fwup.conf` based on the Nerves Rpi3's so you may point to a custom `config.txt` 27 | 2. add a custom `config.txt` based on the Nerves Rpi3's so you may comment out `dtoverlay=pi3-miniuart-bt` 28 | 29 | See: 30 | 31 | - [harald_example_rpi3](https://github.com/verypossible/harald_example_rpi3) 32 | - [harald_example_rpi0](https://github.com/verypossible/harald_example_rpi0) 33 | 34 | ## Read and Write Local Name 35 | 36 | ```elixir 37 | {:ok, pid} = Harald.start_link(id: :bt, adapter: Harald.Transport.UART) 38 | :ok = Harald.subscribe(:bt) 39 | {:ok, bin_read} = Harald.encode_command(ControllerAndBaseband, ReadLocalName) 40 | :ok = Harald.write(:bt, bin_read) 41 | # wait a second 42 | flush() 43 | # command complete 44 | params_write = %{local_name: "new-name"} 45 | {:ok, bin_write} = Harald.encode_command(ControllerAndBaseband, WriteLocalName, params_write) 46 | :ok = Harald.write(:bt, bin_write) 47 | # wait a second 48 | flush() 49 | # command complete 50 | :ok = Harald.write(:bt, bin_read) 51 | # wait a second 52 | flush() 53 | # command complete 54 | ``` 55 | -------------------------------------------------------------------------------- /src/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /src/.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 3rd-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 | bluetooth-*.tar 24 | 25 | -------------------------------------------------------------------------------- /src/changelog.md: -------------------------------------------------------------------------------- 1 | # v0.3.0 2 | 3 | ## Breaking Changes 4 | 5 | Yes. 6 | 7 | ## Enhancements 8 | 9 | The purpose of this library has been narrowed and solidified as stateless Elixir HCI data bindings. This allowed the documentation to be cleaned up. More importantly, while there still are breaking changes in the future, this will minimize the churn and time until a stable v1.0.0 release. 10 | 11 | [Blu](https://github.com/verypossible-labs/blu) is in the infant stages of development but will provide a stateful higher level interface on top of Harald. 12 | 13 | # v0.2.0 14 | 15 | ## Breaking Changes 16 | 17 | - `Apple` 18 | - `deserialize/1` crashes on non-binary terms 19 | - `ArrayedData` 20 | - `deserialize/1` returns `{:error, bin}` instead of crashing on invalid input 21 | - `Device` 22 | - `deserialize/1` 23 | - when attempting to deserialize a non-binary term, crash 24 | - faults nested within a device discovered during deserialization will no 25 | longer be realized through tuples like `{:early_termination, 0}`, instead 26 | the violating binary will be returned as-is 27 | - `ErrorCode` 28 | - `name/1` returns `{:ok, String.t()} | :error` instead of 29 | `String.t() | no_return()` 30 | - `error_code/1` returns `{:ok, non_neg_integer()} | :error` instead of 31 | `non_neg_integer() | no_return()` 32 | - `Event` 33 | - `deserialize/1` returns `{:error, bin}` instead of 34 | `{:error, {:bad_event_code, bin}}` 35 | - `HCI` 36 | - `deserialize/1` the binary returned in an error tuple like 37 | `{:error, binary()}` is now always the binary given to the function instead 38 | of a subbinary 39 | - `InquiryComplete` 40 | - `serialize/1` may now return `{:error, InquiryComplete.t()}` instead of 41 | crashing when failing to serialize the event's status 42 | - `ManufacturerData` 43 | - namespace changed from `Harald` to `Harald.DataType`, modules previously 44 | namespaced under `Harald.ManufacturerData` are impacted 45 | - `deserialize/1` returns `{:error, bin}` instead of 46 | `{:error, {:unhandled_company_id, bin}}` 47 | 48 | ## Bugfixes 49 | 50 | - `Transport` 51 | - errors during deserialization no longer prevent a Bluetooth event from being 52 | dispatched to handlers 53 | 54 | ## Enhancements 55 | 56 | - `DataType` 57 | - add module 58 | - `Serializable` 59 | - add `assert_symmetry/2` 60 | - `ServiceData` 61 | - add module 62 | - `Transport` 63 | - `LE` is always included as a handler WRT `Transport` 64 | -------------------------------------------------------------------------------- /src/config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :harald, 4 | env: Mix.env() 5 | 6 | config :hook, 7 | mappings: [], 8 | resolve_at: :compile_time, 9 | top_level_module_allowlist: [Harald] 10 | 11 | case Mix.env() do 12 | :dev -> 13 | config :mix_test_watch, 14 | clear: true, 15 | tasks: [ 16 | "test --stale", 17 | "format --check-formatted" 18 | ] 19 | 20 | :test -> 21 | config :hook, 22 | resolve_at: :run_time 23 | 24 | _ -> 25 | :ok 26 | end 27 | -------------------------------------------------------------------------------- /src/coveralls.json: -------------------------------------------------------------------------------- 1 | { 2 | "coverage_options": { 3 | "minimum_coverage": 60 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/harald.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald do 2 | @moduledoc """ 3 | An Elixir Bluetooth Host library. 4 | """ 5 | 6 | alias Harald.HCI.{Commands, Events, SynchronousData, Transport} 7 | alias Harald.HCI.ACLData 8 | 9 | def hook_strategy_callback(_term) do 10 | case Application.get_env(:harald, :env) do 11 | :test -> :runtime 12 | _target -> {:compile_time, []} 13 | end 14 | end 15 | 16 | defdelegate decode_acl_data(bin), to: ACLData, as: :decode 17 | 18 | defdelegate decode_command(bin), to: Commands, as: :decode 19 | 20 | defdelegate decode_event(bin), to: Events, as: :decode 21 | 22 | defdelegate decode_synchronous_data(bin), to: SynchronousData, as: :decode 23 | 24 | defdelegate encode_acl_data(bin), to: ACLData, as: :encode 25 | 26 | defdelegate encode_command(ogf_module, ocf_module), to: Commands, as: :encode 27 | 28 | defdelegate encode_command(ogf_module, ocf_module, parameters), to: Commands, as: :encode 29 | 30 | defdelegate start_link(opts), to: Transport 31 | 32 | defdelegate subscribe(id), to: Transport 33 | 34 | defdelegate subscribe(id, pid), to: Transport 35 | 36 | defdelegate write(id, bin_or_command), to: Transport, as: :write 37 | end 38 | -------------------------------------------------------------------------------- /src/lib/harald/assigned_numbers/company_identifiers.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.AssignedNumbers.CompanyIdentifiers do 2 | @moduledoc """ 3 | > Company identifiers are unique numbers assigned by the Bluetooth SIG to member companies 4 | > requesting one. 5 | 6 | Reference: https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers 7 | """ 8 | 9 | @definitions %{ 10 | 0x004C => "Apple, Inc." 11 | } 12 | 13 | @doc """ 14 | Returns the description associated with `id`. 15 | """ 16 | def description(id) 17 | 18 | @doc """ 19 | Returns the ID associated with `description`. 20 | """ 21 | def id(description) 22 | 23 | Enum.each(@definitions, fn 24 | {id, description} -> 25 | def description(unquote(id)), do: unquote(description) 26 | 27 | def id(unquote(description)), do: unquote(id) 28 | end) 29 | 30 | @doc """ 31 | Returns a list of all Company Identifier ids. 32 | """ 33 | defmacro ids, do: unquote(for {id, _} <- @definitions, do: id) 34 | end 35 | -------------------------------------------------------------------------------- /src/lib/harald/assigned_numbers/generic_access_profile.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.AssignedNumbers.GenericAccessProfile do 2 | @moduledoc """ 3 | > Assigned numbers are used in GAP for inquiry response, EIR data type values, 4 | > manufacturer-specific data, advertising data, low energy UUIDs and appearance characteristics, 5 | > and class of device. 6 | 7 | Reference: https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile 8 | """ 9 | 10 | @definitions %{ 11 | 0x01 => "Flags", 12 | 0x02 => "Incomplete List of 16-bit Service Class UUIDs", 13 | 0x03 => "Complete List of 16-bit Service Class UUIDs", 14 | 0x04 => "Incomplete List of 32-bit Service Class UUIDs", 15 | 0x05 => "Complete List of 32-bit Service Class UUIDs", 16 | 0x06 => "Incomplete List of 128-bit Service Class UUIDs", 17 | 0x07 => "Complete List of 128-bit Service Class UUIDs", 18 | 0x08 => "Shortened Local Name", 19 | 0x09 => "Complete Local Name", 20 | 0x0A => "Tx Power Level", 21 | 0x0D => "Class of Device", 22 | 0x0E => "Simple Pairing Hash C-192", 23 | 0x0F => "Simple Pairing Randomizer R-192", 24 | 0x10 => "Device ID", 25 | 0x11 => "Security Manager Out of Band Flags", 26 | 0x12 => "Slave Connection Interval Range", 27 | 0x14 => "List of 16-bit Service Solicitation UUIDs", 28 | 0x15 => "List of 128-bit Service Solicitation UUIDs", 29 | 0x16 => "Service Data - 16-bit UUID", 30 | 0x17 => "Public Target Address", 31 | 0x18 => "Random Target Address", 32 | 0x19 => "Appearance", 33 | 0x1A => "Advertising Interval", 34 | 0x1B => "LE Bluetooth Device Address", 35 | 0x1C => "LE Role", 36 | 0x1D => "Simple Pairing Hash C-256", 37 | 0x1E => "Simple Pairing Randomizer R-256", 38 | 0x1F => "List of 32-bit Service Solicitation UUIDs", 39 | 0x20 => "Service Data - 32-bit UUID", 40 | 0x21 => "Service Data - 128-bit UUID", 41 | 0x22 => "LE Secure Connections Confirmation Value", 42 | 0x23 => "LE Secure Connections Random Value", 43 | 0x24 => "URI", 44 | 0x25 => "Indoor Positioning", 45 | 0x26 => "Transport Discovery Data", 46 | 0x27 => "LE Supported Features", 47 | 0x28 => "Channel Map Update Indication", 48 | 0x29 => "PB-ADV", 49 | 0x2A => "Mesh Message", 50 | 0x2B => "Mesh Beacon", 51 | 0x3D => "3D Information Data", 52 | 0xFF => "Manufacturer Specific Data" 53 | } 54 | 55 | @doc """ 56 | Returns the description associated with `id`. 57 | """ 58 | defmacro description(id) 59 | 60 | @doc """ 61 | Returns the ID associated with `description`. 62 | """ 63 | defmacro id(description) 64 | 65 | # handle a redundent GAP definition 66 | defmacro id("Simple Pairing Hash C"), do: 0x0E 67 | 68 | Enum.each(@definitions, fn 69 | {id, description} -> 70 | defmacro description(unquote(id)), do: unquote(description) 71 | 72 | defmacro id(unquote(description)), do: unquote(id) 73 | end) 74 | 75 | @doc """ 76 | Returns a list of all Generic Access Profile Data Type Values. 77 | """ 78 | defmacro ids, do: unquote(for {id, _} <- @definitions, do: id) 79 | end 80 | -------------------------------------------------------------------------------- /src/lib/harald/controller/error_codes.ex: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/lib/harald/data_type.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.DataType do 2 | @moduledoc """ 3 | Reference: Core Specification Supplement, Part A 4 | """ 5 | 6 | alias Harald.DataType.{ManufacturerData, ServiceData} 7 | 8 | @doc """ 9 | Returns a list of data type implementation modules. 10 | """ 11 | def modules do 12 | [ 13 | ManufacturerData, 14 | ServiceData 15 | ] 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /src/lib/harald/data_type/manufacturer_behaviour.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.DataType.ManufacturerDataBehaviour do 2 | @moduledoc """ 3 | Defines a behaviour that manufacturer data modules should implement. 4 | """ 5 | 6 | @doc """ 7 | Returns the company associated with some manufacturer data. 8 | 9 | See: `Harald.CompanyIdentifiers` 10 | """ 11 | @callback company :: String.t() 12 | end 13 | -------------------------------------------------------------------------------- /src/lib/harald/data_type/manufacturer_data.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.DataType.ManufacturerData do 2 | @moduledoc """ 3 | > The Manufacturer Specific data type is used for manufacturer specific data. 4 | 5 | Reference: Core Specification Supplement, Part A, section 1.4.1 6 | 7 | Modules under the `Harald.ManufacturerData` scope should implement the 8 | `Harald.ManufacturerDataBehaviour` and `Harald.Serializable` behaviours. 9 | """ 10 | 11 | alias Harald.DataType.ManufacturerData.Apple 12 | require Harald.AssignedNumbers.CompanyIdentifiers, as: CompanyIdentifiers 13 | 14 | @modules [Apple] 15 | 16 | @doc """ 17 | Returns a list of implementation modules. 18 | """ 19 | def modules, do: @modules 20 | 21 | @doc """ 22 | Serializes manufacturer data. 23 | """ 24 | def serialize(data) 25 | 26 | Enum.each(@modules, fn 27 | module -> 28 | def serialize({unquote(module.company()), data}) do 29 | data 30 | |> unquote(module).serialize() 31 | |> case do 32 | {:ok, bin} -> 33 | {:ok, <>} 34 | 35 | :error -> 36 | error = %{ 37 | remaining: data, 38 | serialized: <> 39 | } 40 | 41 | {:error, error} 42 | end 43 | end 44 | end) 45 | 46 | def serialize({:error, _} = ret), do: ret 47 | 48 | def serialize(ret), do: {:error, ret} 49 | 50 | @doc """ 51 | Deserializes a manufacturer data binary. 52 | 53 | iex> deserialize(<<76, 0, 2, 21, 172, 185, 137, 206, 253, 163, 76, 179, 137, 41, 101, 34, 252, 127, 2, 42, 181, 255, 224, 255, 225>>) 54 | {:ok, {"Apple, Inc.", {"iBeacon", %{major: 46591, minor: 57599, tx_power: 225, uuid: 229590585283448776073135497520678371882}}}} 55 | """ 56 | def deserialize(binary) 57 | 58 | Enum.each(@modules, fn 59 | module -> 60 | def deserialize( 61 | <> = 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 | -------------------------------------------------------------------------------- /src/lib/harald/data_type/manufacturer_data/apple.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.DataType.ManufacturerData.Apple do 2 | @moduledoc """ 3 | Serialization module for Apple. 4 | 5 | ## iBeacon 6 | 7 | Reference: https://en.wikipedia.org/wiki/IBeacon#Packet_Structure_Byte_Map 8 | """ 9 | 10 | alias Harald.{DataType.ManufacturerDataBehaviour, Serializable} 11 | 12 | @behaviour ManufacturerDataBehaviour 13 | 14 | @behaviour Serializable 15 | 16 | @ibeacon_name "iBeacon" 17 | 18 | @ibeacon_identifier 0x02 19 | 20 | @ibeacon_length 0x15 21 | 22 | @doc """ 23 | Returns the Company Identifier description associated with this module. 24 | 25 | iex> company() 26 | "Apple, Inc." 27 | """ 28 | @impl ManufacturerDataBehaviour 29 | def company, do: "Apple, Inc." 30 | 31 | @doc """ 32 | Returns the iBeacon identifier. 33 | 34 | iex> ibeacon_identifier() 35 | 0x02 36 | """ 37 | def ibeacon_identifier, do: @ibeacon_identifier 38 | 39 | @doc """ 40 | Returns the length of the data following the length byte. 41 | 42 | iex> ibeacon_length() 43 | 0x15 44 | """ 45 | def ibeacon_length, do: @ibeacon_length 46 | 47 | @doc """ 48 | iex> serialize({"iBeacon", %{major: 1, minor: 2, tx_power: 3, uuid: 4}}) 49 | {:ok, <<2, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 1, 0, 2, 3>>} 50 | 51 | iex> serialize({"iBeacon", %{major: 1, minor: 2, tx_power: 3}}) 52 | :error 53 | 54 | iex> serialize(false) 55 | :error 56 | """ 57 | @impl Serializable 58 | def serialize( 59 | {@ibeacon_name, 60 | %{ 61 | major: major, 62 | minor: minor, 63 | tx_power: tx_power, 64 | uuid: uuid 65 | }} 66 | ) do 67 | binary = << 68 | @ibeacon_identifier, 69 | @ibeacon_length, 70 | uuid::size(128), 71 | major::size(16), 72 | minor::size(16), 73 | tx_power 74 | >> 75 | 76 | {:ok, binary} 77 | end 78 | 79 | def serialize(_), do: :error 80 | 81 | @doc """ 82 | 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>>) 83 | {:ok, {"iBeacon", %{major: 1, minor: 2, tx_power: 3, uuid: 4}}} 84 | 85 | iex> deserialize(<<2, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 1, 0, 2>>) 86 | {:error, <<2, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 1, 0, 2>>} 87 | """ 88 | @impl Serializable 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 | -------------------------------------------------------------------------------- /src/lib/harald/data_type/service_data.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.DataType.ServiceData do 2 | @moduledoc """ 3 | > The Service Data data type consists of a service UUID with the data associated with that 4 | > service. 5 | 6 | Reference: Core Specification Supplement, Part A, section 1.11.1 7 | """ 8 | 9 | alias Harald.Serializable 10 | require Harald.AssignedNumbers.GenericAccessProfile, as: GenericAccessProfile 11 | 12 | @behaviour Serializable 13 | 14 | @description_32 "Service Data - 32-bit UUID" 15 | 16 | @doc """ 17 | Returns the three GAP descriptions encompassed by service data. 18 | """ 19 | def gap_descriptions, do: [@description_32] 20 | 21 | @doc """ 22 | iex> serialize({"Service Data - 32-bit UUID", %{data: <<5, 6>>, uuid: 67305985}}) 23 | {:ok, <<32, 1, 2, 3, 4, 5, 6>>} 24 | """ 25 | @impl Serializable 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 | Deserializes 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 | @impl Serializable 45 | def deserialize(<>) do 46 | {status, data} = 47 | case bin do 48 | <> -> 49 | service_data_32 = %{ 50 | uuid: uuid, 51 | data: data 52 | } 53 | 54 | {:ok, service_data_32} 55 | 56 | _ -> 57 | {:error, bin} 58 | end 59 | 60 | {status, {@description_32, data}} 61 | end 62 | 63 | def deserialize(bin), do: {:error, bin} 64 | end 65 | -------------------------------------------------------------------------------- /src/lib/harald/hci.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI do 2 | @moduledoc """ 3 | Reference: version 5.1, vol 2, part E, 1. 4 | """ 5 | 6 | @type bit_range() :: Range.t() 7 | 8 | @type flag() :: boolean() 9 | 10 | @type reserved() :: non_neg_integer() 11 | 12 | @type reserved_map() :: %{bit_range() => reserved()} 13 | end 14 | -------------------------------------------------------------------------------- /src/lib/harald/hci/acl_data.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.ACLData do 2 | @moduledoc """ 3 | Reference: version 5.2, vol 4, part E, 5.4.2. 4 | """ 5 | 6 | alias Harald.Host.L2CAP 7 | alias Harald.HCI.Packet 8 | 9 | @enforce_keys [ 10 | :handle, 11 | :packet_boundary_flag, 12 | :broadcast_flag, 13 | :data_total_length, 14 | :data 15 | ] 16 | 17 | defstruct [ 18 | :handle, 19 | :packet_boundary_flag, 20 | :broadcast_flag, 21 | :data_total_length, 22 | :data 23 | ] 24 | 25 | def decode( 26 | << 27 | 2, 28 | handle::little-size(16), 29 | data_total_length::little-size(16), 30 | data::binary-size(data_total_length) 31 | >> = encoded_bin 32 | ) do 33 | with {:ok, decoded_data} <- L2CAP.decode(data) do 34 | << 35 | encoded_broadcast_flag::size(2), 36 | encoded_packet_boundary_flag::size(2), 37 | connection_handle::size(12) 38 | >> = <> 39 | 40 | decoded = %__MODULE__{ 41 | handle: connection_handle, 42 | packet_boundary_flag: decode_packet_boundary_flag(encoded_packet_boundary_flag), 43 | broadcast_flag: decode_broadcast_flag(encoded_broadcast_flag), 44 | data_total_length: data_total_length, 45 | data: decoded_data 46 | } 47 | 48 | {:ok, decoded} 49 | else 50 | {:error, {:not_implemented, error, _bin}} -> 51 | {:error, {:not_implemented, error, encoded_bin}} 52 | end 53 | end 54 | 55 | def encode(%__MODULE__{ 56 | broadcast_flag: broadcast_flag, 57 | data: data, 58 | data_total_length: data_total_length, 59 | handle: connection_handle, 60 | packet_boundary_flag: packet_boundary_flag 61 | }) do 62 | encoded_packet_boundary_flag = encode_flag(packet_boundary_flag) 63 | encoded_broadcast_flag = encode_flag(broadcast_flag) 64 | {:ok, encoded_data} = L2CAP.encode(data) 65 | indicator = Packet.indicator(:acl_data) 66 | 67 | <> = << 68 | encoded_broadcast_flag::size(2), 69 | encoded_packet_boundary_flag::size(2), 70 | connection_handle::size(12) 71 | >> 72 | 73 | encoded = << 74 | indicator, 75 | encoded_handle::size(16), 76 | data_total_length::little-size(16), 77 | encoded_data::binary-size(data_total_length) 78 | >> 79 | 80 | {:ok, encoded} 81 | end 82 | 83 | def new(handle, packet_boundary_flag, broadcast_flag, %data_module{} = data) do 84 | {:ok, data_bin} = data_module.encode(data) 85 | 86 | acl_data = %__MODULE__{ 87 | handle: handle, 88 | packet_boundary_flag: packet_boundary_flag, 89 | broadcast_flag: broadcast_flag, 90 | data_total_length: byte_size(data_bin), 91 | data: data 92 | } 93 | 94 | {:ok, acl_data} 95 | end 96 | 97 | defp decode_broadcast_flag(0b00 = bc_flag) do 98 | %{description: "Point-to-point (ACL-U, AMP-U, or LE-U)", value: bc_flag} 99 | end 100 | 101 | defp decode_broadcast_flag(0b01 = bc_flag) do 102 | %{description: "BR/EDR broadcast (ASB-U)", value: bc_flag} 103 | end 104 | 105 | defp decode_broadcast_flag(0b10 = bc_flag) do 106 | %{description: "Reserved for future use.", value: bc_flag} 107 | end 108 | 109 | defp decode_broadcast_flag(0b11 = bc_flag) do 110 | %{description: "Reserved for future use.", value: bc_flag} 111 | end 112 | 113 | defp decode_packet_boundary_flag(0b00 = pb_flag) do 114 | %{ 115 | description: 116 | "First non-automatically-flushable packet of a higher layer message (start of a non-automatically-flushable L2CAP PDU) from Host to Controller.", 117 | value: pb_flag 118 | } 119 | end 120 | 121 | defp decode_packet_boundary_flag(0b01 = pb_flag) do 122 | %{ 123 | description: "Continuing fragment of a higher layer message", 124 | value: pb_flag 125 | } 126 | end 127 | 128 | defp decode_packet_boundary_flag(0b10 = pb_flag) do 129 | %{ 130 | description: 131 | "First automatically flushable packet of a higher layer message (start of an automatically-flushable L2CAP PDU).", 132 | value: pb_flag 133 | } 134 | end 135 | 136 | defp decode_packet_boundary_flag(0b11 = pb_flag) do 137 | %{ 138 | description: "A complete L2CAP PDU. Automatically flushable.", 139 | value: pb_flag 140 | } 141 | end 142 | 143 | defp encode_flag(%{value: encoded_flag}) when encoded_flag in [0b00, 0b01, 0b10, 0b11] do 144 | encoded_flag 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /src/lib/harald/hci/commands.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands do 2 | @moduledoc """ 3 | Reference: version 5.1, vol 2, part E, 5.4.1. 4 | """ 5 | 6 | alias Harald.HCI.Packet 7 | alias Harald.HCI.Commands 8 | 9 | alias Harald.HCI.Commands.{ 10 | Command, 11 | ControllerAndBaseband, 12 | InformationalParameters, 13 | LEController 14 | } 15 | 16 | @type ocf() :: non_neg_integer() 17 | @type ocf_module() :: module() 18 | @type ogf() :: non_neg_integer() 19 | @type op_code() :: binary() 20 | 21 | @callback decode(ocf(), binary()) :: {:ok, Command.t()} | :error 22 | @callback ogf() :: ogf() 23 | @callback ocf_to_module(ocf()) :: 24 | {:ok, ocf_module()} | {:error, {:not_implemented, {module(), ocf()}}} 25 | 26 | def decode(bin) when is_binary(bin) do 27 | indicator = Packet.indicator(:command) 28 | 29 | case bin do 30 | <<^indicator, op_code::binary-size(2), parameter_total_length, 31 | parameters::binary-size(parameter_total_length)>> -> 32 | {:ok, %{ocf: ocf, ogf: ogf}} = decode_op_code(op_code) 33 | {:ok, ogf_module} = ogf_to_module(ogf) 34 | {:ok, {ocf_module, parameters}} = ogf_module.decode(ocf, parameters) 35 | Commands.new(ogf_module, ocf_module, parameters) 36 | 37 | <<>> -> 38 | :error 39 | end 40 | end 41 | 42 | def encode(ogf_module, ocf_module, %{} = parameters \\ %{}) 43 | when is_atom(ogf_module) and is_atom(ocf_module) do 44 | {:ok, parameters_bin} = ocf_module.encode(parameters) 45 | parameter_total_length = byte_size(parameters_bin) 46 | {:ok, header} = header(ogf_module.ogf(), ocf_module.ocf(), parameter_total_length) 47 | {:ok, <>} 48 | end 49 | 50 | def header(ogf, ocf, parameter_total_length) 51 | when is_integer(ogf) and is_integer(ocf) and is_integer(parameter_total_length) do 52 | indicator = Packet.indicator(:command) 53 | {:ok, op_code} = encode_op_code(ogf, ocf) 54 | {:ok, <>} 55 | end 56 | 57 | def new(ogf_module, ocf_module, parameters) 58 | when is_atom(ogf_module) and is_atom(ocf_module) and is_map(parameters) do 59 | command = 60 | struct(Command, %{ 61 | command_op_code: %{ 62 | ocf: ocf_module.ocf(), 63 | ogf: ogf_module.ogf(), 64 | ocf_module: ocf_module, 65 | ogf_module: ogf_module 66 | }, 67 | parameters: parameters 68 | }) 69 | 70 | {:ok, command} 71 | end 72 | 73 | @spec encode_op_code(ogf(), ocf()) :: op_code() 74 | def encode_op_code(ogf, ocf) when is_integer(ogf) and is_integer(ocf) do 75 | <> = <> 76 | {:ok, <>} 77 | end 78 | 79 | def decode_op_code(<>) do 80 | <> = <> 81 | {:ok, %{ogf: ogf, ocf: ocf}} 82 | end 83 | 84 | def ogf_to_module(0x03), do: {:ok, ControllerAndBaseband} 85 | def ogf_to_module(0x04), do: {:ok, InformationalParameters} 86 | def ogf_to_module(0x08), do: {:ok, LEController} 87 | def ogf_to_module(ogf), do: {:error, {:not_implemented, {__MODULE__, ogf}}} 88 | end 89 | -------------------------------------------------------------------------------- /src/lib/harald/hci/commands/command.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.Command do 2 | @type ogf() :: non_neg_integer() 3 | 4 | @type ocf() :: non_neg_integer() 5 | 6 | @enforce_keys [ 7 | :command_op_code, 8 | :parameters 9 | ] 10 | 11 | defstruct [ 12 | :command_op_code, 13 | :parameters 14 | ] 15 | 16 | @callback encode(Command.t()) :: {:ok, binary()} | {:error, any()} 17 | @callback decode(binary()) :: {:ok, Command.t()} | {:error, any()} 18 | @callback decode_return_parameters(binary()) :: {:ok, map()} | {:error, any()} 19 | @callback encode_return_parameters(map()) :: {:ok, binary()} | {:error, any()} 20 | @callback ocf() :: {:ok, Commands.ocf()} | {:error, any()} 21 | end 22 | -------------------------------------------------------------------------------- /src/lib/harald/hci/commands/controller_and_baseband.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.ControllerAndBaseband do 2 | @moduledoc """ 3 | Reference: version 5.1, vol 2, part E, 7.3. 4 | """ 5 | 6 | alias Harald.HCI.Commands 7 | 8 | alias Harald.HCI.Commands.ControllerAndBaseband.{ 9 | Flush, 10 | HostBufferSize, 11 | HostNumberOfCompletedPackets, 12 | ReadLocalName, 13 | Reset, 14 | SetControllerToHostFlowControl, 15 | SetEventMask, 16 | SetEventMaskPage2, 17 | WriteFlowControlMode, 18 | WriteLocalName, 19 | WritePinType, 20 | WriteSimplePairingMode 21 | } 22 | 23 | @behaviour Commands 24 | 25 | @impl Commands 26 | def decode(ocf, bin) when is_integer(ocf) and is_binary(bin) do 27 | with {:ok, ocf_module} <- ocf_to_module(ocf), 28 | {:ok, parameters} <- ocf_module.decode(bin) do 29 | {:ok, {ocf_module, parameters}} 30 | end 31 | end 32 | 33 | @impl Commands 34 | def ogf(), do: 0x03 35 | 36 | @impl Commands 37 | def ocf_to_module(0x0001), do: {:ok, SetEventMask} 38 | def ocf_to_module(0x0003), do: {:ok, Reset} 39 | def ocf_to_module(0x0008), do: {:ok, Flush} 40 | def ocf_to_module(0x000A), do: {:ok, WritePinType} 41 | def ocf_to_module(0x0013), do: {:ok, WriteLocalName} 42 | def ocf_to_module(0x0014), do: {:ok, ReadLocalName} 43 | def ocf_to_module(0x0031), do: {:ok, SetControllerToHostFlowControl} 44 | def ocf_to_module(0x0033), do: {:ok, HostBufferSize} 45 | def ocf_to_module(0x0035), do: {:ok, HostNumberOfCompletedPackets} 46 | def ocf_to_module(0x0056), do: {:ok, WriteSimplePairingMode} 47 | def ocf_to_module(0x0063), do: {:ok, SetEventMaskPage2} 48 | def ocf_to_module(0x0066), do: {:ok, ReadFlowControlMode} 49 | def ocf_to_module(0x0067), do: {:ok, WriteFlowControlMode} 50 | def ocf_to_module(ocf), do: {:error, {:not_implemented, {__MODULE__, ocf}}} 51 | end 52 | -------------------------------------------------------------------------------- /src/lib/harald/hci/commands/controller_and_baseband/flush.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.ControllerAndBaseband.Flush do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 4, Part E, 7.3.12. 4 | """ 5 | 6 | alias Harald.HCI.{Commands.Command, ErrorCodes} 7 | 8 | @behaviour Command 9 | 10 | @impl Command 11 | def decode(<>) do 12 | decoded_flush = %{ 13 | connection_handle: %{handle: handle, rfu: rfu} 14 | } 15 | 16 | {:ok, decoded_flush} 17 | end 18 | 19 | @impl Command 20 | def decode_return_parameters(<>) do 21 | {:ok, decoded_status} = ErrorCodes.decode(encoded_status) 22 | 23 | decoded_return_parameters = %{ 24 | status: decoded_status, 25 | connection_handle: %{ 26 | handle: handle, 27 | rfu: rfu 28 | } 29 | } 30 | 31 | {:ok, decoded_return_parameters} 32 | end 33 | 34 | @impl Command 35 | def encode(%{ 36 | connection_handle: %{ 37 | handle: handle, 38 | rfu: rfu 39 | } 40 | }) do 41 | {:ok, <>} 42 | end 43 | 44 | @impl Command 45 | def encode_return_parameters(%{ 46 | status: decoded_status, 47 | connection_handle: %{connection_handle: handle, rfu: rfu} 48 | }) do 49 | {:ok, encoded_status} = ErrorCodes.encode(decoded_status) 50 | {:ok, <>} 51 | end 52 | 53 | @impl Command 54 | def ocf(), do: 0x08 55 | end 56 | -------------------------------------------------------------------------------- /src/lib/harald/hci/commands/controller_and_baseband/host_buffer_size.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.ControllerAndBaseband.HostBufferSize do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 4, Part E, 7.3.39. 4 | """ 5 | 6 | alias Harald.HCI.{Commands.Command, ErrorCodes} 7 | 8 | @behaviour Command 9 | 10 | @impl Command 11 | def decode(<< 12 | host_acl_data_packet_length::little-size(16), 13 | host_synchronous_data_packet_length, 14 | host_total_num_acl_data_packets::little-size(16), 15 | host_total_num_synchronous_data_packets::little-size(16) 16 | >>) do 17 | decoded_host_buffer_size = %{ 18 | host_acl_data_packet_length: host_acl_data_packet_length, 19 | host_synchronous_data_packet_length: host_synchronous_data_packet_length, 20 | host_total_num_acl_data_packets: host_total_num_acl_data_packets, 21 | host_total_num_synchronous_data_packets: host_total_num_synchronous_data_packets 22 | } 23 | 24 | {:ok, decoded_host_buffer_size} 25 | end 26 | 27 | @impl Command 28 | def decode_return_parameters(<>) do 29 | {:ok, decoded_status} = ErrorCodes.decode(encoded_status) 30 | {:ok, %{status: decoded_status}} 31 | end 32 | 33 | @impl Command 34 | def encode(%{ 35 | host_acl_data_packet_length: host_acl_data_packet_length, 36 | host_synchronous_data_packet_length: host_synchronous_data_packet_length, 37 | host_total_num_acl_data_packets: host_total_num_acl_data_packets, 38 | host_total_num_synchronous_data_packets: host_total_num_synchronous_data_packets 39 | }) do 40 | encoded_host_buffer_size = << 41 | host_acl_data_packet_length::little-size(16), 42 | host_synchronous_data_packet_length, 43 | host_total_num_acl_data_packets::little-size(16), 44 | host_total_num_synchronous_data_packets::little-size(16) 45 | >> 46 | 47 | {:ok, encoded_host_buffer_size} 48 | end 49 | 50 | @impl Command 51 | def encode_return_parameters(%{status: decoded_status}) do 52 | {:ok, encoded_status} = ErrorCodes.encode(decoded_status) 53 | {:ok, <>} 54 | end 55 | 56 | @impl Command 57 | def ocf(), do: 0x33 58 | end 59 | -------------------------------------------------------------------------------- /src/lib/harald/hci/commands/controller_and_baseband/host_number_of_completed_packets.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.ControllerAndBaseband.HostNumberOfCompletedPackets do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 4, Part E, 7.3.40. 4 | """ 5 | 6 | alias Harald.HCI.{Commands.Command, ErrorCodes} 7 | 8 | @behaviour Command 9 | 10 | @impl Command 11 | def decode(<>) do 12 | {the_map, remaining_bin} = 13 | Enum.reduce(1..num_handles, {%{}, arrayed_data}, fn 14 | index, {the_map, <>} -> 15 | <> = <> 16 | {Map.put(the_map, index, {handle, rfu}), the_rem_bin} 17 | end) 18 | 19 | {the_map, <<>>} = 20 | Enum.reduce(1..num_handles, {the_map, remaining_bin}, fn 21 | index, {the_map, <>} -> 22 | {Map.update!(the_map, index, fn {handle, rfu} -> {handle, {rfu, num_completed}} end), 23 | the_rem_bin} 24 | end) 25 | 26 | map = 27 | Enum.into(the_map, %{}, fn {_, {handle, {rfu, num_completed}}} -> 28 | {handle, %{rfu: rfu, num_completed: num_completed}} 29 | end) 30 | 31 | {:ok, %{num_handles: num_handles, handles: map}} 32 | end 33 | 34 | @impl Command 35 | def decode_return_parameters(<>) do 36 | {:ok, decoded_status} = ErrorCodes.decode(encoded_status) 37 | {:ok, %{status: decoded_status}} 38 | end 39 | 40 | @impl Command 41 | def encode(%{num_handles: num_handles, handles: handles}) do 42 | {connection_handles, host_num_completed_packets} = 43 | Enum.reduce(handles, {<<>>, <<>>}, fn 44 | {handle, handle_data}, {acc_handle, acc_num_completed} -> 45 | <> = <> 46 | acc_handle = <> 47 | 48 | acc_num_completed = 49 | <> 50 | 51 | {acc_handle, acc_num_completed} 52 | end) 53 | 54 | {:ok, <>} 55 | end 56 | 57 | @impl Command 58 | def encode_return_parameters(%{status: decoded_status}) do 59 | {:ok, encoded_status} = ErrorCodes.encode(decoded_status) 60 | {:ok, <>} 61 | end 62 | 63 | @impl Command 64 | def ocf(), do: 0x35 65 | end 66 | -------------------------------------------------------------------------------- /src/lib/harald/hci/commands/controller_and_baseband/read_flow_control_mode.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.ControllerAndBaseband.ReadFlowControlMode do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 4, Part E, 7.3.72. 4 | """ 5 | 6 | alias Harald.HCI.{Commands.Command, ErrorCodes} 7 | 8 | @behaviour Command 9 | 10 | @impl Command 11 | def decode(<<>>), do: {:ok, %{}} 12 | 13 | @impl Command 14 | def decode_return_parameters(<>) do 15 | {:ok, decoded_status} = ErrorCodes.decode(encoded_status) 16 | 17 | decoded_flow_control_mode = 18 | case encoded_flow_control_mode do 19 | 0 -> :packet 20 | 1 -> :data_block 21 | end 22 | 23 | {:ok, %{flow_control_mode: decoded_flow_control_mode, status: decoded_status}} 24 | end 25 | 26 | @impl Command 27 | def encode(%{}), do: {:ok, %{}} 28 | 29 | @impl Command 30 | def encode_return_parameters(%{ 31 | flow_control_mode: decoded_flow_control_mode, 32 | status: decoded_status 33 | }) do 34 | {:ok, encoded_status} = ErrorCodes.encode(decoded_status) 35 | 36 | encoded_flow_control_mode = 37 | case decoded_flow_control_mode do 38 | :packet -> 0 39 | :data_block -> 1 40 | end 41 | 42 | {:ok, <>} 43 | end 44 | 45 | @impl Command 46 | def ocf(), do: 0x0066 47 | end 48 | -------------------------------------------------------------------------------- /src/lib/harald/hci/commands/controller_and_baseband/read_local_name.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.ControllerAndBaseband.ReadLocalName do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 4, Part E, 7.3.12. 4 | """ 5 | 6 | alias Harald.HCI.Commands.Command 7 | 8 | @behaviour Command 9 | 10 | @impl Command 11 | def decode(<<>>), do: {:ok, %{}} 12 | 13 | @impl Command 14 | def decode_return_parameters(<>) do 15 | {:ok, %{status: status, local_name: local_name}} 16 | end 17 | 18 | @impl Command 19 | def encode(%{}), do: {:ok, <<>>} 20 | 21 | @impl Command 22 | def encode_return_parameters(%{status: status, local_name: local_name}) do 23 | {:ok, <>} 24 | end 25 | 26 | @impl Command 27 | def ocf(), do: 0x14 28 | end 29 | -------------------------------------------------------------------------------- /src/lib/harald/hci/commands/controller_and_baseband/reset.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.ControllerAndBaseband.Reset do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 4, Part E, 7.3.2. 4 | """ 5 | 6 | alias Harald.HCI.Commands.Command 7 | 8 | @behaviour Command 9 | 10 | @impl Command 11 | def decode(<<>>), do: {:ok, %{}} 12 | 13 | @impl Command 14 | def decode_return_parameters(<>), do: {:ok, %{status: status}} 15 | 16 | @impl Command 17 | def encode(%{}), do: {:ok, <<>>} 18 | 19 | @impl Command 20 | def encode_return_parameters(%{status: status}), do: {:ok, <>} 21 | 22 | @impl Command 23 | def ocf(), do: 0x03 24 | end 25 | -------------------------------------------------------------------------------- /src/lib/harald/hci/commands/controller_and_baseband/set_controller_to_host_flow_control_command.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.ControllerAndBaseband.SetControllerToHostFlowControl do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 4, Part E, 7.3.38. 4 | """ 5 | 6 | alias Harald.HCI.{Commands.Command, ErrorCodes} 7 | 8 | @behaviour Command 9 | 10 | @impl Command 11 | def encode(%{flow_control_enable: false}), do: {:ok, <<0>>} 12 | def encode(%{flow_control_enable: :acl_data}), do: {:ok, <<1>>} 13 | def encode(%{flow_control_enable: :synchronous_data}), do: {:ok, <<2>>} 14 | def encode(%{flow_control_enable: :acl_data_and_synchronous_data}), do: {:ok, <<3>>} 15 | 16 | @impl Command 17 | def decode(<<0>>), do: {:ok, %{flow_control_enable: false}} 18 | def decode(<<1>>), do: {:ok, %{flow_control_enable: :acl_data}} 19 | def decode(<<2>>), do: {:ok, %{flow_control_enable: :synchronous_data}} 20 | def decode(<<3>>), do: {:ok, %{flow_control_enable: :acl_data_and_synchronous_data}} 21 | 22 | @impl Command 23 | def decode_return_parameters(<>) do 24 | {:ok, decoded_status} = ErrorCodes.decode(encoded_status) 25 | {:ok, %{status: decoded_status}} 26 | end 27 | 28 | @impl Command 29 | def encode_return_parameters(%{status: decoded_status}) do 30 | {:ok, encoded_status} = ErrorCodes.encode(decoded_status) 31 | {:ok, <>} 32 | end 33 | 34 | @impl Command 35 | def ocf(), do: 0x31 36 | end 37 | -------------------------------------------------------------------------------- /src/lib/harald/hci/commands/controller_and_baseband/write_flow_control_mode.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.ControllerAndBaseband.WriteFlowControlMode do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 4, Part E, 7.3.73. 4 | """ 5 | 6 | alias Harald.HCI.{Commands.Command, ErrorCodes} 7 | 8 | @behaviour Command 9 | 10 | @impl Command 11 | def decode(<>) do 12 | decoded_flow_control_mode = 13 | case encoded_flow_control_mode do 14 | 0 -> :packet 15 | 1 -> :data_block 16 | end 17 | 18 | decoded_write_flow_control_mode = %{flow_control_mode: decoded_flow_control_mode} 19 | {:ok, decoded_write_flow_control_mode} 20 | end 21 | 22 | @impl Command 23 | def decode_return_parameters(<>) do 24 | {:ok, decoded_status} = ErrorCodes.decode(encoded_status) 25 | {:ok, %{status: decoded_status}} 26 | end 27 | 28 | @impl Command 29 | def encode(%{flow_control_mode: decoded_flow_control_mode}) do 30 | encoded_flow_control_mode = 31 | case decoded_flow_control_mode do 32 | :packet -> 0 33 | :data_block -> 1 34 | end 35 | 36 | {:ok, <>} 37 | end 38 | 39 | @impl Command 40 | def encode_return_parameters(%{status: decoded_status}) do 41 | {:ok, encoded_status} = ErrorCodes.encode(decoded_status) 42 | {:ok, <>} 43 | end 44 | 45 | @impl Command 46 | def ocf(), do: 0x0067 47 | end 48 | -------------------------------------------------------------------------------- /src/lib/harald/hci/commands/controller_and_baseband/write_local_name.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.ControllerAndBaseband.WriteLocalName do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 4, Part E, 7.3.11. 4 | """ 5 | 6 | alias Harald.HCI.Commands.Command 7 | 8 | @behaviour Command 9 | 10 | @max_local_name_byte_size 248 11 | 12 | @impl Command 13 | def encode(%{local_name: local_name}) when byte_size(local_name) <= @max_local_name_byte_size do 14 | local_name = String.pad_trailing(local_name, @max_local_name_byte_size, <<0>>) 15 | {:ok, <>} 16 | end 17 | 18 | def encode(%{local_name: local_name}) when byte_size(local_name) > @max_local_name_byte_size do 19 | {:error, :local_name_too_long} 20 | end 21 | 22 | def encode(_), do: {:error, :local_name_required} 23 | 24 | @impl Command 25 | def decode(<>) when byte_size(local_name) <= @max_local_name_byte_size do 26 | {:ok, %{read_local_name: local_name}} 27 | end 28 | 29 | @impl Command 30 | def decode_return_parameters(<>), do: {:ok, %{status: status}} 31 | 32 | @impl Command 33 | def encode_return_parameters(%{status: status}), do: {:ok, <>} 34 | 35 | @impl Command 36 | def ocf(), do: 0x13 37 | end 38 | -------------------------------------------------------------------------------- /src/lib/harald/hci/commands/controller_and_baseband/write_pin_type.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.ControllerAndBaseband.WritePinType do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 4, Part E, 7.3.6. 4 | """ 5 | 6 | alias Harald.HCI.Commands.Command 7 | 8 | @behaviour Command 9 | 10 | @impl Command 11 | def encode(%{pin_type: :variable}), do: {:ok, <<0>>} 12 | def encode(%{pin_type: :fixed}), do: {:ok, <<1>>} 13 | 14 | @impl Command 15 | def decode(<<0>>), do: {:ok, %{pin_type: :variable}} 16 | def decode(<<1>>), do: {:ok, %{pin_type: :fixed}} 17 | 18 | @impl Command 19 | def decode_return_parameters(<>), do: {:ok, %{status: status}} 20 | 21 | @impl Command 22 | def encode_return_parameters(%{status: status}), do: {:ok, <>} 23 | 24 | @impl Command 25 | def ocf(), do: 0x0A 26 | end 27 | -------------------------------------------------------------------------------- /src/lib/harald/hci/commands/controller_and_baseband/write_simple_pairing_mode.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.ControllerAndBaseband.WriteSimplePairingMode do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 4, Part E, 7.3.59. 4 | """ 5 | 6 | alias Harald.HCI.Commands.Command 7 | 8 | @behaviour Command 9 | 10 | @impl Command 11 | def encode(%{simple_pairing_mode: true}), do: {:ok, <<1>>} 12 | def encode(%{simple_pairing_mode: false}), do: {:ok, <<0>>} 13 | 14 | @impl Command 15 | def decode(<<1>>), do: {:ok, %{simple_pairing_mode: true}} 16 | def decode(<<0>>), do: {:ok, %{simple_pairing_mode: false}} 17 | 18 | @impl Command 19 | def decode_return_parameters(<>), do: {:ok, %{status: status}} 20 | 21 | @impl Command 22 | def encode_return_parameters(%{status: status}), do: {:ok, <>} 23 | 24 | @impl Command 25 | def ocf(), do: 0x56 26 | end 27 | -------------------------------------------------------------------------------- /src/lib/harald/hci/commands/controller_and_baseband/write_synchronous_flow_control_enable.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.ControllerAndBaseband.WriteSynchronousFlowControlEnable do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 4, Part E, 7.3.37. 4 | """ 5 | 6 | alias Harald.HCI.Commands.Command 7 | 8 | @behaviour Command 9 | 10 | @impl Command 11 | def encode(%{synchronous_flow_control_enable: true}), do: {:ok, <<1>>} 12 | def encode(%{synchronous_flow_control_enable: false}), do: {:ok, <<0>>} 13 | 14 | @impl Command 15 | def decode(<<1>>), do: {:ok, %{synchronous_flow_control_enable: true}} 16 | def decode(<<0>>), do: {:ok, %{synchronous_flow_control_enable: false}} 17 | 18 | @impl Command 19 | def decode_return_parameters(<>), do: {:ok, %{status: status}} 20 | 21 | @impl Command 22 | def encode_return_parameters(%{status: status}), do: {:ok, <>} 23 | 24 | @impl Command 25 | def ocf(), do: 0x2F 26 | end 27 | -------------------------------------------------------------------------------- /src/lib/harald/hci/commands/informational_parameters.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.InformationalParameters do 2 | @moduledoc """ 3 | Reference: version 5.2, vol 4, part E, 7.8. 4 | """ 5 | 6 | alias Harald.HCI.Commands 7 | 8 | alias Harald.HCI.Commands.InformationalParameters.{ 9 | ReadBdAddr, 10 | ReadBufferSize, 11 | ReadLocalSupportedFeatures 12 | } 13 | 14 | @behaviour Commands 15 | 16 | @impl Commands 17 | def decode(ocf, bin) when is_integer(ocf) and is_binary(bin) do 18 | with {:ok, ocf_module} <- ocf_to_module(ocf), 19 | {:ok, parameters} <- ocf_module.decode(bin) do 20 | {:ok, {ocf_module, parameters}} 21 | end 22 | end 23 | 24 | @impl Commands 25 | def ogf(), do: 0x04 26 | 27 | @impl Commands 28 | def ocf_to_module(0x03), do: {:ok, ReadLocalSupportedFeatures} 29 | def ocf_to_module(0x05), do: {:ok, ReadBufferSize} 30 | def ocf_to_module(0x09), do: {:ok, ReadBdAddr} 31 | def ocf_to_module(ocf), do: {:error, {:not_implemented, {__MODULE__, ocf}}} 32 | end 33 | -------------------------------------------------------------------------------- /src/lib/harald/hci/commands/informational_parameters/read_bd_addr.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.InformationalParameters.ReadBdAddr do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 4, Part E, 7.4.6. 4 | """ 5 | 6 | alias Harald.HCI.{Commands.Command, ErrorCodes} 7 | 8 | @behaviour Command 9 | 10 | @impl Command 11 | def decode(<<>>), do: {:ok, %{}} 12 | 13 | @impl Command 14 | def decode_return_parameters(<>) do 15 | {:ok, decoded_status} = ErrorCodes.decode(status) 16 | {:ok, %{status: decoded_status, bd_addr: bd_addr}} 17 | end 18 | 19 | @impl Command 20 | def encode(%{}), do: {:ok, <<>>} 21 | 22 | @impl Command 23 | def encode_return_parameters(%{status: decoded_status, bd_addr: bd_addr}) do 24 | {:ok, encoded_status} = ErrorCodes.encode(decoded_status) 25 | {:ok, <>} 26 | end 27 | 28 | @impl Command 29 | def ocf(), do: 0x09 30 | end 31 | -------------------------------------------------------------------------------- /src/lib/harald/hci/commands/informational_parameters/read_buffer_size.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.InformationalParameters.ReadBufferSize do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 4, Part E, 7.4.5. 4 | """ 5 | 6 | alias Harald.HCI.{Commands.Command, ErrorCodes} 7 | 8 | @behaviour Command 9 | 10 | @impl Command 11 | def decode(<<>>), do: {:ok, %{}} 12 | 13 | @impl Command 14 | def decode_return_parameters( 15 | <> 18 | ) do 19 | {:ok, decoded_status} = ErrorCodes.decode(status) 20 | 21 | decoded_return_parameters = %{ 22 | status: decoded_status, 23 | acl_data_packet_length: acl_data_packet_length, 24 | synchronous_data_packet_length: synchronous_data_packet_length, 25 | total_num_acl_data_packets: total_num_acl_data_packets, 26 | total_num_synchronous_data_packets: total_num_synchronous_data_packets 27 | } 28 | 29 | {:ok, decoded_return_parameters} 30 | end 31 | 32 | @impl Command 33 | def encode(%{}), do: {:ok, <<>>} 34 | 35 | @impl Command 36 | def encode_return_parameters(%{ 37 | status: decoded_status, 38 | acl_data_packet_length: acl_data_packet_length, 39 | synchronous_data_packet_length: synchronous_data_packet_length, 40 | total_num_acl_data_packets: total_num_acl_data_packets, 41 | total_num_synchronous_data_packets: total_num_synchronous_data_packets 42 | }) do 43 | {:ok, encoded_status} = ErrorCodes.encode(decoded_status) 44 | 45 | encoded_return_parameters = 46 | <> 49 | 50 | {:ok, encoded_return_parameters} 51 | end 52 | 53 | @impl Command 54 | def ocf(), do: 0x05 55 | end 56 | -------------------------------------------------------------------------------- /src/lib/harald/hci/commands/le_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.LEController do 2 | @moduledoc """ 3 | Reference: version 5.2, vol 4, part E, 7.8. 4 | """ 5 | 6 | alias Harald.HCI.Commands 7 | 8 | alias Harald.HCI.Commands.LEController.{ 9 | ReadBufferSizeV1, 10 | ReadLocalSupportedFeatures, 11 | SetAdvertisingData, 12 | SetAdvertisingEnable, 13 | SetAdvertisingParameters, 14 | SetEventMask 15 | } 16 | 17 | @behaviour Commands 18 | 19 | @impl Commands 20 | def decode(ocf, bin) when is_integer(ocf) and is_binary(bin) do 21 | with {:ok, ocf_module} <- ocf_to_module(ocf), 22 | {:ok, parameters} <- ocf_module.decode(bin) do 23 | {:ok, {ocf_module, parameters}} 24 | end 25 | end 26 | 27 | @impl Commands 28 | def ogf(), do: 0x08 29 | 30 | @impl Commands 31 | def ocf_to_module(0x01), do: {:ok, SetEventMask} 32 | def ocf_to_module(0x02), do: {:ok, ReadBufferSizeV1} 33 | def ocf_to_module(0x03), do: {:ok, ReadLocalSupportedFeatures} 34 | def ocf_to_module(0x06), do: {:ok, SetAdvertisingParameters} 35 | def ocf_to_module(0x08), do: {:ok, SetAdvertisingData} 36 | def ocf_to_module(0x0A), do: {:ok, SetAdvertisingEnable} 37 | def ocf_to_module(ocf), do: {:error, {:not_implemented, {__MODULE__, ocf}}} 38 | end 39 | -------------------------------------------------------------------------------- /src/lib/harald/hci/commands/le_controller/read_buffer_size_v1.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.LEController.ReadBufferSizeV1 do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 4, Part E, 7.8.2. 4 | """ 5 | 6 | alias Harald.HCI.{Commands.Command, ErrorCodes} 7 | 8 | @behaviour Command 9 | 10 | @impl Command 11 | def encode(%{}), do: {:ok, <<>>} 12 | 13 | @impl Command 14 | def decode(<<>>), do: {:ok, %{}} 15 | 16 | @impl Command 17 | def decode_return_parameters( 18 | <> 20 | ) do 21 | {:ok, decoded_status} = ErrorCodes.decode(encoded_status) 22 | 23 | {:ok, 24 | %{ 25 | status: decoded_status, 26 | le_acl_data_packet_length: le_acl_data_packet_length, 27 | total_num_le_acl_data_packets: total_num_le_acl_data_packets 28 | }} 29 | end 30 | 31 | @impl Command 32 | def encode_return_parameters(%{ 33 | status: decoded_status, 34 | le_acl_data_packet_length: le_acl_data_packet_length, 35 | total_num_le_acl_data_packets: total_num_le_acl_data_packets 36 | }) do 37 | {:ok, encoded_status} = ErrorCodes.encode(decoded_status) 38 | 39 | {:ok, 40 | <>} 41 | end 42 | 43 | @impl Command 44 | def ocf(), do: 0x02 45 | end 46 | -------------------------------------------------------------------------------- /src/lib/harald/hci/commands/le_controller/set_advertising_data.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.LEController.SetAdvertisingData do 2 | @moduledoc """ 3 | Reference: version 5.2, vol 4, part e, 7.8.7. 4 | 5 | ## Encode 6 | 7 | If `advertising_data_length` 8 | 9 | - is provided: the value will be used regardless of whether it is accurate. 10 | - is not provided: the value will be calculated automatically. 11 | """ 12 | 13 | alias Harald.{HCI.Commands.Command, Host.AdvertisingData} 14 | 15 | @behaviour Command 16 | 17 | @advertising_data_size 31 18 | 19 | @impl Command 20 | def encode(%{advertising_data: decoded_advertising_data} = decoded_parameters) do 21 | {:ok, encoded_advertising_data} = AdvertisingData.encode(decoded_advertising_data) 22 | 23 | padded_encoded_advertising_data = 24 | String.pad_trailing(encoded_advertising_data, @advertising_data_size, <<0>>) 25 | 26 | advertising_data_length = 27 | Map.get_lazy(decoded_parameters, :advertising_data_length, fn -> 28 | byte_size(encoded_advertising_data) 29 | end) 30 | 31 | encoded = << 32 | advertising_data_length, 33 | padded_encoded_advertising_data::binary-size(@advertising_data_size) 34 | >> 35 | 36 | {:ok, encoded} 37 | end 38 | 39 | def encode(_), do: {:error, :encode} 40 | 41 | @impl Command 42 | def decode( 43 | <> 44 | ) do 45 | {:ok, decoded_advertising_data} = AdvertisingData.decode(encoded_advertising_data) 46 | 47 | decoded = %{ 48 | advertising_data_length: advertising_data_length, 49 | advertising_data: decoded_advertising_data 50 | } 51 | 52 | {:ok, decoded} 53 | end 54 | 55 | def decode(_), do: {:error, :decode} 56 | 57 | @impl Command 58 | def decode_return_parameters(<>), do: {:ok, %{status: status}} 59 | 60 | @impl Command 61 | def encode_return_parameters(%{status: status}), do: {:ok, <>} 62 | 63 | @impl Command 64 | def ocf(), do: 0x08 65 | end 66 | -------------------------------------------------------------------------------- /src/lib/harald/hci/commands/le_controller/set_advertising_enable.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.LEController.SetAdvertisingEnable do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 4, Part E, 7.8.9. 4 | """ 5 | 6 | alias Harald.HCI.{Commands.Command, ErrorCodes} 7 | 8 | @behaviour Command 9 | 10 | @impl Command 11 | def decode(<<0>>), do: {:ok, %{advertising_enable: false}} 12 | def decode(<<1>>), do: {:ok, %{advertising_enable: true}} 13 | 14 | @impl Command 15 | def decode_return_parameters(<>) do 16 | {:ok, decoded_status} = ErrorCodes.decode(status) 17 | {:ok, %{status: decoded_status}} 18 | end 19 | 20 | @impl Command 21 | def encode(%{advertising_enable: false}), do: {:ok, <<0>>} 22 | def encode(%{advertising_enable: true}), do: {:ok, <<1>>} 23 | 24 | @impl Command 25 | def encode_return_parameters(%{status: status}) do 26 | {:ok, encoded_status} = ErrorCodes.encode(status) 27 | {:ok, <>} 28 | end 29 | 30 | @impl Command 31 | def ocf(), do: 0x0A 32 | end 33 | -------------------------------------------------------------------------------- /src/lib/harald/hci/commands/le_controller/set_advertising_parameters.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.LEController.SetAdvertisingParameters do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 4, Part E, 7.8.5. 4 | """ 5 | 6 | alias Harald.HCI.Commands.Command 7 | 8 | @behaviour Command 9 | 10 | @impl Command 11 | def encode(%{ 12 | advertising_interval_min: advertising_interval_min, 13 | advertising_interval_max: advertising_interval_max, 14 | advertising_type: advertising_type, 15 | own_address_type: own_address_type, 16 | peer_address_type: peer_address_type, 17 | peer_address: peer_address, 18 | advertising_channel_map: advertising_channel_map, 19 | advertising_filter_policy: advertising_filter_policy 20 | }) do 21 | peer_address = String.pad_trailing(peer_address, 6, <<0>>) 22 | 23 | bin = 24 | <> 27 | 28 | {:ok, bin} 29 | end 30 | 31 | def encode(%{pin_type: :fixed}), do: {:ok, <<1>>} 32 | 33 | @impl Command 34 | def decode( 35 | <> 39 | ) do 40 | parameters = %{ 41 | advertising_interval_min: advertising_interval_min, 42 | advertising_interval_max: advertising_interval_max, 43 | advertising_type: advertising_type, 44 | own_address_type: own_address_type, 45 | peer_address_type: peer_address_type, 46 | peer_address: peer_address, 47 | advertising_channel_map: advertising_channel_map, 48 | advertising_filter_policy: advertising_filter_policy 49 | } 50 | 51 | {:ok, parameters} 52 | end 53 | 54 | @impl Command 55 | def decode_return_parameters(<>), do: {:ok, %{status: status}} 56 | 57 | @impl Command 58 | def encode_return_parameters(%{status: status}), do: {:ok, <>} 59 | 60 | @impl Command 61 | def ocf(), do: 0x06 62 | end 63 | -------------------------------------------------------------------------------- /src/lib/harald/hci/error_codes.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.ErrorCodes do 2 | @moduledoc """ 3 | Reference: version 5.2, vol 1, part f. 4 | """ 5 | 6 | @error_codes %{ 7 | 0x00 => "Success", 8 | 0x01 => "Unknown HCI Command", 9 | 0x02 => "Unknown Connection Identifier", 10 | 0x03 => "Hardware Failure", 11 | 0x04 => "Page Timeout", 12 | 0x05 => "Authentication Failure", 13 | 0x06 => "PIN or Key Missing", 14 | 0x07 => "Memory Capacity Exceeded", 15 | 0x08 => "Connection Timeout", 16 | 0x09 => "Connection Limit Exceeded", 17 | 0x0A => "Synchronous Connection Limit To A Device Exceeded", 18 | 0x0B => "Connection Already Exists", 19 | 0x0C => "Command Disallowed", 20 | 0x0D => "Connection Rejected due to Limited Resources", 21 | 0x0E => "Connection Rejected Due To Security Reasons", 22 | 0x0F => "Connection Rejected due to Unacceptable BD_ADDR", 23 | 0x10 => "Connection Accept Timeout Exceeded", 24 | 0x11 => "Unsupported Feature or Parameter Value", 25 | 0x12 => "Invalid HCI Command Parameters", 26 | 0x13 => "Remote User Terminated Connection", 27 | 0x14 => "Remote Device Terminated Connection due to Low Resources", 28 | 0x15 => "Remote Device Terminated Connection due to Power Off", 29 | 0x16 => "Connection Terminated By Local Host", 30 | 0x17 => "Repeated Attempts", 31 | 0x18 => "Pairing Not Allowed", 32 | 0x19 => "Unknown LMP PDU", 33 | 0x1A => "Unsupported Remote Feature / Unsupported LMP Feature", 34 | 0x1B => "SCO Offset Rejected", 35 | 0x1C => "SCO Interval Rejected", 36 | 0x1D => "SCO Air Mode Rejected", 37 | 0x1E => "Invalid LMP Parameters / Invalid LL Parameters", 38 | 0x1F => "Unspecified Error", 39 | 0x20 => "Unsupported LMP Parameter Value / Unsupported LL Parameter Value", 40 | 0x21 => "Role Change Not Allowed", 41 | 0x22 => "LMP Response Timeout / LL Response Timeout", 42 | 0x23 => "LMP Error Transaction Collision / LL Procedure Collision", 43 | 0x24 => "LMP PDU Not Allowed", 44 | 0x25 => "Encryption Mode Not Acceptable", 45 | 0x26 => "Link Key cannot be Changed", 46 | 0x27 => "Requested QoS Not Supported", 47 | 0x28 => "Instant Passed", 48 | 0x29 => "Pairing With Unit Key Not Supported", 49 | 0x2A => "Different Transaction Collision", 50 | 0x2B => "Reserved for Future Use (0x2B)", 51 | 0x2C => "QoS Unacceptable Parameter", 52 | 0x2D => "QoS Rejected", 53 | 0x2E => "Channel Classification Not Supported", 54 | 0x2F => "Insufficient Security", 55 | 0x30 => "Parameter Out Of Mandatory Range", 56 | 0x31 => "Reserved for Future Use (0x31)", 57 | 0x32 => "Role Switch Pending", 58 | 0x33 => "Reserved for Future Use (0x33)", 59 | 0x34 => "Reserved Slot Violation", 60 | 0x35 => "Role Switch Failed", 61 | 0x36 => "Extended Inquiry Response Too Large", 62 | 0x37 => "Secure Simple Pairing Not Supported By Host", 63 | 0x38 => "Host Busy - Pairing", 64 | 0x39 => "Connection Rejected due to No Suitable Channel Found", 65 | 0x3A => "Controller Busy", 66 | 0x3B => "Unacceptable Connection Parameters", 67 | 0x3C => "Advertising Timeout", 68 | 0x3D => "Connection Terminated due to MIC Failure", 69 | 0x3E => "Connection Failed to be Established", 70 | 0x3F => "MAC Connection Failed", 71 | 0x40 => "Coarse Clock Adjustment Rejected but Will Try to Adjust Using Clock Dragging", 72 | 0x41 => "Type0 Submap Not Defined", 73 | 0x42 => "Unknown Advertising Identifier", 74 | 0x43 => "Limit Reached", 75 | 0x44 => "Operation Cancelled by Host" 76 | } 77 | 78 | Enum.each(@error_codes, fn 79 | {error_code, name} -> 80 | def decode(unquote(error_code)), do: {:ok, unquote(name)} 81 | def encode(unquote(name)), do: {:ok, unquote(error_code)} 82 | end) 83 | 84 | def decode(encoded_error_code) do 85 | {:error, {:decode, {__MODULE__, encoded_error_code}}} 86 | end 87 | 88 | def encode(decoded_error_code) do 89 | {:error, {:encode, {__MODULE__, decoded_error_code}}} 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /src/lib/harald/hci/events.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Events do 2 | @moduledoc """ 3 | Reference: version 5.0, vol 2, part E, 5.4.4. 4 | """ 5 | 6 | alias Harald.HCI.Events 7 | alias Harald.HCI.Events.Event 8 | 9 | alias Harald.HCI.Events.{ 10 | CommandComplete, 11 | DisconnectionComplete, 12 | HardwareError, 13 | LEMeta, 14 | NumberOfCompletedPackets 15 | } 16 | 17 | @type event_code() :: non_neg_integer() 18 | 19 | def encode(%Event{ 20 | code: _event_code, 21 | module: event_module, 22 | parameters: parameters 23 | }) do 24 | event_module.encode(parameters) 25 | end 26 | 27 | def decode(bin) when is_binary(bin) do 28 | case bin do 29 | <> -> 30 | with {:ok, event_module} <- event_code_to_module(event_code), 31 | {:ok, parameters} <- event_module.decode(data) do 32 | Events.new(event_module, parameters) 33 | else 34 | {:error, {:not_implemented, error, _bin}} -> {:error, {:not_implemented, error, bin}} 35 | end 36 | 37 | <<>> -> 38 | :error 39 | end 40 | end 41 | 42 | def event_code_to_module(0x05), do: {:ok, DisconnectionComplete} 43 | def event_code_to_module(0x0E), do: {:ok, CommandComplete} 44 | def event_code_to_module(0x10), do: {:ok, HardwareError} 45 | def event_code_to_module(0x13), do: {:ok, NumberOfCompletedPackets} 46 | def event_code_to_module(0x3E), do: {:ok, LEMeta} 47 | def event_code_to_module(event_code), do: {:error, {:not_implemented, {Event, event_code}}} 48 | 49 | def new(event_module, parameters) 50 | when is_atom(event_module) and (is_binary(parameters) or is_map(parameters)) do 51 | event = 52 | struct(Event, %{ 53 | code: event_module.event_code(), 54 | module: event_module, 55 | parameters: parameters 56 | }) 57 | 58 | {:ok, event} 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /src/lib/harald/hci/events/command_complete.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Events.CommandComplete do 2 | alias Harald.HCI.{Commands, Events.Event} 3 | 4 | @behaviour Event 5 | 6 | @impl Event 7 | def encode(%{ 8 | num_hci_command_packets: num_hci_command_packets, 9 | command_op_code: command_op_code, 10 | return_parameters: return_parameters 11 | }) do 12 | return_parameters = 13 | case command_op_code.ocf_module do 14 | :not_implemented when is_binary(return_parameters) -> 15 | return_parameters 16 | 17 | ocf_module when ocf_module != :not_implemented -> 18 | {:ok, return_parameters} = ocf_module.encode_return_parameters(return_parameters) 19 | return_parameters 20 | end 21 | 22 | {:ok, command_op_code} = Commands.encode_op_code(command_op_code.ogf, command_op_code.ocf) 23 | 24 | ret = << 25 | num_hci_command_packets, 26 | command_op_code::bytes-size(2), 27 | return_parameters::binary 28 | >> 29 | 30 | {:ok, ret} 31 | end 32 | 33 | @impl Event 34 | def decode( 35 | <> 36 | ) do 37 | {:ok, op_code} = Commands.decode_op_code(command_op_code) 38 | 39 | {command_op_code, return_parameters} = 40 | case Commands.ogf_to_module(op_code.ogf) do 41 | {:ok, ogf_module} -> 42 | case ogf_module.ocf_to_module(op_code.ocf) do 43 | {:ok, ocf_module} -> 44 | command_op_code = 45 | op_code 46 | |> Map.take([:ocf, :ogf]) 47 | |> Map.merge(%{ocf_module: ocf_module, ogf_module: ogf_module}) 48 | 49 | {:ok, return_parameters} = ocf_module.decode_return_parameters(return_parameters) 50 | {command_op_code, return_parameters} 51 | 52 | {:error, {:not_implemented, _}} -> 53 | {%{ 54 | ocf: op_code.ocf, 55 | ocf_module: :not_implemented, 56 | ogf: op_code.ogf, 57 | ogf_module: ogf_module 58 | }, return_parameters} 59 | end 60 | 61 | {:error, {:not_implemented, _}} -> 62 | {%{ 63 | ocf: op_code.ocf, 64 | ocf_module: :not_implemented, 65 | ogf: op_code.ogf, 66 | ogf_module: :not_implemented 67 | }, return_parameters} 68 | end 69 | 70 | parameters = %{ 71 | num_hci_command_packets: num_hci_command_packets, 72 | command_op_code: command_op_code, 73 | return_parameters: return_parameters 74 | } 75 | 76 | {:ok, parameters} 77 | end 78 | 79 | @impl Event 80 | def event_code(), do: 0x0E 81 | end 82 | -------------------------------------------------------------------------------- /src/lib/harald/hci/events/disconnection_complete.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Events.DisconnectionComplete do 2 | alias Harald.HCI.{ErrorCodes, Events.Event} 3 | 4 | @behaviour Event 5 | 6 | @impl Event 7 | def decode(<< 8 | encoded_status, 9 | connection_handle::little-size(16), 10 | encoded_reason 11 | >>) do 12 | {:ok, decoded_status} = ErrorCodes.decode(encoded_status) 13 | <> = <> 14 | {:ok, decoded_reason} = ErrorCodes.decode(encoded_reason) 15 | 16 | decoded_disconnection_complete = %{ 17 | status: decoded_status, 18 | connection_handle: %{rfu: rfu, handle: handle}, 19 | reason: decoded_reason 20 | } 21 | 22 | {:ok, decoded_disconnection_complete} 23 | end 24 | 25 | @impl Event 26 | def encode(%{ 27 | status: decoded_status, 28 | connection_handle: %{ 29 | rfu: rfu, 30 | handle: handle 31 | }, 32 | reason: decoded_reason 33 | }) do 34 | {:ok, encoded_status} = ErrorCodes.encode(decoded_status) 35 | <> = <> 36 | {:ok, encoded_reason} = ErrorCodes.encode(decoded_reason) 37 | 38 | encoded_disconnection_complete = << 39 | encoded_status, 40 | encoded_connection_handle::size(16), 41 | encoded_reason 42 | >> 43 | 44 | {:ok, encoded_disconnection_complete} 45 | end 46 | 47 | @impl Event 48 | def event_code(), do: 0x05 49 | end 50 | -------------------------------------------------------------------------------- /src/lib/harald/hci/events/event.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Events.Event do 2 | @enforce_keys [ 3 | :code, 4 | :module, 5 | :parameters 6 | ] 7 | defstruct [ 8 | :code, 9 | :module, 10 | :parameters 11 | ] 12 | 13 | @callback encode(Event.t()) :: {:ok, binary()} | {:error, any()} 14 | @callback decode(binary()) :: {:ok, Event.t()} | {:error, any()} 15 | @callback event_code() :: {:ok, Events.event_code()} | {:error, any()} 16 | end 17 | -------------------------------------------------------------------------------- /src/lib/harald/hci/events/hardware_error.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Events.HardwareError do 2 | alias Harald.HCI.Events.Event 3 | 4 | @behaviour Event 5 | 6 | @impl Event 7 | def decode(<>), do: {:ok, %{hardware_code: hardware_code}} 8 | 9 | @impl Event 10 | def encode(%{hardware_code: hardware_code}), do: {:ok, <>} 11 | 12 | @impl Event 13 | def event_code(), do: 0x10 14 | end 15 | -------------------------------------------------------------------------------- /src/lib/harald/hci/events/le_meta.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Events.LEMeta do 2 | alias Harald.HCI.Events.Event 3 | alias Harald.HCI.Events.LEMeta.{ConnectionComplete, ConnectionUpdateComplete} 4 | 5 | @behaviour Event 6 | 7 | @callback encode(Event.t()) :: {:ok, binary()} | {:error, any()} 8 | @callback decode(binary()) :: {:ok, Event.t()} | {:error, any()} 9 | @callback sub_event_code() :: {:ok, Events.event_code()} | {:error, any()} 10 | 11 | @impl Event 12 | def encode(%{ 13 | sub_event: %{code: code, module: sub_event_module}, 14 | sub_event_parameters: sub_event_parameters 15 | }) do 16 | {:ok, encoded_sub_event} = sub_event_module.encode(sub_event_parameters) 17 | {:ok, <>} 18 | end 19 | 20 | @impl Event 21 | def decode(<> = encoded_bin) do 22 | with {:ok, sub_event_module} <- decode_sub_event_code(sub_event_code), 23 | {:ok, sub_event_parameters} <- sub_event_module.decode(sub_event_parameters) do 24 | decoded_le_meta = %{ 25 | sub_event: %{code: sub_event_code, module: sub_event_module}, 26 | sub_event_parameters: sub_event_parameters 27 | } 28 | 29 | {:ok, decoded_le_meta} 30 | else 31 | {:error, {:not_implemented, error}} -> {:error, {:not_implemented, error, encoded_bin}} 32 | end 33 | end 34 | 35 | def decode_sub_event_code(0x01), do: {:ok, ConnectionComplete} 36 | def decode_sub_event_code(0x03), do: {:ok, ConnectionUpdateComplete} 37 | 38 | def decode_sub_event_code(sub_event_code) do 39 | {:error, {:not_implemented, {__MODULE__, sub_event_code}}} 40 | end 41 | 42 | @impl Event 43 | def event_code(), do: 0x3E 44 | 45 | def new(sub_event_module, parameters) when is_atom(sub_event_module) and is_map(parameters) do 46 | event = 47 | struct(Event, %{ 48 | code: event_code(), 49 | module: __MODULE__, 50 | parameters: %{ 51 | sub_event: %{ 52 | code: sub_event_module.sub_event_code(), 53 | module: sub_event_module 54 | }, 55 | sub_event_parameters: parameters 56 | } 57 | }) 58 | 59 | {:ok, event} 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /src/lib/harald/hci/events/le_meta/advertising_report.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Events.LEMeta.AdvertisingReport do 2 | # @moduledoc """ 3 | # A struct representing a LE Advertising Report. 4 | 5 | # > The LE Advertising Report event indicates that one or more Bluetooth devices have responded to 6 | # > an active scan or have broadcast advertisements that were received during a passive scan. The 7 | # > Controller may queue these advertising reports and send information from multiple devices in 8 | # > one LE Advertising Report event. 9 | # > 10 | # > This event shall only be generated if scanning was enabled using the LE Set Scan Enable 11 | # > command. It only reports advertising events that used legacy advertising PDUs. 12 | 13 | # Reference: Version 5.0, Vol 2, Part E, 7.7.65.2 14 | # """ 15 | 16 | # alias Harald.HCI.{Event.LEMeta.AdvertisingReport.Device} 17 | # alias Harald.Serializable 18 | 19 | # @enforce_keys [:devices] 20 | 21 | # defstruct @enforce_keys 22 | 23 | # @type t :: %__MODULE__{} 24 | 25 | # @behaviour Serializable 26 | 27 | # @subevent_code 0x02 28 | 29 | # @doc """ 30 | # See: `t:Harald.HCI.Event.LEMeta.subevent_code/0`. 31 | # """ 32 | # def subevent_code, do: @subevent_code 33 | 34 | # @doc """ 35 | # Serializes a `Harald.HCI.Event.LEMeta.AdvertisingReport` struct into a LE Advertising Report 36 | # Event. 37 | 38 | # iex> AdvertisingReport.serialize( 39 | # ...> %AdvertisingReport{ 40 | # ...> devices: [ 41 | # ...> %AdvertisingReport.Device{ 42 | # ...> event_type: 0, 43 | # ...> address_type: 1, 44 | # ...> address: 2, 45 | # ...> data: [], 46 | # ...> rss: 4 47 | # ...> }, 48 | # ...> %AdvertisingReport.Device{ 49 | # ...> event_type: 1, 50 | # ...> address_type: 2, 51 | # ...> address: 5, 52 | # ...> data: [{"Service Data - 32-bit UUID", %{uuid: 1, data: <<2>>}}], 53 | # ...> rss: 7 54 | # ...> } 55 | # ...> ] 56 | # ...> } 57 | # ...> ) 58 | # {:ok, 59 | # <<2, 2, 0, 1, 1, 2, 2, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 7, 6, 32, 1, 0, 0, 0, 2, 4, 7>>} 60 | # """ 61 | # @impl Serializable 62 | # def serialize(advertising_report) do 63 | # {:ok, bin} = Device.serialize(advertising_report.devices) 64 | # {:ok, <<@subevent_code, bin::binary>>} 65 | # end 66 | 67 | # @doc """ 68 | # Deserializes a LE Advertising Report Event into `Harald.HCI.Event.LEMeta.AdvertisingReport` 69 | # structs. 70 | 71 | # iex> AdvertisingReport.deserialize( 72 | # ...> <<2, 2, 0, 1, 1, 2, 2, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 7, 6, 32, 1, 0, 0, 0, 2, 4, 73 | # ...> 7>> 74 | # ...> ) 75 | # {:ok, %AdvertisingReport{ 76 | # devices: [ 77 | # %AdvertisingReport.Device{ 78 | # event_type: 0, 79 | # address_type: 1, 80 | # address: 2, 81 | # data: [], 82 | # rss: 4 83 | # }, 84 | # %AdvertisingReport.Device{ 85 | # event_type: 1, 86 | # address_type: 2, 87 | # address: 5, 88 | # data: [{"Service Data - 32-bit UUID", %{uuid: 1, data: <<2>>}}], 89 | # rss: 7 90 | # } 91 | # ] 92 | # } 93 | # } 94 | # """ 95 | # @impl Serializable 96 | # def deserialize(<<@subevent_code, arrayed_bin::binary>>) do 97 | # case Device.deserialize(arrayed_bin) do 98 | # {status, devices} -> {status, %__MODULE__{devices: devices}} 99 | # end 100 | # end 101 | 102 | # def deserialize(bin), do: {:error, bin} 103 | end 104 | -------------------------------------------------------------------------------- /src/lib/harald/hci/events/le_meta/connection_complete.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Events.LEMeta.ConnectionComplete do 2 | alias Harald.HCI.Events.LEMeta 3 | 4 | @behaviour LEMeta 5 | 6 | @impl LEMeta 7 | def decode(<< 8 | status, 9 | connection_handle::little-size(16), 10 | role, 11 | peer_address_type, 12 | peer_address::little-size(48), 13 | connection_interval::little-size(16), 14 | connection_latency::little-size(16), 15 | supervision_timeout::little-size(16), 16 | master_clock_accuracy 17 | >>) do 18 | <> = 19 | <> 20 | 21 | parameters = %{ 22 | status: status, 23 | connection_handle: %{rfu: connection_handle_rfu, handle: connection_handle}, 24 | role: role, 25 | peer_address_type: peer_address_type, 26 | peer_address: peer_address, 27 | connection_latency: connection_latency, 28 | connection_interval: connection_interval, 29 | supervision_timeout: supervision_timeout, 30 | master_clock_accuracy: master_clock_accuracy 31 | } 32 | 33 | {:ok, parameters} 34 | end 35 | 36 | @impl LEMeta 37 | def encode(%{ 38 | status: status, 39 | connection_handle: %{rfu: connection_handle_rfu, handle: connection_handle}, 40 | role: role, 41 | peer_address_type: peer_address_type, 42 | peer_address: peer_address, 43 | connection_latency: connection_latency, 44 | connection_interval: connection_interval, 45 | supervision_timeout: supervision_timeout, 46 | master_clock_accuracy: master_clock_accuracy 47 | }) do 48 | <> = 49 | <> 50 | 51 | {:ok, 52 | << 53 | status, 54 | connection_handle::size(16), 55 | role, 56 | peer_address_type, 57 | peer_address::little-size(48), 58 | connection_interval::little-size(16), 59 | connection_latency::little-size(16), 60 | supervision_timeout::little-size(16), 61 | master_clock_accuracy 62 | >>} 63 | end 64 | 65 | @impl LEMeta 66 | def sub_event_code(), do: 0x01 67 | end 68 | -------------------------------------------------------------------------------- /src/lib/harald/hci/events/le_meta/connection_update_complete.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Events.LEMeta.ConnectionUpdateComplete do 2 | alias Harald.HCI.Events.LEMeta 3 | 4 | @behaviour LEMeta 5 | 6 | @impl LEMeta 7 | def encode(%{ 8 | status: status, 9 | connection_handle: connection_handle, 10 | connection_interval: connection_interval, 11 | connection_latency: connection_latency, 12 | supervision_timeout: supervision_timeout 13 | }) do 14 | {:ok, 15 | << 16 | status, 17 | connection_handle::binary-little-size(2), 18 | connection_interval::little-size(16), 19 | connection_latency::little-size(16), 20 | supervision_timeout::little-size(16) 21 | >>} 22 | end 23 | 24 | @impl LEMeta 25 | def decode(<< 26 | status, 27 | connection_handle::binary-little-size(2), 28 | connection_interval::little-size(16), 29 | connection_latency::little-size(16), 30 | supervision_timeout::little-size(16) 31 | >>) do 32 | parameters = %{ 33 | status: status, 34 | connection_handle: connection_handle, 35 | connection_interval: connection_interval, 36 | connection_latency: connection_latency, 37 | supervision_timeout: supervision_timeout 38 | } 39 | 40 | {:ok, parameters} 41 | end 42 | 43 | @impl LEMeta 44 | def sub_event_code(), do: 0x03 45 | end 46 | -------------------------------------------------------------------------------- /src/lib/harald/hci/events/number_of_completed_packets.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Events.NumberOfCompletedPackets do 2 | alias Harald.HCI.Events.Event 3 | 4 | @behaviour Event 5 | 6 | @impl Event 7 | def encode(%{num_handles: num_handles, handles: handles}) do 8 | {connection_handles, num_completed_packets} = 9 | Enum.reduce(handles, {<<>>, <<>>}, fn 10 | {handle, handle_data}, {acc_handle, acc_num_completed} -> 11 | <> = <> 12 | acc_handle = <> 13 | 14 | acc_num_completed = 15 | <> 16 | 17 | {acc_handle, acc_num_completed} 18 | end) 19 | 20 | {:ok, <>} 21 | end 22 | 23 | @impl Event 24 | def decode(<>) do 25 | {handles, remaining_bin} = 26 | Enum.reduce(1..num_handles, {%{}, arrayed_data}, fn 27 | index, {handles, <>} -> 28 | <> = 29 | <> 30 | 31 | {Map.put(handles, index, {connection_handle, connection_handle_rfu}), the_rem_bin} 32 | end) 33 | 34 | {handles, <<>>} = 35 | Enum.reduce(1..num_handles, {handles, remaining_bin}, fn 36 | index, {handles, <>} -> 37 | {Map.update!(handles, index, fn {connection_handle, connection_handle_rfu} -> 38 | {connection_handle, 39 | %{num_completed_packets: num_completed_packets, rfu: connection_handle_rfu}} 40 | end), the_rem_bin} 41 | end) 42 | 43 | handles = 44 | Enum.into(handles, %{}, fn {_, {num_handle, num_completed}} -> 45 | {num_handle, num_completed} 46 | end) 47 | 48 | {:ok, %{num_handles: num_handles, handles: handles}} 49 | end 50 | 51 | @impl Event 52 | def event_code(), do: 0x13 53 | end 54 | -------------------------------------------------------------------------------- /src/lib/harald/hci/packet.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Packet do 2 | @moduledoc """ 3 | Reference: version 5.0, Vol 4, Part A, 2. 4 | """ 5 | 6 | @type type() :: type_command() | type_acl_data() | type_synchronous_data() | type_event() 7 | 8 | @type type_command() :: :command 9 | @type type_acl_data() :: :acl_data 10 | @type type_synchronous_data() :: :synchronous_data 11 | @type type_event() :: :event 12 | 13 | @type indicator() :: 14 | indicator_command() 15 | | indicator_acl_data() 16 | | indicator_synchronous_data() 17 | | indicator_event() 18 | 19 | @type indicator_command() :: 1 20 | @type indicator_acl_data() :: 2 21 | @type indicator_synchronous_data() :: 3 22 | @type indicator_event() :: 4 23 | 24 | @spec indicator(type()) :: indicator() 25 | def indicator(:command), do: 1 26 | 27 | def indicator(:acl_data), do: 2 28 | 29 | def indicator(:synchronous_data), do: 3 30 | 31 | def indicator(:event), do: 4 32 | end 33 | -------------------------------------------------------------------------------- /src/lib/harald/hci/synchronous_data.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.SynchronousData do 2 | @moduledoc """ 3 | Reference: version 5.2, vol 4, part E, 5.4.3. 4 | """ 5 | 6 | @enforce_keys [ 7 | :connection_handle, 8 | :packet_status_flag, 9 | :rfu, 10 | :data_total_length, 11 | :data 12 | ] 13 | 14 | defstruct [ 15 | :connection_handle, 16 | :packet_status_flag, 17 | :rfu, 18 | :data_total_length, 19 | :data 20 | ] 21 | 22 | def decode(<< 23 | connection_handle::bits-size(12), 24 | encoded_packet_status_flag::size(2), 25 | rfu::size(2), 26 | data_total_length, 27 | data::binary-size(data_total_length) 28 | >>) do 29 | decoded = %__MODULE__{ 30 | connection_handle: connection_handle, 31 | packet_status_flag: decode_packet_status_flag!(encoded_packet_status_flag), 32 | rfu: rfu, 33 | data_total_length: data_total_length, 34 | data: data 35 | } 36 | 37 | {:ok, decoded} 38 | end 39 | 40 | def encode(%__MODULE__{ 41 | connection_handle: connection_handle, 42 | packet_status_flag: decoded_packet_status_flag, 43 | rfu: rfu, 44 | data_total_length: data_total_length, 45 | data: data 46 | }) do 47 | encoded_packet_status_flag = encode_packet_status_flag!(decoded_packet_status_flag) 48 | 49 | encoded = << 50 | connection_handle::bits-size(12), 51 | encoded_packet_status_flag::size(2), 52 | rfu::size(2), 53 | data_total_length, 54 | data::binary 55 | >> 56 | 57 | {:ok, encoded} 58 | end 59 | 60 | def new(connection_handle, packet_status_flag, rfu, data) do 61 | synchronous_data = %__MODULE__{ 62 | connection_handle: connection_handle, 63 | packet_status_flag: packet_status_flag, 64 | rfu: rfu, 65 | data_total_length: byte_size(data), 66 | data: data 67 | } 68 | 69 | {:ok, synchronous_data} 70 | end 71 | 72 | defp decode_packet_status_flag!(0b00 = bc_flag) do 73 | %{ 74 | description: 75 | "Correctly received data. The payload data belongs to received eSCO or SCO packets that the baseband marked as \"good data\".", 76 | value: bc_flag 77 | } 78 | end 79 | 80 | defp decode_packet_status_flag!(0b01 = bc_flag) do 81 | %{ 82 | description: 83 | "Possibly invalid data. At least one eSCO packet has been marked by the baseband as \"data with possible errors\" and all others have been marked as \"good data\" in the eSCO interval(s) corresponding to the HCI Synchronous Data packet.", 84 | value: bc_flag 85 | } 86 | end 87 | 88 | defp decode_packet_status_flag!(0b10 = bc_flag) do 89 | %{ 90 | description: 91 | "No data received. All data from the baseband received during the (e)SCO interval(s) corresponding to the HCI Synchronous Data packet have been marked as \"lost data\" by the baseband. The Payload data octets shall be set to 0.", 92 | value: bc_flag 93 | } 94 | end 95 | 96 | defp decode_packet_status_flag!(0b11 = bc_flag) do 97 | %{ 98 | description: 99 | "Data partially lost. Not all, but at least one (e)SCO packet has been marked as \"lost data\" by the baseband in the (e)SCO intervals corresponding to the HCI Synchronous Data packet. The payload data octets corresponding to the missing (e)SCO packets shall be set to 0.", 100 | value: bc_flag 101 | } 102 | end 103 | 104 | defp encode_packet_status_flag!(%{value: encoded_bc_flag}) 105 | when encoded_bc_flag in [0b00, 0b01, 0b10, 0b11] do 106 | encoded_bc_flag 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /src/lib/harald/hci/transport.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Transport do 2 | @moduledoc """ 3 | A server to manage lower level transports and parse bluetooth events. 4 | """ 5 | 6 | use GenServer 7 | 8 | @type adapter() :: module() 9 | @type adapter_opts() :: keyword(any()) 10 | @type id() :: atom() 11 | 12 | @typedoc """ 13 | ## Options 14 | 15 | `:adapter_opts` - `adapter_opts()`. `[]`. The options provided to the adapter on start. 16 | `:adapter` - `adapter()`. Required. The transport implementation module. 17 | `:id` - `atom()`. Required. Uniquely identifies this transport instance to Harald. 18 | `:subscriber_pids` - `MapSet.t()`. `#MapSet<[]>`. The pids that received data and events will be 19 | sent to. 20 | """ 21 | @type start_link_opts() :: [ 22 | {:adapter, adapter()}, 23 | {:adapter_opts, adapter_opts()}, 24 | {:id, id()}, 25 | {:subscriber_pids, MapSet.t()} 26 | ] 27 | 28 | @impl GenServer 29 | def handle_call({:write, bin}, _from, state) do 30 | {:ok, adapter_state} = state.adapter.write(bin, state.adapter_state) 31 | {:reply, :ok, Map.put(state, :adapter_state, adapter_state)} 32 | end 33 | 34 | def handle_call({:subscribe, pid}, _from, state) do 35 | {:reply, :ok, Map.put(state, :subscriber_pids, MapSet.put(state.subscriber_pids, pid))} 36 | end 37 | 38 | def handle_call({:publish, data_or_event}, _from, state) do 39 | for pid <- state.subscriber_pids do 40 | send(pid, {Harald, data_or_event}) 41 | end 42 | 43 | {:reply, :ok, state} 44 | end 45 | 46 | @impl GenServer 47 | def handle_continue(:setup, state) do 48 | {:ok, adapter_state} = 49 | state.adapter_opts 50 | |> Keyword.put(:transport_pid, self()) 51 | |> state.adapter.setup() 52 | 53 | {:noreply, Map.put(state, :adapter_state, adapter_state)} 54 | end 55 | 56 | @impl GenServer 57 | def init(args) do 58 | state = %{ 59 | adapter: args.adapter, 60 | adapter_opts: args.adapter_opts, 61 | adapter_state: %{}, 62 | id: args.id, 63 | name: args.name, 64 | subscriber_pids: args.subscriber_pids 65 | } 66 | 67 | {:ok, state, {:continue, :setup}} 68 | end 69 | 70 | @doc "Returns the registered name derived from `id`." 71 | def name(id), do: String.to_atom("Harald.Transport.Name.#{id}") 72 | 73 | def publish(transport_pid, data_or_event) do 74 | GenServer.call(transport_pid, {:publish, data_or_event}) 75 | end 76 | 77 | @doc """ 78 | Start the transport. 79 | """ 80 | @spec start_link(start_link_opts()) :: GenServer.on_start() 81 | def start_link(opts) do 82 | opts_map = Map.new(opts) 83 | 84 | with {true, :adapter} <- {Map.has_key?(opts_map, :adapter), :adapter}, 85 | {true, :id} <- {Map.has_key?(opts_map, :id), :id} do 86 | name = name(opts_map.id) 87 | 88 | args = 89 | opts_map 90 | |> Map.put(:name, name) 91 | |> Map.put_new(:adapter_opts, []) 92 | |> Map.update(:subscriber_pids, MapSet.new(), &Enum.into(&1, MapSet.new())) 93 | 94 | GenServer.start_link(__MODULE__, args, name: name) 95 | else 96 | {false, :adapter} -> {:error, {:args, %{adapter: ["required"]}}} 97 | {false, :id} -> {:error, {:args, %{id: ["required"]}}} 98 | end 99 | end 100 | 101 | def subscribe(id, pid \\ self()) do 102 | id 103 | |> name() 104 | |> GenServer.call({:subscribe, pid}) 105 | end 106 | 107 | def write(id, bin) when is_binary(bin) do 108 | id 109 | |> name() 110 | |> GenServer.call({:write, bin}) 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /src/lib/harald/hci/transport/adapter.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Transport.Adapter do 2 | @moduledoc """ 3 | A behaviour for transport adapters. 4 | """ 5 | 6 | alias Harald.Transport 7 | 8 | @type setup_opts() :: [{:transport_pid, pid()}] 9 | 10 | @callback setup(setup_opts()) :: {:ok, Transport.adapter_state()} 11 | @callback write(binary(), Transport.adapter_state()) :: 12 | {:ok, Transport.adapter_state()} | {:error, any()} 13 | end 14 | -------------------------------------------------------------------------------- /src/lib/harald/hci/transport/handler.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Transport.Handler do 2 | @moduledoc """ 3 | A behaviour for transport handlers. 4 | """ 5 | 6 | @callback setup(args :: keyword) :: GenServer.on_start() 7 | end 8 | -------------------------------------------------------------------------------- /src/lib/harald/hci/transport/uart.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Transport.UART do 2 | @moduledoc """ 3 | Reference: version 5.0, vol 4, part A, 1. 4 | """ 5 | 6 | use GenServer 7 | use Hook 8 | alias Harald.HCI.{Packet, Transport} 9 | alias Harald.HCI.Transport.{Adapter, UART.Framing} 10 | 11 | @behaviour Adapter 12 | 13 | @impl GenServer 14 | def init(args) do 15 | {:ok, pid} = hook(Circuits.UART).start_link(name: __MODULE__.Circuits.UART) 16 | adapter_opts = [active: true, framing: {Framing, []}, speed: 115_200, flow_control: :hardware] 17 | :ok = hook(Circuits.UART).open(pid, args[:device], adapter_opts) 18 | {:ok, %{uart_pid: pid, transport_pid: args[:transport_pid]}} 19 | end 20 | 21 | @impl Adapter 22 | def setup(opts) do 23 | with {true, :device} <- {Keyword.has_key?(opts, :device), :device} do 24 | {:ok, pid} = GenServer.start_link(__MODULE__, opts) 25 | {:ok, %{adapter_pid: pid}} 26 | else 27 | {false, :device} -> {:error, {:args, %{device: ["required"]}}} 28 | end 29 | end 30 | 31 | @impl GenServer 32 | def handle_call({:write, bin}, _from, %{uart_pid: uart_pid} = state) do 33 | with :ok <- hook(Circuits.UART).write(uart_pid, bin) do 34 | {:reply, :ok, state} 35 | else 36 | :error -> {:reply, {:error, :circuits_uart_write}} 37 | end 38 | end 39 | 40 | @impl GenServer 41 | def handle_info( 42 | {:circuits_uart, _dev, indicator_and_packet}, 43 | %{transport_pid: transport_pid} = state 44 | ) do 45 | event_indicator = Packet.indicator(:event) 46 | acl_data_indicator = Packet.indicator(:acl_data) 47 | synchronous_data_indicator = Packet.indicator(:synchronous_data) 48 | 49 | tagged_payload = 50 | case indicator_and_packet do 51 | <<^acl_data_indicator, packet::binary()>> -> 52 | Harald.decode_acl_data(<<2, packet::binary>>) 53 | 54 | <<^synchronous_data_indicator, packet::binary()>> -> 55 | Harald.decode_synchronous_data(packet) 56 | 57 | <<^event_indicator, packet::binary()>> -> 58 | Harald.decode_event(packet) 59 | end 60 | 61 | :ok = Transport.publish(transport_pid, tagged_payload) 62 | {:noreply, state} 63 | end 64 | 65 | @impl Adapter 66 | def write(bin, state) do 67 | :ok = GenServer.call(state.adapter_pid, {:write, bin}) 68 | {:ok, state} 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /src/lib/harald/host/advertising_data.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.AdvertisingData do 2 | @moduledoc """ 3 | Reference: version 5.2, vol 3, part c, 11. 4 | """ 5 | 6 | alias Harald.Host.AdvertisingData.{ 7 | CompleteLocalName, 8 | CompleteListOf128BitServiceClassUUIDs 9 | } 10 | 11 | @callback ad_type() :: pos_integer() 12 | @callback decode(binary()) :: {:ok, map()} | {:error, :decode} 13 | @callback encode(map()) :: {:ok, binary()} | {:error, :encode} 14 | @callback new_ad_structure(map()) :: {:ok, map()} | {:error, :new_advertising_data} 15 | @callback new_ad_structure!(map()) :: map() 16 | 17 | def ad_type_to_module(0x09), do: {:ok, CompleteLocalName} 18 | def ad_type_to_module(0x07), do: {:ok, CompleteListOf128BitServiceClassUUIDs} 19 | def ad_type_to_module(ad_type), do: {:error, {:not_implemented, {__MODULE__, ad_type}}} 20 | 21 | def decode(encoded_ad_structures) do 22 | decoded_ad_structures = do_decode(encoded_ad_structures) 23 | {:ok, decoded_ad_structures} 24 | end 25 | 26 | def decode_ad_structure(<>) 27 | when byte_size(encoded_ad_data) == length - 1 do 28 | decoded_ad_structure = encoded_ad_data_to_decoded_ad_structure(ad_type, encoded_ad_data) 29 | 30 | {:ok, decoded_ad_structure} 31 | end 32 | 33 | def encode(decoded_advertising_data) do 34 | decoded_advertising_data.ad_structures 35 | |> Enum.reduce_while(<<>>, fn decoded_ad_structure, acc -> 36 | case encode_ad_structure(decoded_ad_structure) do 37 | {:ok, encoded_ad_structure} -> {:cont, acc <> encoded_ad_structure} 38 | _ -> {:halt, {:error, :encode}} 39 | end 40 | end) 41 | |> case do 42 | {:error, _} = e -> e 43 | encoded_ad_structures -> {:ok, encoded_ad_structures} 44 | end 45 | end 46 | 47 | def encode_ad_structure(decoded_ad_structure) do 48 | {:ok, encoded_ad_data} = decoded_ad_structure.module.encode(decoded_ad_structure.ad_data) 49 | length = byte_size(encoded_ad_data) + 1 50 | encoded_ad_structure = <> 51 | {:ok, encoded_ad_structure} 52 | end 53 | 54 | def new_ad_structure(ad_type_module, decoded_ad_data) do 55 | new_ad_structure(ad_type_module, ad_type_module.ad_type(), decoded_ad_data) 56 | end 57 | 58 | def new_ad_structure(ad_type_module, ad_type, decoded_ad_data) do 59 | %{ 60 | ad_data: decoded_ad_data, 61 | ad_type: ad_type, 62 | module: ad_type_module 63 | } 64 | end 65 | 66 | defp do_decode(rest, decoded_ad_structures \\ []) 67 | 68 | defp do_decode(<<>> = rest, decoded_ad_structures) do 69 | decoded_ad_structures = Enum.reverse(decoded_ad_structures) 70 | %{ad_structures: decoded_ad_structures, insignificant_octets: rest} 71 | end 72 | 73 | defp do_decode(<<0, _::binary>> = rest, decoded_ad_structures) do 74 | case :binary.decode_unsigned(rest) do 75 | 0 -> 76 | decoded_ad_structures = Enum.reverse(decoded_ad_structures) 77 | %{ad_structures: decoded_ad_structures, insignificant_octets: rest} 78 | 79 | _ -> 80 | {:error, :decode} 81 | end 82 | end 83 | 84 | defp do_decode(<>, decoded_ad_structures) do 85 | ad_data_length = length - 1 86 | <> = rest 87 | decoded_ad_structure = encoded_ad_data_to_decoded_ad_structure(ad_type, encoded_ad_data) 88 | decoded_ad_structures = [decoded_ad_structure | decoded_ad_structures] 89 | do_decode(rest, decoded_ad_structures) 90 | end 91 | 92 | defp encoded_ad_data_to_decoded_ad_structure(ad_type, encoded_ad_data) do 93 | {:ok, ad_type_module} = ad_type_to_module(ad_type) 94 | {:ok, decoded_ad_data} = ad_type_module.decode(encoded_ad_data) 95 | new_ad_structure(ad_type_module, ad_type, decoded_ad_data) 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /src/lib/harald/host/advertising_data/complete_list_of_128_bit_service_class_uuids.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.AdvertisingData.CompleteListOf128BitServiceClassUUIDs do 2 | @moduledoc """ 3 | Reference: css v9, part a, 1.1.2 4 | 5 | Additional references: 6 | 7 | - https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile/. 8 | """ 9 | 10 | alias Harald.Host.AdvertisingData 11 | 12 | @behaviour AdvertisingData 13 | 14 | @impl AdvertisingData 15 | def ad_type(), do: 0x07 16 | 17 | @impl AdvertisingData 18 | def decode(service_uuids) do 19 | service_uuid_list = for <>, do: service_uuid 20 | 21 | {:ok, %{service_uuid_list: service_uuid_list}} 22 | end 23 | 24 | @impl AdvertisingData 25 | def encode(%{service_uuid_list: service_uuid_list}) do 26 | bin = for uuid <- service_uuid_list, do: <> 27 | 28 | service_uuids = Enum.join(bin) 29 | 30 | {:ok, service_uuids} 31 | end 32 | 33 | @impl AdvertisingData 34 | def new_ad_structure(service_uuid_list) when is_list(service_uuid_list) do 35 | ret = AdvertisingData.new_ad_structure(__MODULE__, %{service_uuid_list: service_uuid_list}) 36 | 37 | {:ok, ret} 38 | end 39 | 40 | def new_ad_structure(_), do: {:error, :new_ad_structure} 41 | 42 | @impl AdvertisingData 43 | def new_ad_structure!(service_uuid_list) when is_list(service_uuid_list) do 44 | AdvertisingData.new_ad_structure(__MODULE__, %{ 45 | service_class_uuid_list: service_uuid_list 46 | }) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /src/lib/harald/host/advertising_data/complete_local_name.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.AdvertisingData.CompleteLocalName do 2 | @moduledoc """ 3 | Reference: css v9, part a, 1.2. 4 | 5 | Additional references: 6 | 7 | - https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile/. 8 | """ 9 | 10 | alias Harald.Host.AdvertisingData 11 | 12 | @behaviour AdvertisingData 13 | 14 | @impl AdvertisingData 15 | def ad_type(), do: 0x09 16 | 17 | @impl AdvertisingData 18 | def decode(complete_local_name), do: {:ok, %{complete_local_name: complete_local_name}} 19 | 20 | @impl AdvertisingData 21 | def encode(%{complete_local_name: complete_local_name}), do: {:ok, complete_local_name} 22 | 23 | @impl AdvertisingData 24 | def new_ad_structure(complete_local_name) when is_binary(complete_local_name) do 25 | ret = 26 | AdvertisingData.new_ad_structure(__MODULE__, %{complete_local_name: complete_local_name}) 27 | 28 | {:ok, ret} 29 | end 30 | 31 | def new_ad_structure(_), do: {:error, :new_ad_structure} 32 | 33 | @impl AdvertisingData 34 | def new_ad_structure!(complete_local_name) when is_binary(complete_local_name) do 35 | AdvertisingData.new_ad_structure(__MODULE__, %{complete_local_name: complete_local_name}) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /src/lib/harald/host/att.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT do 2 | @moduledoc """ 3 | Reference: version 5.2, vol 3, part f. 4 | """ 5 | 6 | alias Harald.Host.ATT.{ 7 | ExchangeMTUReq, 8 | ExecuteWriteReq, 9 | ExecuteWriteRsp, 10 | FindInformationReq, 11 | FindInformationRsp, 12 | PrepareWriteReq, 13 | PrepareWriteRsp, 14 | ReadBlobReq, 15 | ReadBlobRsp, 16 | ReadByGroupTypeReq, 17 | ReadByGroupTypeRsp, 18 | ReadByTypeReq, 19 | ReadByTypeRsp, 20 | ReadReq, 21 | ReadRsp, 22 | WriteCmd, 23 | WriteReq, 24 | WriteRsp 25 | } 26 | 27 | @enforce_keys [ 28 | :attribute, 29 | :parameters 30 | ] 31 | 32 | defstruct [ 33 | :attribute, 34 | :parameters 35 | ] 36 | 37 | def decode( 38 | << 39 | encoded_attribute_opcode, 40 | encoded_attribute_parameters::binary 41 | >> = encoded_bin 42 | ) do 43 | with {:ok, opcode_module} <- opcode_to_module(encoded_attribute_opcode), 44 | {:ok, decoded_attribute_parameters} <- opcode_module.decode(encoded_attribute_parameters) do 45 | decoded_att = %__MODULE__{ 46 | attribute: %{opcode: encoded_attribute_opcode, module: opcode_module}, 47 | parameters: decoded_attribute_parameters 48 | } 49 | 50 | {:ok, decoded_att} 51 | else 52 | {:error, {:not_implemented, error}} -> {:error, {:not_implemented, error, encoded_bin}} 53 | end 54 | end 55 | 56 | def encode(%__MODULE__{ 57 | attribute: %{opcode: encoded_attribute_opcode, module: opcode_module}, 58 | parameters: decoded_attribute_parameters 59 | }) do 60 | {:ok, encoded_attribute_parameters} = opcode_module.encode(decoded_attribute_parameters) 61 | 62 | encoded_att = << 63 | encoded_attribute_opcode, 64 | encoded_attribute_parameters::binary 65 | >> 66 | 67 | {:ok, encoded_att} 68 | end 69 | 70 | @doc """ 71 | Reference: version 5.2, vol 3, part a, 2. 72 | """ 73 | def id(), do: 0x04 74 | 75 | def new(opcode_module, decoded_attribute_parameters) do 76 | encoded_attribute_opcode = opcode_module.opcode() 77 | 78 | acl_data = %__MODULE__{ 79 | attribute: %{opcode: encoded_attribute_opcode, module: opcode_module}, 80 | parameters: decoded_attribute_parameters 81 | } 82 | 83 | {:ok, acl_data} 84 | end 85 | 86 | def opcode_to_module(0x02), do: {:ok, ExchangeMTUReq} 87 | def opcode_to_module(0x03), do: {:ok, ExchangeMTURsp} 88 | def opcode_to_module(0x04), do: {:ok, FindInformationReq} 89 | def opcode_to_module(0x05), do: {:ok, FindInformationRsp} 90 | def opcode_to_module(0x08), do: {:ok, ReadByTypeReq} 91 | def opcode_to_module(0x09), do: {:ok, ReadByTypeRsp} 92 | def opcode_to_module(0x0A), do: {:ok, ReadReq} 93 | def opcode_to_module(0x0B), do: {:ok, ReadRsp} 94 | def opcode_to_module(0x0C), do: {:ok, ReadBlobReq} 95 | def opcode_to_module(0x0D), do: {:ok, ReadBlobRsp} 96 | def opcode_to_module(0x10), do: {:ok, ReadByGroupTypeReq} 97 | def opcode_to_module(0x11), do: {:ok, ReadByGroupTypeRsp} 98 | def opcode_to_module(0x12), do: {:ok, WriteReq} 99 | def opcode_to_module(0x13), do: {:ok, WriteRsp} 100 | def opcode_to_module(0x16), do: {:ok, PrepareWriteReq} 101 | def opcode_to_module(0x17), do: {:ok, PrepareWriteRsp} 102 | def opcode_to_module(0x18), do: {:ok, ExecuteWriteReq} 103 | def opcode_to_module(0x19), do: {:ok, ExecuteWriteRsp} 104 | def opcode_to_module(0x52), do: {:ok, WriteCmd} 105 | def opcode_to_module(opcode), do: {:error, {:not_implemented, {__MODULE__, opcode}}} 106 | end 107 | -------------------------------------------------------------------------------- /src/lib/harald/host/att/error_codes.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.ErrorCodes do 2 | @moduledoc """ 3 | First 19 errors: 4 | Reference: version 5.2, Vol 3, Section 3.4.1.1 5 | Errors 0xFC - 0xFF: 6 | Reference: Core Specification Supplement Version 9, Part B 7 | """ 8 | @error_codes %{ 9 | 0x01 => "Invalid Handle", 10 | 0x02 => "Read Not Permitted", 11 | 0x03 => "Write Not Permitted", 12 | 0x04 => "Invalid PDU", 13 | 0x05 => "Insufficient Authentication", 14 | 0x06 => "Request Not Supported", 15 | 0x07 => "Invalid Offset", 16 | 0x08 => "Insufficient Authorization", 17 | 0x09 => "Prepare Queue Full", 18 | 0x0A => "Attribute Not Found", 19 | 0x0B => "Attribute Not Long", 20 | 0x0C => "Insufficient Encryption Key Size", 21 | 0x0D => "Invalid Attribute Value Length", 22 | 0x0E => "Unlikely Error", 23 | 0x0F => "Insufficient Encryption", 24 | 0x10 => "Unsupported Group Type", 25 | 0x11 => "Insufficient Resources", 26 | 0x12 => "Database Out Of Sync", 27 | 0x13 => "Value Not Allowed", 28 | 0xFC => "Write Request Rejected", 29 | 0xFD => "Client Characteristic Configuration Descriptor Improperly Configured", 30 | 0xFE => "Procedure Already in Progress", 31 | 0xFF => "Out of Range" 32 | } 33 | 34 | Enum.each(@error_codes, fn 35 | {error_code, _name} 36 | when error_code in 0x80..0x9F -> 37 | def decode(unquote(error_code)), do: {:ok, unquote("Application Error")} 38 | 39 | {error_code, name} -> 40 | def decode(unquote(error_code)), do: {:ok, unquote(name)} 41 | def encode(unquote(name)), do: {:ok, unquote(error_code)} 42 | end) 43 | 44 | def decode(encoded_error_code) do 45 | {:error, {:decode, {__MODULE__, encoded_error_code}}} 46 | end 47 | 48 | def encode(decoded_error_code) do 49 | {:error, {:encode, {__MODULE__, decoded_error_code}}} 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /src/lib/harald/host/att/error_rsp.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.ErrorRsp do 2 | alias Harald.Host.ATT 3 | alias Harald.Host.ATT.ErrorCodes 4 | 5 | @moduledoc """ 6 | Reference: version 5.2, Vol 3, Part F, 3.4.1.1 7 | """ 8 | 9 | def encode(%{ 10 | request_module_in_error: request_module_in_error, 11 | attribute_handle_in_error: attribute_handle_in_error, 12 | error_message: error_message 13 | }) do 14 | request_opcode_in_error = request_module_in_error.opcode() 15 | {:ok, error_code} = ErrorCodes.encode(error_message) 16 | 17 | {:ok, 18 | <>} 20 | end 21 | 22 | def encode(decoded_error_rsp) do 23 | {:error, {:encode, {__MODULE__, decoded_error_rsp}}} 24 | end 25 | 26 | def decode( 27 | <> 29 | ) do 30 | {:ok, request_module_in_error} = ATT.opcode_to_module(request_opcode_in_error) 31 | {:ok, error_message} = ErrorCodes.decode(error_code) 32 | 33 | parameters = %{ 34 | request_module_in_error: request_module_in_error, 35 | attribute_handle_in_error: attribute_handle_in_error, 36 | error_message: error_message 37 | } 38 | 39 | {:ok, parameters} 40 | end 41 | 42 | def decode(encoded_error_rsp) do 43 | {:error, {:decode, {__MODULE__, encoded_error_rsp}}} 44 | end 45 | 46 | def opcode(), do: 0x01 47 | end 48 | -------------------------------------------------------------------------------- /src/lib/harald/host/att/exchange_mtu_req.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.ExchangeMTUReq do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 3, Part F, 3.4.2.1 4 | """ 5 | 6 | def encode(%{client_rx_mtu: decoded_client_rx_mtu}) do 7 | {:ok, <>} 8 | end 9 | 10 | def encode(decoded_exchange_mtu_req) do 11 | {:error, {:encode, {__MODULE__, decoded_exchange_mtu_req}}} 12 | end 13 | 14 | def decode(<>) do 15 | parameters = %{client_rx_mtu: decoded_client_rx_mtu} 16 | {:ok, parameters} 17 | end 18 | 19 | def decode(encoded_exchange_mtu_req) do 20 | {:error, {:decode, {__MODULE__, encoded_exchange_mtu_req}}} 21 | end 22 | 23 | def opcode(), do: 0x02 24 | end 25 | -------------------------------------------------------------------------------- /src/lib/harald/host/att/exchange_mtu_rsp.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.ExchangeMTURsp do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 3, Part F, 3.4.2.1 4 | """ 5 | 6 | def encode(%{server_rx_mtu: decoded_server_rx_mtu}) do 7 | {:ok, <>} 8 | end 9 | 10 | def encode(decoded_exchange_mtu_rsp) do 11 | {:error, {:encode, {__MODULE__, decoded_exchange_mtu_rsp}}} 12 | end 13 | 14 | def decode(<>) do 15 | parameters = %{server_rx_mtu: decoded_server_rx_mtu} 16 | {:ok, parameters} 17 | end 18 | 19 | def decode(encoded_exchange_mtu_rsp) do 20 | {:error, {:decode, {__MODULE__, encoded_exchange_mtu_rsp}}} 21 | end 22 | 23 | def opcode(), do: 0x03 24 | end 25 | -------------------------------------------------------------------------------- /src/lib/harald/host/att/execute_write_req.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.ExecuteWriteReq do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 3, Part F, 3.4.6.3 4 | """ 5 | 6 | def encode(%{flags: flags}) do 7 | binary_flags = 8 | case flags do 9 | :cancel_all -> <<0x00>> 10 | :write_all -> <<0x01>> 11 | end 12 | 13 | {:ok, binary_flags} 14 | end 15 | 16 | def encode(decoded_execute_write_req) do 17 | {:error, {:encode, {__MODULE__, decoded_execute_write_req}}} 18 | end 19 | 20 | def decode(<>) do 21 | flags = 22 | case binary_flags do 23 | 0x00 -> :cancel_all 24 | 0x01 -> :write_all 25 | end 26 | 27 | parameters = %{flags: flags} 28 | {:ok, parameters} 29 | end 30 | 31 | def decode(encoded_execute_write_req) do 32 | {:error, {:decode, {__MODULE__, encoded_execute_write_req}}} 33 | end 34 | 35 | def opcode(), do: 0x18 36 | end 37 | -------------------------------------------------------------------------------- /src/lib/harald/host/att/execute_write_rsp.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.ExecuteWriteRsp do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 3, Part F, 3.4.6.4 4 | """ 5 | 6 | def encode(%{}) do 7 | {:ok, <<>>} 8 | end 9 | 10 | def encode(decoded_execute_write_rsp) do 11 | {:error, {:encode, {__MODULE__, decoded_execute_write_rsp}}} 12 | end 13 | 14 | def decode(<<>>) do 15 | parameters = %{} 16 | {:ok, parameters} 17 | end 18 | 19 | def decode(encoded_execute_write_rsp) do 20 | {:error, {:decode, {__MODULE__, encoded_execute_write_rsp}}} 21 | end 22 | 23 | def opcode(), do: 0x19 24 | end 25 | -------------------------------------------------------------------------------- /src/lib/harald/host/att/find_information_req.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.FindInformationReq do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 3, Part F, 3.4.3.1 4 | """ 5 | 6 | # Starting handle cannot be 0, it is a reserved number 7 | def encode(%{starting_handle: starting_handle, ending_handle: _}) 8 | when starting_handle == 0 do 9 | {:error, :encode} 10 | end 11 | 12 | def encode(%{starting_handle: starting_handle, ending_handle: ending_handle}) 13 | when starting_handle <= ending_handle do 14 | bin = <> 15 | {:ok, bin} 16 | end 17 | 18 | def encode(decoded_find_information_req) do 19 | {:error, {:encode, {__MODULE__, decoded_find_information_req}}} 20 | end 21 | 22 | def decode(<>) do 23 | parameters = %{ 24 | starting_handle: starting_handle, 25 | ending_handle: ending_handle 26 | } 27 | 28 | {:ok, parameters} 29 | end 30 | 31 | def decode(encoded_find_information_req) do 32 | {:error, {:decode, {__MODULE__, encoded_find_information_req}}} 33 | end 34 | 35 | def opcode(), do: 0x04 36 | end 37 | -------------------------------------------------------------------------------- /src/lib/harald/host/att/find_information_rsp.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.FindInformationRsp do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 3, Part F, 3.4.3.2 4 | """ 5 | 6 | def encode(%{format: format, information_data: information_data}) 7 | when format == :handle_and_16_bit_uuid do 8 | encoded_information_data = encode_information_data(16, information_data) 9 | {:ok, <<0x1, encoded_information_data::binary>>} 10 | end 11 | 12 | def encode(%{format: format, information_data: information_data}) 13 | when format == :handle_and_128_bit_uuid do 14 | encoded_information_data = encode_information_data(128, information_data) 15 | {:ok, <<0x2, encoded_information_data::binary>>} 16 | end 17 | 18 | def encode(decoded_find_information_rsp) do 19 | {:error, {:encode, {__MODULE__, decoded_find_information_rsp}}} 20 | end 21 | 22 | def encode_information_data(uuid_size, decoded_information_data) do 23 | encoded_information_data = 24 | for data_element <- decoded_information_data do 25 | handle = data_element.handle 26 | uuid = data_element.uuid 27 | <> 28 | end 29 | 30 | Enum.join(encoded_information_data) 31 | end 32 | 33 | def decode(<>) 34 | when byte_size(information_data) == 0 do 35 | {:error, {:decode, {__MODULE__, <>}}} 36 | end 37 | 38 | def decode(<>) 39 | when format == 0x01 and rem(byte_size(information_data), 4) == 0 do 40 | decoded_information_data = 41 | for <> do 42 | %{handle: handle, uuid: uuid} 43 | end 44 | 45 | parameters = %{format: :handle_and_16_bit_uuid, information_data: decoded_information_data} 46 | {:ok, parameters} 47 | end 48 | 49 | def decode(<>) 50 | when format == 0x02 and rem(byte_size(information_data), 18) == 0 do 51 | decoded_information_data = 52 | for <> do 53 | %{handle: handle, uuid: uuid} 54 | end 55 | 56 | parameters = %{format: :handle_and_128_bit_uuid, information_data: decoded_information_data} 57 | {:ok, parameters} 58 | end 59 | 60 | def decode(encoded_find_information_rsp) do 61 | {:error, {:decode, {__MODULE__, encoded_find_information_rsp}}} 62 | end 63 | 64 | def opcode(), do: 0x05 65 | end 66 | -------------------------------------------------------------------------------- /src/lib/harald/host/att/prepare_write_req.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.PrepareWriteReq do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 3, Part F, 3.4.6.1 4 | """ 5 | 6 | def encode(%{ 7 | attribute_handle: attribute_handle, 8 | value_offset: value_offset, 9 | part_attribute_value: part_attribute_value 10 | }) do 11 | {:ok, 12 | <>} 14 | end 15 | 16 | def encode(decoded_prepare_write_req) do 17 | {:error, {:encode, {__MODULE__, decoded_prepare_write_req}}} 18 | end 19 | 20 | def decode( 21 | <> 23 | ) do 24 | parameters = %{ 25 | attribute_handle: attribute_handle, 26 | value_offset: value_offset, 27 | part_attribute_value: part_attribute_value 28 | } 29 | 30 | {:ok, parameters} 31 | end 32 | 33 | def decode(encoded_prepare_write_req) do 34 | {:error, {:decode, {__MODULE__, encoded_prepare_write_req}}} 35 | end 36 | 37 | def opcode(), do: 0x16 38 | end 39 | -------------------------------------------------------------------------------- /src/lib/harald/host/att/prepare_write_rsp.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.PrepareWriteRsp do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 3, Part F, 3.4.6.2 4 | """ 5 | 6 | def encode(%{ 7 | attribute_handle: attribute_handle, 8 | value_offset: value_offset, 9 | part_attribute_value: part_attribute_value 10 | }) do 11 | {:ok, 12 | <>} 14 | end 15 | 16 | def encode(decoded_prepare_write_rsp) do 17 | {:error, {:encode, {__MODULE__, decoded_prepare_write_rsp}}} 18 | end 19 | 20 | def decode( 21 | <> 23 | ) do 24 | parameters = %{ 25 | attribute_handle: attribute_handle, 26 | value_offset: value_offset, 27 | part_attribute_value: part_attribute_value 28 | } 29 | 30 | {:ok, parameters} 31 | end 32 | 33 | def decode(encoded_prepare_write_rsp) do 34 | {:error, {:decode, {__MODULE__, encoded_prepare_write_rsp}}} 35 | end 36 | 37 | def opcode(), do: 0x17 38 | end 39 | -------------------------------------------------------------------------------- /src/lib/harald/host/att/read_blob_req.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.ReadBlobReq do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 3, Part F, 3.4.4.5 4 | """ 5 | 6 | def encode(%{attribute_handle: attribute_handle, value_offset: value_offset}) do 7 | {:ok, <>} 8 | end 9 | 10 | def encode(decoded_read_blob_req) do 11 | {:error, {:encode, {__MODULE__, decoded_read_blob_req}}} 12 | end 13 | 14 | def decode(<>) do 15 | parameters = %{attribute_handle: attribute_handle, value_offset: value_offset} 16 | {:ok, parameters} 17 | end 18 | 19 | def decode(encoded_read_blob_req) do 20 | {:error, {:decode, {__MODULE__, encoded_read_blob_req}}} 21 | end 22 | 23 | def opcode(), do: 0x0C 24 | end 25 | -------------------------------------------------------------------------------- /src/lib/harald/host/att/read_blob_rsp.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.ReadBlobRsp do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 3, Part F, 3.4.4.6 4 | """ 5 | 6 | def encode(%{part_attribute_value: part_attribute_value}) do 7 | {:ok, <>} 8 | end 9 | 10 | def encode(decoded_read_blob_rsp) do 11 | {:error, {:encode, {__MODULE__, decoded_read_blob_rsp}}} 12 | end 13 | 14 | def decode(<>) do 15 | parameters = %{part_attribute_value: part_attribute_value} 16 | {:ok, parameters} 17 | end 18 | 19 | def decode(encoded_read_blob_rsp) do 20 | {:error, {:decode, {__MODULE__, encoded_read_blob_rsp}}} 21 | end 22 | 23 | def opcode(), do: 0x0D 24 | end 25 | -------------------------------------------------------------------------------- /src/lib/harald/host/att/read_by_group_type_req.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.ReadByGroupTypeReq do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 3, Part F, 3.4.4.9 4 | """ 5 | 6 | def encode(%{ 7 | starting_handle: starting_handle, 8 | ending_handle: ending_handle, 9 | attribute_group_type: attribute_group_type 10 | }) do 11 | {:ok, 12 | <>} 14 | end 15 | 16 | def encode(decoded_read_by_group_type_req) do 17 | {:error, {:encode, {__MODULE__, decoded_read_by_group_type_req}}} 18 | end 19 | 20 | def decode( 21 | <> 23 | ) do 24 | parameters = %{ 25 | starting_handle: starting_handle, 26 | ending_handle: ending_handle, 27 | attribute_group_type: attribute_group_type 28 | } 29 | 30 | {:ok, parameters} 31 | end 32 | 33 | def decode(encoded_read_by_group_type_req) do 34 | {:error, {:decode, {__MODULE__, encoded_read_by_group_type_req}}} 35 | end 36 | 37 | def opcode(), do: 0x10 38 | end 39 | -------------------------------------------------------------------------------- /src/lib/harald/host/att/read_by_group_type_rsp.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.ReadByGroupTypeRsp do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 3, Part F, 3.4.4.10 4 | """ 5 | 6 | def encode(%{length: length, attribute_data_list: attribute_data_list}) do 7 | value_length = (length - 4) * 8 8 | 9 | encoded_attribute_data_list = 10 | for data_element <- attribute_data_list do 11 | attribute_handle = data_element.attribute_handle 12 | end_group_handle = data_element.end_group_handle 13 | attribute_value = data_element.attribute_value 14 | 15 | <> 17 | end 18 | 19 | encoded_attribute_data = Enum.join(encoded_attribute_data_list) 20 | 21 | {:ok, <>} 22 | end 23 | 24 | def encode(decoded_read_by_group_type_rsp) do 25 | {:error, {:encode, {__MODULE__, decoded_read_by_group_type_rsp}}} 26 | end 27 | 28 | def decode(<>) do 29 | value_length = (length - 4) * 8 30 | 31 | decoded_attribute_data_list = 32 | for <> do 34 | %{ 35 | attribute_handle: attribute_handle, 36 | end_group_handle: end_group_handle, 37 | attribute_value: attribute_value 38 | } 39 | end 40 | 41 | parameters = %{ 42 | length: length, 43 | attribute_data_list: decoded_attribute_data_list 44 | } 45 | 46 | {:ok, parameters} 47 | end 48 | 49 | def decode(encoded_read_by_group_type_rsp) do 50 | {:error, {:decode, {__MODULE__, encoded_read_by_group_type_rsp}}} 51 | end 52 | 53 | def opcode(), do: 0x11 54 | end 55 | -------------------------------------------------------------------------------- /src/lib/harald/host/att/read_by_type.req.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.ReadByTypeReq do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 3, Part F, 3.4.4.1. 4 | """ 5 | 6 | def encode(%{ 7 | starting_handle: starting_handle, 8 | ending_handle: ending_handle, 9 | attribute_type: attribute_type 10 | }) do 11 | {:ok, 12 | <>} 14 | end 15 | 16 | def encode(decoded_read_by_type_req) do 17 | {:error, {:encode, {__MODULE__, decoded_read_by_type_req}}} 18 | end 19 | 20 | def decode( 21 | <> 23 | ) do 24 | parameters = %{ 25 | starting_handle: starting_handle, 26 | ending_handle: ending_handle, 27 | attribute_type: attribute_type 28 | } 29 | 30 | {:ok, parameters} 31 | end 32 | 33 | def decode(encoded_read_by_type_req) do 34 | {:error, {:decode, {__MODULE__, encoded_read_by_type_req}}} 35 | end 36 | 37 | def opcode(), do: 0x08 38 | end 39 | -------------------------------------------------------------------------------- /src/lib/harald/host/att/read_by_type_rsp.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.ReadByTypeRsp do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 3, Part F, 3.4.4.2. 4 | """ 5 | 6 | def encode(%{length: length, attribute_data_list: attribute_data_list}) do 7 | value_length = length - 2 8 | 9 | encoded_attribute_data_list = 10 | for data_element <- attribute_data_list do 11 | attribute_handle = data_element.attribute_handle 12 | attribute_value = data_element.attribute_value 13 | <> 14 | end 15 | 16 | encoded_attribute_data = Enum.join(encoded_attribute_data_list) 17 | 18 | {:ok, <>} 19 | end 20 | 21 | def encode(decoded_read_by_group_type_rsp) do 22 | {:error, {:encode, {__MODULE__, decoded_read_by_group_type_rsp}}} 23 | end 24 | 25 | def decode(<>) do 26 | value_length = length - 2 27 | 28 | decoded_attribute_data_list = 29 | for <> do 31 | %{attribute_handle: attribute_handle, attribute_value: attribute_value} 32 | end 33 | 34 | parameters = %{ 35 | length: length, 36 | attribute_data_list: decoded_attribute_data_list 37 | } 38 | 39 | {:ok, parameters} 40 | end 41 | 42 | def decode(encoded_read_by_group_type_rsp) do 43 | {:error, {:decode, {__MODULE__, encoded_read_by_group_type_rsp}}} 44 | end 45 | 46 | def opcode(), do: 0x09 47 | end 48 | -------------------------------------------------------------------------------- /src/lib/harald/host/att/read_req.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.ReadReq do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 3, Part F, 3.4.4.3 4 | """ 5 | 6 | def encode(%{attribute_handle: attribute_handle}) do 7 | {:ok, <>} 8 | end 9 | 10 | def encode(decoded_read_req) do 11 | {:error, {:encode, {__MODULE__, decoded_read_req}}} 12 | end 13 | 14 | def decode(<>) do 15 | parameters = %{attribute_handle: attribute_handle} 16 | {:ok, parameters} 17 | end 18 | 19 | def decode(encoded_read_req) do 20 | {:error, {:decode, {__MODULE__, encoded_read_req}}} 21 | end 22 | 23 | def opcode(), do: 0x0A 24 | end 25 | -------------------------------------------------------------------------------- /src/lib/harald/host/att/read_rsp.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.ReadRsp do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 3, Part F, 3.4.4.4 4 | """ 5 | 6 | def encode(%{attribute_value: attribute_value}) do 7 | {:ok, <>} 8 | end 9 | 10 | def encode(decoded_read_rsp) do 11 | {:error, {:encode, {__MODULE__, decoded_read_rsp}}} 12 | end 13 | 14 | def decode(<>) do 15 | parameters = %{attribute_value: attribute_value} 16 | {:ok, parameters} 17 | end 18 | 19 | def decode(encoded_read_rsp) do 20 | {:error, {:decode, {__MODULE__, encoded_read_rsp}}} 21 | end 22 | 23 | def opcode(), do: 0x0B 24 | end 25 | -------------------------------------------------------------------------------- /src/lib/harald/host/att/write_cmd.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.WriteCmd do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 3, Part F, 3.4.5.3 4 | """ 5 | 6 | def encode(%{attribute_handle: attribute_handle, attribute_value: attribute_value}) do 7 | {:ok, <>} 8 | end 9 | 10 | def encode(decoded_write_cmd) do 11 | {:error, {:encode, {__MODULE__, decoded_write_cmd}}} 12 | end 13 | 14 | def decode(<>) do 15 | parameters = %{attribute_handle: attribute_handle, attribute_value: attribute_value} 16 | {:ok, parameters} 17 | end 18 | 19 | def decode(encoded_write_cmd) do 20 | {:error, {:decode, {__MODULE__, encoded_write_cmd}}} 21 | end 22 | 23 | def opcode(), do: 0x52 24 | end 25 | -------------------------------------------------------------------------------- /src/lib/harald/host/att/write_req.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.WriteReq do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 3, Part F, 3.4.5.1 4 | """ 5 | 6 | def encode(%{attribute_handle: attribute_handle, attribute_value: attribute_value}) do 7 | {:ok, <>} 8 | end 9 | 10 | def encode(decoded_write_req) do 11 | {:error, {:encode, {__MODULE__, decoded_write_req}}} 12 | end 13 | 14 | def decode(<>) do 15 | parameters = %{attribute_handle: attribute_handle, attribute_value: attribute_value} 16 | {:ok, parameters} 17 | end 18 | 19 | def decode(encoded_write_req) do 20 | {:error, {:decode, {__MODULE__, encoded_write_req}}} 21 | end 22 | 23 | def opcode(), do: 0x12 24 | end 25 | -------------------------------------------------------------------------------- /src/lib/harald/host/att/write_rsp.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.WriteRsp do 2 | @moduledoc """ 3 | Reference: version 5.2, Vol 3, Part F, 3.4.5.2 4 | """ 5 | 6 | def encode(%{}) do 7 | {:ok, <<>>} 8 | end 9 | 10 | def encode(decoded_write_rsp) do 11 | {:error, {:encode, {__MODULE__, decoded_write_rsp}}} 12 | end 13 | 14 | def decode(<<>>) do 15 | parameters = %{} 16 | {:ok, parameters} 17 | end 18 | 19 | def decode(encoded_write_rsp) do 20 | {:error, {:decode, {__MODULE__, encoded_write_rsp}}} 21 | end 22 | 23 | def opcode(), do: 0x13 24 | end 25 | -------------------------------------------------------------------------------- /src/lib/harald/host/l2cap.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.L2CAP do 2 | @moduledoc """ 3 | Reference: version 5.2, vol 3, part a. 4 | """ 5 | 6 | alias Harald.Host.ATT 7 | 8 | @enforce_keys [ 9 | :length, 10 | :channel, 11 | :information_payload 12 | ] 13 | 14 | defstruct [ 15 | :length, 16 | :channel, 17 | :information_payload 18 | ] 19 | 20 | def channel_id_to_module(0x04), do: {:ok, ATT} 21 | def channel_id_to_module(channel_id), do: {:error, {:not_implemented, {__MODULE__, channel_id}}} 22 | 23 | def decode( 24 | << 25 | length::little-size(16), 26 | channel_id::little-size(16), 27 | encoded_information_payload::binary-size(length) 28 | >> = encoded_bin 29 | ) do 30 | with {:ok, channel_module} <- channel_id_to_module(channel_id), 31 | {:ok, decoded_information_payload} <- channel_module.decode(encoded_information_payload) do 32 | decoded_l2cap = %__MODULE__{ 33 | length: length, 34 | channel: %{id: channel_id, module: channel_module}, 35 | information_payload: decoded_information_payload 36 | } 37 | 38 | {:ok, decoded_l2cap} 39 | else 40 | {:error, {:not_implemented, error, _bin}} -> 41 | {:error, {:not_implemented, error, encoded_bin}} 42 | end 43 | end 44 | 45 | def encode(%__MODULE__{ 46 | length: length, 47 | channel: %{id: channel_id, module: channel_module}, 48 | information_payload: decoded_information_payload 49 | }) do 50 | {:ok, encoded_information_payload} = channel_module.encode(decoded_information_payload) 51 | 52 | length = 53 | case length do 54 | nil -> byte_size(encoded_information_payload) 55 | length -> length 56 | end 57 | 58 | encoded_l2cap = << 59 | length::little-size(16), 60 | channel_id::little-size(16), 61 | encoded_information_payload::binary-size(length) 62 | >> 63 | 64 | {:ok, encoded_l2cap} 65 | end 66 | 67 | def new(channel_module, decoded_information_payload) do 68 | decoded_l2cap = %__MODULE__{ 69 | length: nil, 70 | channel: %{id: channel_module.id, module: channel_module}, 71 | information_payload: decoded_information_payload 72 | } 73 | 74 | {:ok, encoded_information_payload} = channel_module.encode(decoded_information_payload) 75 | length = byte_size(encoded_information_payload) 76 | {:ok, Map.put(decoded_l2cap, :length, length)} 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /src/lib/harald/le.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.LE do 2 | @moduledoc """ 3 | A collection of high level functions for working with BLE (Bluetooth Low Energy) functionality. 4 | """ 5 | 6 | # use GenServer 7 | # alias Harald.HCI.Event.{LEMeta, LEMeta.AdvertisingReport} 8 | # alias Harald.HCI.{ControllerAndBaseband, LEController} 9 | # alias Harald.{ErrorCode, Transport, Transport.Handler} 10 | # require Logger 11 | 12 | # @behaviour Handler 13 | 14 | # defmodule State do 15 | # @moduledoc false 16 | # defstruct from: nil, devices: %{} 17 | # end 18 | 19 | # @doc """ 20 | # HCI_Read_Local_Name 21 | 22 | # - `OGF` - `0x03` 23 | # - `OCF` - `0x0014` 24 | # """ 25 | # @spec read_local_name(atom()) :: {:ok, String.t()} 26 | # def read_local_name(namespace) do 27 | # GenServer.call(name(namespace), {:read_local_name, namespace}) 28 | # end 29 | 30 | # def scan(namespace, opts \\ []) do 31 | # opts = Keyword.merge([time: 5_000], opts) 32 | # ret = GenServer.call(name(namespace), {:scan, namespace, opts[:time]}, opts[:time] + 1000) 33 | # {:ok, ret} 34 | # end 35 | 36 | # def name(namespace), do: String.to_atom("#{namespace}.#{__MODULE__}") 37 | 38 | # @impl Handler 39 | # def setup(args) do 40 | # GenServer.start_link(__MODULE__, args, name: name(args[:namespace])) 41 | # end 42 | 43 | # @impl GenServer 44 | # def init(_args) do 45 | # {:ok, %State{}} 46 | # end 47 | 48 | # @impl GenServer 49 | # def handle_call({:scan, ns, timeout}, from, state) do 50 | # try do 51 | # :ok = Transport.call(ns, LEController.set_enable_scan(true, true)) 52 | # Process.send_after(self(), {:stop_scan, ns, from}, timeout) 53 | # {:noreply, state} 54 | # catch 55 | # :exit, {:timeout, _} -> {:reply, {:error, :timeout}, state} 56 | # end 57 | # end 58 | 59 | # def handle_call({:read_local_name, ns}, from, state) do 60 | # try do 61 | # :ok = Transport.call(ns, ControllerAndBaseband.read_local_name()) 62 | # {:noreply, %{state | from: from}} 63 | # catch 64 | # :exit, {:timeout, _} -> {:reply, {:error, :timeout}, state} 65 | # end 66 | # end 67 | 68 | # @impl GenServer 69 | # def handle_info( 70 | # {:bluetooth_event, %LEMeta{subevent: %AdvertisingReport{devices: devices}}}, 71 | # state 72 | # ) do 73 | # state = 74 | # Enum.reduce(devices, state, fn device, state -> 75 | # put_device(device.address, device, state) 76 | # end) 77 | 78 | # {:noreply, state} 79 | # end 80 | 81 | # def handle_info( 82 | # {:bluetooth_event, 83 | # {:error, {:unhandled_event_code, <<14, 252, 1, 20, error_code, _::bits>>}}}, 84 | # %{from: from} = state 85 | # ) 86 | # when not is_nil(from) do 87 | # GenServer.reply(from, {:error, ErrorCode.name(error_code)}) 88 | # {:noreply, %{state | from: nil}} 89 | # end 90 | 91 | # # Let other bluetooth events fall through. 92 | # def handle_info({:bluetooth_event, _event}, state) do 93 | # {:noreply, state} 94 | # end 95 | 96 | # def handle_info({:stop_scan, ns, from}, %State{devices: devices}) do 97 | # :ok = Transport.call(ns, LEController.set_enable_scan(false)) 98 | # GenServer.reply(from, devices) 99 | # {:noreply, %State{}} 100 | # end 101 | 102 | # # this catchs the reply from the transport if the try/catch above for a :scan was triggered 103 | # # by a timeout 104 | # def handle_info({ref, :ok}, state) when is_reference(ref), do: {:noreply, state} 105 | 106 | # defp put_device(address, device_report, %State{devices: devices} = state) do 107 | # %State{state | devices: Map.put(devices, address, device_report)} 108 | # end 109 | end 110 | -------------------------------------------------------------------------------- /src/lib/harald/serializable.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.Serializable do 2 | @moduledoc """ 3 | Serializable behaviour. 4 | """ 5 | 6 | @callback serialize(term()) :: {:ok, binary()} | {:error, term()} 7 | 8 | @callback deserialize(binary()) :: {:ok, term()} | {:error, term()} 9 | 10 | @doc """ 11 | Asserts that `bin` will serialize symmetrically. 12 | 13 | If `bin` deserializes into `{:ok, data}`, `data` should serialize perfectly back into `bin`. 14 | """ 15 | defmacro assert_symmetry(mod, bin) do 16 | quote bind_quoted: [bin: bin, mod: mod] do 17 | import ExUnit.Assertions, only: [assert: 1, assert: 2] 18 | 19 | case mod.deserialize(bin) do 20 | {:ok, data} -> 21 | assert {:ok, bin2} = mod.serialize(data) 22 | assert :binary.bin_to_list(bin) == :binary.bin_to_list(bin2) 23 | 24 | {:error, data} when not is_binary(data) -> 25 | true 26 | 27 | {:error, bin2} -> 28 | assert :binary.bin_to_list(bin) == :binary.bin_to_list(bin2) 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /src/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.MixProject do 2 | use Mix.Project 3 | 4 | def application do 5 | [ 6 | extra_applications: [:logger] 7 | ] 8 | end 9 | 10 | def project do 11 | [ 12 | app: :harald, 13 | deps: deps(), 14 | description: description(), 15 | docs: docs(), 16 | elixir: "~> 1.10", 17 | elixirc_paths: elixirc_paths(Mix.env()), 18 | package: package(), 19 | preferred_cli_env: [ 20 | coveralls: :test, 21 | "coveralls.detail": :test, 22 | "coveralls.post": :test, 23 | "coveralls.html": :test 24 | ], 25 | source_url: "https://github.com/verypossible-labs/harald", 26 | start_permanent: Mix.env() == :prod, 27 | test_coverage: [tool: ExCoveralls], 28 | version: "0.3.0" 29 | ] 30 | end 31 | 32 | defp deps do 33 | [ 34 | {:circuits_uart, "~> 1.3"}, 35 | {:ex_doc, "~> 0.23", only: [:dev], runtime: false}, 36 | {:excoveralls, "~> 0.10", only: [:test], runtime: false}, 37 | {:hook, "~> 0.3.0"}, 38 | {:mix_test_watch, "~> 0.9", only: [:dev], runtime: false}, 39 | {:stream_data, "~> 0.1", only: [:test]} 40 | ] 41 | end 42 | 43 | defp description do 44 | """ 45 | An Elixir Bluetooth HCI data binding. 46 | """ 47 | end 48 | 49 | defp docs do 50 | [ 51 | main: "readme", 52 | extras: [] 53 | ] 54 | end 55 | 56 | defp elixirc_paths(:test), do: ["test/support", "lib"] 57 | 58 | defp elixirc_paths(_), do: ["lib"] 59 | 60 | defp package do 61 | [ 62 | licenses: ["MIT"], 63 | links: %{"GitHub" => "https://github.com/verypossible-labs/harald"} 64 | ] 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /src/test/harald/hci/acl_data_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.ACLDataTest do 2 | use Harald.HaraldCase 3 | alias Harald.HCI.ACLData 4 | alias Harald.Host.{ATT, L2CAP} 5 | alias Harald.Host.ATT.ExchangeMTUReq 6 | 7 | test "decode/1" do 8 | handle = 1 9 | 10 | decoded_packet_boundary_flag = %{ 11 | description: 12 | "First automatically flushable packet of a higher layer message (start of an automatically-flushable L2CAP PDU).", 13 | value: 0b10 14 | } 15 | 16 | decoded_broadcast_flag = %{ 17 | description: "Point-to-point (ACL-U, AMP-U, or LE-U)", 18 | value: 0b00 19 | } 20 | 21 | decoded_client_rx_mtu = 100 22 | {:ok, decoded_att_data} = ATT.new(ExchangeMTUReq, %{client_rx_mtu: decoded_client_rx_mtu}) 23 | {:ok, decoded_l2cap_data} = L2CAP.new(ATT, decoded_att_data) 24 | 25 | {:ok, decoded_acl_data} = 26 | ACLData.new( 27 | handle, 28 | decoded_packet_boundary_flag, 29 | decoded_broadcast_flag, 30 | decoded_l2cap_data 31 | ) 32 | 33 | {:ok, encoded_acl_data} = ACLData.encode(decoded_acl_data) 34 | assert {:ok, decoded_acl_data} == ACLData.decode(encoded_acl_data) 35 | end 36 | 37 | test "encode/1" do 38 | handle = 1 39 | packet_boundary_flag = 0b10 40 | 41 | decoded_packet_boundary_flag = %{ 42 | description: 43 | "First automatically flushable packet of a higher layer message (start of an automatically-flushable L2CAP PDU).", 44 | value: packet_boundary_flag 45 | } 46 | 47 | broadcast_flag = 0b00 48 | 49 | decoded_broadcast_flag = %{ 50 | description: "Point-to-point (ACL-U, AMP-U, or LE-U)", 51 | value: broadcast_flag 52 | } 53 | 54 | decoded_client_rx_mtu = 100 55 | {:ok, decoded_att_data} = ATT.new(ExchangeMTUReq, %{client_rx_mtu: decoded_client_rx_mtu}) 56 | {:ok, decoded_l2cap_data} = L2CAP.new(ATT, decoded_att_data) 57 | {:ok, encoded_l2cap_data} = L2CAP.encode(decoded_l2cap_data) 58 | 59 | {:ok, %{data_total_length: decoded_data_total_length} = decoded_acl_data} = 60 | ACLData.new( 61 | handle, 62 | decoded_packet_boundary_flag, 63 | decoded_broadcast_flag, 64 | decoded_l2cap_data 65 | ) 66 | 67 | encoded_indicator = 2 68 | 69 | encoded_data = encoded_l2cap_data 70 | encoded_data_total_length = <> 71 | 72 | <> = << 73 | broadcast_flag::size(2), 74 | packet_boundary_flag::size(2), 75 | handle::size(12) 76 | >> 77 | 78 | expected_encoded_acl_data = << 79 | encoded_indicator, 80 | encoded_handle::size(16), 81 | encoded_data_total_length::binary, 82 | encoded_data::binary 83 | >> 84 | 85 | assert {:ok, actual_encoded_acl_data} = ACLData.encode(decoded_acl_data) 86 | assert_binaries(expected_encoded_acl_data == actual_encoded_acl_data) 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /src/test/harald/hci/commands/controller_and_baseband/read_local_name_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.ControllerAndBaseband.ReadLocalNameTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.HCI.{Commands, Commands.Command, Commands.ControllerAndBaseband} 4 | alias Harald.HCI.Commands.ControllerAndBaseband.ReadLocalName 5 | 6 | test "decode/1" do 7 | expected_bin = <<1, 20, 12, 0>> 8 | 9 | expected_command = %Command{ 10 | command_op_code: %{ 11 | ocf: 0x14, 12 | ocf_module: ReadLocalName, 13 | ogf: 0x3, 14 | ogf_module: ControllerAndBaseband 15 | }, 16 | parameters: %{} 17 | } 18 | 19 | assert {:ok, expected_command} == Commands.decode(expected_bin) 20 | end 21 | 22 | test "decode_return_parameters/1" do 23 | status = 1 24 | local_name = "bob" 25 | padded_local_name = String.pad_trailing(local_name, 248, <<0>>) 26 | return_parameters = <> 27 | expected_return_parameters = %{status: status, local_name: padded_local_name} 28 | 29 | assert {:ok, expected_return_parameters} == 30 | ReadLocalName.decode_return_parameters(return_parameters) 31 | end 32 | 33 | test "encode/1" do 34 | expected_bin = <<1, 20, 12, 0>> 35 | expected_size = byte_size(expected_bin) 36 | params = %{} 37 | assert {:ok, actual_bin} = Commands.encode(ControllerAndBaseband, ReadLocalName, params) 38 | assert expected_size == byte_size(actual_bin) 39 | assert expected_bin == actual_bin 40 | end 41 | 42 | test "encode_return_parameters/1" do 43 | status = 1 44 | local_name_size = 248 45 | local_name = "bob" 46 | padded_local_name = String.pad_trailing(local_name, local_name_size, <<0>>) 47 | encoded_return_parameters = <> 48 | decoded_return_parameters = %{local_name: padded_local_name, status: status} 49 | 50 | assert {:ok, encoded_return_parameters} == 51 | ReadLocalName.encode_return_parameters(decoded_return_parameters) 52 | end 53 | 54 | test "ocf/0" do 55 | assert 0x14 == ReadLocalName.ocf() 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /src/test/harald/hci/commands/controller_and_baseband/write_local_name_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.ControllerAndBaseband.WriteLocalNameTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.HCI.{Commands, Commands.Command, Commands.ControllerAndBaseband} 4 | alias Harald.HCI.Commands.ControllerAndBaseband.WriteLocalName 5 | 6 | test "decode/1" do 7 | name = "bob" 8 | expected_name = String.pad_trailing(name, 248, <<0>>) 9 | expected_bin = <<1, 19, 12, 248, expected_name::binary>> 10 | local_name = String.pad_trailing(name, 248, <<0>>) 11 | 12 | expected_command = %Command{ 13 | command_op_code: %{ 14 | ocf: 0x13, 15 | ocf_module: WriteLocalName, 16 | ogf: 0x3, 17 | ogf_module: ControllerAndBaseband 18 | }, 19 | parameters: %{read_local_name: local_name} 20 | } 21 | 22 | assert {:ok, expected_command} == Commands.decode(expected_bin) 23 | end 24 | 25 | test "decode_return_parameters/1" do 26 | status = 1 27 | return_parameters = <> 28 | expected_return_parameters = %{status: status} 29 | 30 | assert {:ok, expected_return_parameters} == 31 | WriteLocalName.decode_return_parameters(return_parameters) 32 | end 33 | 34 | test "encode/1" do 35 | name = "bob" 36 | expected_name = String.pad_trailing(name, 248, <<0>>) 37 | expected_bin = <<1, 19, 12, 248, expected_name::binary>> 38 | expected_size = byte_size(expected_bin) 39 | params = %{local_name: "bob"} 40 | assert {:ok, actual_bin} = Commands.encode(ControllerAndBaseband, WriteLocalName, params) 41 | assert expected_size == byte_size(actual_bin) 42 | assert expected_bin == actual_bin 43 | end 44 | 45 | test "encode_return_parameters/1" do 46 | status = 1 47 | encoded_return_parameters = <> 48 | decoded_return_parameters = %{status: status} 49 | 50 | assert {:ok, encoded_return_parameters} == 51 | WriteLocalName.encode_return_parameters(decoded_return_parameters) 52 | end 53 | 54 | test "ocf/0" do 55 | assert 0x13 == WriteLocalName.ocf() 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /src/test/harald/hci/commands/controller_and_baseband/write_pin_type_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.ControllerAndBaseband.WritePinTypeTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.HCI.{Commands, Commands.Command, Commands.ControllerAndBaseband} 4 | alias Harald.HCI.Commands.ControllerAndBaseband.WritePinType 5 | 6 | test "decode/1" do 7 | pin_type = 0 8 | expected_bin = <<1, 10, 12, 1, pin_type>> 9 | 10 | expected_command = %Command{ 11 | command_op_code: %{ 12 | ocf: 0xA, 13 | ocf_module: WritePinType, 14 | ogf: 0x3, 15 | ogf_module: ControllerAndBaseband 16 | }, 17 | parameters: %{pin_type: :variable} 18 | } 19 | 20 | assert {:ok, expected_command} == Commands.decode(expected_bin) 21 | end 22 | 23 | test "decode_return_parameters/1" do 24 | status = 1 25 | return_parameters = <> 26 | expected_return_parameters = %{status: status} 27 | 28 | assert {:ok, expected_return_parameters} == 29 | WritePinType.decode_return_parameters(return_parameters) 30 | end 31 | 32 | test "encode/1" do 33 | pin_type = 0 34 | expected_bin = <<1, 10, 12, 1, pin_type>> 35 | expected_size = byte_size(expected_bin) 36 | params = %{pin_type: :variable} 37 | 38 | assert {:ok, actual_bin} = Commands.encode(ControllerAndBaseband, WritePinType, params) 39 | 40 | assert expected_size == byte_size(actual_bin) 41 | assert expected_bin == actual_bin 42 | end 43 | 44 | test "encode_return_parameters/1" do 45 | status = 1 46 | encoded_return_parameters = <> 47 | decoded_return_parameters = %{status: status} 48 | 49 | assert {:ok, encoded_return_parameters} == 50 | WritePinType.encode_return_parameters(decoded_return_parameters) 51 | end 52 | 53 | test "ocf/0" do 54 | assert 0x0A == WritePinType.ocf() 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /src/test/harald/hci/commands/controller_and_baseband/write_simple_pairing_mode_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.ControllerAndBaseband.WriteSimplePairingModeTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.HCI.{Commands, Commands.Command, Commands.ControllerAndBaseband} 4 | alias Harald.HCI.Commands.ControllerAndBaseband.WriteSimplePairingMode 5 | 6 | test "decode/1" do 7 | simple_pairing_mode = 1 8 | expected_bin = <<1, 86, 12, 1, simple_pairing_mode>> 9 | 10 | expected_command = %Command{ 11 | command_op_code: %{ 12 | ocf: 0x56, 13 | ocf_module: WriteSimplePairingMode, 14 | ogf: 0x3, 15 | ogf_module: ControllerAndBaseband 16 | }, 17 | parameters: %{simple_pairing_mode: true} 18 | } 19 | 20 | assert {:ok, expected_command} == Commands.decode(expected_bin) 21 | end 22 | 23 | test "decode_return_parameters/1" do 24 | status = 1 25 | return_parameters = <> 26 | expected_return_parameters = %{status: status} 27 | 28 | assert {:ok, expected_return_parameters} == 29 | WriteSimplePairingMode.decode_return_parameters(return_parameters) 30 | end 31 | 32 | test "encode/1" do 33 | simple_pairing_mode = 1 34 | expected_bin = <<1, 86, 12, 1, simple_pairing_mode>> 35 | expected_size = byte_size(expected_bin) 36 | params = %{simple_pairing_mode: true} 37 | 38 | assert {:ok, actual_bin} = 39 | Commands.encode(ControllerAndBaseband, WriteSimplePairingMode, params) 40 | 41 | assert expected_size == byte_size(actual_bin) 42 | assert expected_bin == actual_bin 43 | end 44 | 45 | test "encode_return_parameters/1" do 46 | status = 1 47 | encoded_return_parameters = <> 48 | decoded_return_parameters = %{status: status} 49 | 50 | assert {:ok, encoded_return_parameters} == 51 | WriteSimplePairingMode.encode_return_parameters(decoded_return_parameters) 52 | end 53 | 54 | test "ocf/0" do 55 | assert 0x56 == WriteSimplePairingMode.ocf() 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /src/test/harald/hci/commands/controller_and_baseband_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.ControllerAndBasebandTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.HCI.Commands.ControllerAndBaseband 4 | 5 | test "ogf/1" do 6 | assert 0x03 == ControllerAndBaseband.ogf() 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /src/test/harald/hci/commands/le_controller/set_advertising_data_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.ControllerAndBaseband.SetAdvertisingDataTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.HCI.Commands.LEController.SetAdvertisingData 4 | alias Harald.Host.AdvertisingData.CompleteLocalName 5 | 6 | test "decode/1" do 7 | {decoded_ad_structure, encoded_ad_structure} = ad_structure_pair() 8 | encoded_advertising_data = encoded_ad_structure 9 | advertising_data_length = byte_size(encoded_advertising_data) 10 | padded_encoded_advertising_data = String.pad_trailing(encoded_advertising_data, 31, <<0>>) 11 | encoded_parameters = <> 12 | 13 | insignificant_octets = 14 | binary_part( 15 | padded_encoded_advertising_data, 16 | advertising_data_length, 17 | 31 - advertising_data_length 18 | ) 19 | 20 | decoded_advertising_data = %{ 21 | ad_structures: [decoded_ad_structure], 22 | insignificant_octets: insignificant_octets 23 | } 24 | 25 | expected_decoded_parameters = %{ 26 | advertising_data_length: advertising_data_length, 27 | advertising_data: decoded_advertising_data 28 | } 29 | 30 | assert {:ok, expected_decoded_parameters} == SetAdvertisingData.decode(encoded_parameters) 31 | end 32 | 33 | test "decode_return_parameters/1" do 34 | status = 1 35 | return_parameters = <> 36 | expected_return_parameters = %{status: status} 37 | 38 | assert {:ok, expected_return_parameters} == 39 | SetAdvertisingData.decode_return_parameters(return_parameters) 40 | end 41 | 42 | test "encode/1" do 43 | {decoded_ad_structure, encoded_ad_structure} = ad_structure_pair() 44 | encoded_advertising_data = encoded_ad_structure 45 | advertising_data_length = byte_size(encoded_advertising_data) 46 | padded_encoded_advertising_data = String.pad_trailing(encoded_advertising_data, 31, <<0>>) 47 | 48 | insignificant_octets = 49 | binary_part( 50 | padded_encoded_advertising_data, 51 | advertising_data_length, 52 | 31 - advertising_data_length 53 | ) 54 | 55 | decoded_advertising_data = %{ 56 | ad_structures: [decoded_ad_structure], 57 | insignificant_octets: insignificant_octets 58 | } 59 | 60 | padded_encoded_advertising_data = String.pad_trailing(encoded_advertising_data, 31, <<0>>) 61 | 62 | expected_encoded_parameters = 63 | <> 64 | 65 | decoded_parameters = %{ 66 | advertising_data_length: advertising_data_length, 67 | advertising_data: decoded_advertising_data 68 | } 69 | 70 | assert {:ok, expected_encoded_parameters} == SetAdvertisingData.encode(decoded_parameters) 71 | end 72 | 73 | test "encode_return_parameters/1" do 74 | status = 1 75 | encoded_return_parameters = <> 76 | decoded_return_parameters = %{status: status} 77 | 78 | assert {:ok, encoded_return_parameters} == 79 | SetAdvertisingData.encode_return_parameters(decoded_return_parameters) 80 | end 81 | 82 | test "ocf/0" do 83 | assert 0x08 == SetAdvertisingData.ocf() 84 | end 85 | 86 | defp ad_structure_pair() do 87 | complete_local_name = "bob" 88 | ad_module = CompleteLocalName 89 | ad_type = ad_module.ad_type() 90 | 91 | decoded_ad_structure = %{ 92 | ad_data: %{complete_local_name: complete_local_name}, 93 | ad_type: ad_type, 94 | module: ad_module 95 | } 96 | 97 | ad_data = complete_local_name 98 | data = <> 99 | length = byte_size(data) 100 | encoded_ad_structure = <> 101 | {decoded_ad_structure, encoded_ad_structure} 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /src/test/harald/hci/commands/le_controller/set_advertising_enable_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.ControllerAndBaseband.SetAdvertisingEnableTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.HCI.Commands.{Command, LEController} 4 | alias Harald.HCI.{Commands, Commands.LEController.SetAdvertisingEnable} 5 | 6 | test "decode/1" do 7 | {advertising_enable_encoded, advertising_enable_decoded} = {1, true} 8 | parameters = <> 9 | parameters_length = byte_size(parameters) 10 | expected_bin = <<1, 0x0A, 32, parameters_length, parameters::binary>> 11 | 12 | expected_command = %Command{ 13 | command_op_code: %{ 14 | ocf: 0x0A, 15 | ocf_module: SetAdvertisingEnable, 16 | ogf: 0x08, 17 | ogf_module: LEController 18 | }, 19 | parameters: %{ 20 | advertising_enable: advertising_enable_decoded 21 | } 22 | } 23 | 24 | assert {:ok, expected_command} == Commands.decode(expected_bin) 25 | end 26 | 27 | test "decode_return_parameters/1" do 28 | encoded_status = 1 29 | decoded_status = "Unknown HCI Command" 30 | return_parameters = <> 31 | expected_return_parameters = %{status: decoded_status} 32 | 33 | assert {:ok, expected_return_parameters} == 34 | SetAdvertisingEnable.decode_return_parameters(return_parameters) 35 | end 36 | 37 | test "encode/1" do 38 | {advertising_enable_encoded, advertising_enable_decoded} = {1, true} 39 | parameters = <> 40 | parameters_length = byte_size(parameters) 41 | expected_bin = <<1, 0x0A, 32, parameters_length, parameters::binary>> 42 | expected_size = byte_size(expected_bin) 43 | 44 | parameters = %{advertising_enable: advertising_enable_decoded} 45 | 46 | assert {:ok, actual_bin} = Commands.encode(LEController, SetAdvertisingEnable, parameters) 47 | 48 | assert expected_size == byte_size(actual_bin) 49 | assert expected_bin == actual_bin 50 | end 51 | 52 | test "encode_return_parameters/1" do 53 | decoded_status = "Unknown HCI Command" 54 | encoded_status = 1 55 | return_parameters = %{status: decoded_status} 56 | expected_return_parameters = <> 57 | 58 | assert {:ok, expected_return_parameters} == 59 | SetAdvertisingEnable.encode_return_parameters(return_parameters) 60 | end 61 | 62 | test "ocf/0" do 63 | assert 0x0A == SetAdvertisingEnable.ocf() 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /src/test/harald/hci/commands/le_controller/set_advertising_parameters_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.ControllerAndBaseband.SetAdvertisingParametersTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.HCI.Commands.{Command, LEController} 4 | alias Harald.HCI.{Commands, Commands.LEController.SetAdvertisingParameters} 5 | 6 | test "decode/1" do 7 | advertising_interval_min = 0x800 8 | advertising_interval_max = 0x800 9 | advertising_type = 0x000 10 | own_address_type = 0x000 11 | peer_address_type = 0x000 12 | peer_address = <<0, 0, 0, 0, 0, 0>> 13 | advertising_channel_map = 0x007 14 | advertising_filter_policy = 0x000 15 | 16 | parameters = 17 | <> 20 | 21 | parameters_length = byte_size(parameters) 22 | expected_bin = <<1, 6, 32, parameters_length, parameters::binary>> 23 | 24 | expected_command = %Command{ 25 | command_op_code: %{ 26 | ocf: 0x06, 27 | ocf_module: SetAdvertisingParameters, 28 | ogf: 0x08, 29 | ogf_module: LEController 30 | }, 31 | parameters: %{ 32 | advertising_interval_min: advertising_interval_min, 33 | advertising_interval_max: advertising_interval_max, 34 | advertising_type: advertising_type, 35 | own_address_type: own_address_type, 36 | peer_address_type: peer_address_type, 37 | peer_address: peer_address, 38 | advertising_channel_map: advertising_channel_map, 39 | advertising_filter_policy: advertising_filter_policy 40 | } 41 | } 42 | 43 | assert {:ok, expected_command} == Commands.decode(expected_bin) 44 | end 45 | 46 | test "decode_return_parameters/1" do 47 | status = 1 48 | return_parameters = <> 49 | expected_return_parameters = %{status: status} 50 | 51 | assert {:ok, expected_return_parameters} == 52 | SetAdvertisingParameters.decode_return_parameters(return_parameters) 53 | end 54 | 55 | test "encode/1" do 56 | advertising_interval_min = 0x800 57 | advertising_interval_max = 0x800 58 | advertising_type = 0x000 59 | own_address_type = 0x000 60 | peer_address_type = 0x000 61 | peer_address = <<0, 0, 0, 0, 0, 0>> 62 | advertising_channel_map = 0x007 63 | advertising_filter_policy = 0x000 64 | 65 | parameters = 66 | <> 69 | 70 | parameters_length = byte_size(parameters) 71 | expected_bin = <<1, 6, 32, parameters_length, parameters::binary>> 72 | expected_size = byte_size(expected_bin) 73 | 74 | parameters = %{ 75 | advertising_interval_min: advertising_interval_min, 76 | advertising_interval_max: advertising_interval_max, 77 | advertising_type: advertising_type, 78 | own_address_type: own_address_type, 79 | peer_address_type: peer_address_type, 80 | peer_address: peer_address, 81 | advertising_channel_map: advertising_channel_map, 82 | advertising_filter_policy: advertising_filter_policy 83 | } 84 | 85 | assert {:ok, actual_bin} = Commands.encode(LEController, SetAdvertisingParameters, parameters) 86 | 87 | assert expected_size == byte_size(actual_bin) 88 | assert expected_bin == actual_bin 89 | end 90 | 91 | test "encode_return_parameters/1" do 92 | status = 1 93 | encoded_return_parameters = <> 94 | decoded_return_parameters = %{status: status} 95 | 96 | assert {:ok, encoded_return_parameters} == 97 | SetAdvertisingParameters.encode_return_parameters(decoded_return_parameters) 98 | end 99 | 100 | test "ocf/0" do 101 | assert 0x06 == SetAdvertisingParameters.ocf() 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /src/test/harald/hci/commands/le_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Commands.LEControllerTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.HCI.Commands.LEController 4 | 5 | test "ogf/1" do 6 | assert 0x08 == LEController.ogf() 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /src/test/harald/hci/events/command_complete_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Events.CommandCompleteTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.HCI.Events 4 | alias Harald.HCI.Events.CommandComplete 5 | alias Harald.HCI.Commands.{ControllerAndBaseband, ControllerAndBaseband.ReadLocalName} 6 | 7 | doctest Events, import: true 8 | 9 | test "decode/1" do 10 | num_hci_command_packets = 10 11 | local_name = String.pad_trailing("bob", 248, <<0>>) 12 | status = 0 13 | encoded_return_parameters = <> 14 | decoded_return_parameters = %{local_name: local_name, status: status} 15 | command_op_code = <<20, 12>> 16 | bin = <> <> command_op_code <> encoded_return_parameters 17 | 18 | expected = %{ 19 | command_op_code: %{ 20 | ocf: 0x14, 21 | ocf_module: ReadLocalName, 22 | ogf: 0x03, 23 | ogf_module: ControllerAndBaseband 24 | }, 25 | num_hci_command_packets: num_hci_command_packets, 26 | return_parameters: decoded_return_parameters 27 | } 28 | 29 | assert {:ok, actual} = CommandComplete.decode(bin) 30 | assert expected == actual 31 | end 32 | 33 | test "encode/1" do 34 | num_hci_command_packets = 10 35 | local_name = String.pad_trailing("bob", 248, <<0>>) 36 | status = 0 37 | encoded_return_parameters = <> 38 | decoded_return_parameters = %{local_name: local_name, status: status} 39 | command_op_code = <<20, 12>> 40 | expected = <> <> command_op_code <> encoded_return_parameters 41 | 42 | parameters = %{ 43 | command_op_code: %{ 44 | ocf: 0x14, 45 | ocf_module: ReadLocalName, 46 | ogf: 0x03, 47 | ogf_module: ControllerAndBaseband 48 | }, 49 | num_hci_command_packets: num_hci_command_packets, 50 | return_parameters: decoded_return_parameters 51 | } 52 | 53 | assert {:ok, actual} = CommandComplete.encode(parameters) 54 | assert expected == actual 55 | end 56 | 57 | test "event_code/0" do 58 | assert 0x0E == CommandComplete.event_code() 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /src/test/harald/hci/events/disconnection_complete_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Events.DisconnectionCompleteTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.HCI.Events.DisconnectionComplete 4 | 5 | describe "decode/1" do 6 | status = 0 7 | connection_handle = 1 8 | connection_handle_rfu = 0 9 | 10 | decoded_connection_handle = %{ 11 | rfu: connection_handle_rfu, 12 | handle: connection_handle 13 | } 14 | 15 | reason = 0 16 | 17 | encoded_disconnection_complete = << 18 | status, 19 | connection_handle::little-size(12), 20 | connection_handle_rfu::size(4), 21 | reason 22 | >> 23 | 24 | decoded_disconnection_complete = %{ 25 | status: "Success", 26 | connection_handle: decoded_connection_handle, 27 | reason: "Success" 28 | } 29 | 30 | assert {:ok, decoded_disconnection_complete} == 31 | DisconnectionComplete.decode(encoded_disconnection_complete) 32 | end 33 | 34 | describe "encode/1" do 35 | status = 0 36 | connection_handle = 1 37 | connection_handle_rfu = 0 38 | 39 | decoded_connection_handle = %{ 40 | rfu: connection_handle_rfu, 41 | handle: connection_handle 42 | } 43 | 44 | reason = 0 45 | 46 | expected_encoded_disconnection_complete = << 47 | status, 48 | connection_handle::little-size(12), 49 | connection_handle_rfu::size(4), 50 | reason 51 | >> 52 | 53 | decoded_disconnection_complete = %{ 54 | status: "Success", 55 | connection_handle: decoded_connection_handle, 56 | reason: "Success" 57 | } 58 | 59 | assert {:ok, actual_encoded_disconnection_complete} = 60 | DisconnectionComplete.encode(decoded_disconnection_complete) 61 | 62 | assert expected_encoded_disconnection_complete == 63 | actual_encoded_disconnection_complete 64 | end 65 | 66 | test "event_code/0" do 67 | assert 0x05 == DisconnectionComplete.event_code() 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /src/test/harald/hci/events/le_meta/connection_complete_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Events.LEMeta.ConnectionCompleteTest do 2 | use Harald.HaraldCase 3 | alias Harald.HCI.Events.LEMeta.ConnectionComplete 4 | 5 | test "decode/1" do 6 | status = 0 7 | connection_handle = 1 8 | connection_handle_rfu = 0 9 | 10 | decoded_connection_handle = %{ 11 | rfu: connection_handle_rfu, 12 | handle: connection_handle 13 | } 14 | 15 | role = 0x01 16 | peer_address_type = 0x01 17 | peer_address = 1 18 | connection_interval = 0x0C80 19 | connection_latency = 0x01F3 20 | supervision_timeout = 0xC80 21 | master_clock_accuracy = 0x01 22 | 23 | bin = << 24 | status, 25 | connection_handle::little-size(12), 26 | connection_handle_rfu::size(4), 27 | role, 28 | peer_address_type, 29 | peer_address::little-size(48), 30 | connection_interval::little-size(16), 31 | connection_latency::little-size(16), 32 | supervision_timeout::little-size(16), 33 | master_clock_accuracy 34 | >> 35 | 36 | expected_parameters = %{ 37 | status: status, 38 | connection_handle: decoded_connection_handle, 39 | role: role, 40 | peer_address_type: peer_address_type, 41 | peer_address: peer_address, 42 | connection_interval: connection_interval, 43 | connection_latency: connection_latency, 44 | supervision_timeout: supervision_timeout, 45 | master_clock_accuracy: master_clock_accuracy 46 | } 47 | 48 | assert {:ok, expected_parameters} == ConnectionComplete.decode(bin) 49 | end 50 | 51 | test "encode/1" do 52 | status = 0 53 | connection_handle = 1 54 | connection_handle_rfu = 0 55 | 56 | decoded_connection_handle = %{ 57 | rfu: connection_handle_rfu, 58 | handle: connection_handle 59 | } 60 | 61 | role = 0x01 62 | peer_address_type = 0x01 63 | peer_address = 1 64 | connection_interval = 0x0C80 65 | connection_latency = 0x01F3 66 | supervision_timeout = 0xC80 67 | master_clock_accuracy = 0x01 68 | 69 | expected_bin = << 70 | status, 71 | connection_handle::little-size(12), 72 | connection_handle_rfu::size(4), 73 | role, 74 | peer_address_type, 75 | peer_address::little-size(48), 76 | connection_interval::little-size(16), 77 | connection_latency::little-size(16), 78 | supervision_timeout::little-size(16), 79 | master_clock_accuracy 80 | >> 81 | 82 | parameters = %{ 83 | status: status, 84 | connection_handle: decoded_connection_handle, 85 | role: role, 86 | peer_address_type: peer_address_type, 87 | peer_address: peer_address, 88 | connection_interval: connection_interval, 89 | connection_latency: connection_latency, 90 | supervision_timeout: supervision_timeout, 91 | master_clock_accuracy: master_clock_accuracy 92 | } 93 | 94 | assert {:ok, actual_bin} = ConnectionComplete.encode(parameters) 95 | assert_binaries(expected_bin == actual_bin) 96 | end 97 | 98 | test "sub_event_code/0" do 99 | assert 0x01 == ConnectionComplete.sub_event_code() 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /src/test/harald/hci/events/le_meta/connection_update_complete_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Events.LEMeta.ConnectionUpdateCompleteTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.HCI.Events.LEMeta.ConnectionUpdateComplete 4 | 5 | test "decode/1" do 6 | status = 0 7 | connection_handle = <<1, 2>> 8 | connection_interval = 0x0C80 9 | connection_latency = 0x01F3 10 | supervision_timeout = 0xC80 11 | 12 | bin = 13 | <> 15 | 16 | expected_parameters = %{ 17 | status: status, 18 | connection_handle: connection_handle, 19 | connection_interval: connection_interval, 20 | connection_latency: connection_latency, 21 | supervision_timeout: supervision_timeout 22 | } 23 | 24 | assert {:ok, expected_parameters} == ConnectionUpdateComplete.decode(bin) 25 | end 26 | 27 | test "encode/1" do 28 | status = 0 29 | connection_handle = <<1, 2>> 30 | connection_interval = 0x0C80 31 | connection_latency = 0x01F3 32 | supervision_timeout = 0xC80 33 | 34 | expected_bin = 35 | <> 37 | 38 | parameters = %{ 39 | status: status, 40 | connection_handle: connection_handle, 41 | connection_interval: connection_interval, 42 | connection_latency: connection_latency, 43 | supervision_timeout: supervision_timeout 44 | } 45 | 46 | assert {:ok, actual_bin} = ConnectionUpdateComplete.encode(parameters) 47 | assert expected_bin == actual_bin 48 | end 49 | 50 | test "sub_event_code/0" do 51 | assert 0x03 == ConnectionUpdateComplete.sub_event_code() 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /src/test/harald/hci/events/le_meta_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.Events.LEMetaTest do 2 | use Harald.HaraldCase 3 | alias Harald.HCI.Events.{LEMeta, LEMeta.ConnectionComplete} 4 | 5 | test "decode/1" do 6 | sub_event_code = ConnectionComplete.sub_event_code() 7 | sub_event_module = ConnectionComplete 8 | status = 0 9 | connection_handle = 1 10 | connection_handle_rfu = 2 11 | 12 | decoded_connection_handle = %{ 13 | rfu: connection_handle_rfu, 14 | handle: connection_handle 15 | } 16 | 17 | role = 0x01 18 | peer_address_type = 0x01 19 | peer_address = 1 20 | connection_interval = 0x0C80 21 | connection_latency = 0x01F3 22 | supervision_timeout = 0xC80 23 | master_clock_accuracy = 0x01 24 | 25 | <> = 26 | <> 27 | 28 | encoded_sub_event_parameters = << 29 | status, 30 | connection_handle::size(16), 31 | role, 32 | peer_address_type, 33 | peer_address::little-size(48), 34 | connection_interval::little-size(16), 35 | connection_latency::little-size(16), 36 | supervision_timeout::little-size(16), 37 | master_clock_accuracy 38 | >> 39 | 40 | decoded_subevent_parameters = %{ 41 | status: status, 42 | connection_handle: decoded_connection_handle, 43 | role: role, 44 | peer_address_type: peer_address_type, 45 | peer_address: peer_address, 46 | connection_interval: connection_interval, 47 | connection_latency: connection_latency, 48 | supervision_timeout: supervision_timeout, 49 | master_clock_accuracy: master_clock_accuracy 50 | } 51 | 52 | encoded_le_meta = <> 53 | 54 | decoded_le_meta = %{ 55 | sub_event: %{code: sub_event_code, module: sub_event_module}, 56 | sub_event_parameters: decoded_subevent_parameters 57 | } 58 | 59 | assert {:ok, actual_decoded_le_meta} = LEMeta.decode(encoded_le_meta) 60 | assert decoded_le_meta == actual_decoded_le_meta 61 | end 62 | 63 | test "encode/1" do 64 | sub_event_code = ConnectionComplete.sub_event_code() 65 | sub_event_module = ConnectionComplete 66 | status = 0 67 | connection_handle = 1 68 | connection_handle_rfu = 2 69 | 70 | decoded_connection_handle = %{ 71 | rfu: connection_handle_rfu, 72 | handle: connection_handle 73 | } 74 | 75 | role = 0x01 76 | peer_address_type = 0x01 77 | peer_address = 1 78 | connection_interval = 0x0C80 79 | connection_latency = 0x01F3 80 | supervision_timeout = 0xC80 81 | master_clock_accuracy = 0x01 82 | 83 | <> = 84 | <> 85 | 86 | encoded_sub_event_parameters = << 87 | status, 88 | connection_handle::size(16), 89 | role, 90 | peer_address_type, 91 | peer_address::little-size(48), 92 | connection_interval::little-size(16), 93 | connection_latency::little-size(16), 94 | supervision_timeout::little-size(16), 95 | master_clock_accuracy 96 | >> 97 | 98 | decoded_subevent_parameters = %{ 99 | status: status, 100 | connection_handle: decoded_connection_handle, 101 | role: role, 102 | peer_address_type: peer_address_type, 103 | peer_address: peer_address, 104 | connection_interval: connection_interval, 105 | connection_latency: connection_latency, 106 | supervision_timeout: supervision_timeout, 107 | master_clock_accuracy: master_clock_accuracy 108 | } 109 | 110 | expected_encoded_le_meta = <> 111 | 112 | decoded_le_meta = %{ 113 | sub_event: %{code: sub_event_code, module: sub_event_module}, 114 | sub_event_parameters: decoded_subevent_parameters 115 | } 116 | 117 | assert {:ok, actual_encoded_le_meta} = LEMeta.encode(decoded_le_meta) 118 | assert_binaries(expected_encoded_le_meta == actual_encoded_le_meta) 119 | end 120 | 121 | test "event_code/0" do 122 | assert 0x3E == LEMeta.event_code() 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /src/test/harald/hci/synchronous_data_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCI.SynchronousDataTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.HCI.SynchronousData 4 | 5 | test "decode/1" do 6 | connection_handle = <<1::size(8), 2::size(4)>> 7 | packet_status_flag = 0b00 8 | rfu = 0b00 9 | data = <<1, 3, 3, 7>> 10 | data_total_length = byte_size(data) 11 | 12 | encoded_acl_data = << 13 | connection_handle::bits-size(12), 14 | packet_status_flag::size(2), 15 | rfu::size(2), 16 | data_total_length, 17 | data::binary 18 | >> 19 | 20 | decoded_acl_data = %SynchronousData{ 21 | connection_handle: connection_handle, 22 | packet_status_flag: %{ 23 | description: 24 | "Correctly received data. The payload data belongs to received eSCO or SCO packets that the baseband marked as \"good data\".", 25 | value: packet_status_flag 26 | }, 27 | rfu: rfu, 28 | data_total_length: data_total_length, 29 | data: data 30 | } 31 | 32 | assert {:ok, decoded_acl_data} == SynchronousData.decode(encoded_acl_data) 33 | end 34 | 35 | test "encode/1" do 36 | connection_handle = <<1::size(8), 2::size(4)>> 37 | packet_status_flag = 0b00 38 | rfu = 0b00 39 | data = <<1, 3, 3, 7>> 40 | data_total_length = byte_size(data) 41 | 42 | encoded_acl_data = << 43 | connection_handle::bits-size(12), 44 | packet_status_flag::size(2), 45 | rfu::size(2), 46 | data_total_length, 47 | data::binary 48 | >> 49 | 50 | decoded_acl_data = %SynchronousData{ 51 | connection_handle: connection_handle, 52 | packet_status_flag: %{ 53 | description: 54 | "Correctly received data. The payload data belongs to received eSCO or SCO packets that the baseband marked as \"good data\".", 55 | value: packet_status_flag 56 | }, 57 | rfu: rfu, 58 | data_total_length: data_total_length, 59 | data: data 60 | } 61 | 62 | assert {:ok, encoded_acl_data} == SynchronousData.encode(decoded_acl_data) 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /src/test/harald/hci_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.HCITest do 2 | # use ExUnit.Case, async: true 3 | # use ExUnitProperties 4 | # alias Harald.Generators.HCI, as: HCIGen 5 | # alias Harald.{HCI, HCI.Packet} 6 | # require Harald.Serializable, as: Serializable 7 | 8 | # doctest Harald.HCI, import: true 9 | 10 | # test "opcode/2" do 11 | # check all ogf <- StreamData.integer(0..63), 12 | # ocf <- StreamData.integer(0..1023) do 13 | # <> = HCI.opcode(ogf, ocf) 14 | # assert <<^ogf::size(6), ^ocf::size(10)>> = <> 15 | # end 16 | # end 17 | 18 | # test "command/1" do 19 | # check all opcode <- StreamData.integer(0..65_535) do 20 | # assert <<1, ^opcode::size(16), 0>> = HCI.command(<>) 21 | # end 22 | # end 23 | 24 | # describe "command/2" do 25 | # test "with binary opts" do 26 | # check all opcode <- StreamData.integer(0..65_535), 27 | # opts <- StreamData.binary(min_length: 1) do 28 | # s = byte_size(opts) 29 | 30 | # assert <<1, opcode::size(16), s>> <> opts == HCI.command(<>, opts) 31 | # end 32 | # end 33 | 34 | # test "with list opts" do 35 | # check all opcode <- StreamData.integer(0..65_535), 36 | # opts <- StreamData.binary(min_length: 1), 37 | # bool_opt <- StreamData.boolean() do 38 | # s = byte_size(opts) + 1 39 | # bool_int = if bool_opt, do: 1, else: 0 40 | 41 | # assert <> <> opts == 42 | # HCI.command(<>, [bool_opt, opts]) 43 | # end 44 | # end 45 | # end 46 | 47 | # test "to_bin/1" do 48 | # check all bin <- StreamData.binary() do 49 | # assert bin == HCI.to_bin(bin) 50 | # end 51 | # end 52 | 53 | # property "symmetric (de)serialization" do 54 | # check all bin <- HCIGen.packet(), 55 | # rand_bin <- StreamData.binary() do 56 | # Serializable.assert_symmetry(HCI, bin) 57 | # Serializable.assert_symmetry(HCI, rand_bin) 58 | # end 59 | # end 60 | end 61 | -------------------------------------------------------------------------------- /src/test/harald/host/att/error_codes_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.ErrorCodesTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.Host.ATT.ErrorCodes 4 | 5 | test "encode/1" do 6 | name = "Write Not Permitted" 7 | expected_code = 0x03 8 | assert {:ok, expected_code} == ErrorCodes.encode(name) 9 | end 10 | 11 | test "decode/1" do 12 | expected_name = "Write Not Permitted" 13 | code = 0x03 14 | assert {:ok, expected_name} == ErrorCodes.decode(code) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /src/test/harald/host/att/error_rsp_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.ErrorRspTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.Host.ATT.{WriteReq, ErrorRsp, ErrorCodes} 4 | 5 | test "encode/1" do 6 | request_module_in_error = WriteReq 7 | attribute_handle_in_error = 1 8 | error_message = "Write Not Permitted" 9 | 10 | parameters = %{ 11 | request_module_in_error: request_module_in_error, 12 | attribute_handle_in_error: attribute_handle_in_error, 13 | error_message: error_message 14 | } 15 | 16 | module_opcode = request_module_in_error.opcode() 17 | {:ok, error_code} = ErrorCodes.encode(error_message) 18 | 19 | expected_bin = 20 | <> 22 | 23 | expected_size = byte_size(expected_bin) 24 | 25 | assert {:ok, actual_bin} = ErrorRsp.encode(parameters) 26 | assert expected_bin == actual_bin 27 | assert expected_size == byte_size(actual_bin) 28 | end 29 | 30 | test "decode/1" do 31 | request_module_in_error = WriteReq 32 | attribute_handle_in_error = 1 33 | error_message = "Write Not Permitted" 34 | 35 | expected_parameters = %{ 36 | request_module_in_error: request_module_in_error, 37 | attribute_handle_in_error: attribute_handle_in_error, 38 | error_message: error_message 39 | } 40 | 41 | module_opcode = request_module_in_error.opcode() 42 | {:ok, error_code} = ErrorCodes.encode(error_message) 43 | 44 | bin = 45 | <> 47 | 48 | assert {:ok, expected_parameters} == ErrorRsp.decode(bin) 49 | end 50 | 51 | test "opcode/0" do 52 | assert 0x01 == ErrorRsp.opcode() 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /src/test/harald/host/att/exchange_mtu_req_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.ExchangeMTUReqTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.Host.ATT.ExchangeMTUReq 4 | 5 | test "encode/1" do 6 | client_rx_mtu = 185 7 | parameters = %{client_rx_mtu: client_rx_mtu} 8 | expected_bin = <> 9 | expected_size = byte_size(expected_bin) 10 | 11 | assert {:ok, actual_bin} = ExchangeMTUReq.encode(parameters) 12 | assert expected_bin == actual_bin 13 | assert expected_size == byte_size(actual_bin) 14 | end 15 | 16 | test "decode/1" do 17 | client_rx_mtu = 185 18 | bin = <> 19 | expected_parameters = %{client_rx_mtu: client_rx_mtu} 20 | 21 | assert {:ok, expected_parameters} == ExchangeMTUReq.decode(bin) 22 | end 23 | 24 | test "opcode/0" do 25 | assert 0x02 == ExchangeMTUReq.opcode() 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /src/test/harald/host/att/execute_write_req_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.ExecuteWriteReqTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.Host.ATT.ExecuteWriteReq 4 | 5 | test "encode/1" do 6 | parameters = %{flags: :write_all} 7 | 8 | expected_bin = <<0x01>> 9 | expected_size = byte_size(expected_bin) 10 | 11 | assert {:ok, actual_bin} = ExecuteWriteReq.encode(parameters) 12 | assert expected_bin == actual_bin 13 | assert expected_size == byte_size(actual_bin) 14 | end 15 | 16 | test "decode/1" do 17 | bin = <<0x01>> 18 | 19 | expected_parameters = %{flags: :write_all} 20 | 21 | assert {:ok, expected_parameters} == ExecuteWriteReq.decode(bin) 22 | end 23 | 24 | test "opcode/0" do 25 | assert 0x18 == ExecuteWriteReq.opcode() 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /src/test/harald/host/att/execute_write_rsp_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.ExecuteWriteRspTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.Host.ATT.ExecuteWriteRsp 4 | 5 | test "encode/1" do 6 | parameters = %{} 7 | 8 | expected_bin = <<>> 9 | expected_size = byte_size(expected_bin) 10 | 11 | assert {:ok, actual_bin} = ExecuteWriteRsp.encode(parameters) 12 | assert expected_bin == actual_bin 13 | assert expected_size == byte_size(actual_bin) 14 | end 15 | 16 | test "decode/1" do 17 | bin = <<>> 18 | 19 | expected_parameters = %{} 20 | 21 | assert {:ok, expected_parameters} == ExecuteWriteRsp.decode(bin) 22 | end 23 | 24 | test "opcode/0" do 25 | assert 0x19 == ExecuteWriteRsp.opcode() 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /src/test/harald/host/att/find_information_req_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.FindInformationReqTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.Host.ATT.FindInformationReq 4 | 5 | describe "encode/1" do 6 | test "default" do 7 | starting_handle = 1 8 | ending_handle = 10 9 | parameters = %{starting_handle: starting_handle, ending_handle: ending_handle} 10 | expected_bin = <> 11 | expected_size = byte_size(expected_bin) 12 | 13 | assert {:ok, actual_bin} = FindInformationReq.encode(parameters) 14 | assert expected_bin == actual_bin 15 | assert expected_size == byte_size(actual_bin) 16 | end 17 | 18 | test "zero test" do 19 | starting_handle = 0 20 | ending_handle = 1 21 | parameters = %{starting_handle: starting_handle, ending_handle: ending_handle} 22 | 23 | assert {:error, _} = FindInformationReq.encode(parameters) 24 | end 25 | 26 | test "ending_handle < starting_handle" do 27 | starting_handle = 10 28 | ending_handle = 1 29 | parameters = %{starting_handle: starting_handle, ending_handle: ending_handle} 30 | 31 | assert {:error, _} = FindInformationReq.encode(parameters) 32 | end 33 | end 34 | 35 | test "decode/1" do 36 | starting_handle = 10 37 | ending_handle = 20 38 | bin = <> 39 | expected_parameters = %{starting_handle: starting_handle, ending_handle: ending_handle} 40 | 41 | assert {:ok, expected_parameters} == FindInformationReq.decode(bin) 42 | end 43 | 44 | test "opcode/0" do 45 | assert 0x04 == FindInformationReq.opcode() 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /src/test/harald/host/att/prepare_write_req_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.PrepareWriteReqTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.Host.ATT.PrepareWriteReq 4 | 5 | test "encode/1" do 6 | attribute_handle = 185 7 | value_offset = 5 8 | part_attribute_value = <<72, 101, 108, 108, 111>> 9 | 10 | parameters = %{ 11 | attribute_handle: attribute_handle, 12 | value_offset: value_offset, 13 | part_attribute_value: part_attribute_value 14 | } 15 | 16 | expected_bin = 17 | <> 19 | 20 | expected_size = byte_size(expected_bin) 21 | 22 | assert {:ok, actual_bin} = PrepareWriteReq.encode(parameters) 23 | assert expected_bin == actual_bin 24 | assert expected_size == byte_size(actual_bin) 25 | end 26 | 27 | test "decode/1" do 28 | attribute_handle = 185 29 | value_offset = 5 30 | part_attribute_value = <<72, 101, 108, 108, 111>> 31 | 32 | bin = 33 | <> 35 | 36 | expected_parameters = %{ 37 | attribute_handle: attribute_handle, 38 | value_offset: value_offset, 39 | part_attribute_value: part_attribute_value 40 | } 41 | 42 | assert {:ok, expected_parameters} == PrepareWriteReq.decode(bin) 43 | end 44 | 45 | test "opcode/0" do 46 | assert 0x16 == PrepareWriteReq.opcode() 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /src/test/harald/host/att/prepare_write_rsp_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.PrepareWriteRspTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.Host.ATT.PrepareWriteRsp 4 | 5 | test "encode/1" do 6 | attribute_handle = 185 7 | value_offset = 5 8 | part_attribute_value = <<72, 101, 108, 108, 111>> 9 | 10 | parameters = %{ 11 | attribute_handle: attribute_handle, 12 | value_offset: value_offset, 13 | part_attribute_value: part_attribute_value 14 | } 15 | 16 | expected_bin = 17 | <> 19 | 20 | expected_size = byte_size(expected_bin) 21 | 22 | assert {:ok, actual_bin} = PrepareWriteRsp.encode(parameters) 23 | assert expected_bin == actual_bin 24 | assert expected_size == byte_size(actual_bin) 25 | end 26 | 27 | test "decode/1" do 28 | attribute_handle = 185 29 | value_offset = 5 30 | part_attribute_value = <<72, 101, 108, 108, 111>> 31 | 32 | bin = 33 | <> 35 | 36 | expected_parameters = %{ 37 | attribute_handle: attribute_handle, 38 | value_offset: value_offset, 39 | part_attribute_value: part_attribute_value 40 | } 41 | 42 | assert {:ok, expected_parameters} == PrepareWriteRsp.decode(bin) 43 | end 44 | 45 | test "opcode/0" do 46 | assert 0x17 == PrepareWriteRsp.opcode() 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /src/test/harald/host/att/read_blob_req_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.ReadBlobReqTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.Host.ATT.ReadBlobReq 4 | 5 | test "encode/1" do 6 | attribute_handle = 185 7 | value_offset = 20 8 | parameters = %{attribute_handle: attribute_handle, value_offset: value_offset} 9 | expected_bin = <> 10 | expected_size = byte_size(expected_bin) 11 | 12 | assert {:ok, actual_bin} = ReadBlobReq.encode(parameters) 13 | assert expected_bin == actual_bin 14 | assert expected_size == byte_size(actual_bin) 15 | end 16 | 17 | test "decode/1" do 18 | attribute_handle = 185 19 | value_offset = 20 20 | bin = <> 21 | expected_parameters = %{attribute_handle: attribute_handle, value_offset: value_offset} 22 | 23 | assert {:ok, expected_parameters} == ReadBlobReq.decode(bin) 24 | end 25 | 26 | test "opcode/0" do 27 | assert 0x0C == ReadBlobReq.opcode() 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /src/test/harald/host/att/read_blob_rsp_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.ReadBlobRspTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.Host.ATT.ReadBlobRsp 4 | 5 | test "encode/1" do 6 | part_attribute_value = <<4, 8, 15, 16, 23, 42>> 7 | parameters = %{part_attribute_value: part_attribute_value} 8 | expected_bin = <> 9 | expected_size = byte_size(expected_bin) 10 | 11 | assert {:ok, actual_bin} = ReadBlobRsp.encode(parameters) 12 | assert expected_bin == actual_bin 13 | assert expected_size == byte_size(actual_bin) 14 | end 15 | 16 | test "decode/1" do 17 | part_attribute_value = <<4, 8, 15, 16, 23, 42>> 18 | bin = <> 19 | expected_parameters = %{part_attribute_value: part_attribute_value} 20 | 21 | assert {:ok, expected_parameters} == ReadBlobRsp.decode(bin) 22 | end 23 | 24 | test "opcode/0" do 25 | assert 0x0D == ReadBlobRsp.opcode() 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /src/test/harald/host/att/read_by_group_type_req_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.ReadByGroupTypeReqTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.Host.ATT.ReadByGroupTypeReq 4 | 5 | test "encode/1" do 6 | starting_handle = 1 7 | ending_handle = 0xFFFF 8 | attribute_group_type = 0x2800 9 | 10 | parameters = %{ 11 | starting_handle: starting_handle, 12 | ending_handle: ending_handle, 13 | attribute_group_type: attribute_group_type 14 | } 15 | 16 | expected_bin = 17 | <> 19 | 20 | expected_size = byte_size(expected_bin) 21 | 22 | assert {:ok, actual_bin} = ReadByGroupTypeReq.encode(parameters) 23 | assert expected_bin == actual_bin 24 | assert expected_size == byte_size(actual_bin) 25 | end 26 | 27 | test "decode/1" do 28 | starting_handle = 1 29 | ending_handle = 0xFFFF 30 | attribute_group_type = 0x2800 31 | 32 | bin = 33 | <> 35 | 36 | expected_parameters = %{ 37 | starting_handle: starting_handle, 38 | ending_handle: ending_handle, 39 | attribute_group_type: attribute_group_type 40 | } 41 | 42 | assert {:ok, expected_parameters} == ReadByGroupTypeReq.decode(bin) 43 | end 44 | 45 | test "opcode/0" do 46 | assert 0x10 == ReadByGroupTypeReq.opcode() 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /src/test/harald/host/att/read_by_group_type_rsp_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.ReadByGroupTypeRspTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.Host.ATT.ReadByGroupTypeRsp 4 | 5 | describe "encode/1" do 6 | test "single" do 7 | length = 6 8 | 9 | attribute_handle = 40 10 | end_group_handle = 50 11 | attribute_value = 0x3040 12 | 13 | attribute_data_list = [ 14 | %{ 15 | attribute_handle: attribute_handle, 16 | end_group_handle: end_group_handle, 17 | attribute_value: attribute_value 18 | } 19 | ] 20 | 21 | parameters = %{length: length, attribute_data_list: attribute_data_list} 22 | 23 | expected_bin = 24 | <> 26 | 27 | expected_size = byte_size(expected_bin) 28 | 29 | assert {:ok, actual_bin} = ReadByGroupTypeRsp.encode(parameters) 30 | assert expected_bin == actual_bin 31 | assert expected_size == byte_size(actual_bin) 32 | end 33 | 34 | test "double" do 35 | length = 6 36 | 37 | attribute_handle = 10 38 | end_group_handle = 20 39 | attribute_value = 0x3040 40 | 41 | attribute_handle2 = 50 42 | end_group_handle2 = 60 43 | attribute_value2 = 0x7080 44 | 45 | attribute_data_list = [ 46 | %{ 47 | attribute_handle: attribute_handle, 48 | end_group_handle: end_group_handle, 49 | attribute_value: attribute_value 50 | }, 51 | %{ 52 | attribute_handle: attribute_handle2, 53 | end_group_handle: end_group_handle2, 54 | attribute_value: attribute_value2 55 | } 56 | ] 57 | 58 | parameters = %{length: length, attribute_data_list: attribute_data_list} 59 | 60 | expected_bin = 61 | <> 64 | 65 | expected_size = byte_size(expected_bin) 66 | 67 | assert {:ok, actual_bin} = ReadByGroupTypeRsp.encode(parameters) 68 | assert expected_bin == actual_bin 69 | assert expected_size == byte_size(actual_bin) 70 | end 71 | end 72 | 73 | describe "decode/1" do 74 | test "single" do 75 | length = 6 76 | 77 | attribute_handle = 40 78 | end_group_handle = 50 79 | attribute_value = 0x3040 80 | 81 | attribute_data_list = [ 82 | %{ 83 | attribute_handle: attribute_handle, 84 | end_group_handle: end_group_handle, 85 | attribute_value: attribute_value 86 | } 87 | ] 88 | 89 | expected_parameters = %{length: length, attribute_data_list: attribute_data_list} 90 | 91 | bin = 92 | <> 94 | 95 | assert {:ok, expected_parameters} == ReadByGroupTypeRsp.decode(bin) 96 | end 97 | 98 | test "double" do 99 | length = 6 100 | 101 | attribute_handle = 10 102 | end_group_handle = 20 103 | attribute_value = 0x3040 104 | 105 | attribute_handle2 = 50 106 | end_group_handle2 = 60 107 | attribute_value2 = 0x7080 108 | 109 | attribute_data_list = [ 110 | %{ 111 | attribute_handle: attribute_handle, 112 | end_group_handle: end_group_handle, 113 | attribute_value: attribute_value 114 | }, 115 | %{ 116 | attribute_handle: attribute_handle2, 117 | end_group_handle: end_group_handle2, 118 | attribute_value: attribute_value2 119 | } 120 | ] 121 | 122 | expected_parameters = %{length: length, attribute_data_list: attribute_data_list} 123 | 124 | bin = 125 | <> 128 | 129 | assert {:ok, expected_parameters} == ReadByGroupTypeRsp.decode(bin) 130 | end 131 | end 132 | 133 | test "opcode/0" do 134 | assert 0x11 == ReadByGroupTypeRsp.opcode() 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /src/test/harald/host/att/read_req_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.ReadReqTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.Host.ATT.ReadReq 4 | 5 | test "encode/1" do 6 | attribute_handle = 185 7 | parameters = %{attribute_handle: attribute_handle} 8 | expected_bin = <> 9 | expected_size = byte_size(expected_bin) 10 | 11 | assert {:ok, actual_bin} = ReadReq.encode(parameters) 12 | assert expected_bin == actual_bin 13 | assert expected_size == byte_size(actual_bin) 14 | end 15 | 16 | test "decode/1" do 17 | attribute_handle = 185 18 | bin = <> 19 | expected_parameters = %{attribute_handle: attribute_handle} 20 | 21 | assert {:ok, expected_parameters} == ReadReq.decode(bin) 22 | end 23 | 24 | test "opcode/0" do 25 | assert 0x0A == ReadReq.opcode() 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /src/test/harald/host/att/read_rsp_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.ReadRspTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.Host.ATT.ReadRsp 4 | 5 | test "encode/1" do 6 | attribute_value = <<4, 8, 15, 16, 23, 42>> 7 | parameters = %{attribute_value: attribute_value} 8 | expected_bin = <> 9 | expected_size = byte_size(expected_bin) 10 | 11 | assert {:ok, actual_bin} = ReadRsp.encode(parameters) 12 | assert expected_bin == actual_bin 13 | assert expected_size == byte_size(actual_bin) 14 | end 15 | 16 | test "decode/1" do 17 | attribute_value = <<4, 8, 15, 16, 23, 42>> 18 | bin = <> 19 | expected_parameters = %{attribute_value: attribute_value} 20 | 21 | assert {:ok, expected_parameters} == ReadRsp.decode(bin) 22 | end 23 | 24 | test "opcode/0" do 25 | assert 0x0B == ReadRsp.opcode() 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /src/test/harald/host/att/write_cmd_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.WriteCmdTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.Host.ATT.WriteCmd 4 | 5 | test "encode/1" do 6 | attribute_handle = 185 7 | attribute_value = <<72, 101, 108, 108, 111>> 8 | parameters = %{attribute_handle: attribute_handle, attribute_value: attribute_value} 9 | expected_bin = <> 10 | expected_size = byte_size(expected_bin) 11 | 12 | assert {:ok, actual_bin} = WriteCmd.encode(parameters) 13 | assert expected_bin == actual_bin 14 | assert expected_size == byte_size(actual_bin) 15 | end 16 | 17 | test "decode/1" do 18 | attribute_handle = 185 19 | attribute_value = <<72, 101, 108, 108, 111>> 20 | bin = <> 21 | expected_parameters = %{attribute_handle: attribute_handle, attribute_value: attribute_value} 22 | 23 | assert {:ok, expected_parameters} == WriteCmd.decode(bin) 24 | end 25 | 26 | test "opcode/0" do 27 | assert 0x52 == WriteCmd.opcode() 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /src/test/harald/host/att/write_req_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.WriteReqTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.Host.ATT.WriteReq 4 | 5 | test "encode/1" do 6 | attribute_handle = 185 7 | attribute_value = <<72, 101, 108, 108, 111>> 8 | parameters = %{attribute_handle: attribute_handle, attribute_value: attribute_value} 9 | expected_bin = <> 10 | expected_size = byte_size(expected_bin) 11 | 12 | assert {:ok, actual_bin} = WriteReq.encode(parameters) 13 | assert expected_bin == actual_bin 14 | assert expected_size == byte_size(actual_bin) 15 | end 16 | 17 | test "decode/1" do 18 | attribute_handle = 185 19 | attribute_value = <<72, 101, 108, 108, 111>> 20 | bin = <> 21 | expected_parameters = %{attribute_handle: attribute_handle, attribute_value: attribute_value} 22 | 23 | assert {:ok, expected_parameters} == WriteReq.decode(bin) 24 | end 25 | 26 | test "opcode/0" do 27 | assert 0x12 == WriteReq.opcode() 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /src/test/harald/host/att/write_rsp_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATT.WriteRspTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.Host.ATT.WriteRsp 4 | 5 | test "encode/1" do 6 | parameters = %{} 7 | expected_bin = <<>> 8 | expected_size = byte_size(expected_bin) 9 | 10 | assert {:ok, actual_bin} = WriteRsp.encode(parameters) 11 | assert expected_bin == actual_bin 12 | assert expected_size == byte_size(actual_bin) 13 | end 14 | 15 | test "decode/1" do 16 | bin = <<>> 17 | expected_parameters = %{} 18 | 19 | assert {:ok, expected_parameters} == WriteRsp.decode(bin) 20 | end 21 | 22 | test "opcode/0" do 23 | assert 0x13 == WriteRsp.opcode() 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /src/test/harald/host/att_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.ATTTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.Host.ATT 4 | alias Harald.Host.ATT.ExchangeMTUReq 5 | 6 | test "decode/1" do 7 | decoded_mtu = 185 8 | decoded_exchange_mtu_req = 2 9 | 10 | encoded_att = << 11 | decoded_exchange_mtu_req, 12 | decoded_mtu::little-size(16) 13 | >> 14 | 15 | opcode_module = ExchangeMTUReq 16 | decoded_att_parameters = %{client_rx_mtu: 185} 17 | {:ok, decoded_att} = ATT.new(opcode_module, decoded_att_parameters) 18 | 19 | assert {:ok, decoded_att} == ATT.decode(encoded_att) 20 | end 21 | 22 | test "encode/1" do 23 | decoded_mtu = 185 24 | decoded_exchange_mtu_req = 2 25 | 26 | encoded_att = << 27 | decoded_exchange_mtu_req, 28 | decoded_mtu::little-size(16) 29 | >> 30 | 31 | opcode_module = ExchangeMTUReq 32 | decoded_att_parameters = %{client_rx_mtu: 185} 33 | {:ok, decoded_att} = ATT.new(opcode_module, decoded_att_parameters) 34 | 35 | assert {:ok, encoded_att} == ATT.encode(decoded_att) 36 | end 37 | 38 | test "id/1" do 39 | assert 0x04 == ATT.id() 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /src/test/harald/host/l2cap_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.Host.L2CAPTest do 2 | use ExUnit.Case, async: true 3 | alias Harald.Host.{ATT, L2CAP} 4 | alias Harald.Host.ATT.ExchangeMTUReq 5 | 6 | test "decode/1" do 7 | decoded_mtu = 185 8 | decoded_exchange_mtu_rsp = 2 9 | 10 | encoded_att_data = << 11 | decoded_exchange_mtu_rsp, 12 | decoded_mtu::little-size(16) 13 | >> 14 | 15 | decoded_att_length = byte_size(encoded_att_data) 16 | decoded_channel_id = 4 17 | 18 | encoded_l2cap = << 19 | decoded_att_length::little-size(16), 20 | decoded_channel_id::little-size(16), 21 | encoded_att_data::binary 22 | >> 23 | 24 | opcode_module = ExchangeMTUReq 25 | decoded_att_parameters = %{client_rx_mtu: 185} 26 | {:ok, decoded_att} = ATT.new(opcode_module, decoded_att_parameters) 27 | {:ok, decoded_l2cap} = L2CAP.new(ATT, decoded_att) 28 | 29 | assert {:ok, decoded_l2cap} == L2CAP.decode(encoded_l2cap) 30 | end 31 | 32 | test "encode/1" do 33 | decoded_mtu = 185 34 | decoded_exchange_mtu_rsp = 2 35 | 36 | encoded_att_data = << 37 | decoded_exchange_mtu_rsp, 38 | decoded_mtu::little-size(16) 39 | >> 40 | 41 | decoded_att_length = byte_size(encoded_att_data) 42 | decoded_channel_id = 4 43 | 44 | encoded_l2cap = << 45 | decoded_att_length::little-size(16), 46 | decoded_channel_id::little-size(16), 47 | encoded_att_data::binary 48 | >> 49 | 50 | decoded_att_opcode = ExchangeMTUReq 51 | decoded_att_parameters = %{client_rx_mtu: 185} 52 | {:ok, decoded_att} = ATT.new(decoded_att_opcode, decoded_att_parameters) 53 | {:ok, decoded_l2cap} = L2CAP.new(ATT, decoded_att) 54 | 55 | assert {:ok, encoded_l2cap} == L2CAP.encode(decoded_l2cap) 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /src/test/harald/transport/uart/framing_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Harald.Transport.UART.FramingTest do 2 | use ExUnit.Case, async: true 3 | use ExUnitProperties 4 | alias Harald.HCI.Transport.UART.Framing 5 | alias Harald.HCI.Transport.UART.Framing.State 6 | 7 | doctest Harald.HCI.Transport.UART.Framing, import: true 8 | 9 | describe "init/1" do 10 | property "returns a fresh state" do 11 | check all(args <- StreamData.term()) do 12 | assert {:ok, %State{}} == Framing.init(args) 13 | end 14 | end 15 | end 16 | 17 | describe "add_framing/2" do 18 | property "returns data and state unchanged" do 19 | check all( 20 | data <- StreamData.binary(), 21 | state <- StreamData.term() 22 | ) do 23 | assert {:ok, data, state} == Framing.add_framing(data, state) 24 | end 25 | end 26 | end 27 | 28 | describe "flush/2" do 29 | property ":transmit returns state unchanged" do 30 | check all(state <- StreamData.term()) do 31 | assert state == Framing.flush(:transmit, state) 32 | end 33 | end 34 | 35 | property ":receive returns a fresh state" do 36 | check all(state <- StreamData.term()) do 37 | assert %State{} == Framing.flush(:receive, state) 38 | end 39 | end 40 | 41 | property ":both returns a fresh state" do 42 | check all(state <- StreamData.term()) do 43 | assert %State{} == Framing.flush(:both, state) 44 | end 45 | end 46 | end 47 | 48 | describe "frame_timeout/1" do 49 | property "returns the state and an empty binary" do 50 | check all(state <- StreamData.term()) do 51 | assert {:ok, [state], <<>>} == Framing.frame_timeout(state) 52 | end 53 | end 54 | end 55 | 56 | describe "remove_framing/2" do 57 | property "bad packet types return the remaining data in error" do 58 | check all( 59 | packet_type <- StreamData.integer(), 60 | packet_type not in 2..4, 61 | rest <- StreamData.binary(), 62 | binary = <> 63 | ) do 64 | assert {:ok, [{:error, {:bad_packet_type, binary}}], %State{}} == 65 | Framing.remove_framing(binary, %State{}) 66 | end 67 | end 68 | 69 | property "returns when receiving a complete binary of packets" do 70 | check all( 71 | tuples <- 72 | StreamData.list_of({StreamData.integer(2..4), StreamData.binary()}, 73 | min_length: 1 74 | ), 75 | max_runs: 1 76 | ) do 77 | packets = 78 | Enum.map(tuples, fn 79 | {2, binary} -> 80 | length = byte_size(binary) 81 | <<2, 1, 2, length::little-size(16), binary::binary>> 82 | 83 | {3, binary} -> 84 | length = byte_size(binary) 85 | <<3, 1, 2, length::little-size(16), binary::binary>> 86 | 87 | {4, binary} -> 88 | length = byte_size(binary) 89 | <<4, 0, length, binary::binary>> 90 | end) 91 | 92 | bin = Enum.join(packets) 93 | 94 | assert {:ok, ^packets, %{frame: "", remaining_bytes: nil}} = 95 | Framing.remove_framing(bin, %State{}) 96 | end 97 | end 98 | 99 | property "returns when receiving a binary of packets that will end in_frame" do 100 | check all(binaries <- StreamData.list_of(StreamData.binary(), min_length: 1)) do 101 | [<> | tail] = 102 | Enum.map(binaries, fn binary -> 103 | length = byte_size(binary) 104 | <<4, 0, length, binary::binary>> 105 | end) 106 | 107 | bin = Enum.join(tail ++ [bin_head]) 108 | 109 | assert {:in_frame, ^tail, %{frame: ^bin_head}} = Framing.remove_framing(bin, %State{}) 110 | end 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /src/test/support/harald_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Harald.HaraldCase do 2 | @moduledoc """ 3 | Harald test helpers. 4 | """ 5 | 6 | use ExUnit.CaseTemplate 7 | 8 | using do 9 | quote do 10 | import Harald.HaraldCase 11 | end 12 | end 13 | 14 | defmacro assert_binaries({:==, _, [left, right]}) do 15 | quote do 16 | format = fn bin -> 17 | space = ?\s 18 | 19 | ", " <> elements = 20 | bin 21 | |> :binary.bin_to_list() 22 | |> Enum.reduce("", fn element, acc -> acc <> ", #{inspect(element)}" end) 23 | 24 | "<<#{elements}>>" 25 | end 26 | 27 | left = format.(unquote(left)) 28 | right = format.(unquote(right)) 29 | assert left == right 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /src/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | {:ok, _} = Hook.Server.start_link([]) 2 | ExUnit.start() 3 | :ok = Application.ensure_started(:stream_data) 4 | --------------------------------------------------------------------------------