Packages

SpaceEx allows you to write Elixir code to control virtual rockets in Kerbal Space Program, by connecting to the kRPC mod.

Current section

Files

Jump to
space_ex lib space_ex stream.ex
Raw

lib/space_ex/stream.ex

defmodule SpaceEx.Stream do
use GenServer
alias SpaceEx.Stream
@moduledoc """
Enables data to be streamed as it changes, instead of polled repeatedly.
Streams are an efficient way to repeatedly retrieve data from the game.
To set up a stream, you can use `SpaceEx.Stream.stream/2`.
Alternatively, you can create `SpaceEx.Procedure` that references the
procedure you want to run, and then pass that to `SpaceEx.Stream.create/2`.
In most situations where you find yourself asking for the same piece of data
over and over, you're probably better off using a stream. This will reduce
the load on both ends and make your code run faster.
However, if your code checks the value infrequently (e.g. once per second or
less), but the value is changing constantly (altitude, current time, etc.),
you should consider either using polling, or reducing the stream's rate.
Example usage:
```elixir
require SpaceEx.Procedure
stream =
SpaceEx.SpaceCenter.get_ut(conn)
|> SpaceEx.Procedure.call
|> SpaceEx.Stream.create(start: true)
SpaceEx.Stream.get(stream) # 83689.09043863538
Process.sleep(100)
SpaceEx.Stream.get(stream) # 83689.1904386354
Process.sleep(100)
SpaceEx.Stream.get(stream) # 83689.29043863542
# You can also create a nifty "magic variable" shorthand:
ut = SpaceEx.Stream.get_fn(stream)
ut.() # 83689.29043863544
Process.sleep(100)
ut.() # 83689.39043863546
Process.sleep(100)
ut.() # 83689.49043863548
# You can even create both the stream and the shortcut at once:
{stream, ut} =
SpaceEx.SpaceCenter.get_ut(conn)
|> SpaceEx.Stream.stream
SpaceEx.Stream.get(stream) # 83689.09043863540
ut.() # 83689.49043863541
```
"""
defmodule State do
@moduledoc false
@enforce_keys [:id, :conn]
defstruct(
id: nil,
conn: nil,
result: nil,
waitlist: [],
)
end
@enforce_keys [:id, :conn, :pid, :decoder]
defstruct(
id: nil,
conn: nil,
pid: nil,
decoder: nil,
)
@doc """
Creates a stream, and optionally starts it.
`procedure` should be a `SpaceEx.Procedure` structure. The stream's value
will be the result of calling this procedure over and over (with the same
arguments each time).
See the module documentation for usage examples.
## Options
* `:start` — when `false`, the stream is created, but not started. Default: `start: true`.
* `:rate` — the stream's update rate, in updates per second. Default: unlimited.
"""
def create(procedure, opts \\ []) do
conn = procedure.conn
start = opts[:start] || true
{:ok, stream_obj} = SpaceEx.KRPC.add_stream(conn, procedure, start)
stream_id = stream_obj.id
if rate = opts[:rate] do
SpaceEx.KRPC.set_stream_rate(conn, stream_id, rate)
end
{:ok, pid} = start_link(conn, stream_id)
SpaceEx.StreamConnection.register_stream(conn, stream_id, pid)
decoder = fn value ->
procedure.module.rpc_decode_return_value(procedure.function, value)
end
%Stream{id: stream_id, conn: conn, pid: pid, decoder: decoder}
end
@doc """
Creates a stream directly from function call syntax.
This is equivalent to calling `SpaceEx.Procedure.create/1` and `SpaceEx.Stream.create/2`.
## Example
```elixir
stream =
SpaceEx.SpaceCenter.Flight.get_mean_altitude(conn, flight)
|> SpaceEx.Stream.create
SpaceEx.Stream.get(stream) # 76.64177794696297
```
"""
defmacro stream(function_call, opts \\ []) do
quote do
require SpaceEx.Procedure
SpaceEx.Procedure.create(unquote(function_call))
|> SpaceEx.Stream.create(unquote(opts))
end
end
@doc """
Creates a stream and query function directly from function call syntax.
This is equivalent to calling `SpaceEx.Procedure.create/1`,
`SpaceEx.Stream.create/2`, and `SpaceEx.Stream.get_fn/1`, all in sequence.
Returns a tuple containing the stream and the getter function.
## Example
```elixir
{stream, altitude} =
SpaceEx.SpaceCenter.Flight.get_mean_altitude(conn, flight)
|> SpaceEx.Stream.stream_fn
altitude.() |> IO.inspect # 76.64177794696297
```
"""
defmacro stream_fn(function_call, opts \\ []) do
quote do
stream = SpaceEx.Stream.stream(unquote(function_call), unquote(opts))
{stream, SpaceEx.Stream.get_fn(stream)}
end
end
@doc """
Returns an anonymous function that can be used to query the stream.
This function can make code cleaner, by emulating a sort of "magic variable"
that constantly updates itself to the current value. For example, if you
assign `apoapsis = SpaceEx.Stream.get_fn(apo_stream)`, you can now use
`apoapsis.()` to get the up-to-date value at any time.
"""
def get_fn(stream) do
fn -> get(stream) end
end
@doc """
Get (and decode) the current value from a stream.
This will retrieve the latest stream value and decode it. (Because streams
can receive hundreds of updates every second, stream values are not decoded
until requested.)
Note that if a stream has not received any data yet, this function will block
for up to `timeout` milliseconds (default: 5 seconds) for the first
value to arrive.
A timeout usually indicates that the stream was created with `start: false`,
and it was not subsequently started before the timeout expired.
"""
def get(stream, timeout \\ 5000) do
result = GenServer.call(stream.pid, :get, timeout)
if result.error do
raise result.error.description
else
stream.decoder.(result.value)
end
end
@doc """
Set the update rate of a stream.
`rate` is the number of updates per second. Setting the rate to `0` or `nil`
will remove all rate limiting and update as often as possible.
"""
def set_rate(stream, rate) do
SpaceEx.KRPC.set_stream_rate(stream.conn, stream.stream_id, rate || 0)
end
@doc false
def start_link(conn, stream_id) do
GenServer.start_link(__MODULE__, %State{id: stream_id, conn: conn})
end
# If stream has no data yet, add caller to waitlist.
# We'll notify them when the first value comes in.
def handle_call(:get, from, %State{result: nil} = state) do
waitlist = [from | state.waitlist]
{:noreply, %State{state | waitlist: waitlist}}
end
# Otherwise, just send the current result.
def handle_call(:get, _from, state) do
{:reply, state.result, state}
end
# If this is the first message, also notify waitlist.
def handle_info({:stream_result, id, result}, %State{id: id, result: nil} = state) do
Enum.each(state.waitlist, &GenServer.reply(&1, result))
{:noreply, %State{state | result: result}}
end
# Otherwise, just store the result.
def handle_info({:stream_result, id, result}, %State{id: id} = state) do
{:noreply, %State{state | result: result}}
end
end