{"id":23697959,"url":"https://github.com/blue-heron/blue_heron","last_synced_at":"2025-04-06T10:11:52.384Z","repository":{"id":40279176,"uuid":"286566913","full_name":"blue-heron/blue_heron","owner":"blue-heron","description":"Use Bluetooth LE in Elixir","archived":false,"fork":false,"pushed_at":"2024-03-24T17:37:58.000Z","size":324,"stargazers_count":90,"open_issues_count":14,"forks_count":14,"subscribers_count":12,"default_branch":"main","last_synced_at":"2024-05-01T22:24:02.731Z","etag":null,"topics":["ble","elixir"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/blue-heron.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-08-10T19:57:20.000Z","updated_at":"2024-06-21T05:44:51.473Z","dependencies_parsed_at":"2023-09-21T17:23:18.208Z","dependency_job_id":"9869232b-0c7e-4207-a94c-26bc18e3b95a","html_url":"https://github.com/blue-heron/blue_heron","commit_stats":{"total_commits":73,"total_committers":9,"mean_commits":8.11111111111111,"dds":0.6164383561643836,"last_synced_commit":"eec928e1160f3ec13f229653a0f0592a1dd7f57b"},"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blue-heron%2Fblue_heron","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blue-heron%2Fblue_heron/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blue-heron%2Fblue_heron/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blue-heron%2Fblue_heron/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/blue-heron","download_url":"https://codeload.github.com/blue-heron/blue_heron/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247464222,"owners_count":20942970,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ble","elixir"],"created_at":"2024-12-30T07:12:13.114Z","updated_at":"2025-04-06T10:11:52.367Z","avatar_url":"https://github.com/blue-heron.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# BlueHeron\n\n[![Hex version](https://img.shields.io/hexpm/v/blue_heron.svg \"Hex version\")](https://hex.pm/packages/blue_heron)\n[![API docs](https://img.shields.io/hexpm/v/blue_heron.svg?label=hexdocs \"API docs\")](https://hexdocs.pm/blue_heron/BlueHeron.html)\n[![mix test](https://github.com/blue-heron/blue_heron/actions/workflows/elixir.yaml/badge.svg)](https://github.com/blue-heron/blue_heron/actions/workflows/elixir.yaml)\n[![REUSE status](https://api.reuse.software/badge/github.com/nerves-project/nerves)](https://api.reuse.software/info/github.com/nerves-project/nerves)\n\nBlueHeron is a new Elixir Bluetooth LE Library that communicates directly with\nBluetooth modules via HCI. It is VERY much under construction, and we expect the\nuser API to change completely.\n\nOn the plus side, BlueHeron has no dependencies on Linux's `bluez` stack so if\nyou either can't use `bluez`, don't want to, or have a simple BLE use case,\nplease join us in building this out! We gather on the [Elixir Lang\nslack](https://elixir-slackin.herokuapp.com/) in the `#nerves-bluetooth`\nchannel.\n\n## Goals\n\nBlueHeron development was started since SmartRent had a need for a very simple\nBLE interface on one of its Nerves devices.\nThe existing Elixir BLE library, [Harald](https://github.com/verypossible-labs/harald),\ndidn't have enough functionality and we made so many modifications that it no\nlonger felt like the library followed the spirit of what Harald wanted to be.\n\nOur goals here are to make a one-stop BLE library with support for the\nfollowing:\n\n- [x] BLE peripheral and GATT server support (Peripheral role)\n- [x] Support BLE beacons (Broadcaster role)\n- [ ] Scan for and connect to BLE peripheral devices (Central role)\n- [ ] GATT client support\n\nThe current focus is on filling out the peripheral role. The API is quite unstable\nat the moment, but we're aiming for a high level API so that most users don't\nneed to become Bluetooth experts. Currently, the raw API is helping us learn and\niron out quirks quickly. See [Rationale](#Rationale) for more about why we're\ndoing building this library.\n\nIf you are interested in adding support for the other roles, please let us know\neither here or on Slack. While we're very interested in part of this library for\nwork, we're also having fun with BLE and figure that we might as well see if we\ncan hit some Nerves use cases too.\n\n## Hardware compatibility\n\nWe have only tested BlueHeron with a limited number of Bluetooth adapters.\nHere's what's known:\n\n| Bluetooth module or chipset            | Connection | Works? | Firmware               | Notes\n| -------------------------------------- | ---------- | ------ | ---------------------- | -----\n| Cypress CYW43438 (RPi0W and RPi 3B)    | UART       | Yes    | ?                      | BlueHeron doesn't need to load the firmware for this one to work.\n| Cypress CYW43455 (RPi 3A+ and 3B+)     | UART       | No     | ?                      | Retry when #21 is fixed\n\n## Upgrading from 0.4.x\n\nThe [0.5.0 release](https://github.com/blue-heron/blue_heron/releases/tag/v0.5.0) brought with it a bunch of updates. The most notable one\nis the addition of a supervision tree. Upgrading to this release requires a few steps.\n\n1) remove `:blue_heron_transport_uart` from your `mix.exs`. Transport code was consolidated in the 0.5 release.\n2) Update your `config.exs` with the same parameters as the Transport options. For example, on a RPI0, you would\n   change \n   ```elixir\n    config = %BlueHeronTransportUART{\n      device: \"/dev/ttyS0\",\n      uart_opts: [\n        speed: 115_200,\n      ]\n    }\n    {:ok, ctx} = BlueHeron.transport(config)\n   ```\n   into a config.exs entry: \n   ```elixir\n   config :blue_heron,\n    transport: [\n      device: \"/dev/ttyS0\",\n      speed: 115_200,\n      flow_control: :hardware\n    ]\n   ```\n3) Remove `BlueHeron.Peripheral.start_link/2` calls. The Peripheral\n   server is now started as part of BlueHeron's supervision tree.\n4) Remove any modules that have `@behaviour BlueHeron.GATT.Server`. Services\n   are now registered with `BlueHeron.Peripheral.register_service/1`. The\n   data structure remains the same.\n5) Update any calls to `BlueHeron.Peripheral.set_advertising_data/1` and similar\n   to use the new `BlueHeron.Broadcaster.set_advertising_data/1`. The data to\n   those calls is the same.\n\n## Getting started\n\nBelow are examples of the roles supported by BlueHeron currently.\n\n### Broadcaster Role\n\nThe simplest role to implement is a BLE Broadcaster. This is a device that doesn't *do* anything by itself,\nbut broadcasts it's information publically. A good example of this role is Apple's [iBeacon](https://en.wikipedia.org/wiki/IBeacon).\n\n```elixir\niex(1)\u003e flags_ad = BlueHeron.AdvertisingData.flags([le_general_discoverable_mode: true, br_edr_not_supported: true])\n\u003c\u003c2, 1, 6\u003e\u003e\niex(2)\u003e uuid = \u003c\u003c0xF018E00E0ECE45B09617B744833D89BA::128\u003e\u003e\n\u003c\u003c240, 24, 224, 14, 14, 206, 69, 176, 150, 23, 183, 68, 131, 61, 137, 186\u003e\u003e\niex(3)\u003e major = 1\n1\niex(4)\u003e minor = 0\n0\niex(5)\u003e tx_power = -60\niex(6)\u003e ibeacon = BlueHeron.AdvertisingData.IBeacon.new(uuid, major, minor, tx_power)\n\u003c\u003c76, 0, 2, 21, 240, 24, 224, 14, 14, 206, 69, 176, 150, 23, 183, 68, 131, 61,\n  137, 186, 0, 1, 0, 0, 196\u003e\u003e\niex(7)\u003e ibeacon_ad = BlueHeron.AdvertisingData.manufacturer_specific_data(ibeacon)\n\u003c\u003c26, 255, 76, 0, 2, 21, 240, 24, 224, 14, 14, 206, 69, 176, 150, 23, 183, 68,\n  131, 61, 137, 186, 0, 1, 0, 0, 196\u003e\u003e\nBlueHeron.Broadcaster.set_advertising_data(flags_ad \u003c\u003e BlueHeron.AdvertisingData.manufacturer_specific_data(ibeacon))\n:ok\niex(8)\u003e BlueHeron.Broadcaster.start_advertising()\n:ok\n```\n\n### Peripheral\n\nA Peripheral is a Broadcaster that allows a connection via the GATT and GAP Service Discovery protocols. This allows a device\nto *do* something. To setup a Peripheral, first we need to enable the Broadcaster role so our device can be found.\n\n```elixir\n# stop advertising while we change the payload.\niex(1)\u003e BlueHeron.Broadcaster.stop_advertising()\n:ok\n# flags will be the same as before.\niex(2)\u003e flags_ad = BlueHeron.AdvertisingData.flags([le_general_discoverable_mode: true, br_edr_not_supported: true])\n\u003c\u003c2, 1, 6\u003e\u003e\niex(3)\u003e short_name = \"nerves\"\n\"nerves\"\niex(4)\u003e short_name_ad = BlueHeron.AdvertisingData.short_name(short_name)\n\"\\a\\bnerves\"\niex(5)\u003e incomplete_list_of_service_ids = BlueHeron.AdvertisingData.incomplete_list_of_service_uuids([0xF018E00E0ECE45B09617B744833D89BA])\n\u003c\u003c17, 6, 186, 137, 61, 131, 68, 183, 23, 150, 176, 69, 206, 14, 14, 224, 24,\n  240\u003e\u003e\niex(6)\u003e BlueHeron.Broadcaster.set_advertising_data(flags_ad \u003c\u003e short_name_ad \u003c\u003e incomplete_list_of_service_ids)\n:ok\niex(7)\u003e BlueHeron.Broadcaster.start_advertising()\n:ok\n```\n\nThis will set the scanned name to be `nerves`. \n\nSince the AdvertisingData payload is limited to only 31 bytes, if we want to set additional information, we\ncan put it in the `ScanResponseData`. This is an additional payload that is scanned and usually cached on\nCentral devices. For example setting the long name can be done with:\n\n```elixir\n# stop advertising while we change the payload.\niex(1)\u003e BlueHeron.Broadcaster.stop_advertising()\n:ok\n# flags will be the same as before.\niex(2)\u003e flags_ad = BlueHeron.AdvertisingData.flags([le_general_discoverable_mode: true, br_edr_not_supported: true])\n\u003c\u003c2, 1, 6\u003e\u003e\niex(3)\u003e long_name = \"nerves-\" \u003c\u003e Nerves.Runtime.serial_number()\n\"nerves-00000000b5f1bea0\"\niex(4)\u003e long_name_ad = BlueHeron.AdvertisingData.complete_name(long_name)\n\u003c\u003c24, 9, 110, 101, 114, 118, 101, 115, 45, 48, 48, 48, 48, 48, 48, 48, 48, 98,\n  53, 102, 49, 98, 101, 97, 48\u003e\u003e\niex(5)\u003e BlueHeron.Broadcaster.set_scan_response_data(long_name_ad)\n:ok\niex(6)\u003e BlueHeron.Broadcaster.start_advertising()\n:ok\n```\n\nNow that the device is advertising, we need to implement the service we listed: `0xF018E00E0ECE45B09617B744833D89BA`, as well as\nimplement the `GAP` and `GATT` profiles. \n\n```elixir\niex(7)\u003e gap_service = BlueHeron.GATT.Service.new(%{\n...(7)\u003e   id: :gap,\n...(7)\u003e   type: 0x1800,\n...(7)\u003e   characteristics: [\n...(7)\u003e     BlueHeron.GATT.Characteristic.new(%{\n...(7)\u003e       id: {:gap, :device_name},\n...(7)\u003e       type: 0x2A00,\n...(7)\u003e       properties: 0b0000010\n...(7)\u003e     }),\n...(7)\u003e     BlueHeron.GATT.Characteristic.new(%{\n...(7)\u003e       id: {:gap, :appearance},\n...(7)\u003e       type: 0x2A01,\n...(7)\u003e       properties: 0b0000010\n...(7)\u003e     })\n...(7)\u003e   ],\n...(7)\u003e   read: fn \n...(7)\u003e     {:gap, :device_name} -\u003e \n...(7)\u003e       \"nerves-\" \u003c\u003e Nerves.Runtime.serial_number()\n...(7)\u003e     {:gap, :appearance} -\u003e\n...(7)\u003e       \u003c\u003c0x008D::little-16\u003e\u003e\n...(7)\u003e   end\n...(7)\u003e })\n%BlueHeron.GATT.Service{\n  id: :gap,\n  type: 6144,\n  characteristics: [\n    %BlueHeron.GATT.Characteristic{\n      id: {:gap, :device_name},\n      type: 10752,\n      properties: 2,\n      permissions: nil,\n      descriptor: nil,\n      handle: nil,\n      value_handle: nil,\n      descriptor_handle: nil\n    },\n    %BlueHeron.GATT.Characteristic{\n      id: {:gap, :appearance},\n      type: 10753,\n      properties: 2,\n      permissions: nil,\n      descriptor: nil,\n      handle: nil,\n      value_handle: nil,\n      descriptor_handle: nil\n    }\n  ],\n  handle: nil,\n  end_group_handle: nil,\n  read: #Function\u003c42.39164016/1 in :erl_eval.expr/6\u003e,\n  write: #Function\u003c3.104805658/2 in BlueHeron.GATT.Service.default_write_callback\u003e,\n  subscribe: #Function\u003c5.104805658/1 in BlueHeron.GATT.Service.default_subscribe_callback\u003e,\n  unsubscribe: #Function\u003c7.104805658/1 in BlueHeron.GATT.Service.default_unsubscribe_callback\u003e\n}\niex(8)\u003e gatt_service = BlueHeron.GATT.Service.new(%{\n...(8)\u003e   id: :gatt,\n...(8)\u003e   type: 0x1801,\n...(8)\u003e   characteristics: [\n...(8)\u003e     BlueHeron.GATT.Characteristic.new(%{\n...(8)\u003e       id: {:gatt, :service_changed}, \n...(8)\u003e       type: 0x2A05,\n...(8)\u003e       properties: 0b100000\n...(8)\u003e     })\n...(8)\u003e   ]\n...(8)\u003e })\n%BlueHeron.GATT.Service{\n  id: :gatt,\n  type: 6145,\n  characteristics: [\n    %BlueHeron.GATT.Characteristic{\n      id: {:gatt, :service_changed},\n      type: 10757,\n      properties: 32,\n      permissions: nil,\n      descriptor: nil,\n      handle: nil,\n      value_handle: nil,\n      descriptor_handle: nil\n    }\n  ],\n  handle: nil,\n  end_group_handle: nil,\n  read: #Function\u003c1.104805658/1 in BlueHeron.GATT.Service.default_read_callback\u003e,\n  write: #Function\u003c3.104805658/2 in BlueHeron.GATT.Service.default_write_callback\u003e,\n  subscribe: #Function\u003c5.104805658/1 in BlueHeron.GATT.Service.default_subscribe_callback\u003e,\n  unsubscribe: #Function\u003c7.104805658/1 in BlueHeron.GATT.Service.default_unsubscribe_callback\u003e\n}\niex(9)\u003e custom_service = BlueHeron.GATT.Service.new(%{\n...(9)\u003e   id: :test,\n...(9)\u003e   type: 0xF018E00E0ECE45B09617B744833D89BA,\n...(9)\u003e   characteristics: [\n...(9)\u003e     BlueHeron.GATT.Characteristic.new(%{\n...(9)\u003e       id: {:test, :char_1},\n...(9)\u003e       type: 0x2e0f8e717a7d4690998377626bc6b657,\n...(9)\u003e       properties: 0b0000010,\n...(9)\u003e       permissions: [:read_auth]\n...(9)\u003e     }),\n...(9)\u003e     BlueHeron.GATT.Characteristic.new(%{\n...(9)\u003e       id: {:test, :char_2},\n...(9)\u003e       type: 0x3e0f8e717a7d4690998377626bc6b657,\n...(9)\u003e       properties: 0b0001000,\n...(9)\u003e       permissions: [:write_auth]\n...(9)\u003e     }),\n...(9)\u003e   ],\n...(9)\u003e   read: fn \n...(9)\u003e     {:test, :char_1} -\u003e \n...(9)\u003e       \"hello, world\"\n...(9)\u003e   end,\n...(9)\u003e   write: fn\n...(9)\u003e     {:test, :char_2}, value -\u003e\n...(9)\u003e       require Logger\n...(9)\u003e       Logger.info(\"write #{inspect(value)}\")\n...(9)\u003e   end\n...(9)\u003e })\n%BlueHeron.GATT.Service{\n  id: :test,\n  type: 319143878486512296490943150958665632186,\n  characteristics: [\n    %BlueHeron.GATT.Characteristic{\n      id: {:test, :char_1},\n      type: 61225261351838855375776121692935861847,\n      properties: 2,\n      permissions: [:read_auth],\n      descriptor: nil,\n      handle: nil,\n      value_handle: nil,\n      descriptor_handle: nil\n    },\n    %BlueHeron.GATT.Characteristic{\n      id: {:test, :char_2},\n      type: 82492909284397509342237034657421375063,\n      properties: 8,\n      permissions: [:write_auth],\n      descriptor: nil,\n      handle: nil,\n      value_handle: nil,\n      descriptor_handle: nil\n    }\n  ],\n  handle: nil,\n  end_group_handle: nil,\n  read: #Function\u003c42.39164016/1 in :erl_eval.expr/6\u003e,\n  write: #Function\u003c41.39164016/2 in :erl_eval.expr/6\u003e,\n  subscribe: #Function\u003c5.104805658/1 in BlueHeron.GATT.Service.default_subscribe_callback\u003e,\n  unsubscribe: #Function\u003c7.104805658/1 in BlueHeron.GATT.Service.default_unsubscribe_callback\u003e\n}\niex(10)\u003e BlueHeron.Peripheral.add_service(gap_service)\n:ok\niex(11)\u003e BlueHeron.Peripheral.add_service(gatt_service)\n:ok\niex(12)\u003e BlueHeron.Peripheral.add_service(encrypted_service)\n:ok\n```\n\nOnce completed, you should be able to connect to the `nerves` BLE device, it will do a encrypted `Bonding` procedure, and finally allow\nyou to `read` and `write` the implemented services.\n\n## Helpful docs\n\n* [Bluetooth Core Specification v5.2](https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=478726)\n\n## Rationale\n\nThis library will likely feel like a whole lot of reinvention of the wheel for\nanyone familiar with Bluetooth stacks in embedded Linux. It took around three\nyears for one of us to get to this point of starting a new library, so it's\nworth sketching out why.\n\nThe obvious approach is to use Linux's `bluez` stack. It certainly worked, but\nwas complicated for most people to set up when using Nerves. Consequently, it\nwas hard to debug and a thankless task for anyone attempting to support Nerves\nusers. It was far easier to use `bluez` on a batteries-included OS distribution\nlike Raspbian.\n\nThe next approach was to use a smart Bluetooth module like an Adafruit Bluefruit\nmodule or a Roving Networks (now Microchip) Bluetooth module. These have an `AT`\nstyle command set for doing common Bluetooth things (for example,\nsee\n[here](https://learn.adafruit.com/introducing-adafruit-ble-bluetooth-low-energy-friend/command-mode)),\nand are fairly easy to use once you got used to the interface. Not everyone\nwanted to use these for various reasons (cost being a big one).\n\nA good alternative was to use a C Bluetooth stack that talks directly to a\nBluetooth module via UART or USB using the HCI protocol. These are\ntypically marketed towards microcontroller users, but can be made to work on\nminimal Linux configurations too. Some of the options have good documentation\nand are commercially supported.\n\nIntegrating the C stack still required work, and since our needs were so simple,\nwe simultaneously looked at an Elixir implementation. Elixir has a way of making\ndull work surprisingly enjoyable, and it's especially suited to communication\nprotocols. When the proof-of-concept started working in not much time, we\ndecided that we'd much rather spend our time in Elixir than anywhere else.\n\nWill this library have more features than `bluez`? Not even close. Will it do\nwhat we need? Yes. Will it have fewer bugs, be more robust, etc.? Don't know,\nbut its small size and few parts is easier to get our heads around and debug\nwhen issues come up. Is it fun to work on? Yes, so we got permission to\nopen-source it, so we could use it for hobby projects too.\n\n## Debugging\n\nIt can be a chore to debug Bluetooth. Errors can happen at a few different\nlayers from the baseband all the way up to the high level software. Below are\nsome useful pieces of hardware and software that can be used to debug issues\nin BlueHeron or generally snoop on BLE devices.\n\n### Nordic Semiconductor BLE Sniffer\n\nThis is a custom firmware for devboards that can be used to sniff BLE packets.\nMore info can be found on [Nordic's website](https://docs.nordicsemi.com/bundle/nrfutil_ble_sniffer_pdf/resource/nRF_Sniffer_BLE_UG_v4.0.0.pdf)\n\n* [Adafruit BLE Sniffer](https://www.adafruit.com/product/2269)\n* [Nordic nrf52840 dongle](https://www.nordicsemi.com/Products/Development-hardware/nrf52840-dongle)\n\n## Support\n\nWe provide best-effort support via the [Elixir Forum](https://elixirforum.com/)\nand the [#nerves-bluetooth channel on the Elixir\nSlack](https://elixir-slackin.herokuapp.com/). If you need more immediate\nsupport or feature additions, commercial support is provided by [Binary\nNoggin](https://binarynoggin.com).\n\n## License\n\nThe source code is released under Apache License 2.0.\n\nCheck [NOTICE](NOTICE) and [LICENSE](LICENSE) files for more information.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fblue-heron%2Fblue_heron","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fblue-heron%2Fblue_heron","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fblue-heron%2Fblue_heron/lists"}