Packages
grizzly
9.1.4
9.1.4
9.1.2
9.1.1
9.1.0
9.0.0
8.15.3
8.15.2
8.15.1
8.15.0
8.14.0
8.13.0
8.12.0
8.11.3
8.11.2
8.11.1
8.11.0
8.10.0
8.9.0
8.8.1
8.8.0
8.7.1
8.7.0
8.6.12
8.6.11
8.6.10
8.6.9
8.6.8
8.6.7
retired
8.6.6
8.6.5
8.6.4
8.6.3
8.6.2
8.6.1
8.6.0
8.5.3
8.5.2
8.5.1
8.5.0
8.4.0
8.3.0
8.2.3
8.2.2
8.2.1
8.2.0
8.1.0
8.0.1
8.0.0
7.4.3
7.4.2
7.4.1
7.4.0
7.3.0
7.2.0
7.1.4
7.1.3
7.1.2
7.1.1
7.1.0
7.0.4
7.0.3
7.0.2
7.0.1
7.0.0
6.8.8
6.8.7
6.8.6
6.8.5
6.8.4
6.8.3
6.8.2
6.8.1
6.8.0
6.7.1
6.7.0
6.6.1
6.6.0
6.5.1
6.5.0
6.4.0
6.3.0
6.2.0
6.1.1
6.1.0
6.0.1
6.0.0
5.4.1
5.4.0
5.3.0
5.2.8
5.2.7
5.2.6
5.2.5
5.2.4
5.2.3
5.2.2
5.2.1
5.2.0
5.1.2
5.1.1
5.1.0
5.0.2
5.0.1
5.0.0
4.0.1
4.0.0
3.0.0
2.1.0
2.0.0
1.0.1
1.0.0
0.22.7
0.22.6
0.22.5
0.22.4
0.22.3
0.22.2
0.22.1
0.22.0
0.21.1
0.21.0
0.20.2
0.20.1
0.20.0
0.19.1
0.19.0
0.18.3
0.18.2
0.18.1
0.18.0
0.17.7
0.17.6
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.2
0.16.1
0.16.0
0.15.11
0.15.10
0.15.9
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.8
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.0
0.12.3
0.12.2
0.12.1
0.12.0
0.11.0
0.10.3
0.10.2
0.10.1
0.10.0
0.9.0
0.9.0-rc.4
0.9.0-rc.3
0.9.0-rc.2
0.9.0-rc.1
0.9.0-rc.0
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.0
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.4.3
0.4.2
Elixir Z-Wave library
Current section
Files
Jump to
Current section
Files
lib/grizzly/zwave/param_spec.ex
defmodule Grizzly.ZWave.ParamSpec do
@moduledoc """
Data structure describing a parameter for a Z-Wave command.
"""
import Integer, only: [is_even: 1]
alias Grizzly.ZWave.DecodeError
alias Grizzly.ZWave.DSK
alias Grizzly.ZWave.Encoding
alias Grizzly.ZWave.ZWEnum
schema =
NimbleOptions.new!(
name: [
type: :atom,
doc: "The parameter's name",
required: true
],
type: [
type: {:in, [:int, :uint, :boolean, :binary, :enum, :constant, :dsk, :custom, :any]},
doc: "The parameter's type",
required: true
],
size: [
type:
{:or,
[
:non_neg_integer,
{:in, [:variable]},
{:tuple, [{:in, [:variable]}, :non_neg_integer]}
]},
default: 8,
doc: "The size of the parameter in bits, or :variable for variable length"
],
default: [
type: :any
],
required: [
type: :boolean,
default: true
],
opts: [
type: :keyword_list,
default: []
]
)
@schema schema
@type type ::
:int
| :uint
| :boolean
| :binary
| :enum
| :constant
| :reserved
| :bitmask
| {:length, atom()}
| :any
@type t :: %__MODULE__{
name: atom(),
type: type(),
size: non_neg_integer() | :variable,
default: any(),
required: boolean(),
opts: keyword()
}
defstruct name: nil,
type: nil,
size: 8,
default: nil,
required: true,
opts: []
@doc "Validate a command spec."
def validate(%__MODULE__{} = spec) do
spec
|> Map.from_struct()
|> NimbleOptions.validate(@schema)
end
@doc "Validate a command spec, raising on error."
def validate!(%__MODULE__{} = spec) do
spec
|> Map.from_struct()
|> NimbleOptions.validate!(@schema)
end
@doc """
Returns the size of the parameter in bits (or :variable) for variable-length params.
## Examples
iex> spec = %Grizzly.ZWave.ParamSpec{type: :uint, size: 16}
iex> Grizzly.ZWave.ParamSpec.num_bits(spec)
16
iex> spec = %Grizzly.ZWave.ParamSpec{type: :binary, size: 32}
iex> Grizzly.ZWave.ParamSpec.num_bits(spec)
32
iex> spec = %Grizzly.ZWave.ParamSpec{type: :int, size: :variable}
iex> Grizzly.ZWave.ParamSpec.num_bits(spec)
:variable
iex> spec = %Grizzly.ZWave.ParamSpec{type: :int, size: {:variable, :another_param}}
iex> Grizzly.ZWave.ParamSpec.num_bits(spec)
:variable
"""
@spec num_bits(t()) :: non_neg_integer() | :variable
def num_bits(%__MODULE__{size: size}) when is_integer(size), do: size
def num_bits(%__MODULE__{size: :variable}), do: :variable
def num_bits(%__MODULE__{size: {:variable, _}}), do: :variable
@doc """
Takes the number of bits specified by the param spec from the front of the bitstring.
Returns `{:ok, {value, rest}}` where `value` is the taken bits and `rest` is the
remaining bits. If there are not enough bits to take, an error is returned.
## Examples
iex> spec = %Grizzly.ZWave.ParamSpec{type: :uint, size: 8}
iex> Grizzly.ZWave.ParamSpec.take_bits(spec, <<0x01, 0x02, 0x03>>, [])
{:ok, {8, <<0x01>>, <<0x02, 0x03>>}}
iex> spec = %Grizzly.ZWave.ParamSpec{type: :int, size: 16}
iex> Grizzly.ZWave.ParamSpec.take_bits(spec, <<0x01, 0x02, 0x03>>, [])
{:ok, {16, <<0x01, 0x02>>, <<0x03>>}}
iex> spec = %Grizzly.ZWave.ParamSpec{type: :int, size: :variable}
iex> Grizzly.ZWave.ParamSpec.take_bits(spec, <<0x01, 0x02, 0x03>>, [])
{:ok, {24, <<0x01, 0x02, 0x03>>, <<>>}}
iex> spec = %Grizzly.ZWave.ParamSpec{type: :int, size: {:variable, :length_param}}
iex> Grizzly.ZWave.ParamSpec.take_bits(spec, <<0x01, 0x02, 0x03>>, length_param: 2)
{:ok, {16, <<0x01, 0x02>>, <<0x03>>}}
"""
@spec take_bits(t(), bitstring(), keyword()) ::
{:ok, {bits_taken :: non_neg_integer(), value :: bitstring(), rest :: bitstring()}}
| {:error, DecodeError.t()}
def take_bits(param_spec, bitstring, other_params)
def take_bits(%__MODULE__{type: type, size: size}, bitstring, _other_params)
when type in [:binary, :dsk] and is_integer(size) and rem(size, 8) == 0 and
bit_size(bitstring) >= size do
<<value::bitstring-size(size), rest::bitstring>> = bitstring
{:ok, {size, value, rest}}
end
def take_bits(%__MODULE__{type: :binary, size: size} = param, bitstring, _other_params)
when is_integer(size) do
{:error,
%DecodeError{
param: param.name,
value: bitstring,
reason: "not enough bits to decode parameter"
}}
end
def take_bits(
%__MODULE__{size: {:variable, length_param}} = param_spec,
bitstring,
other_params
) do
length_in_bits =
case Keyword.fetch(other_params, length_param) do
{:ok, length_in_bytes} when is_integer(length_in_bytes) ->
length_in_bytes * 8
_ ->
raise ArgumentError,
"Length parameter #{length_param} not found in other_params or is not an integer"
end
# Change type to :any to avoid hitting binary-specific clauses which expect size in bytes
take_bits(%__MODULE__{param_spec | type: :any, size: length_in_bits}, bitstring, other_params)
end
def take_bits(%__MODULE__{size: :variable}, bitstring, _other_params) do
{:ok, {bit_size(bitstring), bitstring, <<>>}}
end
def take_bits(%__MODULE__{size: size}, bitstring, _other_params)
when is_integer(size) and bit_size(bitstring) >= size do
<<value::bitstring-size(size), rest::bitstring>> = bitstring
{:ok, {size, value, rest}}
end
def take_bits(%__MODULE__{} = param, bitstring, _other_params) do
{:error,
%DecodeError{
param: param.name,
value: bitstring,
reason: "not enough bits to decode parameter"
}}
end
@doc """
Whether the parameter should be included in the resulting list of parameters
when decoding a command.
"""
@spec include_when_decoding?(t()) :: boolean()
def include_when_decoding?(param_spec) do
param_spec.opts[:hidden] != true and param_spec.type != :reserved
end
@doc """
Encodes an Elixir term into a bitstring according to the parameter specification.
"""
@spec encode_value(t(), term(), keyword()) :: bitstring()
def encode_value(param_spec, value, other_params \\ [])
def encode_value(%__MODULE__{type: :enum, size: size} = spec, value, _) do
values_map = Keyword.fetch!(spec.opts, :values)
<<ZWEnum.fetch!(values_map, value)::size(size)>>
end
def encode_value(%__MODULE__{type: :bitmask, size: :variable} = spec, value, _)
when is_list(value) do
values_map = Keyword.fetch!(spec.opts, :values)
Encoding.encode_enum_bitmask(values_map, value)
end
def encode_value(%__MODULE__{type: :bitmask, size: size} = spec, value, _)
when is_list(value) and is_integer(size) and (rem(size, 8) == 0 or size < 8) do
values_map = Keyword.fetch!(spec.opts, :values)
full_bitmask = Encoding.encode_enum_bitmask(values_map, value, min_bytes: div(size, 8))
# Only truncate to size if the size is less than 8 bytes. Multi-byte bitmasks
# are always byte-aligned.
if size < 8 do
<<_truncated::size(8 - size), right_sized_bitmask::bitstring-size(size)>> = full_bitmask
right_sized_bitmask
else
full_bitmask
end
end
def encode_value(%__MODULE__{type: :any, size: size} = spec, value, _) do
encoder = Keyword.get(spec.opts, :encode)
result =
if is_function(encoder, 1) do
encoder.(value)
else
raise "No valid encoder function for enum param #{spec.name}"
end
result =
case result do
{:ok, v} ->
v
{:error, _} ->
raise "Error encoding enum param #{spec.name} with value: #{inspect(value)}"
v ->
v
end
case result do
v when is_integer(v) -> <<v::size(size)>>
v when is_binary(v) or is_bitstring(v) -> v
_ -> raise "Invalid value for enum param #{spec.name}: #{inspect(result)}"
end
end
def encode_value(%__MODULE__{type: :int} = param_spec, value, other_params)
when is_integer(value) do
size = encoded_size(param_spec, value, other_params)
<<value::signed-size(size)>>
end
def encode_value(%__MODULE__{type: :uint} = param_spec, value, other_params)
when is_integer(value) do
size = encoded_size(param_spec, value, other_params)
<<value::size(size)>>
end
def encode_value(%__MODULE__{type: :boolean, size: size} = param_spec, value, _)
when is_boolean(value) do
cond do
Keyword.has_key?(param_spec.opts, value) ->
<<param_spec.opts[value]::size(size)>>
value == false ->
<<0::size(size)>>
true ->
<<0xFF::size(size)>>
end
end
def encode_value(%__MODULE__{type: :constant, size: size} = param_spec, _, _) do
<<param_spec.opts[:value]::size(size)>>
end
def encode_value(%__MODULE__{type: :reserved, size: size}, _, _) do
<<0::size(size)>>
end
def encode_value(%__MODULE__{type: :binary, size: size}, value, _)
when is_bitstring(value) and is_integer(size) do
<<value::bitstring-size(size)>>
end
def encode_value(%__MODULE__{type: :binary, size: :variable}, value, _)
when is_bitstring(value) do
value
end
def encode_value(%__MODULE__{type: :dsk, size: size}, value, _) when is_integer(size) do
case value do
%DSK{raw: raw} ->
<<raw::bitstring-size(size)>>
bin when is_binary(value) and byte_size(value) > 16 ->
<<DSK.parse!(bin).raw::bitstring-size(size)>>
bin when is_binary(value) and byte_size(value) <= 16 and is_even(byte_size(value)) ->
<<DSK.new(bin).raw::bitstring-size(size)>>
end
end
def encode_value(
%__MODULE__{type: {:length, other_param}, size: size} = _spec,
_value,
other_params
) do
length_value =
case Keyword.fetch(other_params, other_param) do
{:ok, v} when is_binary(v) -> byte_size(v)
{:ok, _} -> raise "Cannot encode length of non-binary parameter"
_ -> raise "Cannot encode length of #{inspect(other_param)}: parameter not found"
end
<<length_value::size(size)>>
end
def encode_value(
%__MODULE__{type: :binary, size: {:variable, other_param}},
value,
other_params
)
when is_bitstring(value) do
expected_size =
case Keyword.fetch(other_params, other_param) do
{:ok, length_in_bytes} when is_integer(length_in_bytes) -> length_in_bytes
_ -> raise "Length parameter #{other_param} not found in params or is not an integer"
end
actual_size = bit_size(value)
if actual_size != expected_size do
raise "Binary parameter size mismatch: expected #{expected_size} bits, got #{actual_size} bits"
end
value
end
defp encoded_size(%__MODULE__{size: size}, _value, _other_params)
when is_integer(size) do
size
end
defp encoded_size(
%__MODULE__{size: {:variable, length_param}},
_value,
other_params
) do
case Keyword.fetch(other_params, length_param) do
{:ok, length_in_bytes} when is_integer(length_in_bytes) ->
length_in_bytes * 8
_ ->
raise "Length parameter #{length_param} not found in params or is not an integer: #{inspect(other_params)}"
end
end
@doc """
Decodes an Elixir term from a bitstring according to the parameter specification.
"""
@spec decode_value(t(), bitstring()) :: {:ok, term()} | {:error, DecodeError.t()}
def decode_value(param_spec, bitstring, other_params \\ [])
def decode_value(%__MODULE__{type: :uint, size: size} = param_spec, binary, _other_params) do
case binary do
<<v::size(^size)>> ->
{:ok, v}
_ ->
{:error, %DecodeError{param: param_spec.name, value: binary}}
end
end
def decode_value(%__MODULE__{type: :int, size: size} = param_spec, binary, _other_params) do
case binary do
<<v::signed-size(^size)>> -> {:ok, v}
_ -> {:error, %DecodeError{param: param_spec.name, value: binary}}
end
end
def decode_value(%__MODULE__{type: :enum, size: size} = param_spec, binary, _other_params) do
values_map = Keyword.fetch!(param_spec.opts, :values)
case binary do
<<raw_value::size(^size)>> ->
case ZWEnum.fetch_key(values_map, raw_value) do
{:ok, value} ->
{:ok, value}
:error ->
case param_spec.opts[:if_unknown] do
:raw -> {:ok, raw_value}
{:value, default_value} -> {:ok, default_value}
_ -> {:error, %DecodeError{param: param_spec.name, value: raw_value}}
end
end
_ ->
{:error, %DecodeError{param: param_spec.name, value: binary}}
end
end
def decode_value(%__MODULE__{type: :bitmask, size: size} = param_spec, binary, _other_params) do
values_map = Keyword.fetch!(param_spec.opts, :values)
case binary do
<<raw_value::bitstring-size(^size)>> ->
# Ensure we're byte-aligned
raw_value =
if rem(bit_size(raw_value), 8) != 0 do
<<0::size(8 - rem(bit_size(raw_value), 8)), raw_value::bitstring>>
else
raw_value
end
decoded_values = Encoding.decode_enum_bitmask(values_map, raw_value)
{:ok, decoded_values}
_ ->
{:error, %DecodeError{param: param_spec.name, value: binary}}
end
end
def decode_value(%__MODULE__{type: :any, size: size} = param_spec, binary, _other_params) do
decoder = Keyword.get(param_spec.opts, :decode)
case binary do
<<raw_value::size(^size)>> ->
if is_function(decoder, 1) do
case decoder.(raw_value) do
{:ok, v} ->
{:ok, v}
{:error, %DecodeError{}} = err ->
err
{:error, _} ->
{:error, %DecodeError{param: param_spec.name, value: raw_value}}
v ->
{:ok, v}
end
else
{:ok, raw_value}
end
_ ->
{:error, %DecodeError{param: param_spec.name, value: binary}}
end
end
def decode_value(%__MODULE__{type: :constant} = param_spec, _binary, _other_params) do
{:ok, param_spec.opts[:value]}
end
def decode_value(%__MODULE__{type: :reserved}, _binary, _other_params) do
{:ok, nil}
end
def decode_value(%__MODULE__{type: :boolean, size: size} = param_spec, binary, _other_params) do
case binary do
<<v::size(^size)>> -> {:ok, v != 0}
_ -> {:error, %DecodeError{param: param_spec.name, value: binary}}
end
end
def decode_value(%__MODULE__{type: {:length, _}} = param_spec, binary, other_params) do
decode_value(%__MODULE__{param_spec | type: :uint}, binary, other_params)
end
def decode_value(%__MODULE__{type: :binary, size: size} = param_spec, binary, _other_params)
when is_integer(size) do
case binary do
<<v::bitstring-size(size)>> ->
{:ok, v}
_ ->
{:error, %DecodeError{param: param_spec.name, value: binary}}
end
end
def decode_value(%__MODULE__{type: :binary, size: :variable}, binary, _other_params) do
{:ok, binary}
end
def decode_value(%__MODULE__{type: :dsk}, binary, _other_params) do
{:ok, DSK.new(binary)}
end
def decode_value(
%__MODULE__{type: :binary, size: {:variable, length_param}} = spec,
binary,
other_params
) do
length =
case Keyword.fetch(other_params, length_param) do
{:ok, v} when is_integer(v) ->
v
_ ->
{:error,
%DecodeError{
param: spec.name,
value: nil,
reason: "Length parameter #{inspect(length_param)} not found or is not an integer"
}}
end
{:ok, <<binary::bitstring-size(length)>>}
end
def decode_value(%__MODULE__{} = param_spec, _binary, _other_params) do
raise "Decoding for param type #{inspect(param_spec.type)} not implemented for param #{param_spec.name}"
end
end