blue-heron / blue_heron

Use Bluetooth LE in Elixir
Apache License 2.0
96 stars 15 forks source link

Question about AdvertisingReport events not received by the process. #111

Closed acadeau closed 7 months ago

acadeau commented 8 months ago

Hello πŸ‘‹ ,

First of all, thanks for your awesome work, the examples help me to understand how your lib works.

I have a small project where I retrieve data from a hydrometer that emits its data through BLE. I've used the scanner example , adapted to my usage, but my GenServer didn't received any AdvertisingReport event and I had the following warning message in the log: BLE: Unknown HCI frame ....

After multiple explorations through the codebase, I've edited the handle_hci_packet function in lib/blue_heron/hci/transport.ex :

defp handle_hci_packet(packet, data) do
    case deserialize(packet) do
      %{status: 0} = reply ->
        for pid <- data.handlers, do: send(pid, {:HCI_EVENT_PACKET, reply})
        {:ok, reply, data}

      %{return_parameters: %{status: 0}} = reply ->
        for pid <- data.handlers, do: send(pid, {:HCI_EVENT_PACKET, reply})
        {:ok, reply, data}

      %{code: 0x13} = reply ->
        # Handle HCI.Event.NumberOfCompletedPackets
        for pid <- data.handlers, do: send(pid, {:HCI_EVENT_PACKET, reply})
        {:ok, reply, data}

      %{code: 62, subevent_code: 5} = reply ->
        # Handle HCI.Event.LEMeta.LongTermKeyRequest
        for pid <- data.handlers, do: send(pid, {:HCI_EVENT_PACKET, reply})
        {:ok, reply, data}

      %{code: 62, subevent_code: 3} = reply ->
        # handle HCI.Event.LEMeta.ConnectionUpdateComplete
        for pid <- data.handlers, do: send(pid, {:HCI_EVENT_PACKET, reply})
        {:ok, reply, data}

      %{code: 62, num_reports: _} = reply ->
        # handle HCI.Event.LEMeta.AdvertisingReport
        for pid <- data.handlers, do: send(pid, {:HCI_EVENT_PACKET, reply})
        {:ok, reply, data}

      %{opcode: _opcode} = reply ->
        {:error, reply, data}

      %{} = reply ->
        Logger.warning("BLE: Unknown HCI frame #{inspect(reply)}")
        {:error, reply, data}

      {:error, unknown} ->
        {:error, unknown, data}
    end
  end

Before proposing a pull request, I was wondering if I did something wrong in my code and if I misunderstood how the library works. Here's the code (I haven't cleaned it up yet):

defmodule BlueHeronScan do
  use GenServer
  require Logger

  alias BlueHeron.HCI.Command.{
    ControllerAndBaseband.WriteLocalName,
    LEController.SetScanEnable
  }

  alias BlueHeron.HCI.Event.{
    LEMeta.AdvertisingReport,
    LEMeta.AdvertisingReport.Device
  }

  alias BlueHeron.DataType.ManufacturerData.Apple

  @init_commands [%WriteLocalName{name: "TiltHydrometerScan"}]

  @default_uart_config %{
    device: "ttyACM0",
    uart_opts: [speed: 115_200],
    init_commands: @init_commands
  }

  @default_usb_config %{
    vid: 0x0BDA,
    pid: 0xB82C,
    init_commands: @init_commands
  }

  @tilt_hydrometer_ids %{
    # A495BB10C5B14B44B5121370F02D74DE
    218_770_837_680_077_257_813_263_174_056_301_917_406 => "red",
    # A495BB20C5B14B44B5121370F02D74DE
    218_770_838_947_727_858_041_492_575_553_005_122_782 => "green",
    # A495BB30C5B14B44B5121370F02D74DE
    218_770_840_215_378_458_269_721_977_049_708_328_158 => "black",
    # A495BB40C5B14B44B5121370F02D74DE
    218_770_841_483_029_058_497_951_378_546_411_533_534 => "purple",
    # A495BB50C5B14B44B5121370F02D74DE
    218_770_842_750_679_658_726_180_780_043_114_738_910 => "orange",
    # A495BB60C5B14B44B5121370F02D74DE
    218_770_844_018_330_258_954_410_181_539_817_944_286 => "blue",
    # A495BB70C5B14B44B5121370F02D74DE
    218_770_845_285_980_859_182_639_583_036_521_149_662 => "yellow",
    # A495BB80C5B14B44B5121370F02D74DE
    218_770_846_553_631_459_410_868_984_533_224_355_038 => "pink"
  }

  def start_link(transport_type, config \\ %{})

  def start_link(:uart, config) do
    config = struct(BlueHeronTransportUART, Map.merge(@default_uart_config, config))
    GenServer.start_link(__MODULE__, config, name: __MODULE__)
  end

  def start_link(:usb, config) do
    config = struct(BlueHeronTransportUSB, Map.merge(@default_usb_config, config))
    GenServer.start_link(__MODULE__, config, name: __MODULE__)
  end

  def enable(pid) do
    GenServer.call(pid, :scan_enable)
  end

  @doc """
  Disable BLE scanning.
  """
  def disable(pid) do
    send(pid, :scan_disable)
  end

  def devices(pid) do
    GenServer.call(pid, :devices)
  end

  @impl GenServer
  def init(config) do
    # Create a context for BlueHeron to operate with.
    {:ok, ctx} = BlueHeron.transport(config)

    # Subscribe to HCI and ACL events.
    BlueHeron.add_event_handler(ctx)

    {:ok, %{ctx: ctx, working: false, devices: %{}}}
  end

  @impl GenServer
  def handle_info({:BLUETOOTH_EVENT_STATE, :HCI_STATE_WORKING}, state) do
    # Enable BLE Scanning. This will deliver messages to the process mailbox
    # when other devices broadcast.
    state = %{state | working: true}
    scan(state, true)
    {:noreply, state}
  end

  # Scan AdvertisingReport packets.
  @impl GenServer
  def handle_info(
        {:HCI_EVENT_PACKET, %AdvertisingReport{devices: devices} = event},
        state
      ) do
    {:noreply, Enum.reduce(devices, state, &scan_device/2)}
  end

  # Ignore other HCI Events.
  @impl GenServer
  def handle_info({:HCI_EVENT_PACKET, _val}, state) do
    # Logger.debug("#{__MODULE__} ignore HCI Event #{inspect(val)}")
    {:noreply, state}
  end

  def handle_info(:scan_disable, state) do
    {:noreply, state}
  end

  @impl GenServer
  def handle_call(:devices, _from, state) do
    {:reply, {:ok, state.devices}, state}
  end

  def handle_call(:scan_enable, _from, state) do
    {:reply, scan(state, true), state}
  end

  defp scan(%{working: false}, _enable) do
    {:error, :not_working}
  end

  defp scan(%{ctx: ctx = %BlueHeron.Context{}}, enable) do
    BlueHeron.hci_command(ctx, %SetScanEnable{le_scan_enable: enable})
    status = if(enable, do: "enabled", else: "disabled")
    Logger.info("#{__MODULE__} #{status} scanning")
  end

  defp scan_device(device, state) do
    case device do
      %Device{address: addr, data: data, rss: rssi} ->
        Enum.reduce(data, state, fn e, acc ->
          case parse_tilt_hydrometer(e) do
            {:ok, tilt} -> Map.put(acc, :devices, Map.put(tilt, :rssi, rssi))
            _ -> acc
          end
        end)

      _ ->
        state
    end
  end

  defp parse_tilt_hydrometer({"Manufacturer Specific Data", data}) do
    <<_::little-16, sdata::binary>> = data

    case Apple.deserialize(sdata) do
      {:ok, {"iBeacon", tilt}} -> Map.fetch(@tilt_hydrometer_ids, tilt.uuid) |> dbg
      {:error, _} -> 1
    end

    with {:ok, {"iBeacon", tilt}} <- Apple.deserialize(sdata),
         {:ok, color} <- Map.fetch(@tilt_hydrometer_ids, tilt.uuid) do
      dbg(color)

      {:ok,
       %{
         # farenheit
         temperature: tilt.major,
         gravity: tilt.minor / 1000,
         color: color,
         tx_power: tilt.tx_power
       }}
    end
  end

  defp parse_tilt_hydrometer(_) do
    {:error}
  end
end

Did I miss something?

trarbr commented 8 months ago

Hello πŸ˜„

Thank you for filing a very detailed issue. It looks like the issue was inadvertently introduced a while back. I will happily review a PR that introduces the missing case clause for HCI.Event.LEMeta.AdvertisingReport, so feel free to submit it.

trarbr commented 7 months ago

Fixed in #112