Packages
elevator_project
0.1.0
Implementation of concurrent message passing system for parallell elevators.
Current section
Files
Jump to
Current section
Files
lib/elevator_operator.ex
defmodule ElevatorOperator do
@moduledoc """
Finite state machine responible for running one elevator, implemented using the behaviour GenStateMachine.
The state machine has the states `idle`, `moving` and `door_open`, and keeps track of the current floor,
moving direction, obstructed sensor state and door timer.
Uses the following modules:
- `Orders`
- `Driver`
- `OrderDistributor`
"""
use GenStateMachine
alias ElevatorOperator, as: Elevator
@enforce_keys [:floor, :direction, :is_obstructed, :timer_ref]
defstruct [:floor, :direction, :is_obstructed, :timer_ref]
@doc false
def start_link(_init_arg) do
GenStateMachine.start_link(__MODULE__, [], name: __MODULE__)
end
# API -------------------------------------------------
@doc """
Signals the push of an order button.
## Parameters
- order: Order corresponding to pressed order button :: %Order{}
## Return
- no_return
"""
def order_button_press(%Order{} = order) do
GenStateMachine.cast(__MODULE__, {:request_button_press, order})
end
@doc """
Signals that the elevator has arrived at a floor.
## Parameters
- floor: The floor the elevator has arrived at :: integer()
## Return
- no_return
"""
def floor_arrival(floor) do
GenStateMachine.cast(__MODULE__, {:floor_arrival, floor})
end
@doc """
Signals that the state of the obstruction switch has changed.
## Return
- no_return
"""
def obstruction(is_obstructed) do
GenStateMachine.cast(__MODULE__, {:obstruction_sensor_update, is_obstructed})
end
@doc """
Signals that the door timer has been started or stopped,
and changes the timer reference.
## Return
- no_return
"""
def timer_update(timer_ref) do
GenStateMachine.cast(__MODULE__, {:timer_update, timer_ref})
end
@doc """
Returns the current state of the elevator.
## Return
- Current state of the elevator, tuple of form
{floor, direction, state, orders} :: {integer(), atom(), atom(), %MapSet}
"""
def get_data() do
GenStateMachine.call(__MODULE__, :get_data)
end
# Initialization and termination callbacks ------------
def init(_init_arg) do
if not Enum.empty?(Node.list()) do
OrderDistributor.request_backup()
end
case Driver.get_floor_sensor_state() do
:between_floors ->
Driver.set_door_open_light(:off)
Driver.set_motor_direction(:down)
e = %Elevator{
floor: nil,
direction: :down,
is_obstructed: false,
timer_ref: nil
}
{:ok, :moving, e}
floor ->
e = %Elevator{
floor: floor,
direction: :stop,
is_obstructed: false,
timer_ref: nil
}
{:ok, :idle, e}
end
end
def terminate(_reason, _state, _data) do
Driver.set_motor_direction(:stop)
end
# Order button press callbacks ------------------------
def handle_event(:cast, {:request_button_press, %Order{} = order}, :door_open, %Elevator{} = e) do
if e.floor == order.floor do
OrderDistributor.distribute_completed(order)
ElevatorOperator.DoorTimer.start(e)
end
:keep_state_and_data
end
def handle_event(:cast, {:request_button_press, _order}, :moving, _data) do
:keep_state_and_data
end
def handle_event(:cast, {:request_button_press, %Order{} = order}, :idle, %Elevator{} = e) do
if e.floor == order.floor do
OrderDistributor.distribute_completed(order)
Driver.set_door_open_light(:on)
ElevatorOperator.DoorTimer.start(e)
{:next_state, :door_open, e}
else
direction = choose_direction(e)
Driver.set_motor_direction(direction)
{:next_state, :moving, %{e | direction: direction}}
end
end
# Floor arrival callbacks -----------------------------
def handle_event(:cast, {:floor_arrival, floor}, :moving, %Elevator{} = e) do
Driver.set_floor_indicator(floor)
if should_stop?(%{e | floor: floor}) do
Driver.set_motor_direction(:stop)
Driver.set_door_open_light(:on)
OrderDistributor.distribute_completed(own_orders(floor))
ElevatorOperator.DoorTimer.start(e)
{:next_state, :door_open, %{e | floor: floor, direction: :stop}}
else
{:keep_state, %{e | floor: floor}}
end
end
def handle_event(:cast, {:floor_arrival, floor}, _state, %Elevator{} = e) do
Driver.set_floor_indicator(floor)
{:keep_state, %{e | floor: floor}}
end
# Door timeout callbacks ------------------------------
def handle_event(:info, :door_timeout, :door_open, %Elevator{} = e) do
Driver.set_door_open_light(:off)
direction = choose_direction(e)
Driver.set_motor_direction(direction)
case direction do
:stop -> {:next_state, :idle, %{e | direction: direction, timer_ref: nil}}
_ -> {:next_state, :moving, %{e | direction: direction, timer_ref: nil}}
end
end
def handle_event(:info, :door_timeout, _state, _data) do
:keep_state_and_data
end
# Obstruction switch callbacks ------------------------
def handle_event(:cast, {:obstruction_sensor_update, is_obstructed}, :door_open, %Elevator{} = e) do
updated_e = %{e | is_obstructed: is_obstructed}
if is_obstructed do
ElevatorOperator.DoorTimer.stop(updated_e)
else
ElevatorOperator.DoorTimer.start(updated_e)
end
{:keep_state, updated_e}
end
def handle_event(:cast, {:obstruction_sensor_update, is_obstructed}, _state, %Elevator{} = e) do
{:keep_state, %{e | is_obstructed: is_obstructed}}
end
# Timer callbacks -------------------------------------
def handle_event(:cast, {:timer_update, timer_ref}, _state, %Elevator{} = e) do
{:keep_state, %{e | timer_ref: timer_ref}}
end
# Data retrieval callbacks ----------------------------
def handle_event({:call, from}, :get_data, state, %Elevator{} = e) do
data = {
e.floor,
e.direction,
state,
own_orders()
}
{:keep_state_and_data, [{:reply, from, data}]}
end
# Helper functions ------------------------------------
defp own_orders() do
Enum.filter(
Orders.get(),
fn %Order{} = order -> order.owner == Node.self() end
)
end
defp own_orders(floor) do
Enum.filter(
Orders.get(),
fn %Order{} = order -> order.owner == Node.self() and order.floor == floor end
)
end
defp choose_direction(%Elevator{} = e) do
case e.direction do
:up ->
cond do
orders_above?(e) -> :up
orders_below?(e) -> :down
true -> :stop
end
_ ->
cond do
orders_below?(e) -> :down
orders_above?(e) -> :up
true -> :stop
end
end
end
defp should_stop?(%Elevator{} = e) do
case e.direction do
:up ->
relevant_orders =
Enum.filter(
own_orders(e.floor),
fn %Order{} = order -> order.button_type in [:cab, :hall_up] end
)
not orders_above?(e) or not Enum.empty?(relevant_orders)
:down ->
relevant_orders =
Enum.filter(
own_orders(e.floor),
fn %Order{} = order -> order.button_type in [:cab, :hall_down] end
)
not orders_below?(e) or not Enum.empty?(relevant_orders)
_ ->
true
end
end
defp orders_above?(%Elevator{} = e) do
own_orders()
|> Enum.filter(fn %Order{} = order -> order.floor > e.floor end)
|> Enum.any?()
end
defp orders_below?(%Elevator{} = e) do
own_orders()
|> Enum.filter(fn %Order{} = order -> order.floor < e.floor end)
|> Enum.any?()
end
end
defmodule ElevatorOperator.DoorTimer do
@moduledoc """
Finite state machine responible for running one elevator, implemented using the behaviour GenStateMachine.
The state machine has the states `idle`, `moving` and `door_open`, and keeps track of the current floor,
moving direction, obstructed sensor state and door timer.
Uses the following modules:
- `ElevatorOperator`
"""
alias ElevatorOperator, as: Elevator
@door_timer_duration 2_000
@doc """
Starts the door timer if the elevator is not obstructed. Calls `ElevatorOperator.timer_update/1`
to update the timer reference to the newly set timer.
## Parameters
- e: Struct containing the current state of the elevator :: %ElevatorOperator{}
"""
def start(%Elevator{is_obstructed: false} = e) do
if e.timer_ref do
Process.cancel_timer(e.timer_ref)
end
timer_ref = Process.send_after(self(), :door_timeout, @door_timer_duration)
Elevator.timer_update(timer_ref)
end
def start(%Elevator{is_obstructed: true}) do
end
@doc """
Stops the door timer if it is active. Calls `ElevatorOperator.timer_update/1`
to update the timer reference to `nil`.
## Parameters
- e: Struct containing the current state of the elevator :: %ElevatorOperator{}
"""
def stop(%Elevator{} = e) do
if e.timer_ref do
Process.cancel_timer(e.timer_ref)
Elevator.timer_update(nil)
end
end
end