Skip to content

Commit

Permalink
Add error handling to Peripheral and SMP
Browse files Browse the repository at this point in the history
  • Loading branch information
ConnorRigby committed Jan 2, 2025
1 parent 98a33a4 commit ed986fd
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 29 deletions.
17 changes: 15 additions & 2 deletions lib/blue_heron/broadcaster.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ defmodule BlueHeron.Broadcaster do
use GenServer
require Logger

alias BlueHeron.ErrorCode

alias BlueHeron.HCI.Command.LEController.{
SetAdvertisingParameters,
SetAdvertisingData,
Expand All @@ -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
Expand All @@ -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
Expand All @@ -59,20 +65,24 @@ 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

@doc """
Enable advertisement
"""
@spec start_advertising() :: :ok | {:error, :setup_incomplete} | {:error, ErrorCode.name()}
def start_advertising() do
GenServer.call(__MODULE__, :start_advertising)
end

@doc """
Disable advertisement
"""
@spec stop_advertising() :: :ok | {:error, :setup_incomplete} | {:error, ErrorCode.name()}
def stop_advertising() do
GenServer.call(__MODULE__, :stop_advertising)
end
Expand Down Expand Up @@ -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
71 changes: 71 additions & 0 deletions lib/blue_heron/error_code.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
7 changes: 4 additions & 3 deletions lib/blue_heron/hci/event.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
84 changes: 67 additions & 17 deletions lib/blue_heron/hci/transport.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: %{}
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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)}")
Expand All @@ -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)}")
Expand All @@ -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
Expand All @@ -248,18 +288,15 @@ 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,
%{setup_complete: true, current: nil, caller: nil} = state
) 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(
Expand All @@ -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
2 changes: 1 addition & 1 deletion lib/blue_heron/hci/transport/uart.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 ->
Expand Down
Loading

0 comments on commit ed986fd

Please sign in to comment.