Skip to content

Support FILETRANSFER #26

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions lib/jeff.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ defmodule Jeff do
alias Jeff.MFG.Encoder
alias Jeff.Reply
alias Jeff.Reply.ErrorCode
alias Jeff.{ACU, Command, Command.FileTransfer, Device, Reply}

@type acu() :: GenServer.server()
@type device_opt() :: ACU.device_opt()
Expand Down Expand Up @@ -174,6 +175,33 @@ defmodule Jeff do
ACU.send_command(acu, address, MFG, params) |> handle_reply()
end

@doc """
Send file data to a PD
"""
@spec file_transfer(acu(), osdp_address(), binary()) ::
Reply.FileTransferStatus.t() | Reply.ErrorCode.t()
def file_transfer(acu, address, data) when is_binary(data) do
with %{name: PDCAP, data: caps} <- ACU.send_command(acu, address, CAP) do
max = caps[:receive_buffer_size] || 128
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OSDP spec says you can send the receive_buffer_size that the reader reports from the capabilities, but I found that it doesn't work. The only size consistently finishing was using 128 bytes


FileTransfer.command_set(data, max)
|> run_file_transfer(acu, address)
end
end

defp run_file_transfer([cmd | rem], acu, address) do
ACU.send_command(acu, address, FILETRANSFER, Map.to_list(cmd))
|> FileTransfer.adjust_from_reply(rem)
|> case do
{:cont, next, delay} ->
:timer.sleep(delay)
run_file_transfer(next, acu, address)

{:halt, data} ->
data
end
end

defp handle_reply({:ok, %{data: %ErrorCode{code: code} = data}}) when code > 0,
do: {:error, data}

Expand Down
2 changes: 2 additions & 0 deletions lib/jeff/command.ex
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ defmodule Jeff.Command do
ComSettings,
EncryptionKey,
EncryptionServer,
FileTransfer,
LedSettings,
Mfg,
OutputSettings,
Expand Down Expand Up @@ -130,6 +131,7 @@ defmodule Jeff.Command do
defp encode(KEYSET, params), do: EncryptionKey.encode(params)
defp encode(CHLNG, params), do: ChallengeData.encode(params)
defp encode(SCRYPT, params), do: EncryptionServer.encode(params)
defp encode(FILETRANSFER, params), do: FileTransfer.encode(params)
defp encode(MFG, params), do: Mfg.encode(params)
defp encode(ACURXSIZE, size: size), do: <<size::size(16)-little>>
defp encode(ABORT, _params), do: nil
Expand Down
137 changes: 137 additions & 0 deletions lib/jeff/command/file_transfer.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
defmodule Jeff.Command.FileTransfer do
@moduledoc """
File Transfer Command Settings

OSDP v2.2 Specification Reference: 6.26
"""

defstruct type: 1,
total_size: 0,
offset: 0,
fragment_size: 0,
data: <<>>

@type t :: %__MODULE__{
type: 1..255,
total_size: pos_integer(),
offset: pos_integer(),
data: binary(),
fragment_size: pos_integer()
}

@type param() ::
{:type, 1..255}
| {:total_size, pos_integer()}
| {:offset, pos_integer()}
| {:fragment_size, pos_integer()}
| {:data, binary()}
@type params() :: t() | [param()]

@spec new(params()) :: t()
def new(params) do
struct(__MODULE__, params)
end

@spec encode(params()) :: <<_::64>>
def encode(params) do
settings = new(params)

<<
settings.type,
settings.total_size::size(4)-unit(8)-little,
settings.offset::size(4)-unit(8)-little,
settings.fragment_size::size(2)-unit(8)-little,
settings.data::binary
>>
end

@doc """
Create set of FileTransfer commands to run

FileTransfers may require multiple command/reply pairs in order to transmit
all the data to the PD. This function helps chunk the data according to the
max byte length allowed by the PD. In most cases, you would run
`Jeff.capabilities/2` before this check for the Receive Buffer size reported
by the PD and use that as the max value.

The first message will always be 128 bytes of data if the max value is larger

You can then cycle through sending these commands and check the returned
FTSTAT reply (%Jeff.Reply.FileTransferStatus{}) with `adjust_from_reply/2` to
adjust the command set as needed
"""
@spec command_set(binary(), pos_integer()) :: [t()]
def command_set(data, max \\ 128) do
base = new(total_size: byte_size(data))
chunk_data(data, base, max, [])
end

@doc """
Adjust file transfer command set based on the FTSTAT reply

Mostly used internally to potentially adjust the remaining command set
based on the FTSTAT reply from the previous command. In some cases the
next set of commands may need to be adjusted or prevented and this provides
the functional core to make that decision
"""
@spec adjust_from_reply(Jeff.Reply.t(), [t()]) ::
{:cont, [t()], pos_integer()}
| {:halt, Jeff.Reply.FileTransferStatus.t() | Jeff.Reply.ErrorCode.t()}
def adjust_from_reply(%{name: NAK, data: error_code}, _commands), do: {:halt, error_code}
def adjust_from_reply(%{name: FTSTAT, data: ftstat}, []), do: {:halt, ftstat}

def adjust_from_reply(%{name: FTSTAT, data: %{status: :finishing} = ftstat}, commands) do
# OSDP v2.2 Section 7.25
# Finishing status requires we send an "idle" message until we get a different
# status. In idle message, fragment size == 0 and offset == total size
idle = hd(commands)
commands = maybe_adjust_message_size(ftstat, commands)
{:cont, [%{idle | fragment_size: 0, offset: idle.total_size} | commands], ftstat.delay}
end

def adjust_from_reply(%{name: FTSTAT, data: %{status: status} = ftstat}, commands)
when status in [:ok, :processed, :rebooting] do
{:cont, maybe_adjust_message_size(ftstat, commands), ftstat.delay}
end

def adjust_from_reply(%{name: FTSTAT, data: ftstat}, _commands), do: {:halt, ftstat}

defp maybe_adjust_message_size(%{update_msg_max: max}, commands)
when not is_nil(max) and max > 0 do
base = hd(commands)
data = for %{data: d} <- commands, into: <<>>, do: d
chunk_data(data, base, max, [])
end

defp maybe_adjust_message_size(_ftstat, commands), do: commands

defp chunk_data(data, _base, _max, acc) when byte_size(data) == 0 do
Enum.reverse(acc)
end

defp chunk_data(<<data::binary-128, rest::binary>>, base, max, []) when max >= 128 do
# First command must be 128 bytes in cases where the PD max receive buffer is
# more than 128
chunk_data(rest, base, max, [%{base | data: data, fragment_size: 128}])
end

defp chunk_data(data, base, max, acc) when byte_size(data) >= max do
<<frag::binary-size(max), rest::binary>> = data
cmd = %{base | data: frag, fragment_size: max, offset: next_offset(base, acc)}
chunk_data(rest, base, max, [cmd | acc])
end

defp chunk_data(data, base, _max, acc) do
frag_size = byte_size(data)
# cmd = new(total_size: total, data: data, fragment_size: frag_size, offset: o + frag_size)
cmd = %{base | data: data, fragment_size: frag_size, offset: next_offset(base, acc)}
Enum.reverse([cmd | acc])
end

# Start from the base offset
defp next_offset(%{offset: o}, []) when is_integer(o), do: o
# First command and no base offset, use 0
defp next_offset(_base, []), do: 0
# offset from the last command
defp next_offset(_base, [%{offset: o, fragment_size: fs} | _]), do: o + fs
end
2 changes: 2 additions & 0 deletions lib/jeff/reply.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ defmodule Jeff.Reply do
ComData,
EncryptionClient,
ErrorCode,
FileTransferStatus,
IdReport,
InputStatus,
KeypadData,
Expand Down Expand Up @@ -132,6 +133,7 @@ defmodule Jeff.Reply do
defp decode(CCRYPT, data), do: EncryptionClient.decode(data)
defp decode(MFGREP, data), do: MfgReply.decode(data)
defp decode(RMAC_I, data), do: data
defp decode(FTSTAT, data), do: FileTransferStatus.decode(data)
defp decode(_name, nil), do: nil
defp decode(_name, <<>>), do: nil
defp decode(name, data), do: Module.concat(__MODULE__, name).decode(data)
Expand Down
60 changes: 60 additions & 0 deletions lib/jeff/reply/file_transfer_status.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
defmodule Jeff.Reply.FileTransferStatus do
@moduledoc """
File Transfer Status (osdp_FTSTAT)

OSDP v2.2 Specification Reference: 7.25
"""

defstruct [
:separate_poll_response?,
:leave_secure_channel?,
:interleave_ok?,
:delay,
:status,
:update_msg_max
]

@type status ::
:ok
| :processed
| :rebooting
| :finishing
| :abort
| :unrecognized_contents
| :malformed
| integer()

@type t :: %__MODULE__{
separate_poll_response?: boolean(),
leave_secure_channel?: boolean(),
interleave_ok?: boolean(),
delay: pos_integer(),
status: status(),
update_msg_max: pos_integer()
}

@spec decode(binary()) :: t()
def decode(
<<_::5, spr::1, leave::1, interleave::1, delay::16-little, status::16-little-signed,
update_msg_max::16-little>>
) do
%__MODULE__{
separate_poll_response?: spr == 1,
leave_secure_channel?: leave == 1,
interleave_ok?: interleave == 1,
delay: delay,
status: decode_status(status),
update_msg_max: update_msg_max
}
end

defp decode_status(0), do: :ok
defp decode_status(1), do: :processed
defp decode_status(2), do: :rebooting
defp decode_status(3), do: :finishing
defp decode_status(-1), do: :abort
defp decode_status(-2), do: :unrecognized_contents
defp decode_status(-3), do: :malformed
# All other statuses are reserved - Report the raw status for now
defp decode_status(status), do: status
end
Loading
Loading