diff --git a/README.md b/README.md index a58fda6..baf9d73 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,11 @@ # TTK4145 Elevator -### LOC stats (Elixir) -**Lib:** 1080\ -**Test:** 585 +Elevator application for controlling `n` elevators across `m` floors +in a distributed fashion, implemented in Elixir. + +## Documentation + +See `doc/index.html` for module documentation. ## Running nodes @@ -97,3 +100,8 @@ sudo ./scripts/packetloss.sh -ie - `-i` is incomming traffic - `-o`is outgoing - `-e`is Elixir/Erlang and autodetects Beam ports + +## LOC stats (Elixir) + +**Lib:** 1063\ +**Test:** 585 diff --git a/lib/elevator/application.ex b/lib/elevator/application.ex index 57e7552..6a634ca 100644 --- a/lib/elevator/application.ex +++ b/lib/elevator/application.ex @@ -1,4 +1,10 @@ defmodule Elevator.Application do + @moduledoc """ + Main entry point, starting the supervisor. + Supervisor children ordering is somewhat based on abstraction level: + modules "closer" to hardware are started last because they are more likely to crash. + Combined with the rest_for_one strategy, this lets us keep high level state when a lower level module crashes. + """ use Application @spec start(Application.start_type(), term()) :: {:ok, pid()} | {:error, term()} diff --git a/lib/elevator/cab_orders.ex b/lib/elevator/cab_orders.ex index 3a0ec42..d324916 100644 --- a/lib/elevator/cab_orders.ex +++ b/lib/elevator/cab_orders.ex @@ -1,14 +1,20 @@ defmodule Elevator.CabOrders do @moduledoc """ - Module responsible for all changes occuring to the cab_order part of the state. + Module responsible for all changes occurring to cab orders. + This is a stateful module, storing our current view of the cab order states. + + This module handles the following events that can change cab orders: + - Button is pressed. + - Arrived at floor. + - Received cab orders from another node *with a higher version number*. + + Only increments our own version number; peer's version numbers are kept as is. """ use GenServer - @type floor :: Elevator.floor() - @type cab_orders_snapshot :: %{ version: non_neg_integer(), - orders: MapSet.t(floor()) + orders: MapSet.t(Elevator.floor()) } @type cab_order_map :: %{Node.t() => cab_orders_snapshot()} @@ -33,7 +39,7 @@ defmodule Elevator.CabOrders do @doc """ Retrieve *this* node's current cab orders. """ - @spec get_my_orders() :: MapSet.t(floor()) + @spec get_my_orders() :: MapSet.t(Elevator.floor()) def get_my_orders(), do: GenServer.call(__MODULE__, :get_my_orders) @doc """ @@ -47,13 +53,13 @@ defmodule Elevator.CabOrders do @doc """ Add a cab order and increment our own version number. """ - @spec button_press(floor()) :: :ok + @spec button_press(Elevator.floor()) :: :ok def button_press(floor), do: GenServer.cast(__MODULE__, {:button_press, floor}) @doc """ Remove a cab order and increment our own version number. """ - @spec arrived_at_floor(floor()) :: :ok + @spec arrived_at_floor(Elevator.floor()) :: :ok def arrived_at_floor(floor), do: GenServer.cast(__MODULE__, {:arrived_at_floor, floor}) # Calls -------------------------------------------------- @@ -85,7 +91,8 @@ defmodule Elevator.CabOrders do end @impl true - @spec handle_cast({:button_press, floor()}, cab_order_map()) :: {:noreply, cab_order_map()} + @spec handle_cast({:button_press, Elevator.floor()}, cab_order_map()) :: + {:noreply, cab_order_map()} def handle_cast({:button_press, floor}, order_map) do new_order_map = Map.update!(order_map, Node.self(), fn %{version: old_version, orders: old_orders} -> @@ -96,7 +103,8 @@ defmodule Elevator.CabOrders do end @impl true - @spec handle_cast({:arrived_at_floor, floor()}, cab_order_map()) :: {:noreply, cab_order_map()} + @spec handle_cast({:arrived_at_floor, Elevator.floor()}, cab_order_map()) :: + {:noreply, cab_order_map()} def handle_cast({:arrived_at_floor, floor}, order_map) do new_order_map = Map.update!(order_map, Node.self(), fn %{version: old_version, orders: old_orders} -> diff --git a/lib/elevator/communicator.ex b/lib/elevator/communicator.ex index c02c917..09a2cc3 100644 --- a/lib/elevator/communicator.ex +++ b/lib/elevator/communicator.ex @@ -1,6 +1,8 @@ defmodule Elevator.Communicator do @moduledoc """ Module responsible for all communication with other elevators. + For each peer, their status and last message time is stored. + Runs a loop broadcasting our hall- and cab orders periodically. """ alias Elevator.FSM.State @@ -8,12 +10,8 @@ defmodule Elevator.Communicator do alias Elevator.CabOrders alias Elevator.HallOrders - require Logger use GenServer - @type hall_order_map :: Elevator.HallOrders.hall_order_map() - @type cab_order_map :: Elevator.CabOrders.cab_order_map() - @type peer_status_map :: %{ Node.t() => %{operational: boolean(), timestamp: Time.t()} } @@ -21,8 +19,8 @@ defmodule Elevator.Communicator do @type communicator_message :: %{ from: Node.t(), operational: boolean(), - hall_order_map: hall_order_map(), - cab_order_map: cab_order_map() + hall_order_map: HallOrders.hall_order_map(), + cab_order_map: CabOrders.cab_order_map() } @type communicator_options :: [do_resend: boolean()] diff --git a/lib/elevator/fsm/state.ex b/lib/elevator/fsm/state.ex index 91b7278..791df23 100644 --- a/lib/elevator/fsm/state.ex +++ b/lib/elevator/fsm/state.ex @@ -5,10 +5,9 @@ defmodule Elevator.FSM.State do Acts as the single source of truth for what the elevator *is* right now - its floor, direction, behavior, and fault conditions. """ - require Logger - defstruct behavior: :moving, - between_floors: true, + defstruct behavior: :idle, + between_floors: false, direction: :down, door_open_time: Time.utc_now(), floor: :unknown, @@ -16,7 +15,6 @@ defmodule Elevator.FSM.State do motor_timed_out: false, obstructed: false - @type floor :: Elevator.floor() @type elev_behavior :: :moving | :idle | :door_open @type t :: %__MODULE__{ @@ -48,7 +46,7 @@ defmodule Elevator.FSM.State do @doc """ Updates floor and between_floors status. """ - @spec set_floor(:between_floors | floor()) :: :ok + @spec set_floor(:between_floors | Elevator.floor()) :: :ok def set_floor(floor), do: GenServer.cast(__MODULE__, {:set_floor, floor}) @doc """ diff --git a/lib/elevator/fsm/transition.ex b/lib/elevator/fsm/transition.ex index bfc7411..fe5c0ab 100644 --- a/lib/elevator/fsm/transition.ex +++ b/lib/elevator/fsm/transition.ex @@ -1,12 +1,11 @@ defmodule Elevator.FSM.Transition do @moduledoc """ - Loop handling FSM transitions. + Loop driving FSM transitions at a fixed interval. One iteration of the loop does the following: - Checks door and motor timeouts - Reads and updates state and orders - Sets hardware outputs """ - require Logger alias Elevator.CabOrders alias Elevator.FSM.State diff --git a/lib/elevator/hall_orders.ex b/lib/elevator/hall_orders.ex index 78f0400..89f8865 100644 --- a/lib/elevator/hall_orders.ex +++ b/lib/elevator/hall_orders.ex @@ -1,7 +1,10 @@ defmodule Elevator.HallOrders do @moduledoc """ - Module responsible for all changes occuring to the hall_order part of the state. - The events that can change hall orders are: + Module responsible for all changes occurring to the hall orders. + This is a stateful module storing our current view on the hall order states. + See `m:Elevator.HallOrders.Order` for information about single hall order states. + + This module handles the following events that can change hall orders: - Button is pressed. - Arrived at floor. - Received hall orders from another node. @@ -10,19 +13,16 @@ defmodule Elevator.HallOrders do alias Elevator.HallOrders.Order alias Elevator.HallOrders.Cost alias Elevator.Communicator - require Logger use GenServer - @type floor :: Elevator.floor() - @type hall_button_type :: :hall_down | :hall_up - @type hall_button :: {floor(), hall_button_type()} + @type hall_button :: {Elevator.floor(), hall_button_type()} - @type hall_order_cost_map :: %{Node.t() => non_neg_integer()} + @type cost_map :: %{Node.t() => non_neg_integer()} @type hall_order_state :: :idle | {:pending, MapSet.t()} - | {:handling, hall_order_map()} + | {:handling, cost_map()} | {:arrived, MapSet.t()} @type hall_order_map :: %{hall_button() => hall_order_state()} @@ -66,14 +66,14 @@ defmodule Elevator.HallOrders do @doc """ Places the corresponding order in pending state if it is in idle. """ - @spec button_press(floor(), hall_button_type()) :: :ok + @spec button_press(Elevator.floor(), hall_button_type()) :: :ok def button_press(floor, button_type), do: GenServer.cast(__MODULE__, {:button_press, floor, button_type}) @doc """ Advances the order to arrived if it is in handling. """ - @spec arrived_at_floor(floor(), :up | :down) :: :ok + @spec arrived_at_floor(Elevator.floor(), :up | :down) :: :ok def arrived_at_floor(floor, direction), do: GenServer.cast(__MODULE__, {:arrived_at_floor, floor, direction}) @@ -86,14 +86,14 @@ defmodule Elevator.HallOrders do @doc """ Retrieve only the orders we are going to take. """ - @spec get_my_orders() :: %{floor() => MapSet.t(hall_button_type())} + @spec get_my_orders() :: %{Elevator.floor() => MapSet.t(hall_button_type())} def get_my_orders(), do: GenServer.call(__MODULE__, :get_my_orders) @doc """ Get all orders in handling state, in the same format as get_my_orders. These are the orders we turn the light on for. """ - @spec get_handling_orders() :: %{floor() => MapSet.t(hall_button_type())} + @spec get_handling_orders() :: %{Elevator.floor() => MapSet.t(hall_button_type())} def get_handling_orders(), do: GenServer.call(__MODULE__, :get_handling_orders) # Calls -------------------------------------------------- @@ -159,6 +159,8 @@ defmodule Elevator.HallOrders do {:noreply, new_order_map, {:continue, :hall_update_state}} end + # Changes to who is alive can occur even if we receive no events. + # Therefore we periodically check if some barrier-set states should advance. @impl true def handle_info(:refresh_hall_orders, order_map) do Process.send_after(self(), :refresh_hall_orders, @hall_order_refresh_period_ms) @@ -167,9 +169,7 @@ defmodule Elevator.HallOrders do # Continues -------------------------------------------------- - @doc """ - May advance some states, in which case continue is called until convergence. - """ + # Some states may be advanced if their barrier sets are full. @impl true def handle_continue(:hall_update_state, order_map) do my_orders = my_orders_from_order_map(order_map) @@ -184,7 +184,9 @@ defmodule Elevator.HallOrders do # Return the orders where we have the lowest cost among serving nodes. # Only consider orders where all serving nodes have a cost. - @spec my_orders_from_order_map(hall_order_map()) :: %{floor() => MapSet.t(hall_button_type())} + @spec my_orders_from_order_map(hall_order_map()) :: %{ + Elevator.floor() => MapSet.t(hall_button_type()) + } defp my_orders_from_order_map(order_map) do who_can_serve = Communicator.who_can_serve() @@ -202,7 +204,7 @@ defmodule Elevator.HallOrders do @type enum_orders :: hall_order_map() | Enumerable.t({hall_button(), any()}) - @spec orders_by_floor(enum_orders()) :: %{floor() => MapSet.t(hall_button())} + @spec orders_by_floor(enum_orders()) :: %{Elevator.floor() => MapSet.t(hall_button_type())} defp orders_by_floor(orders) do # Restructure order map to the format floor => MapSet(button_type) orders diff --git a/lib/elevator/hall_orders/cost.ex b/lib/elevator/hall_orders/cost.ex index 9367fd6..5e850e5 100644 --- a/lib/elevator/hall_orders/cost.ex +++ b/lib/elevator/hall_orders/cost.ex @@ -3,22 +3,22 @@ defmodule Elevator.HallOrders.Cost do Hall order cost utilities. Cost is estimated by simulating the local elevator with current orders plus the candidate hall order. + See `m:Elevator.HallOrders.Simulation` for simulation logic. """ + alias Elevator.HallOrders alias Elevator.CabOrders alias Elevator.FSM.State alias Elevator.OrderUtils alias Elevator.HallOrders.Simulation - require Logger - - @type floor :: Elevator.floor() - @type hall_button_type :: Elevator.HallOrders.hall_button_type() - @type cost_map :: Elevator.HallOrders.hall_order_cost_map() @doc """ Compute the cost (time to serve) of a candidate hall order by simulating single elevator logic. """ - @spec compute_cost({floor(), hall_button_type()}, %{floor() => MapSet.t(hall_button_type())}) :: + @spec compute_cost( + {Elevator.floor(), Elevator.HallOrders.hall_button_type()}, + %{Elevator.floor() => MapSet.t(Elevator.HallOrders.hall_button_type())} + ) :: non_neg_integer() def compute_cost({floor, hall_button_type}, my_hall_orders) do state = State.get_state() @@ -42,7 +42,7 @@ defmodule Elevator.HallOrders.Cost do Merge two cost maps. Uses pessimistic merge: If two conflicting costs for the same node are found, keep the higher one. """ - @spec merge_cost(cost_map(), cost_map()) :: cost_map() + @spec merge_cost(HallOrders.cost_map(), HallOrders.cost_map()) :: HallOrders.cost_map() def merge_cost(cost_map, other_cost_map) do Map.merge(cost_map, other_cost_map, fn _node, cost, other_cost -> max(cost, other_cost) @@ -53,7 +53,7 @@ defmodule Elevator.HallOrders.Cost do Returns if we are supposed to take the order given the cost map. Assumes who_can_serve is a subset of cost_map keys. """ - @spec assigned_to_me?(cost_map(), MapSet.t(node())) :: boolean() + @spec assigned_to_me?(HallOrders.cost_map(), MapSet.t(node())) :: boolean() def assigned_to_me?(cost_map, who_can_serve) do {min_node, _} = Enum.filter(cost_map, fn {node, _} -> MapSet.member?(who_can_serve, node) end) diff --git a/lib/elevator/hall_orders/order.ex b/lib/elevator/hall_orders/order.ex index 6deee3f..165713f 100644 --- a/lib/elevator/hall_orders/order.ex +++ b/lib/elevator/hall_orders/order.ex @@ -9,24 +9,22 @@ defmodule Elevator.HallOrders.Order do - pending: Someone pressed a button, but everyone does not know it. Light: off - handling: All alive nodes know about the order and has indicated their cost to serve it. Light: on. - arrived: A node is signalling that the order has been served. Light: off + + This module contains functions for transitioning between states. """ + alias Elevator.HallOrders alias Elevator.HallOrders.Cost alias Elevator.Communicator - @type floor :: Elevator.floor() - @type hall_button :: Elevator.HallOrders.hall_button() - @type hall_button_type :: Elevator.HallOrders.hall_button_type() - @type hall_order_state :: Elevator.HallOrders.hall_order_state() - @doc """ Update a hall order based on an incoming hall order from another node. """ @spec update_from_incoming( - hall_button(), - hall_order_state(), - hall_order_state(), - %{floor() => MapSet.t(hall_button_type())} - ) :: hall_order_state() + HallOrders.hall_button(), + HallOrders.hall_order_state(), + HallOrders.hall_order_state(), + %{Elevator.floor() => MapSet.t(HallOrders.hall_button_type())} + ) :: HallOrders.hall_order_state() def update_from_incoming(order_key, order_state, incoming_order_state, my_hall_orders) do order_state |> merge_with_incoming(incoming_order_state) @@ -36,13 +34,12 @@ defmodule Elevator.HallOrders.Order do @doc """ Advances a pending or arrived order if the respective barrier set is full. - Returns `{true, new_value}` if the state changed, `{false, old_value}` otherwise. """ @spec update_from_barrier_state( - hall_button(), - hall_order_state(), - %{floor() => MapSet.t(hall_button_type())} - ) :: hall_order_state() + HallOrders.hall_button(), + HallOrders.hall_order_state(), + %{Elevator.floor() => MapSet.t(HallOrders.hall_button_type())} + ) :: HallOrders.hall_order_state() def update_from_barrier_state(order_key, order_state, my_hall_orders) do order_state |> transition_from_barrier_state(Communicator.who_is_alive()) @@ -50,14 +47,15 @@ defmodule Elevator.HallOrders.Order do |> ensure_self_in_cost_map(order_key, my_hall_orders) end - @spec update_from_button_press(hall_order_state()) :: hall_order_state() + @spec update_from_button_press(HallOrders.hall_order_state()) :: HallOrders.hall_order_state() def update_from_button_press(:idle) do ensure_self_in_barriers({:pending, MapSet.new()}) end def update_from_button_press(order_state), do: order_state - @spec update_from_button_press(hall_order_state()) :: hall_order_state() + @spec update_from_arrived_at_floor(HallOrders.hall_order_state()) :: + HallOrders.hall_order_state() def update_from_arrived_at_floor({:handling, _}) do ensure_self_in_barriers({:arrived, MapSet.new()}) end diff --git a/lib/elevator/hall_orders/simulation.ex b/lib/elevator/hall_orders/simulation.ex index 459b61e..5e5087f 100644 --- a/lib/elevator/hall_orders/simulation.ex +++ b/lib/elevator/hall_orders/simulation.ex @@ -3,6 +3,8 @@ defmodule Elevator.HallOrders.Simulation do Pure hall-order cost simulation. """ + alias Elevator.HallOrders + alias Elevator.OrderUtils alias Elevator.FSM.State alias Elevator.FSM.Transition @@ -10,19 +12,15 @@ defmodule Elevator.HallOrders.Simulation do @max_simulation_steps 256 @unreachable_cost 30000 - @type floor :: Elevator.floor() - @type hall_button_type :: Elevator.HallOrders.hall_button_type() - @type combined_order_map :: Elevator.OrderUtils.combined_order_map() - defmodule SimState do @enforce_keys [:orders, :elevator_state, :target, :time_ms, :steps_left] defstruct [:orders, :elevator_state, :target, :time_ms, :steps_left] end @type simulation :: %SimState{ - orders: combined_order_map(), + orders: OrderUtils.combined_order_map(), elevator_state: State.t(), - target: {floor(), hall_button_type()}, + target: {Elevator.floor(), HallOrders.hall_button_type()}, time_ms: non_neg_integer(), steps_left: non_neg_integer() } @@ -33,7 +31,7 @@ defmodule Elevator.HallOrders.Simulation do @spec unreachable_cost() :: non_neg_integer() def unreachable_cost, do: @unreachable_cost - @spec initial_time_ms(State.t(), floor()) :: number() + @spec initial_time_ms(State.t(), Elevator.floor()) :: non_neg_integer() def initial_time_ms(elevator_state, target_floor) do cond do elevator_state.behavior == :door_open and target_floor != elevator_state.floor -> @@ -47,7 +45,11 @@ defmodule Elevator.HallOrders.Simulation do end end - @spec simulate_time_until_served(combined_order_map(), State.t(), {floor(), hall_button_type()}) :: + @spec simulate_time_until_served( + OrderUtils.combined_order_map(), + State.t(), + {Elevator.floor(), HallOrders.hall_button_type()} + ) :: non_neg_integer() def simulate_time_until_served(_orders, %{floor: :unknown} = _elevator_state, _target), do: @unreachable_cost diff --git a/lib/elevator/hardware/input_poller.ex b/lib/elevator/hardware/input_poller.ex index 276aaae..1c9af08 100644 --- a/lib/elevator/hardware/input_poller.ex +++ b/lib/elevator/hardware/input_poller.ex @@ -5,7 +5,6 @@ defmodule Elevator.Hardware.InputPoller do """ use GenServer - require Logger alias Elevator.CabOrders alias Elevator.HallOrders diff --git a/lib/elevator/hardware/outputs.ex b/lib/elevator/hardware/outputs.ex index 5580a0c..7110961 100644 --- a/lib/elevator/hardware/outputs.ex +++ b/lib/elevator/hardware/outputs.ex @@ -1,9 +1,9 @@ defmodule Elevator.Hardware.Outputs do @moduledoc """ Sets driver outputs given state and orders. + The `Outputs.set_outputs/2` function is called by `m:Elevator.FSM.Transition` after each transition. """ - require Logger alias Elevator.Hardware.Driver alias Elevator.FSM