Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# TTK4145 Elevator

### LOC stats (Elixir)
**Lib:** <!-- LIB_COUNT -->1080<!-- END_LIB_COUNT -->\
**Test:** <!-- TEST_COUNT -->585<!-- END_TEST_COUNT -->
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
Expand Down Expand Up @@ -97,3 +100,8 @@ sudo ./scripts/packetloss.sh <percentage> -ie
- `-i` is incomming traffic
- `-o`is outgoing
- `-e`is Elixir/Erlang and autodetects Beam ports

## LOC stats (Elixir)

**Lib:** <!-- LIB_COUNT -->1063<!-- END_LIB_COUNT -->\
**Test:** <!-- TEST_COUNT -->585<!-- END_TEST_COUNT -->
6 changes: 6 additions & 0 deletions lib/elevator/application.ex
Original file line number Diff line number Diff line change
@@ -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()}
Expand Down
26 changes: 17 additions & 9 deletions lib/elevator/cab_orders.ex
Original file line number Diff line number Diff line change
@@ -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()}
Expand All @@ -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 """
Expand All @@ -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 --------------------------------------------------
Expand Down Expand Up @@ -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} ->
Expand All @@ -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} ->
Expand Down
10 changes: 4 additions & 6 deletions lib/elevator/communicator.ex
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
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
alias Elevator
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()}
}

@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()]
Expand Down
8 changes: 3 additions & 5 deletions lib/elevator/fsm/state.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,16 @@ 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,
last_floor_time: Time.utc_now(),
motor_timed_out: false,
obstructed: false

@type floor :: Elevator.floor()
@type elev_behavior :: :moving | :idle | :door_open

@type t :: %__MODULE__{
Expand Down Expand Up @@ -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 """
Expand Down
3 changes: 1 addition & 2 deletions lib/elevator/fsm/transition.ex
Original file line number Diff line number Diff line change
@@ -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
Expand Down
36 changes: 19 additions & 17 deletions lib/elevator/hall_orders.ex
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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()}
Expand Down Expand Up @@ -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})

Expand All @@ -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 --------------------------------------------------
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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()

Expand All @@ -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
Expand Down
16 changes: 8 additions & 8 deletions lib/elevator/hall_orders/cost.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
Expand All @@ -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)
Expand Down
Loading
Loading