lib/qmi/codec/wireless_data.ex

defmodule QMI.Codec.WirelessData do
  @moduledoc """
  Codec for making wireless data service requests
  """

  import Bitwise

  @event_report 0x0001
  @start_network_interface 0x0020
  @packet_service_status_ind 0x0022
  @modify_profile_settings 0x0028

  # When a stat is configured to be reported but no data has been recorded
  # before the indication is sent, the value is `0xFFFFFFFF` which is treated as
  # a missing value in many other places in the specification. Using libqmi as a
  # reference we use this value to check if the value is missing.
  # https://gitlab.freedesktop.org/mobile-broadband/libqmi/-/blob/master/src/qmicli/qmicli-wds.c#L1068-1091
  @missing_stat_value 0xFFFFFFFF

  @typedoc """
  Options for whens starting a network interface

  * `:apn` - the name of our APN
  """
  @type start_network_interface_opt() :: {:apn, String.t()} | {:profile_3gpp_index, integer()}

  @typedoc """
  Report from starting the network interface
  """
  @type start_network_report() :: %{packet_data_handle: non_neg_integer()}

  @typedoc """
  The type of reasons the call ended
  """
  @type call_end_reason_type() ::
          :unspecified
          | :mobile_ip
          | :internal
          | :call_manger_defined
          | :three_gpp_specification_defined
          | :ppp
          | :ehrpd
          | :ipv6
          | :handoff

  @typedoc """
  Name of the technology
  """
  @type tech_name() ::
          :cdma | :umts | :wlan_local_brkout | :iwlan_s2b | :epc | :embms | :modem_link_local

  @typedoc """
  The indication for a change in the current packet data connection status
  """
  @type packet_status_indication() :: %{
          name: :packet_status_indication,
          status: :disconnected | :connected | :suspended | :authenticating,
          reconfiguration_required: boolean(),
          call_end_reason: integer() | nil,
          call_end_reason_type: call_end_reason_type() | nil,
          ip_family: 4 | 6 | nil,
          tech_name: tech_name() | nil,
          bearer_id: integer() | nil,
          xlat_capable: boolean() | nil
        }

  @typedoc """
  The indication for the wireless data service's event report

  To configure what information is sent through this indication see
  `set_event_report/1`.
  """
  @type event_report_indication() :: %{
          required(:name) => :event_report_indication,
          optional(:tx_bytes) => integer(),
          optional(:rx_bytes) => integer(),
          optional(:tx_packets) => integer(),
          optional(:rx_packets) => integer(),
          optional(:tx_overflows) => integer(),
          optional(:rx_overflows) => integer(),
          optional(:tx_errors) => integer(),
          optional(:rx_errors) => integer(),
          optional(:tx_drops) => integer(),
          optional(:rx_drops) => integer()
        }

  @doc """
  Send command to request the that the network interface starts to receive data
  """
  @spec start_network_interface([start_network_interface_opt()]) :: QMI.request()
  def start_network_interface(opts \\ []) do
    {tlvs, size} = make_tlvs(opts, [], 0)
    payload = [<<@start_network_interface::little-16, size::little-16>>, tlvs]

    %{service_id: 0x01, payload: payload, decode: &parse_start_network_interface_resp/1}
  end

  defp make_tlvs([], tlvs, size) do
    {tlvs, size}
  end

  defp make_tlvs([{:apn, apn} | rest], tlvs, size) do
    apn_size = byte_size(apn)
    new_size = size + 3 + apn_size
    new_tlvs = [tlvs, <<0x14, apn_size::little-16>>, apn]
    make_tlvs(rest, new_tlvs, new_size)
  end

  defp make_tlvs([{:profile_3gpp_index, profile_index} | rest], tlvs, size) do
    tlv = <<0x31, 0x01::little-16, profile_index>>
    make_tlvs(rest, [tlvs, tlv], size + byte_size(tlv))
  end

  defp parse_start_network_interface_resp(
         <<@start_network_interface::little-16, size::little-16, tlvs::size(size)-binary>>
       ) do
    {:ok, parse_start_network_interface_tlvs(%{}, tlvs)}
  end

  defp parse_start_network_interface_resp(_other) do
    {:error, :unexpected_response}
  end

  defp parse_start_network_interface_tlvs(parsed, <<>>) do
    parsed
  end

  defp parse_start_network_interface_tlvs(
         parsed,
         <<0x01, 0x04, 0x00, packet_data_handle::little-32, rest::binary>>
       ) do
    parsed
    |> Map.put(:packet_data_handle, packet_data_handle)
    |> parse_start_network_interface_tlvs(rest)
  end

  defp parse_start_network_interface_tlvs(
         parsed,
         <<_type, length::little-16, _values::size(length)-unit(8)-binary, rest::binary>>
       ) do
    parse_start_network_interface_tlvs(parsed, rest)
  end

  @doc """
  Try to parse an indication from the wireless data service
  """
  @spec parse_indication(binary()) ::
          {:ok, packet_status_indication() | event_report_indication()}
          | {:error, :invalid_indication}
  def parse_indication(
        <<@packet_service_status_ind::16-little, size::16-little, tlvs::binary-size(size)>>
      ) do
    result = parse_packet_status_indication(packet_status_indication_init(), tlvs)

    {:ok, result}
  end

  def parse_indication(<<@event_report::little-16, size::16-little, tlvs::binary-size(size)>>) do
    {:ok, parse_event_report_indication(%{name: :event_report_indication}, tlvs)}
  end

  def parse_indication(_other) do
    {:error, :invalid_indication}
  end

  defp packet_status_indication_init() do
    %{
      name: :packet_status_indication,
      status: nil,
      reconfiguration_required: nil,
      call_end_reason: nil,
      call_end_reason_type: nil,
      ip_family: nil,
      tech_name: nil,
      bearer_id: nil,
      xlat_capable: nil
    }
  end

  defp parse_packet_status_indication(indication, <<>>) do
    indication
  end

  defp parse_packet_status_indication(
         indication,
         <<0x01, 0x02, 0x00, connection_status, reconfig_required, rest::binary>>
       ) do
    indication
    |> Map.put(:status, parse_packet_status(connection_status))
    |> Map.put(:reconfiguration_required, reconfig_required == 1)
    |> parse_packet_status_indication(rest)
  end

  defp parse_packet_status_indication(
         indication,
         <<0x10, 0x02, 0x00, call_end_reason::16-little, rest::binary>>
       ) do
    indication
    |> Map.put(:call_end_reason, call_end_reason)
    |> parse_packet_status_indication(rest)
  end

  defp parse_packet_status_indication(
         indication,
         <<0x11, 0x04, 0x00, call_end_reason_type::16-little, call_end_reason::16-little,
           rest::binary>>
       ) do
    indication
    |> Map.put(:call_end_reason_type, parse_call_end_reason_type(call_end_reason_type))
    |> Map.put(:call_end_reason, call_end_reason)
    |> parse_packet_status_indication(rest)
  end

  defp parse_packet_status_indication(indication, <<0x12, 0x01, 0x00, ip_family, rest::binary>>) do
    indication
    |> Map.put(:ip_family, ip_family)
    |> parse_packet_status_indication(rest)
  end

  defp parse_packet_status_indication(
         indication,
         <<0x13, 0x02, 0x00, tech_name::16-little, rest::binary>>
       ) do
    indication
    |> Map.put(:tech_name, parse_tech_name(tech_name))
    |> parse_packet_status_indication(rest)
  end

  defp parse_packet_status_indication(indication, <<0x14, 0x01, 0x00, bearer_id, rest::binary>>) do
    indication
    |> Map.put(:bearer_id, bearer_id)
    |> parse_packet_status_indication(rest)
  end

  defp parse_packet_status_indication(
         indication,
         <<0x15, 0x01, 0x00, xlat_capable, rest::binary>>
       ) do
    indication
    |> Map.put(:xlat_capable, xlat_capable == 1)
    |> parse_packet_status_indication(rest)
  end

  defp parse_packet_status_indication(
         indication,
         <<_type, length::16-little, _values::binary-size(length), rest::binary>>
       ) do
    parse_packet_status_indication(indication, rest)
  end

  defp parse_packet_status(0x01), do: :disconnected
  defp parse_packet_status(0x02), do: :connected
  defp parse_packet_status(0x03), do: :suspended
  defp parse_packet_status(0x04), do: :authenticating

  defp parse_tech_name(0x8001), do: :cdma
  defp parse_tech_name(0x8004), do: :umts
  defp parse_tech_name(0x8020), do: :wlan_local_brkout
  defp parse_tech_name(0x8021), do: :iwlan_s2b
  defp parse_tech_name(0x8880), do: :epc
  defp parse_tech_name(0x8882), do: :embms
  defp parse_tech_name(0x8888), do: :modem_link_local

  defp parse_call_end_reason_type(0x00), do: :unspecified
  defp parse_call_end_reason_type(0x01), do: :mobile_ip
  defp parse_call_end_reason_type(0x02), do: :internal
  defp parse_call_end_reason_type(0x03), do: :call_manager_defined
  defp parse_call_end_reason_type(0x06), do: :three_gpp_specification_defined
  defp parse_call_end_reason_type(0x07), do: :ppp
  defp parse_call_end_reason_type(0x08), do: :ehrpd
  defp parse_call_end_reason_type(0x09), do: :ipv6
  defp parse_call_end_reason_type(0x0C), do: :handoff

  @spec parse_event_report_indication(event_report_indication(), binary()) ::
          event_report_indication()
  def parse_event_report_indication(event_report_indication, <<>>), do: event_report_indication

  def parse_event_report_indication(
        event_report_indication,
        <<0x10, 0x04::little-16, tx_packets_count::little-32, rest::binary>>
      ) do
    tx_packets = get_stat_value(tx_packets_count)

    event_report_indication
    |> Map.put(:tx_packets, tx_packets)
    |> parse_event_report_indication(rest)
  end

  def parse_event_report_indication(
        event_report_indication,
        <<0x11, 0x04::little-16, rx_packets_count::little-32, rest::binary>>
      ) do
    rx_packets = get_stat_value(rx_packets_count)

    event_report_indication
    |> Map.put(:rx_packets, rx_packets)
    |> parse_event_report_indication(rest)
  end

  def parse_event_report_indication(
        event_report_indication,
        <<0x12, 0x04::little-16, tx_error_count::little-32, rest::binary>>
      ) do
    tx_errors = get_stat_value(tx_error_count)

    event_report_indication
    |> Map.put(:tx_errors, tx_errors)
    |> parse_event_report_indication(rest)
  end

  def parse_event_report_indication(
        event_report_indication,
        <<0x13, 0x04::little-16, rx_error_count::little-32, rest::binary>>
      ) do
    rx_errors = get_stat_value(rx_error_count)

    event_report_indication
    |> Map.put(:rx_errors, rx_errors)
    |> parse_event_report_indication(rest)
  end

  def parse_event_report_indication(
        event_report_indication,
        <<0x14, 0x04::little-16, tx_overflow_count::little-32, rest::binary>>
      ) do
    tx_overflows = get_stat_value(tx_overflow_count)

    event_report_indication
    |> Map.put(:tx_overflows, tx_overflows)
    |> parse_event_report_indication(rest)
  end

  def parse_event_report_indication(
        event_report_indication,
        <<0x15, 0x04::little-16, rx_overflow_count::little-32, rest::binary>>
      ) do
    rx_overflows = get_stat_value(rx_overflow_count)

    event_report_indication
    |> Map.put(:rx_overflows, rx_overflows)
    |> parse_event_report_indication(rest)
  end

  def parse_event_report_indication(
        event_report_indication,
        <<0x19, 0x08::little-16, tx_byte_count::little-64, rest::binary>>
      ) do
    event_report_indication
    |> Map.put(:tx_bytes, tx_byte_count)
    |> parse_event_report_indication(rest)
  end

  def parse_event_report_indication(
        event_report_indication,
        <<0x1A, 0x08::little-16, rx_byte_count::little-64, rest::binary>>
      ) do
    event_report_indication
    |> Map.put(:rx_bytes, rx_byte_count)
    |> parse_event_report_indication(rest)
  end

  def parse_event_report_indication(
        event_report_indication,
        <<0x25, 0x04::little-16, tx_packets_dropped::little-32, rest::binary>>
      ) do
    tx_drops = get_stat_value(tx_packets_dropped)

    event_report_indication
    |> Map.put(:tx_drops, tx_drops)
    |> parse_event_report_indication(rest)
  end

  def parse_event_report_indication(
        event_report_indication,
        <<0x26, 0x04::little-16, rx_packets_dropped::little-32, rest::binary>>
      ) do
    rx_drops = get_stat_value(rx_packets_dropped)

    event_report_indication
    |> Map.put(:rx_drops, rx_drops)
    |> parse_event_report_indication(rest)
  end

  def parse_event_report_indication(
        event_report_indication,
        <<_type, length::little-16, _values::binary-size(length), rest::binary>>
      ) do
    parse_event_report_indication(event_report_indication, rest)
  end

  defp get_stat_value(value) when value == @missing_stat_value, do: 0
  defp get_stat_value(value), do: value

  @typedoc """
  The type of measurement you are wanting to be reported

  * `:tx_bytes` - number of bytes transmitted
  * `:rx_bytes` - number of bytes received
  * `:tx_packets` - number of transmit packets sent without error
  * `:rx_packets` - number of packets received without error
  * `:tx_overflows` - number of packets dropped due to tx buffer
    overflowed (out of memory)
  * `:rx_overflows` - number of packets dropped due to rx buffer
    overflowed (out of memory)
  * `:tx_errors` - number of outgoing packets with framing errors
  * `:rx_errors` - number of incoming packets with framing errors
  * `:tx_drops` - number outgoing packets dropped
  * `:rx_drops` - number incoming packets dropped
  """
  @type statistic_measurement() ::
          :tx_bytes
          | :rx_bytes
          | :tx_packets
          | :rx_packets
          | :tx_overflows
          | :rx_overflows
          | :tx_errors
          | :rx_errors
          | :tx_drops
          | :rx_drops

  @typedoc """
  Options for the wireless data event report configuration

  * `:statistics_interval` - an interval in seconds on when to report statistics
    about transmit and receive operations
  * `:statistics` - list which statistic measurements to report

  For transmit and receive statistics, if no interval is provided the default is
  60 seconds. If the `:statistics` option is not provided it will default to
  `:all`. The report is only sent in the interval if there was a change in any
  of the statistics. So, if no changes took place the report will be skipped for
  an interval.

  To configure event report to not included any stats pass `:none` to
  the `:statistics` option.
  """
  @type event_report_opt() ::
          {:statistics_interval, non_neg_integer()}
          | {:statistics, [statistic_measurement()] | :all | :none}

  @doc """
  Request to set the wireless data services event report options
  """
  @spec set_event_report([event_report_opt()]) :: QMI.request()
  def set_event_report(opts \\ []) do
    tlvs = make_tlvs(opts)
    length = byte_size(tlvs)

    payload = [<<@event_report::little-16, length::little-16>>, tlvs]

    %{service_id: 0x01, payload: payload, decode: &parse_set_event_response/1}
  end

  defp make_tlvs(opts) do
    case opts[:statistics] do
      :none ->
        <<>>

      stats ->
        # if no stats are specified we will get them all
        default_stats = stats || :all
        interval = opts[:statistics_interval] || 60
        mask = statistics_mask(default_stats)

        <<0x11, 0x05::little-16, interval, mask::little-32>>
    end
  end

  @spec statistics_mask(:all | [statistic_measurement()]) :: non_neg_integer()
  def statistics_mask(:all) do
    0x03FF
  end

  def statistics_mask(stats) do
    for stat <- stats, reduce: 0 do
      mask ->
        stat_to_integer(stat) ||| mask
    end
  end

  @spec stat_to_integer(statistic_measurement()) :: 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512
  def stat_to_integer(:tx_packets), do: 0x01
  def stat_to_integer(:rx_packets), do: 0x02
  def stat_to_integer(:tx_errors), do: 0x04
  def stat_to_integer(:rx_errors), do: 0x08
  def stat_to_integer(:tx_overflows), do: 0x10
  def stat_to_integer(:rx_overflows), do: 0x20
  def stat_to_integer(:tx_bytes), do: 0x40
  def stat_to_integer(:rx_bytes), do: 0x80
  def stat_to_integer(:tx_drops), do: 0x0100
  def stat_to_integer(:rx_drops), do: 0x0200

  defp parse_set_event_response(
         <<@event_report::little-16, size::little-16, _values::binary-size(size)>>
       ) do
    :ok
  end

  @typedoc """
  The profile technology type
  """
  @type profile_type() :: :profile_type_3gpp | :profile_type_3gpp2 | :profile_type_epc

  @typedoc """
  The profile settings

  * `:roaming_disallowed` - if using roaming is allowed or not
  * `:profile_type` - the profile type - see `profile_type()` type docs for more
    information
  """
  @type profile_setting() :: {:roaming_disallowed, boolean()} | {:profile_type, profile_type()}

  @typedoc """
  Response from issuing a modify profile settings request
  """
  @type modify_profile_settings_response() :: %{extended_error_code: integer() | nil}

  @doc """
  Modify the QMI profile setting

  When providing the profile settings if `:profile_type` is not included this
  function will default to `:profile_type_3gpp`
  """
  @spec modify_profile_settings(profile_index :: integer(), [profile_setting()]) :: QMI.request()
  def modify_profile_settings(index, settings) do
    profile_type = settings[:profile_type] || :profile_type_3gpp
    settings = Keyword.put(settings, :profile_info, {index, profile_type})

    {tlvs, tlvs_byte_size} = make_modify_profile_settings_tlvs(settings, [], 0)

    %{
      service_id: 0x01,
      payload: [
        <<@modify_profile_settings::little-16, tlvs_byte_size::little-16>>,
        tlvs
      ],
      decode: &parse_modify_profile_settings_resp/1
    }
  end

  defp make_modify_profile_settings_tlvs([], encoded, total_size) do
    {encoded, total_size}
  end

  defp make_modify_profile_settings_tlvs(
         [{:profile_info, {profile_index, type}} | rest],
         encoded,
         size
       ) do
    type_byte = encode_profile_type(type)
    encoded_tlv = <<0x01, 0x02::little-16, type_byte, profile_index>>

    make_modify_profile_settings_tlvs(
      rest,
      encoded ++ [encoded_tlv],
      size + byte_size(encoded_tlv)
    )
  end

  defp make_modify_profile_settings_tlvs(
         [{:roaming_disallowed, disallowed?} | rest],
         encoded,
         size
       ) do
    disallowed_byte = if disallowed?, do: 0x01, else: 0x00

    tlvs = <<0x3E, 0x01::little-16, disallowed_byte>>

    make_modify_profile_settings_tlvs(rest, encoded ++ [tlvs], size + byte_size(tlvs))
  end

  defp make_modify_profile_settings_tlvs([_unknown | rest], encoded, size) do
    make_modify_profile_settings_tlvs(rest, encoded, size)
  end

  defp encode_profile_type(:profile_type_3gpp), do: 0x00
  defp encode_profile_type(:profile_type_3gpp2), do: 0x01
  defp encode_profile_type(:profile_type_epc), do: 0x02

  defp parse_modify_profile_settings_resp(
         <<@modify_profile_settings::little-16, size::little-16, values::binary-size(size)>>
       ) do
    result = parse_profile_settings_resp_tlvs(values, %{extended_error_code: nil})
    {:ok, result}
  end

  defp parse_profile_settings_resp_tlvs(<<>>, parsed) do
    parsed
  end

  defp parse_profile_settings_resp_tlvs(<<0>>, parsed) do
    parsed
  end

  defp parse_profile_settings_resp_tlvs(
         <<0xE0, 0x02::little-16, code::little-16, rest::binary>>,
         parsed
       ) do
    parsed = Map.put(parsed, :extended_error_code, code)

    parse_profile_settings_resp_tlvs(rest, parsed)
  end

  defp parse_profile_settings_resp_tlvs(
         <<_type, len::little, _value::binary-size(len), rest::binary>>,
         parsed
       ) do
    parse_profile_settings_resp_tlvs(rest, parsed)
  end
end