diff --git a/lib/blue_heron/broadcaster.ex b/lib/blue_heron/broadcaster.ex index fbea06e..afc6b5a 100644 --- a/lib/blue_heron/broadcaster.ex +++ b/lib/blue_heron/broadcaster.ex @@ -17,6 +17,8 @@ defmodule BlueHeron.Broadcaster do use GenServer require Logger + alias BlueHeron.ErrorCode + alias BlueHeron.HCI.Command.LEController.{ SetAdvertisingParameters, SetAdvertisingData, @@ -37,6 +39,8 @@ defmodule BlueHeron.Broadcaster do see [Vol 3] Part C, Section 11 of the BLE core specification. Additionally see: Core Specification Supplement, Part A, Data Types Specification """ + @spec set_advertising_parameters(binary()) :: + :ok | {:error, :setup_incomplete} | {:error, ErrorCode.name()} def set_advertising_parameters(params) do GenServer.call(__MODULE__, {:set_advertising_parameters, params}) end @@ -48,6 +52,8 @@ defmodule BlueHeron.Broadcaster do see [Vol 3] Part C, Section 11 of the BLE core specification. Additionally see: Core Specification Supplement, Part A, Data Types Specification """ + @spec set_advertising_data(binary()) :: + :ok | {:error, :setup_incomplete} | {:error, ErrorCode.name()} def set_advertising_data(data) do GenServer.call(__MODULE__, {:set_advertising_data, data}) end @@ -59,6 +65,8 @@ defmodule BlueHeron.Broadcaster do see [Vol 3] Part C, Section 11 of the BLE core specification. Additionally see: Core Specification Supplement, Part A, Data Types Specification """ + @spec set_scan_response_data(binary()) :: + :ok | {:error, :setup_incomplete} | {:error, ErrorCode.name()} def set_scan_response_data(data) do GenServer.call(__MODULE__, {:set_scan_response_data, data}) end @@ -66,6 +74,7 @@ defmodule BlueHeron.Broadcaster do @doc """ Enable advertisement """ + @spec start_advertising() :: :ok | {:error, :setup_incomplete} | {:error, ErrorCode.name()} def start_advertising() do GenServer.call(__MODULE__, :start_advertising) end @@ -73,6 +82,7 @@ defmodule BlueHeron.Broadcaster do @doc """ Disable advertisement """ + @spec stop_advertising() :: :ok | {:error, :setup_incomplete} | {:error, ErrorCode.name()} def stop_advertising() do GenServer.call(__MODULE__, :stop_advertising) end @@ -147,14 +157,17 @@ defmodule BlueHeron.Broadcaster do {:ok, %CommandComplete{return_parameters: %{status: error}}} -> {^error, reply, _} = BlueHeron.ErrorCode.to_atom(error) - {:reply, reply, state} + {:reply, {:error, reply}, state} {:ok, %CommandStatus{status: 0x00}} -> {:reply, :ok, state} {:ok, %CommandStatus{status: error}} -> {^error, reply, _} = BlueHeron.ErrorCode.to_atom(error) - {:reply, reply, state} + {:reply, {:error, reply}, state} + + {:error, error} -> + {:reply, {:error, error}, state} end end end diff --git a/lib/blue_heron/error_code.ex b/lib/blue_heron/error_code.ex index 40d3a53..9434970 100644 --- a/lib/blue_heron/error_code.ex +++ b/lib/blue_heron/error_code.ex @@ -8,6 +8,77 @@ defmodule BlueHeron.ErrorCode do Reference: Version 5.0, Vol 2, Part D, 1 """ + @type name :: + :unknown_hci_command + | :unknown_connection_id + | :hardware_failure + | :page_timeout + | :auth_failure + | :pin_or_key_missing + | :memory_capacity_exceeded + | :connection_timeout + | :connection_limit_exceeded + | :synchronous_connection_limit_to_a_device_exceeded + | :connection_already_exists + | :command_disallowed + | :connection_rejected_due_to_limited_resources + | :connection_rejected_due_to_security_reasons + | :connection_rejected_due_to_unacceptable_bd_addr + | :connection_accept_timeout_exceeded + | :unsupported_feature_or_parameter_value + | :invalid_hci_command_parameters + | :remote_user_terminated_connection + | :remote_device_terminated_connection_due_to_low_resources + | :remote_device_terminated_connection_due_to_power_off + | :connection_terminated_by_local_host + | :repeated_attempts + | :pairing_not_allowed + | :unknown_lmp_pdu + | :unsupported_remote_feature + | :sco_offset_rejected + | :sco_interval_rejected + | :sco_air_mode_rejected + | :invalid_lmp_parameters + | :unspecified_error + | :unsupported_lmp_parameter_value + | :role_change_not_allowed + | :lmp_response_timeout + | :lmp_error_transaction_collision + | :lmp_pdu_not_allowed + | :encryption_mode_not_acceptable + | :link_key_cannot_be_changed + | :requested_qos_not_supported + | :instant_passed + | :pairing_with_unit_key_not_supported + | :different_transaction_collision + | :reserved + | :qos_unacceptable_parameter + | :qos_rejected + | :channel_classification_not_supported + | :insufficient_security + | :parameter_out_of_mandatory_range + | :reserved + | :role_switch_pending + | :reserved + | :reserved_slot_violation + | :role_switch_failed + | :extended_inquiry_response_too_large + | :secure_simple_pairing_not_supported + | :host_busy_pairing + | :connection_rejected_no_suitable_channel + | :controller_busy + | :unacceptable_connection_parameters + | :advertising_timeout + | :connection_terminated_due_to_mic_failure + | :connection_failed_to_be_established + | :mac_connection_failed + | :course_clock_adjustment_rejected + | :type0_submap_not_defined + | :unknown_advertising_identifier + | :limit_reached + | :operation_cancelled_by_host + | :packet_too_long + # Reference: Version 5.2, Vol 1, Part F, 1.3 @error_codes [ {0x00, :ok, "Success"}, diff --git a/lib/blue_heron/hci/event.ex b/lib/blue_heron/hci/event.ex index ec2b071..ca974bc 100644 --- a/lib/blue_heron/hci/event.ex +++ b/lib/blue_heron/hci/event.ex @@ -32,16 +32,17 @@ defmodule BlueHeron.HCI.Event do alias BlueHeron.HCI.Event @modules [ - Event.EncryptionChange, Event.CommandComplete, Event.CommandStatus, - Event.NumberOfCompletedPackets, Event.DisconnectionComplete, + Event.EncryptionChange, Event.InquiryComplete, Event.LEMeta.AdvertisingReport, Event.LEMeta.ConnectionComplete, + Event.LEMeta.ConnectionUpdateComplete, + Event.LEMeta.EnhancedConnectionCompleteV1, Event.LEMeta.LongTermKeyRequest, - Event.LEMeta.ConnectionUpdateComplete + Event.NumberOfCompletedPackets ] @doc "returns the list of parsable modules" diff --git a/lib/blue_heron/hci/transport.ex b/lib/blue_heron/hci/transport.ex index 2a4dbfc..a945879 100644 --- a/lib/blue_heron/hci/transport.ex +++ b/lib/blue_heron/hci/transport.ex @@ -14,15 +14,26 @@ defmodule BlueHeron.HCI.Transport do import BlueHeron.HCI.Deserializable, only: [deserialize: 1] import BlueHeron.HCI.Serializable, only: [serialize: 1] - def buffer_acl(frame) do - BlueHeron.ACLBuffer.buffer(frame) - end + @type command_complete :: %BlueHeron.HCI.Event.CommandComplete{} + @type command_status :: %BlueHeron.HCI.Event.CommandStatus{} + @doc "Send an HCI frame" + @spec send_hci(map()) :: + {:ok, command_complete() | command_status()} | {:error, :setup_incomplete | :timeout} def send_hci(frame) do GenServer.call(__MODULE__, {:send_hci, frame}) end + @doc """ + Buffer an ACL frame to be sent + """ + @spec buffer_acl(map()) :: :ok + def buffer_acl(frame) do + BlueHeron.ACLBuffer.buffer(frame) + end + @doc false + @spec send_acl(map()) :: :ok | {:error, :setup_incomplete} def send_acl(frame) do GenServer.call(__MODULE__, {:send_acl, frame}) end @@ -125,6 +136,7 @@ defmodule BlueHeron.HCI.Transport do transport_init_timer: nil, setup_commands: @default_setup_commands, current: nil, + current_timer: nil, setup_complete: false, caller: nil, setup_params: %{} @@ -163,17 +175,37 @@ defmodule BlueHeron.HCI.Transport do end end + def handle_info(:current_timeout, state) do + new_state = cancel_timer(state) + + case state.caller do + nil -> + Logger.warning("Setup command timeout: #{inspect(state.current)}") + hci_bin = serialize(state.current) + :ok = BlueHeron.HCI.Transport.UART.send_command(state.transport, hci_bin) + timer = Process.send_after(self(), :current_timeout, 5000) + {:noreply, %{new_state | current_timer: timer}} + + caller -> + Logger.warning("HCI command timeout: #{inspect(state.current)}") + _ = GenServer.reply(caller, {:error, :timeout}) + {:noreply, %{new_state | current: nil, caller: nil}} + end + end + @impl GenServer def handle_continue(:setup_transport, %{setup_commands: [command | rest]} = state) do + new_state = cancel_timer(state) hci_bin = serialize(command) - :ok = BlueHeron.HCI.Transport.UART.send_command(state.transport, hci_bin) - {:noreply, %{state | setup_commands: rest, current: command}} + :ok = BlueHeron.HCI.Transport.UART.send_command(new_state.transport, hci_bin) + timer = Process.send_after(self(), :current_timeout, 5000) + {:noreply, %{new_state | setup_commands: rest, current: command, current_timer: timer}} end def handle_continue(:setup_transport, %{setup_commands: []} = state) do :ok = BlueHeron.Registry.broadcast({:BLUETOOTH_EVENT_STATE, :HCI_STATE_WORKING}) - new_state = %{state | setup_complete: true} - {:noreply, new_state} + new_state = cancel_timer(state) + {:noreply, %{new_state | setup_complete: true}} end @impl GenServer @@ -187,9 +219,8 @@ defmodule BlueHeron.HCI.Transport do return_parameters: %{status: 0} = return } -> new_setup_params = Map.merge(state.setup_params, Map.delete(return, :status)) - - {:noreply, %{state | setup_params: new_setup_params, current: nil}, - {:continue, :setup_transport}} + new_state = %{cancel_timer(state) | setup_params: new_setup_params, current: nil} + {:noreply, new_state, {:continue, :setup_transport}} %BlueHeron.HCI.Event.CommandComplete{ opcode: ^opcode, @@ -201,7 +232,8 @@ defmodule BlueHeron.HCI.Transport do "Setup Command error: #{status} (#{inspect(status_message)}) return: #{inspect(return)} command: #{inspect(current)}" ) - {:noreply, %{state | current: nil}, {:continue, :setup_transport}} + new_state = %{cancel_timer(state) | current: nil} + {:noreply, new_state, {:continue, :setup_transport}} packet -> Logger.error("Unknown HCI packet during setup: #{inspect(packet)}") @@ -218,7 +250,14 @@ defmodule BlueHeron.HCI.Transport do opcode: ^opcode } = command_complete -> _ = GenServer.reply(caller, {:ok, command_complete}) - {:noreply, %{state | current: nil, caller: nil}} + new_state = %{cancel_timer(state) | current: nil, caller: nil} + {:noreply, new_state} + + %BlueHeron.HCI.Event.CommandStatus{num_hci_command_packets: 1, opcode: ^opcode} = + command_status -> + _ = GenServer.reply(caller, {:ok, command_status}) + new_state = %{cancel_timer(state) | current: nil, caller: nil} + {:noreply, new_state} packet -> Logger.error("Unknown HCI packet during command: #{inspect(packet)}") @@ -230,6 +269,7 @@ defmodule BlueHeron.HCI.Transport do {:transport_data, :hci, packet}, %{setup_complete: true} = state ) do + Logger.info("HCI Packet: #{inspect(packet)}") :ok = BlueHeron.Registry.broadcast({:HCI_EVENT_PACKET, packet}) {:noreply, state} end @@ -248,10 +288,6 @@ defmodule BlueHeron.HCI.Transport do {:reply, {:ok, value}, state} end - def handle_call({:get_setup_param, _param}, _from, %{setup_complete: false} = state) do - {:reply, {:error, :setup_incomplete, state}} - end - def handle_call( {:send_hci, command}, from, @@ -259,7 +295,8 @@ defmodule BlueHeron.HCI.Transport do ) do hci_bin = serialize(command) :ok = BlueHeron.HCI.Transport.UART.send_command(state.transport, hci_bin) - {:noreply, %{state | current: command, caller: from}} + timer = Process.send_after(self(), :current_timeout, 5000) + {:noreply, %{state | current: command, current_timer: timer, caller: from}} end def handle_call( @@ -271,4 +308,17 @@ defmodule BlueHeron.HCI.Transport do :ok = BlueHeron.HCI.Transport.UART.send_acl(state.transport, acl_bin) {:reply, :ok, state} end + + def handle_call(_call, _from, %{setup_complete: false} = state) do + {:reply, {:error, :setup_incomplete}, state} + end + + defp cancel_timer(state) do + if state.current_timer do + _ = Process.cancel_timer(state.current_timer) + %{state | current_timer: nil} + else + state + end + end end diff --git a/lib/blue_heron/hci/transport/uart.ex b/lib/blue_heron/hci/transport/uart.ex index a55af2f..205f143 100644 --- a/lib/blue_heron/hci/transport/uart.ex +++ b/lib/blue_heron/hci/transport/uart.ex @@ -52,7 +52,7 @@ defmodule BlueHeron.HCI.Transport.UART do def handle_info({:open, device, opts}, state) when is_binary(device) and is_list(opts) do case UART.open(state.uart_pid, device, opts) do :ok -> - Logger.info("Opened UART for HCI transport") + Logger.info("Opened UART for HCI transport: #{device} #{inspect(opts)}") :ok error -> diff --git a/lib/blue_heron/peripheral.ex b/lib/blue_heron/peripheral.ex index a380027..a025be6 100644 --- a/lib/blue_heron/peripheral.ex +++ b/lib/blue_heron/peripheral.ex @@ -19,6 +19,7 @@ defmodule BlueHeron.Peripheral do alias BlueHeron.HCI.Event.LEMeta.{ ConnectionComplete, + EnhancedConnectionCompleteV1, LongTermKeyRequest, ConnectionUpdateComplete } @@ -113,7 +114,21 @@ defmodule BlueHeron.Peripheral do end def handle_info({:HCI_EVENT_PACKET, %ConnectionComplete{} = event}, state) do - Logger.info("Peripheral connect #{event.connection_handle}") + Logger.info("Peripheral ConnectionComplete #{event.connection_handle}") + + connection = %{ + peer_address: event.peer_address, + peer_address_type: event.peer_address_type, + handle: event.connection_handle + } + + :ok = BlueHeron.SMP.set_connection(connection) + + {:noreply, %{state | connection: connection}} + end + + def handle_info({:HCI_EVENT_PACKET, %EnhancedConnectionCompleteV1{} = event}, state) do + Logger.info("Peripheral EnhancedConnectionCompleteV1 #{event.connection_handle}") connection = %{ peer_address: event.peer_address, @@ -178,11 +193,16 @@ defmodule BlueHeron.Peripheral do {:HCI_ACL_DATA_PACKET, %ACL{handle: handle, data: %L2Cap{cid: 0x0006, data: request}}}, state ) do - response = SMP.handle(request) + case SMP.handle(request) do + {:error, reason} -> + Logger.error("Failed to handle SMP request: #{inspect(request)} : #{inspect(reason)}") - if response do - acl_response = build_l2cap_acl(handle, 0x0006, response) - BlueHeron.HCI.Transport.buffer_acl(acl_response) + nil -> + :ok + + response -> + acl_response = build_l2cap_acl(handle, 0x0006, response) + BlueHeron.HCI.Transport.buffer_acl(acl_response) end {:noreply, state} @@ -230,7 +250,7 @@ defmodule BlueHeron.Peripheral do {:ok, result} -> acl = build_l2cap_acl(state.connection.handle, 0x0004, result) - Logger.info("Sending notification: #{acl}") + Logger.info("Sending notification: #{inspect(acl)}") reply = BlueHeron.HCI.Transport.buffer_acl(acl) {:reply, reply, state} diff --git a/lib/blue_heron/smp.ex b/lib/blue_heron/smp.ex index 6a75f7b..2a19d09 100644 --- a/lib/blue_heron/smp.ex +++ b/lib/blue_heron/smp.ex @@ -154,6 +154,10 @@ defmodule BlueHeron.SMP do {:reply, nil, state} end + def handle_call({:handle, <<0x03, _confirm::binary>>}, _from, %{connection: nil} = state) do + {:reply, {:error, :no_connection}, state} + end + def handle_call({:handle, <<0x03, confirm::binary>>}, _from, state) do # Handle Pairing Confirm @@ -167,6 +171,10 @@ defmodule BlueHeron.SMP do {:reply, <<0x03>> <> reverse(response), %{state | pairing: pairing}} end + def handle_call({:handle, <<0x04, _random::binary>>}, _from, %{connection: false} = state) do + {:reply, {:error, :no_connection}, state} + end + def handle_call({:handle, <<0x04, random::binary>>}, _from, state) do # Handle Pairing Random