Browse Source

spin off bindsight to own project

master
beadsland 1 month ago
parent
commit
0d297155ec
41 changed files with 1 additions and 2209 deletions
  1. 1
    0
      README.md
  2. 0
    5
      sysd/bindsight/.formatter.exs
  3. 0
    26
      sysd/bindsight/.gitignore
  4. 0
    11
      sysd/bindsight/README.md
  5. 0
    15
      sysd/bindsight/config/config.exs
  6. 0
    16
      sysd/bindsight/config/dev.exs
  7. 0
    6
      sysd/bindsight/config/test.exs
  8. 0
    40
      sysd/bindsight/lib/bindsight.ex
  9. 0
    40
      sysd/bindsight/lib/bindsight/common/camera.ex
  10. 0
    75
      sysd/bindsight/lib/bindsight/common/library.ex
  11. 0
    150
      sysd/bindsight/lib/bindsight/common/mintjulep.ex
  12. 0
    63
      sysd/bindsight/lib/bindsight/common/tasker.ex
  13. 0
    68
      sysd/bindsight/lib/bindsight/stage/slosh/chunk.ex
  14. 0
    148
      sysd/bindsight/lib/bindsight/stage/slosh/digest.ex
  15. 0
    72
      sysd/bindsight/lib/bindsight/stage/slosh/request.ex
  16. 0
    64
      sysd/bindsight/lib/bindsight/stage/slosh/spigot.ex
  17. 0
    48
      sysd/bindsight/lib/bindsight/stage/sloshsupervisor.ex
  18. 0
    76
      sysd/bindsight/lib/bindsight/stage/slurp/batch.ex
  19. 0
    40
      sysd/bindsight/lib/bindsight/stage/slurp/broadcast.ex
  20. 0
    42
      sysd/bindsight/lib/bindsight/stage/slurp/flushsnoop.ex
  21. 0
    67
      sysd/bindsight/lib/bindsight/stage/slurp/spigot.ex
  22. 0
    68
      sysd/bindsight/lib/bindsight/stage/slurp/validate.ex
  23. 0
    47
      sysd/bindsight/lib/bindsight/stage/slurpsupervisor.ex
  24. 0
    46
      sysd/bindsight/lib/bindsight/stage/snoopsupervisor.ex
  25. 0
    37
      sysd/bindsight/lib/bindsight/stage/spew/broadcast.ex
  26. 0
    45
      sysd/bindsight/lib/bindsight/stage/spew/ripcord.ex
  27. 0
    64
      sysd/bindsight/lib/bindsight/stage/spew/spigot.ex
  28. 0
    29
      sysd/bindsight/lib/bindsight/stage/spewcounter.ex
  29. 0
    51
      sysd/bindsight/lib/bindsight/stage/spewsupervisor.ex
  30. 0
    53
      sysd/bindsight/lib/bindsight/webapi/error.ex
  31. 0
    106
      sysd/bindsight/lib/bindsight/webapi/frames.ex
  32. 0
    43
      sysd/bindsight/lib/bindsight/webapi/home.ex
  33. 0
    68
      sysd/bindsight/lib/bindsight/webapi/router.ex
  34. 0
    54
      sysd/bindsight/lib/bindsight/webapi/server.ex
  35. 0
    55
      sysd/bindsight/lib/bindsight/webapi/verify.ex
  36. 0
    56
      sysd/bindsight/mix.exs
  37. 0
    57
      sysd/bindsight/test/common_test.exs
  38. 0
    89
      sysd/bindsight/test/plug_test.exs
  39. 0
    76
      sysd/bindsight/test/slurp_test.exs
  40. 0
    92
      sysd/bindsight/test/spew_test.exs
  41. 0
    1
      sysd/bindsight/test/test_helper.exs

+ 1
- 0
README.md View File

@@ -47,6 +47,7 @@ For more discussion, see:
* Perl + Date::Manip
* ImageMagick
* [Elixir](https://elixir-lang.org/install.html)
* [BindSight](https://wiki.hackmanhattan.com/BindSight)

### API Credentials


+ 0
- 5
sysd/bindsight/.formatter.exs View File

@@ -1,5 +0,0 @@
# Used by "mix format"
[
line_length: 80,
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

+ 0
- 26
sysd/bindsight/.gitignore View File

@@ -1,26 +0,0 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
relay-*.tar

# Ignore locks
*.lock

+ 0
- 11
sysd/bindsight/README.md View File

@@ -1,11 +0,0 @@
# BindSight

_Concurrent frame-scrubbing webcam broadcast gateway daemon._

Devoted service to stream doorcam and spacecam to Bricodash and public
gateway, respectively, while tracking activity and performance of these
and other webcams at the space. Will be more efficient and reliable than
spawning PHP and Python processes on an as-they-come basis.

For documentation and project status,
see https://wiki.hackmanhattan.com/BindSight

+ 0
- 15
sysd/bindsight/config/config.exs View File

@@ -1,15 +0,0 @@
import Config

config :bindsight,
common_api: :mjpg_streamer,
cameras: %{
space: "http://wrtnode-webcam.lan:8080/",
door: "http://rfid-access-building.lan:8080/",
cr10: "http://octoprint-main.lan:8080/",
hydro: "http://hydrocontroller.lan:8081/"
},
cowboy_port: 2020

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"

+ 0
- 16
sysd/bindsight/config/dev.exs View File

@@ -1,16 +0,0 @@
import Config

config :bindsight,
common_api: :mjpg_streamer,
cameras: %{
test: {"http://rfid-access-building.lan:8080/", :mjpg_streamer}
},
cowboy_acceptors: 5,
cluck_errors: true,
register_shortnames: true

config :logger, :console,
format: "$time $metadata[$level] $levelpad$message\n",
compile_time_purge_matching: [
[level_lower_than: :info]
]

+ 0
- 6
sysd/bindsight/config/test.exs View File

@@ -1,6 +0,0 @@
import Config

config :bindsight,
ignore_noproc: true

import_config "dev.exs"

+ 0
- 40
sysd/bindsight/lib/bindsight.ex View File

@@ -1,40 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight do
@moduledoc "Concurrent frame-scrubbing webcam broadcast gateway daemon."

def start(_type, _args) do
Port.open({:spawn, "epmd -daemon"}, [:binary])
{:ok, hostname} = :inet.gethostname()

{:ok, _pid} =
[String.to_atom("bindsight@#{hostname}")]
|> :net_kernel.start()

children = [
{Registry, keys: :unique, name: Registry.BindSight},
BindSight.Stage.SloshSupervisor,
BindSight.Stage.SlurpSupervisor,
BindSight.WebAPI.Server,
BindSight.Stage.SpewCounter,
BindSight.Stage.SpewSupervisor
]

Supervisor.start_link(children, strategy: :one_for_one, restart: :permanent)
end
end

+ 0
- 40
sysd/bindsight/lib/bindsight/common/camera.ex View File

@@ -1,40 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.Common.Camera do
@moduledoc "API interface definitions for common camera and server types."

def build_request(uri, api, action) do
case api do
:mjpg_streamer -> mjpg_streamer(uri, action)
_ -> raise("#{uri}: unknown camera interface: " <> inspect(api))
end
end

def mjpg_streamer(uri, action) do
case action do
:snapshot -> uri |> query_put(:action, :snapshot)
:stream -> uri |> query_put(:action, :stream)
end
end

defp query_put(uri, key, value) do
query = if uri.query, do: uri.query, else: ""
query = query |> URI.decode_query(%{Atom.to_string(key) => value})
uri |> Map.put(:query, URI.encode_query(query))
end
end

+ 0
- 75
sysd/bindsight/lib/bindsight/common/library.ex View File

@@ -1,75 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.Common.Library do
@moduledoc "Commonly used functions are functions used commonly."

use Memoize

@doc "Retrieve camera URL by name from config."
def get_camera_url(name), do: name |> get_camera_info() |> elem(0)

@doc "Retrieve camera API by name from config."
def get_camera_api(name), do: name |> get_camera_info() |> elem(1)

defp get_camera_info(name) do
cameras = Application.fetch_env!(:bindsight, :cameras)

case cameras[name] do
{url, api} when is_binary(url) and is_atom(api) ->
{url, api}

url when is_binary(url) ->
{url, get_env(:common_api, :no_api_configured)}

input ->
raise ArgumentError,
"#{name} config expected string or string/atom tuple: " <>
inspect(input)
end
end

@doc "Return unique registerable name for process, shortname if dev/test."
def get_register_name(tup) when is_tuple(tup) do
list = tup |> Tuple.to_list()

if Enum.all?(list, fn x -> is_atom(x) end) do
list |> Enum.join(":") |> get_register_name()
else
{:via, Registry, {Registry.BindSight, tup}}
end
end

def get_register_name(name) do
if get_env(:register_shortnames, false) do
"#{name}" |> String.to_atom()
else
"bindsight_#{name}" |> String.to_atom()
end
end

@doc "Get config parameters for our application."
def get_env(key, default \\ nil) do
Application.get_env(:bindsight, key, default)
end

@doc "Return full path and query from a URI."
def query_path(uri), do: [uri.path, uri.query] |> Enum.join("?")

@doc "Return IOList interspersed with colons."
def error_chain(list), do: Enum.intersperse(list, ": ")
end

+ 0
- 150
sysd/bindsight/lib/bindsight/common/mintjulep.ex View File

@@ -1,150 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.Common.MintJulep do
@moduledoc "Poll to obtain MJPEG snapshots as Mint messages."

@type state() :: {mod :: atom(), uri :: %URI{}, conn :: :deferred | term()}

@type genstage_return() ::
{:noreply, [event :: term()], new_state :: state()}
| {:noreply, [event :: term()], new_state :: state(), :hibernate}
| {:stop, reason :: term(), new_state :: state()}

@callback handle_normal_info(msg :: :timeout | term(), state()) ::
genstage_return()

@callback handle_mint(resp :: [term()], state()) ::
genstage_return()

@optional_callbacks handle_normal_info: 2

defmacro __using__(_) do
quote([]) do
@behaviour BindSight.Common.MintJulep

use GenStage
require Logger

alias BindSight.Common.Library
alias BindSight.Common.MintJulep

@impl true
def handle_info(message, _state = {mod, uri, :deferred}),
do:
handle_info(message, _state = {mod, uri, MintJulep.connect(mod, uri)})

def handle_info(:unfold_deferred_state, state),
do: {:noreply, [], state}

def handle_info(message, state = {mod, uri, conn}) do
case Mint.HTTP.stream(conn, message) do
:unknown ->
apply(mod, :handle_normal_info, [message, state])

# HACK When mint reads cowboy stream, it never flushes data_buffer,
# but instead passes messages with empty response lists.
#
# This rule does what ought to be happening in
# mint.http1.collapse_body_buffer/2 (lines 761-770).
#
# Possibly related to cowboy glitch of appending charset to boundary
# in multipart content-type.

{:ok, conn, []} ->
buff = IO.iodata_to_binary(conn.request[:data_buffer])
conn = %{conn | request: Map.put(conn.request, :data_buffer, [])}

apply(mod, :handle_mint, [
[{:data, nil, buff}],
_state = {mod, uri, conn}
])

{:ok, conn, resp} ->
apply(mod, :handle_mint, [resp, _state = {mod, uri, conn}])

{:error, conn, err, resp} ->
{:noreply, resp,
_state = MintJulep.sip(_state = {mod, uri, conn}, :response, err)}
end
end

def handle_normal_info(resp, state = {mod, uri, conn}) do
["MintJulep", uri, "unknown info message", inspect(resp)]
|> Library.error_chain()
|> Logger.error()

{:noreply, [], state}
end

defoverridable handle_normal_info: 2
end
end

require Logger

alias BindSight.Common.Library

def start_link(mod, args, opts), do: GenStage.start_link(mod, args, opts)

@doc "Return state for polling cameras, logging error if any."
def sip(mod, uri = %URI{}) when is_atom(mod) do
send(self(), :unfold_deferred_state)
{mod, uri, :deferred}
end

def sip(_state = {mod, uri, conn}, call \\ nil, err \\ nil) do
if conn, do: Mint.HTTP.close(conn)

if err do
path = Library.query_path(uri)

[["Failed ", call], [uri.host, ":", uri.port, "/", path], inspect(err)]
|> Library.error_chain()
|> Logger.warn()

Process.sleep(1000)
end

sip(mod, uri)
end

@doc "Callback to connect to camera host and issue a request thereto."
def connect(mod, uri) do
case mint_connect(uri.scheme |> String.to_atom(), uri.host, uri.port) do
{:ok, conn} -> request(mod, uri, conn)
{:error, err} -> sip({mod, uri, nil}, :connect, err)
end
end

# Try ipv6 by default, but fail-over to ipv4 gracefully.
defp mint_connect(scheme, host, port) do
opts = [transport_opts: [{:tcp_module, :inet6_tcp}]]

case Mint.HTTP.connect(scheme, host, port, opts) do
{:ok, conn} -> {:ok, conn}
_ -> Mint.HTTP.connect(scheme, host, port, [])
end
end

defp request(mod, uri, conn) do
case Mint.HTTP.request(conn, "GET", Library.query_path(uri), []) do
{:ok, conn, _ref} -> conn
{:error, conn, err} -> sip({mod, uri, conn}, :request, err)
end
end
end

+ 0
- 63
sysd/bindsight/lib/bindsight/common/tasker.ex View File

@@ -1,63 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.Common.Tasker do
@moduledoc "Behavior for GenStage to launch a dedicated feeder task."

defmacro __using__(_) do
quote location: :keep do
@behaviour BindSight.Common.Tasker
alias BindSight.Common.Tasker
end
end

@callback perform_task(name :: atom, opts :: [key: term]) :: term

@defaults %{
tasks: :need_a_unique_name,
restart: :permanent
}

def start_task(mod, opts, name: name) do
%{tasks: tasks, restart: restart} = Enum.into(opts, @defaults)
fun = fn -> launch_task(mod, opts, name: name) end
Task.Supervisor.start_child(tasks, fun, restart: restart)
end

defp launch_task(mod, opts, name: name) do
register_task(name)
mod.perform_task(name, opts)
end

defp register_task(name, tries \\ 0) do
if Process.alive?(self()) or tries == 100 do
Process.register(self(), "#{name}:task" |> String.to_atom())
else
Process.sleep(10)
register_task(name, tries + 1)
end
end

def sync_notify(name, event, timeout \\ 5000, tries \\ 0) do
if Process.whereis(name) != nil or tries == 100 do
GenStage.call(name, {:notify, event}, timeout)
else
Process.sleep(10)
sync_notify(name, event, timeout, tries + 1)
end
end
end

+ 0
- 68
sysd/bindsight/lib/bindsight/stage/slosh/chunk.ex View File

@@ -1,68 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.Stage.Slosh.Chunk do
@moduledoc "Slurp consumer-producer to frames from chunks."

use GenStage
require Logger

@defaults %{source: :producer_not_specified, name: __MODULE__}

def start_link(opts \\ []) do
%{source: source, name: name} = Enum.into(opts, @defaults)
GenStage.start_link(__MODULE__, source, name: name)
end

def init(source) do
{:producer_consumer, [], subscribe_to: [source]}
end

def handle_events([head | tail], from, rlist) do
case head do
{:data, _ref, _data} -> handle_data([head | tail], from, rlist)
_ -> handle_events(tail, from, [head | rlist])
end
end

def handle_events([], _from, rlist) do
list = rlist |> Enum.reverse() |> adhere()
[hold | rlist] = Enum.reverse(list)
{:noreply, Enum.reverse(rlist), [hold]}
end

defp adhere([{:text, ref, x} | [{:text, ref, y} | tail]]),
do: adhere([{:text, ref, x <> y} | tail])

defp adhere([{:data, ref, x} | [{:data, ref, y} | tail]]),
do: adhere([{:data, ref, x <> y} | tail])

defp adhere([head | tail]), do: [head | adhere(tail)]
defp adhere([]), do: []

defp handle_data([{:data, ref, data} | tail], from, rlist) do
chunks =
Regex.split(~r/(?=[\r\n\-])/, data)
|> Enum.map(fn x -> Regex.split(~r/(?<=[\r\n\-])/, x) end)
|> List.flatten()
|> Enum.map(fn x ->
if String.printable?(x), do: {:text, ref, x}, else: {:data, ref, x}
end)

handle_events(tail, from, Enum.reverse(chunks) ++ rlist)
end
end

+ 0
- 148
sysd/bindsight/lib/bindsight/stage/slosh/digest.ex View File

@@ -1,148 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.Stage.Slosh.Digest do
@moduledoc "Slurp consumer-producer to frames from chunks."

use GenStage
require Logger

@defaults %{source: :producer_not_specified, name: __MODULE__}

defstruct status: nil,
bound: "Lorem ipsum dolor sit amet",
boundsize: nil,
eolex: nil,
eohex: nil,
data: [],
done: false,
frames: []

@bcharsnospace "[:alnum:]\\'\\(\\(\\+\\_\\,\\-\\.\\/\\:\\=\\?" # per RFC 1341
@boundary "[#{@bcharsnospace}]+|\"[\ #{@bcharsnospace}]+\""

alias BindSight.Stage.Slosh.Digest

def start_link(opts \\ []) do
%{source: source, name: name} = Enum.into(opts, @defaults)
GenStage.start_link(__MODULE__, source, name: name)
end

def init(source) do
{:producer_consumer, _state = %Digest{}, subscribe_to: [source]}
end

def handle_events([head | tail], from, state = %Digest{}) do
status = state.status
boundsize = state.boundsize

case head do
{:status, _ref, status} ->
handle_events(tail, from, _state = %{state | status: status})

{:headers, _ref, hdrs} when status == 200 ->
handle_headers(hdrs, tail, from, state)

{:headers, _ref, hdrs} ->
Logger.warn("Request failed: #{state.status}: " <> inspect(hdrs))
handle_events(tail, from, state)

{:frame_headers, _ref, _hdrs} ->
frame = state.data |> Enum.reverse() |> Enum.join()
frames = if frame == "", do: state.frames, else: [frame | state.frames]

handle_events(tail, from, _state = %{state | frames: frames, data: []})

{:text, _ref, text} when byte_size(text) > boundsize ->
handle_text([head | tail], from, state)

{:text, ref, text} ->
handle_events([{:data, ref, text} | tail], from, state)

{:data, _ref, ""} ->
handle_events(tail, from, state)

{:data, _ref, data} ->
handle_events(tail, from, _state = %{state | data: [data | state.data]})

{:done, _ref} ->
handle_events(tail, from, _state = %{state | done: true})
end
end

def handle_events([], _from, state = %Digest{}) do
cond do
state.done and state.status == 200 ->
{:noreply, [state.data], _state = %Digest{}}

state.done ->
{:noreply, [], _state = %Digest{}}

state.frames ->
{:noreply, Enum.reverse(state.frames),
_state = %Digest{state | frames: []}}

true ->
{:noreply, [], state}
end
end

defp handle_headers([], events, from, state),
do: handle_events(events, from, state)

defp handle_headers([{"content-type", ctype} | tail], events, from, state) do
{:ok, pattern} = Regex.compile(";boundary=(?<bound>#{@boundary})")
bound = "--" <> Regex.named_captures(pattern, ctype)["bound"]

boundsize = byte_size(bound)
state = %Digest{state | bound: bound, boundsize: boundsize}

handle_headers(tail, events, from, state)
end

defp handle_headers([_head | tail], events, from, state),
do: handle_headers(tail, events, from, state)

defp handle_text([head = {:text, ref, text} | tail], from, state) do
if String.contains?(text, state.bound) do
strip_boundary(head, tail, from, state)
else
handle_events([{:data, ref, text} | tail], from, state)
end
end

defp strip_boundary({:text, ref, text}, tail, from, state) do
[pre, post] = String.split(text, state.bound, parts: 2)

state = if state.eohex == nil, do: determine_eol(text, state), else: state

[headers, post] = Regex.split(state.eohex, post, parts: 2)

events =
[{:data, ref, pre}, {:frame_headers, ref, headers}, {:data, ref, post}] ++
tail

handle_events(events, from, state)
end

defp determine_eol(text, state) do
[eol] = Regex.run(~r/[\n\r]+/, text, parts: 1)
{:ok, eolex} = Regex.compile(eol)
{:ok, eohex} = Regex.compile(eol <> eol)
%Digest{state | eolex: eolex, eohex: eohex}
end
end

+ 0
- 72
sysd/bindsight/lib/bindsight/stage/slosh/request.ex View File

@@ -1,72 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.Stage.Slosh.Request do
@moduledoc "Slurp spigot producer to request snapshots from a camera."

use BindSight.Common.MintJulep

alias BindSight.Common.Camera
alias BindSight.Common.Library

@defaults %{camera: :test, name: __MODULE__, url: nil}

def start_link(opts \\ []) do
%{name: name} = Enum.into(opts, @defaults)
MintJulep.start_link(__MODULE__, opts, name: name)
end

@impl true
def init(opts) do
%{camera: camera, url: url} = Enum.into(opts, @defaults)

uri =
if url do
url |> URI.parse()
else
camera
|> Library.get_camera_url()
|> URI.parse()
|> Camera.build_request(Library.get_camera_api(camera), :stream)
end

{:producer, _state = MintJulep.sip(__MODULE__, uri)}
end

@impl true
def handle_mint(resp, state) do
if Enum.reduce(resp, nil, fn x, accu -> find_done(x, accu) end) do
# because snap
Process.sleep(100)
{:noreply, resp, _state = MintJulep.sip(state)}
else
{:noreply, resp, state}
end
end

defp find_done(x, accu) do
case x do
{:done, _ref} -> true
_ -> accu
end
end

@impl true
def handle_demand(_demand, state) do
{:noreply, [], state}
end
end

+ 0
- 64
sysd/bindsight/lib/bindsight/stage/slosh/spigot.ex View File

@@ -1,64 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.Stage.Slosh.Spigot do
@moduledoc "GenStage pipeline segment for processing a single camera feed."

use Supervisor

alias BindSight.Common.Library

@defaults %{camera: :test, url: nil}

def start_link(opts \\ []) do
%{camera: camera} = Enum.into(opts, @defaults)

Supervisor.start_link(__MODULE__, opts,
name: name({:spigot, :slosh, camera})
)
end

@impl true
def init(opts) do
%{camera: camera, url: url} = Enum.into(opts, @defaults)

children = [
{BindSight.Stage.Slosh.Request,
[
camera: camera,
url: url,
name: name({:request, camera})
]},
{BindSight.Stage.Slosh.Chunk,
[
source: name({:request, camera}),
name: name({:chunk, camera})
]},
{BindSight.Stage.Slosh.Digest,
[
source: name({:chunk, camera}),
name: name({:digest, camera})
]}
]

Supervisor.init(children, strategy: :rest_for_one)
end

def tap(camera), do: name({:digest, camera})

defp name(tup), do: Library.get_register_name(tup)
end

+ 0
- 48
sysd/bindsight/lib/bindsight/stage/sloshsupervisor.ex View File

@@ -1,48 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.Stage.SloshSupervisor do
@moduledoc "Supervise slurp spigot (camera pipeline segment) for each camera."

use Supervisor
require Logger

alias BindSight.Common.Library

def start_link(_opts) do
Logger.info("Sloshing streams...")

Supervisor.start_link(__MODULE__, nil,
name: Library.get_register_name(:sloshsup)
)
end

@impl true
def init(_) do
children =
Library.get_env(:cameras)
|> Map.keys()
|> Enum.map(fn x -> speccer(x) end)

Supervisor.init(children, strategy: :one_for_one)
end

defp speccer(camera) do
opts = %{camera: camera}
Supervisor.child_spec({BindSight.Stage.Slosh.Spigot, opts}, id: camera)
end
end

+ 0
- 76
sysd/bindsight/lib/bindsight/stage/slurp/batch.ex View File

@@ -1,76 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.Stage.Slurp.Batch do
@moduledoc "Consumer-producer to render events in batches. Assumes max_demand = 2."

use GenStage
use BindSight.Common.Tasker
require Logger

@defaults %{source: :producer_not_specified, camera: :test, name: __MODULE__}

def start_link(opts \\ []) do
%{name: name} = Enum.into(opts, @defaults)
GenStage.start_link(__MODULE__, opts, name: name)
Tasker.start_task(__MODULE__, opts, name: name)
end

def init(opts) do
%{camera: camera} = Enum.into(opts, @defaults)
{:producer, {camera, Okasaki.Queue.new(), 0}}
end

def perform_task(name, opts) do
%{source: source} = Enum.into(opts, @defaults)

[source]
|> GenStage.stream()
|> Enum.map(fn x -> Tasker.sync_notify(name, x) end)
end

def handle_call({:notify, event}, _from, {camera, queue, pending}) do
queue = Okasaki.Queue.insert(queue, event)
{:noreply, batch, state} = handle_demand(0, {camera, queue, pending})
{:reply, :ok, batch, state}
end

def handle_demand(demand, {camera, queue, pending}) do
demand = demand + pending
onhold = Okasaki.Queue.size(queue)

if demand == 0 or onhold == 0 do
{:noreply, [], {camera, queue, demand}}
else
size = min(demand, onhold)
{batch, queue} = assemble_batch(queue, size, [])

if size > 1 do
Logger.info(["Batch of ", size, " frames dispatched from ", camera])
end

{:noreply, [{:batch, batch}], {camera, queue, demand - size}}
end
end

defp assemble_batch(queue, 0, batch), do: {Enum.reverse(batch), queue}

defp assemble_batch(queue, size, batch) do
{:ok, {item, queue}} = Okasaki.Queue.remove(queue)
assemble_batch(queue, size - 1, [item | batch])
end
end

+ 0
- 40
sysd/bindsight/lib/bindsight/stage/slurp/broadcast.ex View File

@@ -1,40 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.Stage.Slurp.Broadcast do
@moduledoc "Slurp spigot endpoint to serve arbitrary number of clients."

use GenStage

@defaults %{source: :producer_not_specified, name: __MODULE__}

def start_link(opts \\ []) do
%{source: source, name: name} = Enum.into(opts, @defaults)
GenStage.start_link(__MODULE__, source, name: name)
end

def init(source) do
dispatch = GenStage.BroadcastDispatcher

{:producer_consumer, :stateless,
subscribe_to: [source], dispatcher: dispatch}
end

def handle_events(events, _from, :stateless) do
{:noreply, events, :stateless}
end
end

+ 0
- 42
sysd/bindsight/lib/bindsight/stage/slurp/flushsnoop.ex View File

@@ -1,42 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.Stage.Slurp.FlushSnoop do
@moduledoc "Slurp snoop to keep camera feed flushing even when no spew clients."

use GenStage

alias BindSight.Common.Library

@defaults %{source: :producer_not_specified, name: __MODULE__}

def start_link(opts \\ []) do
%{camera: camera, source: source} = Enum.into(opts, @defaults)

GenStage.start_link(__MODULE__, source,
name: Library.get_register_name({:flush, camera, :snoop})
)
end

def init(source) do
{:consumer, :stateless, subscribe_to: [source]}
end

def handle_events(_events, _from, :stateless) do
{:noreply, [], :stateless}
end
end

+ 0
- 67
sysd/bindsight/lib/bindsight/stage/slurp/spigot.ex View File

@@ -1,67 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.Stage.Slurp.Spigot do
@moduledoc "GenStage pipeline segment for processing a single camera feed."

use Supervisor

alias BindSight.Common.Library

def start_link(camera \\ :test) do
Supervisor.start_link(__MODULE__, camera,
name: name({:spigot, :slurp, camera})
)
end

@impl true
def init(camera) do
children = [
{Task.Supervisor,
name: name({:tasks, :slurp, camera}), strategy: :one_for_one},
{BindSight.Stage.Slurp.Batch,
[
source: BindSight.Stage.Slosh.Spigot.tap(camera),
camera: camera,
name: name({:batch, camera}),
tasks: name({:tasks, :slurp, camera})
]},
{BindSight.Stage.Slurp.Validate,
[
source: {name({:batch, camera}), max_demand: 2},
camera: camera,
name: name({:validate, camera})
]},
{BindSight.Stage.Slurp.Broadcast,
[source: name({:validate, camera}), name: name({:broadcast, camera})]},
{BindSight.Stage.SnoopSupervisor,
[
source: name({:broadcast, camera}),
camera: camera,
name: name({:snoops, camera}),
always: [BindSight.Stage.Slurp.FlushSnoop],
config: :slurp_snoops
]}
]

Supervisor.init(children, strategy: :rest_for_one)
end

def tap(camera), do: name({:broadcast, camera})

defp name(tup), do: Library.get_register_name(tup)
end

+ 0
- 68
sysd/bindsight/lib/bindsight/stage/slurp/validate.ex View File

@@ -1,68 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.Stage.Slurp.Validate do
@moduledoc """
Slurp spigot consumer-producer to check for corrupt and greytoss frames.
"""

use GenStage
require Logger

@defaults %{source: :producer_not_specified, camera: :test, name: __MODULE__}

def start_link(opts \\ []) do
%{name: name} = Enum.into(opts, @defaults)
GenStage.start_link(__MODULE__, opts, name: name)
end

def init(opts) do
%{source: source, camera: camera} = Enum.into(opts, @defaults)
{:producer_consumer, camera, subscribe_to: [source]}
end

def handle_events([{:batch, batch}], from, camera) do
handle_events(batch, from, camera)
end

def handle_events(events, _from, camera) do
fun = fn x -> validate_frame(x) == :ok end
checks = events |> Task.async_stream(fun) |> Enum.map(fn {:ok, x} -> x end)

good =
events
|> Enum.zip(checks)
|> Enum.map(fn {evt, chk} -> if chk, do: evt end)
|> Enum.filter(fn x -> x != nil end)

discard = length(events) - length(good)

if discard > 0 do
Logger.warn(["Discarding ", discard, " bad frames from ", camera])
end

{:noreply, good, camera}
end

@doc "Confirm binary is valid JPEG."
def validate_frame(binary) do
case ExImageInfo.info(binary) do
{"image/jpeg", _, _, _} -> :ok
_ -> :corrupt_frame
end
end
end

+ 0
- 47
sysd/bindsight/lib/bindsight/stage/slurpsupervisor.ex View File

@@ -1,47 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.Stage.SlurpSupervisor do
@moduledoc "Supervise slurp spigot (camera pipeline segment) for each camera."

use Supervisor
require Logger

alias BindSight.Common.Library

def start_link(_opts) do
Logger.info("Slurping cameras...")

Supervisor.start_link(__MODULE__, nil,
name: Library.get_register_name(:slurpsup)
)
end

@impl true
def init(_) do
children =
Library.get_env(:cameras)
|> Map.keys()
|> Enum.map(fn x -> speccer(x) end)

Supervisor.init(children, strategy: :one_for_one)
end

defp speccer(camera) do
Supervisor.child_spec({BindSight.Stage.Slurp.Spigot, camera}, id: camera)
end
end

+ 0
- 46
sysd/bindsight/lib/bindsight/stage/snoopsupervisor.ex View File

@@ -1,46 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.Stage.SnoopSupervisor do
@moduledoc "Supervise configured snoops on either slurp or spew spigot."

use Supervisor

alias BindSight.Common.Library

@defaults %{source: :producer_not_specified, name: __MODULE__, always: []}

def start_link(opts) do
%{name: name} = Enum.into(opts, @defaults)
Supervisor.start_link(__MODULE__, opts, name: name)
end

@impl true
def init(opts) do
%{always: always, config: config} = Enum.into(opts, @defaults)

children =
(always ++ Library.get_env(config, []))
|> Enum.map(fn x -> speccer(x, opts) end)

Supervisor.init(children, strategy: :one_for_one)
end

defp speccer(snoop, opts) do
Supervisor.child_spec({snoop, opts}, [])
end
end

+ 0
- 37
sysd/bindsight/lib/bindsight/stage/spew/broadcast.ex View File

@@ -1,37 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.Stage.Spew.Broadcast do
@moduledoc "Spew spigot endpoint that terminates spigot on unsubscription."

use GenStage

@defaults %{source: :producer_not_specified, name: __MODULE__}

def start_link(opts \\ []) do
%{source: source, name: name} = Enum.into(opts, @defaults)
GenStage.start_link(__MODULE__, source, name: name)
end

def init(source) do
{:producer_consumer, :stateless, subscribe_to: [source]}
end

def handle_events(events, _from, :stateless) do
{:noreply, events, :stateless}
end
end

+ 0
- 45
sysd/bindsight/lib/bindsight/stage/spew/ripcord.ex View File

@@ -1,45 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.Stage.Spew.Ripcord do
@moduledoc "Spew spigot startpoint to serve client and arbitrary number of snoops."

use GenStage, restart: :temporary

@defaults %{source: :producer_not_specified, name: __MODULE__}

def start_link(opts \\ []) do
%{name: name} = Enum.into(opts, @defaults)
GenStage.start_link(__MODULE__, opts, name: name)
end

def init(opts) do
%{source: source, spigot: spigot} = Enum.into(opts, @defaults)
dispatch = GenStage.BroadcastDispatcher

{:producer_consumer, spigot, subscribe_to: [source], dispatcher: dispatch}
end

def handle_events(events, _from, spigot) do
{:noreply, events, spigot}
end

def handle_cancel(_reason, _from, spigot) do
Supervisor.stop(spigot)
{:stop, :normal, :stateless}
end
end

+ 0
- 64
sysd/bindsight/lib/bindsight/stage/spew/spigot.ex View File

@@ -1,64 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.Stage.Spew.Spigot do
@moduledoc "GenStage pipeline segment for processing a single client request."

use Supervisor, restart: :temporary

alias BindSight.Common.Library
alias BindSight.Stage.Slurp.Spigot

@defaults %{camera: :test, session: -1}

def start_link(opts) do
%{session: session} = Enum.into(opts, @defaults)
spigot = {:spigot, :spew, session}
opts = [spigot: spigot] ++ opts
Supervisor.start_link(__MODULE__, opts, name: name(spigot))
end

@impl true
def init(opts) do
%{camera: camera, spigot: spigot} = Enum.into(opts, @defaults)

children = [
{BindSight.Stage.Spew.Broadcast,
[source: Spigot.tap(camera), name: name({:broadcast, spigot})]},
{BindSight.Stage.Spew.Ripcord,
[
source: name({:broadcast, spigot}),
name: name({:ripcord, spigot}),
spigot: name(spigot)
]},
{BindSight.Stage.SnoopSupervisor,
[
source: name({:broadcast, spigot}),
name: name({:snoops, spigot}),
config: :spew_snoops
] ++
opts}
]

Supervisor.init(children, strategy: :rest_for_one)
end

# name(:broadcast, camera)
def tap(session), do: name({:ripcord, {:spigot, :spew, session}})

defp name(tup), do: Library.get_register_name(tup)
end

+ 0
- 29
sysd/bindsight/lib/bindsight/stage/spewcounter.ex View File

@@ -1,29 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.Stage.SpewCounter do
@moduledoc "Simple counter to track "
use Agent

def start_link(_opts) do
Agent.start_link(fn -> 0 end, name: __MODULE__)
end

def next do
Agent.get_and_update(__MODULE__, fn x -> {x + 1, x + 1} end)
end
end

+ 0
- 51
sysd/bindsight/lib/bindsight/stage/spewsupervisor.ex View File

@@ -1,51 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.Stage.SpewSupervisor do
@moduledoc "Supervise spew spigot (client pipeline segment) for each client."

use DynamicSupervisor
require Logger

alias BindSight.Stage.Spew.Spigot
alias BindSight.Stage.SpewCounter

@defaults %{camera: :test}

def start_link(_) do
Logger.info("Ready to spew clients...")
DynamicSupervisor.start_link(__MODULE__, [], name: :spewsup)
end

@impl true
def init(_) do
DynamicSupervisor.init(strategy: :one_for_one)
end

def start_session(opts) do
%{camera: camera} = Enum.into(opts, @defaults)
session = SpewCounter.next()

{:ok, _pid} =
DynamicSupervisor.start_child(
:spewsup,
{Spigot, camera: camera, session: session}
)

session
end
end

+ 0
- 53
sysd/bindsight/lib/bindsight/webapi/error.ex View File

@@ -1,53 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.WebAPI.Error do
@moduledoc "Plug for generic 500 status error message."

import Plug.Conn
import Phoenix.HTML
import Phoenix.HTML.Tag

alias BindSight.Common.Library

@error_msg "Ditty's gone catawampous, it has."

def send(conn, opts) do
error =
if Library.get_env(:cluck_errors) do
IO.puts(cluck(opts))
body = [content_tag(:h1, @error_msg), content_tag(:pre, cluck(opts))]
content_tag(:body, body) |> safe_to_string
else
@error_msg
end

send_resp(conn, conn.status, error)
end

defp cluck(opts) do
kind = spect(opts, :kind)
reason = spect(opts, :reason)
stack = spect(opts, :stack)
"#{kind}\n#{reason}\n#{stack}\n"
end

defp spect(opts, label) do
str = inspect(opts[label], label: label, pretty: true, limit: 7)
"#{label}: #{str}"
end
end

+ 0
- 106
sysd/bindsight/lib/bindsight/webapi/frames.ex View File

@@ -1,106 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.WebAPI.Frames do
@moduledoc "Plug to deliver snapshots and streams to client."

import Plug.Conn

alias BindSight.Stage.Spew.Spigot
alias BindSight.Stage.SpewSupervisor

@defaults %{camera: :test, action: :snapshot}
@boundary "SNAP-HACKLE-STOP"

def send(conn, opts) do
%{camera: camera, action: action} = Enum.into(opts, @defaults)

case action do
:snapshot -> send_snapshot(camera, conn)
:stream -> send_stream(camera, conn)
end
end

defp send_snapshot(camera, conn) do
[frame | _] = camera |> get_stream |> Enum.take(1)

conn
|> put_resp_content_type("image/jpg")
|> send_resp(200, frame)
end

defp send_stream(camera, conn) do
stream = get_stream(camera)

contype = "multipart/x-mixed-replace;boundary=#{@boundary}"

conn =
conn
|> put_resp_header("connection", "close")
|> put_resp_content_type(contype)
|> send_chunked(200)

stream |> Stream.map(fn x -> send_frame(conn, x) end) |> Stream.run()
conn
end

defp send_frame(conn, frame) do
time = System.os_time()

# HACK Cowboy manditorally appends any content-type header with a
# charset encoding, even if data is entirely binary. This means that
# in cases of multipart content-types, the boundary assignment is followed
# by a semicolor and then the charset designation.
#
# Mint expects boundary to always continue to end of line, and thus takes
# up "; charset=utf-8" as part of the boundary line. Thus, we must
# include this appended suffix when requesting multipart content-types
# from a cowboy server, or mint will hang indefinitely.
#
# Inclusion of charset is possibly RFC-compliance issue. However, cowboy
# will append even if charset already specified earlier on line.
#
# OTOH, Chrome successfully processes multipart content-type without
# being confused by cowboy's appending charset after boundary, so perhaps
# Mint is misbehaving by not using semicolon as delimiter therefor.
#
# That said, we expect boundary to be followed by end of line when marking
# next part's header block, and thus if Mint is misbehaving by expecting
# boundary to terminate the content-type line, we are likewise misbehaving
# by expecting boundary to terminate the pre-header line.

headers =
[
"",
"--#{@boundary}",
"X-Timestamp: #{time}",
"Content-Type: image/jpg",
"\n"
]
|> Enum.join("\n")

{:ok, conn} = chunk(conn, headers)
{:ok, _conn} = chunk(conn, frame)
end

defp get_stream(camera) do
camera = camera |> String.to_existing_atom()
session = SpewSupervisor.start_session(camera: camera)

[{Spigot.tap(session), mad_demand: 1}] |> GenStage.stream()
end
end

+ 0
- 43
sysd/bindsight/lib/bindsight/webapi/home.ex View File

@@ -1,43 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.WebAPI.Home do
@moduledoc "Plug for BindSight WebAPI service home page."

import Plug.Conn
import Phoenix.HTML
import Phoenix.HTML.Tag
use Memoize

def send(conn, opts) do
divs = links(opts[:cameras])
body = content_tag(:body, [content_tag(:h1, "Howdy!"), divs])
conn |> send_resp(200, body |> safe_to_string)
end

defp links([]), do: []

defp links([head | tail]) do
html = [camera(head), rest(head, :snapshot), rest(head, :stream), tag(:hr)]
[html, links(tail)]
end

defp spacer, do: raw(" &mdash; ")
defp camera(text), do: content_tag(:h2, text, style: "display:inline")
defp rest(cam, act), do: [spacer(), anchor(act, "#{cam}/#{act}")]
defp anchor(text, url), do: content_tag(:a, text, href: url)
end

+ 0
- 68
sysd/bindsight/lib/bindsight/webapi/router.ex View File

@@ -1,68 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.WebAPI.Router do
@moduledoc "Router for BindSight WebAPI."

use Plug.Router
use Plug.ErrorHandler

alias BindSight.Common.Library
alias BindSight.WebAPI.Error
alias BindSight.WebAPI.Frames
alias BindSight.WebAPI.Home

@cameras Library.get_env(:cameras) |> Map.keys()

plug(BindSight.WebAPI.Verify, cameras: @cameras, actions: [:snapshot, :stream])

plug(:match)
plug(:dispatch)

get "/" do
Home.send(conn, cameras: @cameras)
end

get "/:camera/snapshot" do
Frames.send(aggressive_nocache(conn), camera: camera, action: :snapshot)
end

get "/:camera/stream" do
Frames.send(aggressive_nocache(conn), camera: camera, action: :stream)
end

match _ do
send_resp(conn, 404, "Don't take any wooden nickels.")
end

defp handle_errors(conn, %{kind: kind, reason: reason, stack: stack}) do
Error.send(conn, kind: kind, reason: reason, stack: stack)
end

# Note, cache-control defaults to "max-age=0, private, must-revalidate",
# per https://hexdocs.pm/plug/Plug.Conn.html
# But we want to be sure older, non-compliant browsers behave themselves.
defp aggressive_nocache(conn) do
cache =
"no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0"

conn
|> put_resp_header("cache-control", cache)
|> put_resp_header("expires", "Thu, 9 Sep 1999 0:0:00 GMT")
|> put_resp_header("pragma", "no-cache")
end
end

+ 0
- 54
sysd/bindsight/lib/bindsight/webapi/server.ex View File

@@ -1,54 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.WebAPI.Server do
@moduledoc "Cowboy server instance for BindSight WebAPI."

use Supervisor
require Logger

alias BindSight.Common.Library

def start_link(_) do
Logger.info("Yeehaw cowboy...")

Supervisor.start_link(__MODULE__, nil,
name: Library.get_register_name(:wapisup)
)
end

def init(_) do
port = Library.get_env(:cowboy_port, 2020)
acceptors = Library.get_env(:cowboy_acceptors, 100)

transport = [num_acceptors: acceptors]
protocol = [idle_timeout: :infinity]

children = [
{Plug.Cowboy,
scheme: :http,
plug: BindSight.WebAPI.Router,
options: [
port: port,
transport_options: transport,
protocol_options: protocol
]}
]

Supervisor.init(children, strategy: :one_for_one)
end
end

+ 0
- 55
sysd/bindsight/lib/bindsight/webapi/verify.ex View File

@@ -1,55 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.WebAPI.Verify do
@moduledoc "Plug to check syntax and terms of WebAPI requests."

import Plug.Conn

def init(options), do: options

def call(%Plug.Conn{request_path: path} = conn, opts) do
components =
path
|> String.trim("/")
|> String.split("/")
|> Enum.map(fn x -> atomize(x) end)

verify_request(conn, opts[:cameras], opts[:actions], components)
end

defp atomize(s) do
String.to_existing_atom(s)
rescue
_ in ArgumentError -> nil
end

defp verify_request(conn, _, _, comps) when length(comps) != 2, do: conn

defp verify_request(conn, cameras, actions, [cam, act]) do
cond do
cam not in cameras ->
conn |> send_resp(418, "That dog won't hunt (unknown camera).") |> halt

act not in actions ->
conn |> send_resp(400, "All down but nine (unknown action).") |> halt

true ->
conn
end
end
end

+ 0
- 56
sysd/bindsight/mix.exs View File

@@ -1,56 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSight.MixProject do
@moduledoc "Bricodash project file."
use Mix.Project

def project do
[
app: :bindsight,
version: "0.1.0",
elixir: "~> 1.9",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end

# Run "mix help compile.app" to learn about applications.
def application do
[
mod: {BindSight, []},
extra_applications: [:logger]
]
end

# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:credo, "~> 1.0.0", only: [:dev, :test], runtime: false},
{:dialyxir, "~> 0.5", only: [:dev], runtime: false},
{:castore, "~> 0.1.0"},
{:mint, "~> 0.2.0"},
{:ex_image_info, "~> 0.2.4"},
{:cowboy, "~> 2.6.3"},
{:plug_cowboy, "~> 2.1.0"},
{:phoenix_html, "~> 2.13.3"},
{:memoize, "~> 1.2"},
{:gen_stage, "~> 0.11"},
{:okasaki, "~> 1.0.0"},
]
end
end

+ 0
- 57
sysd/bindsight/test/common_test.exs View File

@@ -1,57 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule BindSightTest do
@moduledoc "Test basic utility functions used by other modules."

use ExUnit.Case
require Logger

doctest BindSight
doctest BindSight.Common.Camera
doctest BindSight.Common.Library
doctest BindSight.Common.MintJulep
doctest BindSight.Common.Tasker

alias BindSight.Common.Library
alias BindSight.Stage.Slurp.Validate

test "get :test camera path" do
assert Library.get_camera_url(:test) ==
"http://rfid-access-building.lan:8080/"
end

test "grab and validate a request/digest snapshot" do
[data | _] =
[:"digest:test"]
|> GenStage.stream()
|> Enum.take(1)

assert Validate.validate_frame(data) == :ok
end

test "grab and validate multiple request/digest snapshots" do
data =
[:"digest:test"]
|> GenStage.stream()
|> Enum.take(3)

assert Validate.validate_frame(Enum.at(data, 0)) == :ok
assert Validate.validate_frame(Enum.at(data, 1)) == :ok
assert Validate.validate_frame(Enum.at(data, 2)) == :ok
end
end

+ 0
- 89
sysd/bindsight/test/plug_test.exs View File

@@ -1,89 +0,0 @@
####
## Copyright © 2019 Beads Land-Trujillo.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU Affero General Public License as published
## by the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU Affero General Public License for more details.
##
## You should have received a copy of the GNU Affero General Public License
## along with this program. If not, see <https://www.gnu.org/licenses/>.
####

defmodule PlugTest do
@moduledoc "Test cowboy plug functionality."

use ExUnit.Case
use Plug.Test

doctest BindSight.WebAPI.Server
doctest BindSight.WebAPI.Router
doctest BindSight.WebAPI.Verify
doctest BindSight.WebAPI.Error
doctest BindSight.WebAPI.Home
doctest BindSight.WebAPI.Frames

alias BindSight.Common.Library
alias BindSight.Stage.Slosh.Spigot
alias BindSight.Stage.Slurp.Validate
alias BindSight.WebAPI.Router

@opts Router.init([])

test "returns home page" do
conn = :get |> conn("/", "") |> Router.call(@opts)

assert conn.state == :sent
assert conn.status == 200