defmodule LocationSimulator.Worker do @moduledoc """ Worker is a simulator that use for generating GPS data. Worker will call client follow :callback setting in config. Two start/stop event is used for start or init data and clean if need. event api is used for put GPS data to client. Callbacks are declared in `LocationSimulator.Event` module. """ require Logger import LocationSimulator.Gps @doc """ Support for starting from supervisor. """ def start_link(arg) do pid = spawn_link(__MODULE__, :init, [arg]) {:ok, pid} end @doc """ Generate child spec for supervisor. """ def child_spec(opts) when is_map(opts) do %{ id: Map.get(opts, :id, __MODULE__), start: {__MODULE__, :start_link, [opts]}, type: :worker, restart: Map.get(opts, :restart, :temporary), shutdown: Map.get(opts, :shutdown, :brutal_kill) } end @doc """ Entry point of worker, start event will be fired from here. Init some data and go to loop function for generating GPS data. Root GPS data will be generated in this function. """ def init(config) when is_map(config) do state = %{ start_time: get_timestamp(), success: 0, failed: 0, error: 0 } config = case Map.get(config, :direction, :random) do :random -> direction = Enum.random([:north, :south, :east, :west, :north_east, :north_west, :south_east, :south_west]) Map.put(config, :direction, direction) _ -> config end id = Map.get(config, :id, self()) %{callback: mod} = config Logger.debug("#{inspect id}, call start event") # send start event config = case mod.start(config, state) do {:ok, new_config} -> new_config {:error, reason} -> err = "call start event failed: #{inspect reason}" Logger.error(err) raise err end :rand.seed(:exsss) # random start point. {lati, long} = generate_pos() # get started altitude, default is 0 alti = case Map.get(config, :altitude) do n when is_integer(n) -> n _ -> 0 end gps = %{ timestamp: 0, long: long, lati: lati, alti: alti } state = Map.put(state, :gps, gps) counter = Map.get(config, :event, :infinity) Logger.debug("start loop with counter: #{inspect counter}") loop_event(config, state, counter) end @doc """ Main function of worker, GPS data will be generated in here then trigger client's api. After fired all GPS data/ client send signal to stop, the function will fire stop event. """ defp loop_event(config, state, 0) do stop_time = get_timestamp() state = Map.put(state, :stop_time, stop_time) %{callback: mod} = config # send stop event config = case mod.stop(config, state) do {:ok, new_config} -> new_config {:error, reason} -> err = "call start event failed: #{inspect reason}" Logger.error(err) raise err end %{start_time: start_time} = state %{success: s} = state %{failed: f} = state %{error: e} = state id = Map.get(config, :id, self()) Logger.debug "Worker(#{inspect id}) DONE!, time: #{stop_time - start_time}s, success: #{s}, failed: #{f}, error: #{e}" end defp loop_event(config, %{gps: last_gps} = state, counter) do direction = Map.get(config, :direction, :up_right) # generate next gps based on last gps. {lati, long} = generate_next_pos(last_gps.lati, last_gps.long, random_lati_step(direction), random_long_step(direction)) # get next altitude alti = case Map.get(config, :altitude_way) do :up -> last_gps.alti + Enum.random(0..2) :down -> last_gps.alti - Enum.random(0..2) _ -> last_gps.alti end %{interval: interval} = config %{random_range: random_range} = config sleep_time = interval + Enum.random(0..random_range) Process.sleep(sleep_time) new_gps = %{ timestamp: sleep_time + last_gps.timestamp, long: long, lati: lati, alti: alti } state = Map.put(state, :gps, new_gps) %{callback: mod} = config {config, state, counter} = case mod.event(config, state) do {:ok, config} -> {config, Map.update!(state, :success, &(&1 + 1)), counter} {:error, reason} -> Logger.debug "failed to post data return code: #{inspect reason}" {config, Map.update!(state, :failed, &(&1 + 1)), counter} {:stop, reason} -> Logger.debug "stop worker by client, reason: #{inspect reason}" {config, state, :stop} end case counter do :infinity -> loop_event(config, state, counter) :stop -> id = Map.get(config, :id, self()) Logger.debug("#{inspect id} stop by callback, reason: #{inspect id}") loop_event(config, state, 0) _ -> loop_event(config, state, counter-1) end end defp get_timestamp do :os.system_time(:seconds) end # direction: :north, :south, :east, :west, :north_east, :north_west, :south_east, :south_west defp random_long_step(direction) when is_atom(direction) do # TO-DO: move random step distance to config case direction do n when n in [:north_east, :south_east] -> Enum.random(1..5) l when l in [:north_west, :south_west] -> Enum.random(-5..-1) k when k in [:north, :south] -> 0 :east -> Enum.random(1..5) :west -> Enum.random(-5..-1) end end defp random_lati_step(direction) when is_atom(direction) do case direction do n when n in [:north_east, :north_west] -> Enum.random(1..5) l when l in [:south_east, :south_west] -> Enum.random(-5..-1) k when k in [:east, :west] -> 0 :north -> Enum.random(1..5) :south -> Enum.random(-5..-1) end end end