From 78c953d70ce171963d6fe62a887c28c4b595daf9 Mon Sep 17 00:00:00 2001 From: Brett Heath-Wlaz Date: Thu, 5 Oct 2023 09:40:10 -0400 Subject: [PATCH] formalize audio announcement logic (#701) --- lib/content/audio/approaching.ex | 14 + lib/content/audio/next_train_countdown.ex | 16 + lib/content/audio/predictions.ex | 87 +---- lib/content/audio/train_is_arriving.ex | 13 + lib/content/audio/train_is_boarding.ex | 28 ++ lib/content/message/stopped_train.ex | 12 +- lib/signs/realtime.ex | 15 +- lib/signs/utilities/audio.ex | 381 ++++++++++--------- lib/signs/utilities/reader.ex | 118 +----- test/content/messages/stopped_train_test.exs | 21 +- test/signs/realtime_test.exs | 189 +++++---- test/signs/utilities/audio_test.exs | 273 +------------ test/signs/utilities/reader_test.exs | 111 ------ test/signs/utilities/updater_test.exs | 25 -- 14 files changed, 441 insertions(+), 862 deletions(-) delete mode 100644 test/signs/utilities/reader_test.exs diff --git a/lib/content/audio/approaching.ex b/lib/content/audio/approaching.ex index f33022287..9de91be5f 100644 --- a/lib/content/audio/approaching.ex +++ b/lib/content/audio/approaching.ex @@ -4,6 +4,7 @@ defmodule Content.Audio.Approaching do """ require Logger + alias Content.Message alias PaEss.Utilities @enforce_keys [:destination] @@ -19,6 +20,19 @@ defmodule Content.Audio.Approaching do crowding_description: {atom(), atom()} | nil } + def from_message(%Message.Predictions{} = message, include_crowding?) do + [ + %__MODULE__{ + destination: message.destination, + trip_id: message.trip_id, + platform: message.platform, + route_id: message.route_id, + new_cars?: message.new_cars?, + crowding_description: if(include_crowding?, do: message.crowding_description) + } + ] + end + defimpl Content.Audio do @attention_passengers "783" @now_approaching_new_rl_cars "786" diff --git a/lib/content/audio/next_train_countdown.ex b/lib/content/audio/next_train_countdown.ex index 3ed84609a..d05469b99 100644 --- a/lib/content/audio/next_train_countdown.ex +++ b/lib/content/audio/next_train_countdown.ex @@ -20,6 +20,22 @@ defmodule Content.Audio.NextTrainCountdown do } require Logger + alias Content.Message + + def from_message(%Message.Predictions{} = message) do + [ + %__MODULE__{ + destination: message.destination, + route_id: message.route_id, + minutes: if(message.minutes == :approaching, do: 1, else: message.minutes), + verb: if(message.terminal?, do: :departs, else: :arrives), + track_number: Content.Utilities.stop_track_number(message.stop_id), + platform: message.platform, + station_code: message.station_code, + zone: message.zone + } + ] + end defimpl Content.Audio do alias PaEss.Utilities diff --git a/lib/content/audio/predictions.ex b/lib/content/audio/predictions.ex index b2620bc07..6762f455b 100644 --- a/lib/content/audio/predictions.ex +++ b/lib/content/audio/predictions.ex @@ -7,12 +7,7 @@ defmodule Content.Audio.Predictions do require Logger require Content.Utilities - alias Content.Audio.BoardingButton - alias Content.Audio.TrackChange - alias Content.Audio.TrainIsBoarding - alias Content.Audio.TrainIsArriving - alias Content.Audio.Approaching - alias Content.Audio.NextTrainCountdown + alias Content.Audio @heavy_rail_routes ["Red", "Orange", "Blue"] @@ -27,90 +22,18 @@ defmodule Content.Audio.Predictions do multi_source? ) do cond do - TrackChange.park_track_change?(predictions) and predictions.minutes == :boarding -> - [ - %TrackChange{ - destination: predictions.destination, - route_id: predictions.route_id, - berth: predictions.stop_id - } - ] - predictions.minutes == :boarding -> - [ - %TrainIsBoarding{ - destination: predictions.destination, - trip_id: predictions.trip_id, - route_id: predictions.route_id, - track_number: Content.Utilities.stop_track_number(predictions.stop_id) - } - ] ++ - if predictions.station_code == "BBOW" && predictions.zone == "e" do - [%BoardingButton{}] - else - [] - end + Audio.TrainIsBoarding.from_message(predictions) predictions.minutes == :arriving -> - [ - %TrainIsArriving{ - destination: predictions.destination, - trip_id: predictions.trip_id, - platform: predictions.platform, - route_id: predictions.route_id, - crowding_description: - if(predictions.crowding_data_confidence == :high, - do: predictions.crowding_description - ) - } - ] + Audio.TrainIsArriving.from_message(predictions, false) predictions.minutes == :approaching and (line == :top or multi_source?) and predictions.route_id in @heavy_rail_routes -> - [ - %Approaching{ - destination: predictions.destination, - trip_id: predictions.trip_id, - platform: predictions.platform, - route_id: predictions.route_id, - new_cars?: predictions.new_cars?, - crowding_description: - if(predictions.crowding_data_confidence == :high, - do: predictions.crowding_description - ) - } - ] - - predictions.minutes == :approaching -> - [ - %NextTrainCountdown{ - destination: predictions.destination, - route_id: predictions.route_id, - minutes: 1, - verb: if(predictions.terminal?, do: :departs, else: :arrives), - track_number: Content.Utilities.stop_track_number(predictions.stop_id), - platform: predictions.platform, - station_code: predictions.station_code, - zone: predictions.zone - } - ] - - is_integer(predictions.minutes) -> - [ - %NextTrainCountdown{ - destination: predictions.destination, - route_id: predictions.route_id, - minutes: predictions.minutes, - verb: if(predictions.terminal?, do: :departs, else: :arrives), - track_number: Content.Utilities.stop_track_number(predictions.stop_id), - platform: predictions.platform, - station_code: predictions.station_code, - zone: predictions.zone - } - ] + Audio.Approaching.from_message(predictions, false) true -> - [] + Audio.NextTrainCountdown.from_message(predictions) end end end diff --git a/lib/content/audio/train_is_arriving.ex b/lib/content/audio/train_is_arriving.ex index 119665f58..f60ecb008 100644 --- a/lib/content/audio/train_is_arriving.ex +++ b/lib/content/audio/train_is_arriving.ex @@ -4,6 +4,7 @@ defmodule Content.Audio.TrainIsArriving do """ require Logger + alias Content.Message alias PaEss.Utilities @enforce_keys [:destination] @@ -17,6 +18,18 @@ defmodule Content.Audio.TrainIsArriving do crowding_description: {atom(), atom()} | nil } + def from_message(%Message.Predictions{} = message, include_crowding?) do + [ + %__MODULE__{ + destination: message.destination, + trip_id: message.trip_id, + platform: message.platform, + route_id: message.route_id, + crowding_description: if(include_crowding?, do: message.crowding_description) + } + ] + end + defimpl Content.Audio do def to_params( %Content.Audio.TrainIsArriving{crowding_description: crowding_description} = audio diff --git a/lib/content/audio/train_is_boarding.ex b/lib/content/audio/train_is_boarding.ex index a95d03a6f..a86f07cc0 100644 --- a/lib/content/audio/train_is_boarding.ex +++ b/lib/content/audio/train_is_boarding.ex @@ -4,6 +4,8 @@ defmodule Content.Audio.TrainIsBoarding do """ require Logger + alias Content.Audio + alias Content.Message @enforce_keys [:destination, :route_id, :track_number] defstruct @enforce_keys ++ [:trip_id] @@ -15,6 +17,32 @@ defmodule Content.Audio.TrainIsBoarding do track_number: Content.Utilities.track_number() } + def from_message(%Message.Predictions{} = message) do + if Audio.TrackChange.park_track_change?(message) do + [ + %Audio.TrackChange{ + destination: message.destination, + route_id: message.route_id, + berth: message.stop_id + } + ] + else + [ + %__MODULE__{ + destination: message.destination, + trip_id: message.trip_id, + route_id: message.route_id, + track_number: Content.Utilities.stop_track_number(message.stop_id) + } + ] ++ + if message.station_code == "BBOW" && message.zone == "e" do + [%Audio.BoardingButton{}] + else + [] + end + end + end + defimpl Content.Audio do @the_next "501" @train_to "507" diff --git a/lib/content/message/stopped_train.ex b/lib/content/message/stopped_train.ex index dcb73d2ac..88b0e3cf6 100644 --- a/lib/content/message/stopped_train.ex +++ b/lib/content/message/stopped_train.ex @@ -13,13 +13,16 @@ defmodule Content.Message.StoppedTrain do require Logger @enforce_keys [:destination, :stops_away] - defstruct @enforce_keys ++ [:certainty, :stop_id] + defstruct @enforce_keys ++ [:certainty, :stop_id, :trip_id, :route_id, :direction_id] @type t :: %__MODULE__{ destination: PaEss.destination(), stops_away: non_neg_integer(), certainty: non_neg_integer() | nil, - stop_id: String.t() + stop_id: String.t(), + trip_id: Predictions.Prediction.trip_id(), + route_id: String.t(), + direction_id: 0 | 1 } @spec from_prediction(Predictions.Prediction.t()) :: t() | nil @@ -36,7 +39,10 @@ defmodule Content.Message.StoppedTrain do destination: destination, stops_away: stops_away, certainty: prediction.arrival_certainty || prediction.departure_certainty, - stop_id: prediction.stop_id + stop_id: prediction.stop_id, + trip_id: prediction.trip_id, + route_id: prediction.route_id, + direction_id: prediction.direction_id } {:error, _} -> diff --git a/lib/signs/realtime.ex b/lib/signs/realtime.ex index f372ef099..acfd93490 100644 --- a/lib/signs/realtime.ex +++ b/lib/signs/realtime.ex @@ -38,6 +38,11 @@ defmodule Signs.Realtime do announced_approachings: [], announced_approachings_with_crowding: [], announced_passthroughs: [], + announced_boardings: [], + announced_stalls: [], + announced_custom_text: nil, + announced_alert: false, + prev_prediction_keys: nil, uses_shuttles: true ] @@ -68,6 +73,11 @@ defmodule Signs.Realtime do announced_approachings: [Predictions.Prediction.trip_id()], announced_approachings_with_crowding: [Predictions.Prediction.trip_id()], announced_passthroughs: [Predictions.Prediction.trip_id()], + announced_boardings: [Predictions.Prediction.trip_id()], + announced_stalls: [{Predictions.Prediction.trip_id(), non_neg_integer()}], + announced_custom_text: String.t() | nil, + prev_prediction_keys: [{String.t(), 0 | 1}] | nil, + announced_alert: boolean(), uses_shuttles: boolean() } @@ -157,10 +167,7 @@ defmodule Signs.Realtime do sign |> announce_passthrough_trains(predictions) |> Utilities.Updater.update_sign(new_top, new_bottom, current_time) - |> Utilities.Reader.do_interrupting_reads( - sign.current_content_top, - sign.current_content_bottom - ) + |> Utilities.Reader.do_announcements() |> Utilities.Reader.read_sign() |> decrement_ticks() diff --git a/lib/signs/utilities/audio.ex b/lib/signs/utilities/audio.ex index 54442c249..332688c8b 100644 --- a/lib/signs/utilities/audio.ex +++ b/lib/signs/utilities/audio.ex @@ -11,199 +11,139 @@ defmodule Signs.Utilities.Audio do @announced_history_length 5 @heavy_rail_routes ["Red", "Orange", "Blue"] - @doc "Takes a changed line, and returns if it should read immediately" - @spec should_interrupting_read?( - Signs.Realtime.line_content(), - Signs.Realtime.t(), - Content.line_location() - ) :: boolean() - # If minutes is an integer, we don't interrupt - def should_interrupting_read?(%Content.Message.Predictions{minutes: x}, _sign, _line) - when is_integer(x) do - false - end - - # If train is approaching and it's not a heavy rail route, we don't interrupt - def should_interrupting_read?( - %Content.Message.Predictions{minutes: :approaching, route_id: route_id}, - _sign, - _line - ) - when route_id not in @heavy_rail_routes do - false - end + @spec get_announcements(Signs.Realtime.t()) :: {[Content.Audio.t()], Signs.Realtime.t()} + def get_announcements(sign) do + items = decode_sign(sign) - # If train is arriving or approaching and it's being shown on the bottom line, check if it is multi-source and if we announce arriving - def should_interrupting_read?( - %Content.Message.Predictions{minutes: arriving_or_approaching} = prediction, - %Signs.Realtime{source_config: config}, - :bottom - ) - when arriving_or_approaching in [:arriving, :approaching] do - SourceConfig.multi_source?(config) and - announce_arriving?(config, prediction) + {[], sign} + |> get_custom_announcements(items) + |> get_alert_announcements(items) + |> get_prediction_announcements(items) end - # If train is arriving or approaching, check if we announce arriving for this stop - def should_interrupting_read?( - %Content.Message.Predictions{minutes: arriving_or_approaching} = prediction, - %Signs.Realtime{source_config: config}, - _line - ) - when arriving_or_approaching in [:arriving, :approaching] do - announce_arriving?(config, prediction) - end - - # If train is boarding, check if we announce boarding for this stop - # Special case: if arriving announcement was skipped, then interrupt and announce boarding even if we don't normally announce boarding - def should_interrupting_read?( - %Content.Message.Predictions{minutes: :boarding, trip_id: trip_id} = prediction, - %Signs.Realtime{ - id: sign_id, - announced_arrivals: announced_arrivals, - source_config: config - }, - _line - ) do - case announce_boarding?(config, prediction) do - true -> - true - - false -> - if trip_id not in announced_arrivals do - Logger.info( - "announced_brd_when_arr_skipped trip_id=#{inspect(trip_id)} sign_id=#{inspect(sign_id)}" - ) - - true + defp get_custom_announcements({audios, sign}, items) do + case Enum.find(items, &match?({:custom, _, _}, &1)) do + {:custom, top, bottom} -> + [audio] = Audio.Custom.from_messages(top, bottom) + + if sign.announced_custom_text != audio.message do + {audios ++ [audio], %{sign | announced_custom_text: audio.message}} else - false + {audios, sign} end - end - end - - def should_interrupting_read?(%Content.Message.Empty{}, _sign, _line) do - false - end - - def should_interrupting_read?(%Content.Message.StoppedTrain{}, _sign, :bottom) do - false - end - - def should_interrupting_read?(%Content.Message.Headways.Bottom{}, _sign, _line) do - false - end - - def should_interrupting_read?(%Content.Message.Headways.Paging{}, _sign, _line) do - false - end - def should_interrupting_read?(%Content.Message.Alert.NoServiceUseShuttle{}, _sign, _line) do - false + nil -> + {audios, %{sign | announced_custom_text: nil}} + end end - def should_interrupting_read?(%Content.Message.Alert.DestinationNoService{}, _sign, _line) do - false - end + defp get_alert_announcements({audios, sign}, items) do + case Enum.find(items, &match?({:alert, _, _}, &1)) do + {:alert, top, bottom} -> + new_audios = + case top do + %Message.Alert.NoService{} -> + Audio.Closure.from_messages(top, bottom) - def should_interrupting_read?(_content, _sign, _line) do - true - end + %Message.Alert.DestinationNoService{} -> + Audio.NoServiceToDestination.from_message(top) - defp announce_arriving?(source_config, prediction) do - source_config - |> SourceConfig.get_source_by_stop_and_direction(prediction.stop_id, prediction.direction_id) - |> case do - nil -> - false + %Message.Alert.NoServiceUseShuttle{} -> + Audio.NoServiceToDestination.from_message(top) + end - source -> - source.announce_arriving? - end - end + if !sign.announced_alert do + {audios ++ new_audios, %{sign | announced_alert: true}} + else + {audios, sign} + end - defp announce_boarding?(source_config, prediction) do - source_config - |> SourceConfig.get_source_by_stop_and_direction(prediction.stop_id, prediction.direction_id) - |> case do nil -> - false - - source -> - source.announce_boarding? + {audios, %{sign | announced_alert: false}} end end - @spec from_sign(Signs.Realtime.t()) :: {[Content.Audio.t()], Signs.Realtime.t()} - def from_sign(sign) do - multi_source? = SourceConfig.multi_source?(sign.source_config) - - audios = get_audio(sign.current_content_top, sign.current_content_bottom, multi_source?) - - {new_audios, new_approaching_trips, new_arriving_trips} = - Enum.reduce( - audios, - {[], sign.announced_approachings, sign.announced_arrivals}, - fn audio, {new_audios, new_approaching_trips, new_arriving_trips} -> - case audio do - %Audio.TrainIsArriving{trip_id: trip_id, crowding_description: crowding_description} - when not is_nil(trip_id) -> - cond do - # If we've already announced the arrival, don't announce it - audio.trip_id in sign.announced_arrivals -> - {new_audios, new_approaching_trips, new_arriving_trips} - - # If the arrival has high-confidence crowding info but we've already announced crowding with the approaching message, announce it without crowding - crowding_description && audio.trip_id in sign.announced_approachings_with_crowding -> - {new_audios ++ [%{audio | crowding_description: nil}], new_approaching_trips, - [audio.trip_id | new_arriving_trips]} - - # else, announce normally - true -> - {new_audios ++ [audio], new_approaching_trips, - [audio.trip_id | new_arriving_trips]} - end - - %Audio.Approaching{trip_id: trip_id} when not is_nil(trip_id) -> - if audio.trip_id in sign.announced_approachings do - {new_audios, new_approaching_trips, new_arriving_trips} - else - {new_audios ++ [audio], [audio.trip_id | new_approaching_trips], - new_arriving_trips} - end - - _ -> - {new_audios ++ [audio], new_approaching_trips, new_arriving_trips} + defp get_prediction_announcements({audios, sign}, items) do + {new_audios, sign} = + Stream.filter(items, &match?({:predictions, _}, &1)) + |> Enum.flat_map_reduce(sign, fn {:predictions, messages}, sign -> + Stream.with_index(messages) + |> Enum.flat_map_reduce(sign, fn {message, index}, sign -> + cond do + # Announce boarding if configured to. Also, if we normally announce arrivals, but the + # prediction went straight to boarding, announce boarding instead. + match?(%Message.Predictions{minutes: :boarding}, message) && + message.trip_id not in sign.announced_boardings && + (announce_boarding?(sign, message) || + (announce_arriving?(sign, message) && + message.trip_id not in sign.announced_arrivals)) -> + {Audio.TrainIsBoarding.from_message(message), + update_in(sign.announced_boardings, &cache_value(&1, message.trip_id))} + + # Announce arriving if configured to + match?(%Message.Predictions{minutes: :arriving}, message) && + message.trip_id not in sign.announced_arrivals && + announce_arriving?(sign, message) -> + include_crowding? = + message.crowding_data_confidence == :high && + message.trip_id not in sign.announced_approachings_with_crowding + + {Audio.TrainIsArriving.from_message(message, include_crowding?), + update_in(sign.announced_arrivals, &cache_value(&1, message.trip_id))} + + # Announce approaching if configured to + match?(%Message.Predictions{minutes: :approaching}, message) && + message.trip_id not in sign.announced_approachings && + announce_arriving?(sign, message) && + message.route_id in @heavy_rail_routes -> + include_crowding? = message.crowding_data_confidence == :high + + {Audio.Approaching.from_message(message, include_crowding?), + sign + |> update_in( + [Access.key!(:announced_approachings)], + &cache_value(&1, message.trip_id) + ) + |> update_in( + [Access.key!(:announced_approachings_with_crowding)], + &if(include_crowding?, do: cache_value(&1, message.trip_id), else: &1) + )} + + # Announce stopped trains + match?(%Message.StoppedTrain{}, message) && index == 0 && + {message.trip_id, message.stops_away} not in sign.announced_stalls -> + {Audio.StoppedTrain.from_message(message), + update_in( + sign.announced_stalls, + &cache_value(&1, {message.trip_id, message.stops_away}) + )} + + # If we didn't have any predictions for a particular route/direction last update, but + # now we do, announce the next prediction. + match?(%Message.Predictions{}, message) && is_integer(message.minutes) && index == 0 && + sign.prev_prediction_keys && + {message.route_id, message.direction_id} not in sign.prev_prediction_keys -> + {Audio.NextTrainCountdown.from_message(message), sign} + + true -> + {[], sign} end - end - ) + end) + end) - new_announced_approaching_with_crowding = - for %Audio.Approaching{trip_id: trip_id, crowding_description: crowding_description} <- - new_audios, - not is_nil(trip_id) and not is_nil(crowding_description) do - trip_id - end + log_crowding(new_audios, sign.id) sign = %{ sign - | announced_approachings: Enum.take(new_approaching_trips, @announced_history_length), - announced_approachings_with_crowding: - Enum.take( - new_announced_approaching_with_crowding ++ sign.announced_approachings_with_crowding, - @announced_history_length - ), - announced_arrivals: Enum.take(new_arriving_trips, @announced_history_length) + | prev_prediction_keys: + for {:predictions, list} <- items, message <- list, uniq: true do + {message.route_id, message.direction_id} + end } + # Disable crowding messages for now new_audios = - if SourceConfig.multi_source?(sign.source_config) do - sort_audio(new_audios) - else - new_audios - end - |> tap(&log_crowding(&1, sign.id)) - |> Enum.map(fn %{__struct__: audio_type} = audio -> + Enum.map(new_audios, fn %{__struct__: audio_type} = audio -> if audio_type in [Audio.Approaching, Audio.TrainIsArriving] and sign.id not in [] do %{audio | crowding_description: nil} else @@ -211,7 +151,96 @@ defmodule Signs.Utilities.Audio do end end) - {new_audios, sign} + {audios ++ new_audios, sign} + end + + defp cache_value(list, value), do: [value | list] |> Enum.take(@announced_history_length) + + # Reconstructs higher level information about what's being shown on the sign, in a form that's + # suitable for computing audio messages. Eventually the goal is to produce this information + # earlier in the pipeline, rather than deriving it here. + @spec decode_sign(Signs.Realtime.t()) :: [ + {:custom, Content.Message.t(), Content.Message.t()} + | {:alert, Content.Message.t(), Content.Message.t() | nil} + | {:predictions, [Content.Message.t()]} + ] + defp decode_sign(sign) do + case sign do + %Signs.Realtime{current_content_top: top, current_content_bottom: bottom} + when top.__struct__ == Message.Custom or bottom.__struct__ == Message.Custom -> + [{:custom, top, bottom}] + + %Signs.Realtime{ + current_content_top: %Message.Alert.NoService{} = top, + current_content_bottom: bottom + } -> + [{:alert, top, bottom}] + + %Signs.Realtime{ + current_content_top: %Message.GenericPaging{} = top, + current_content_bottom: %Message.GenericPaging{} = bottom + } -> + Enum.zip(top.messages, bottom.messages) |> Enum.map(&decode_lines/1) + + # Mezzanine signs get separate treatment for each half, e.g. they will return two + # separate prediction lists with one prediction each. + %Signs.Realtime{source_config: {_, _}} -> + decode_line(sign.current_content_top) ++ decode_line(sign.current_content_bottom) + + _ -> + decode_lines({sign.current_content_top, sign.current_content_bottom}) + end + end + + defp decode_lines({top, bottom}) do + case {top, bottom} do + {top, bottom} + when top.__struct__ in [Message.Predictions, Message.StoppedTrain] and + bottom.__struct__ in [Message.Predictions, Message.StoppedTrain] -> + [{:predictions, [top, bottom]}] + + {top, _} when top.__struct__ in [Message.Predictions, Message.StoppedTrain] -> + [{:predictions, [top]}] + + _ -> + [] + end + end + + defp decode_line(line) do + case line do + %Message.Predictions{} -> [{:predictions, [line]}] + %Message.StoppedTrain{} -> [{:predictions, [line]}] + %Message.Alert.NoServiceUseShuttle{} -> [{:alert, line, nil}] + %Message.Alert.DestinationNoService{} -> [{:alert, line, nil}] + _ -> [] + end + end + + defp announce_arriving?( + %Signs.Realtime{source_config: source_config}, + %Message.Predictions{stop_id: stop_id, direction_id: direction_id} + ) do + case SourceConfig.get_source_by_stop_and_direction(source_config, stop_id, direction_id) do + nil -> false + source -> source.announce_arriving? + end + end + + defp announce_boarding?( + %Signs.Realtime{source_config: source_config}, + %Message.Predictions{stop_id: stop_id, direction_id: direction_id} + ) do + case SourceConfig.get_source_by_stop_and_direction(source_config, stop_id, direction_id) do + nil -> false + source -> source.announce_boarding? + end + end + + @spec from_sign(Signs.Realtime.t()) :: {[Content.Audio.t()], Signs.Realtime.t()} + def from_sign(sign) do + multi_source? = SourceConfig.multi_source?(sign.source_config) + {get_audio(sign.current_content_top, sign.current_content_bottom, multi_source?), sign} end defp log_crowding(new_audios, sign_id) do @@ -238,18 +267,6 @@ defmodule Signs.Utilities.Audio do end) end - @spec sort_audio([Content.Audio.t()]) :: [Content.Audio.t()] - defp sort_audio(audios) do - Enum.sort_by(audios, fn audio -> - case audio do - %Content.Audio.TrainIsBoarding{} -> 1 - %Content.Audio.TrainIsArriving{} -> 2 - %Content.Audio.Approaching{} -> 3 - _ -> 4 - end - end) - end - @spec get_audio(Signs.Realtime.line_content(), Signs.Realtime.line_content(), boolean()) :: [Content.Audio.t()] defp get_audio( diff --git a/lib/signs/utilities/reader.ex b/lib/signs/utilities/reader.ex index 8794cc1d2..81132af7c 100644 --- a/lib/signs/utilities/reader.ex +++ b/lib/signs/utilities/reader.ex @@ -1,14 +1,11 @@ defmodule Signs.Utilities.Reader do - @moduledoc """ - Periodically sends audio requests to read the contents of the sign. - If the headsign on the second line is different from the top line, will - read that as well. - """ - alias Signs.Utilities.Messages - @spec read_sign(Signs.Realtime.t()) :: Signs.Realtime.t() def read_sign(%{tick_read: 0} = sign) do - {_announced, sign} = send_audio_update(sign) + {audios, sign} = Signs.Utilities.Audio.from_sign(sign) + + if audios != [] do + send_audio(sign, audios) + end %{sign | tick_read: sign.read_period_seconds} end @@ -17,104 +14,19 @@ defmodule Signs.Utilities.Reader do sign end - @spec do_interrupting_reads( - atom - | %{:current_content_bottom => any, :current_content_top => any, optional(any) => any}, - any, - any - ) :: - atom - | %{:current_content_bottom => any, :current_content_top => any, optional(any) => any} - def do_interrupting_reads( - sign, - old_top, - old_bottom - ) do - case {Messages.same_content?(old_top, sign.current_content_top), - Messages.same_content?(old_bottom, sign.current_content_bottom)} do - {true, true} -> - sign - - # update top - {false, true} -> - if Signs.Utilities.Audio.should_interrupting_read?(sign.current_content_top, sign, :top) do - interrupting_read(sign) - else - sign - end - - # update bottom - {true, false} -> - if Signs.Utilities.Audio.should_interrupting_read?( - sign.current_content_bottom, - sign, - :bottom - ) do - interrupting_read(sign) - else - sign - end - - # update both - {false, false} -> - new_top_already_interrupting_read? = - is_boarding_message?(sign.current_content_top) && - Messages.same_content?(old_bottom, sign.current_content_top) - - if !new_top_already_interrupting_read? && - (Signs.Utilities.Audio.should_interrupting_read?( - sign.current_content_top, - sign, - :top - ) || - Signs.Utilities.Audio.should_interrupting_read?( - sign.current_content_bottom, - sign, - :bottom - )) do - interrupting_read(sign) - else - sign - end - end - end - - @spec interrupting_read(Signs.Realtime.t()) :: Signs.Realtime.t() - def interrupting_read(%{tick_read: 0} = sign) do - sign - end - - def interrupting_read(sign) do - case send_audio_update(sign) do - {true, sign} -> - if sign.tick_read < 120 do - %{sign | tick_read: sign.tick_read + sign.read_period_seconds} - else - sign - end + @spec do_announcements(Signs.Realtime.t()) :: Signs.Realtime.t() + def do_announcements(sign) do + {audios, sign} = Signs.Utilities.Audio.get_announcements(sign) - {false, sign} -> - sign + if audios != [] do + send_audio(sign, audios) + update_in(sign.tick_read, &if(&1 < 120, do: &1 + sign.read_period_seconds, else: &1)) + else + sign end end - @spec send_audio_update(Signs.Realtime.t()) :: {boolean(), Signs.Realtime.t()} - defp send_audio_update(sign) do - case Signs.Utilities.Audio.from_sign(sign) do - {[], sign} -> - {false, sign} - - {audios, sign} -> - sign.sign_updater.send_audio(sign.audio_id, audios, 5, 60, sign.id) - {true, sign} - end - end - - @spec is_boarding_message?(Content.Message.t()) :: boolean - defp is_boarding_message?(msg) do - case msg do - %Content.Message.Predictions{minutes: :boarding} -> true - _ -> false - end + defp send_audio(sign, audios) do + sign.sign_updater.send_audio(sign.audio_id, audios, 5, 60, sign.id) end end diff --git a/test/content/messages/stopped_train_test.exs b/test/content/messages/stopped_train_test.exs index b9080148b..65fa251e4 100644 --- a/test/content/messages/stopped_train_test.exs +++ b/test/content/messages/stopped_train_test.exs @@ -35,31 +35,22 @@ defmodule Content.Message.StoppedTrainTest do test "parses 'Stopped n stations away' message" do prediction = %{@prediction | boarding_status: "Stopped 5 stops away"} - assert Content.Message.StoppedTrain.from_prediction(prediction) == - %Content.Message.StoppedTrain{ - destination: :alewife, - stops_away: 5 - } + assert %Content.Message.StoppedTrain{destination: :alewife, stops_away: 5} = + Content.Message.StoppedTrain.from_prediction(prediction) end test "parses singular stop" do prediction = %{@prediction | boarding_status: "Stopped 1 stop away"} - assert Content.Message.StoppedTrain.from_prediction(prediction) == - %Content.Message.StoppedTrain{ - destination: :alewife, - stops_away: 1 - } + assert %Content.Message.StoppedTrain{destination: :alewife, stops_away: 1} = + Content.Message.StoppedTrain.from_prediction(prediction) end test "parses 2-digit stops" do prediction = %{@prediction | boarding_status: "Stopped 10 stops away"} - assert Content.Message.StoppedTrain.from_prediction(prediction) == - %Content.Message.StoppedTrain{ - destination: :alewife, - stops_away: 10 - } + assert %Content.Message.StoppedTrain{destination: :alewife, stops_away: 10} = + Content.Message.StoppedTrain.from_prediction(prediction) end test "handles unknown final stop_id" do diff --git a/test/signs/realtime_test.exs b/test/signs/realtime_test.exs index e402838f3..cb052418f 100644 --- a/test/signs/realtime_test.exs +++ b/test/signs/realtime_test.exs @@ -53,6 +53,16 @@ defmodule Signs.RealtimeTest do current_content_bottom: %HB{range: {11, 13}} } + @terminal_sign %{ + @sign + | source_config: %{ + @sign.source_config + | sources: [ + %{@src | terminal?: true, announce_arriving?: false, announce_boarding?: true} + ] + } + } + @no_service_audio {:canned, {"107", ["861", "21000", "864", "21000", "863"], :audio}} setup :verify_on_exit! @@ -134,7 +144,9 @@ defmodule Signs.RealtimeTest do expect(Engine.Alerts.Mock, :max_stop_status, fn _, _ -> :suspension_closed_station end) expect_messages({"custom", "message"}) expect_audios([{:ad_hoc, {"custom message", :audio}}]) - Signs.Realtime.handle_info(:run_loop, @sign) + + assert {_, %{announced_custom_text: "custom message"}} = + Signs.Realtime.handle_info(:run_loop, @sign) end test "when sign is disabled, it's empty" do @@ -180,7 +192,7 @@ defmodule Signs.RealtimeTest do expect(Engine.Alerts.Mock, :max_stop_status, fn _, _ -> :station_closure end) expect_messages({"No train service", ""}) expect_audios([@no_service_audio]) - Signs.Realtime.handle_info(:run_loop, @sign) + assert {_, %{announced_alert: true}} = Signs.Realtime.handle_info(:run_loop, @sign) end test "predictions take precedence over alerts" do @@ -303,7 +315,7 @@ defmodule Signs.RealtimeTest do test "When the train is stopped a long time away, but not quite max time, shows stopped" do expect(Engine.Predictions.Mock, :for_stop, fn _, _ -> - [prediction(destination: :mattapan, arrival: 1100, stopped: 8)] + [prediction(destination: :mattapan, arrival: 1100, stopped: 8, trip_id: "1")] end) expect_messages( @@ -330,7 +342,7 @@ defmodule Signs.RealtimeTest do ], :audio}} ]) - Signs.Realtime.handle_info(:run_loop, @sign) + assert {_, %{announced_stalls: [{"1", 8}]}} = Signs.Realtime.handle_info(:run_loop, @sign) end test "When the train is stopped a long time away from a terminal, shows max time instead of stopped" do @@ -346,11 +358,7 @@ defmodule Signs.RealtimeTest do end) expect_messages({"Mattapan 30+ min", ""}) - - Signs.Realtime.handle_info(:run_loop, %{ - @sign - | source_config: %{@sign.source_config | sources: [%{@src | terminal?: true}]} - }) + Signs.Realtime.handle_info(:run_loop, @terminal_sign) end test "When the train is stopped a long time away, shows max time instead of stopped" do @@ -365,7 +373,7 @@ defmodule Signs.RealtimeTest do test "only the first prediction in a source list can be BRD" do expect(Engine.Predictions.Mock, :for_stop, fn _, _ -> [ - prediction(destination: :mattapan, arrival: 0, stops_away: 0), + prediction(destination: :mattapan, arrival: 0, stops_away: 0, trip_id: "1"), prediction(destination: :mattapan, arrival: 100, stops_away: 1) ] end) @@ -373,11 +381,10 @@ defmodule Signs.RealtimeTest do expect_messages({"Mattapan BRD", "Mattapan 2 min"}) expect_audios([ - {:canned, {"109", ["501", "21000", "507", "21000", "4100", "21000", "544"], :audio}}, - {:canned, {"160", ["4100", "503", "5002"], :audio}} + {:canned, {"109", ["501", "21000", "507", "21000", "4100", "21000", "544"], :audio}} ]) - Signs.Realtime.handle_info(:run_loop, @sign) + assert {_, %{announced_boardings: ["1"]}} = Signs.Realtime.handle_info(:run_loop, @sign) end test "Sorts boarding status to the top" do @@ -392,26 +399,7 @@ defmodule Signs.RealtimeTest do expect_audios([ {:canned, - {"111", ["501", "21000", "537", "21000", "507", "21000", "4203", "21000", "544"], :audio}}, - {:canned, - {"117", - [ - "501", - "21000", - "536", - "21000", - "507", - "21000", - "4202", - "21000", - "503", - "21000", - "504", - "21000", - "5003", - "21000", - "505" - ], :audio}} + {"111", ["501", "21000", "537", "21000", "507", "21000", "4203", "21000", "544"], :audio}} ]) Signs.Realtime.handle_info(:run_loop, @sign) @@ -426,40 +414,17 @@ defmodule Signs.RealtimeTest do end) expect_messages({"Clvlnd Cir ARR", "Riverside 1 min"}) - - expect_audios([ - {:canned, {"103", ["90007"], :audio_visual}}, - {:canned, - {"117", - [ - "501", - "21000", - "538", - "21000", - "507", - "21000", - "4084", - "21000", - "503", - "21000", - "504", - "21000", - "5001", - "21000", - "532" - ], :audio}} - ]) - + expect_audios([{:canned, {"103", ["90007"], :audio_visual}}]) Signs.Realtime.handle_info(:run_loop, @sign) end test "allows ARR on second line if platform does have multiple berths" do expect(Engine.Predictions.Mock, :for_stop, fn _, _ -> - [prediction(destination: :cleveland_circle, arrival: 15, stop_id: "1")] + [prediction(destination: :cleveland_circle, arrival: 15, stop_id: "1", trip_id: "1")] end) expect(Engine.Predictions.Mock, :for_stop, fn _, _ -> - [prediction(destination: :riverside, arrival: 16, stop_id: "2")] + [prediction(destination: :riverside, arrival: 16, stop_id: "2", trip_id: "2")] end) expect_messages({"Clvlnd Cir ARR", "Riverside ARR"}) @@ -490,11 +455,7 @@ defmodule Signs.RealtimeTest do end) expect_messages({"Boston Col 3 min", "Clvlnd Cir 4 min"}) - - Signs.Realtime.handle_info(:run_loop, %{ - @sign - | source_config: %{@sign.source_config | sources: [%{@src | terminal?: true}]} - }) + Signs.Realtime.handle_info(:run_loop, @terminal_sign) end test "properly handles case where destination can't be determined" do @@ -512,19 +473,22 @@ defmodule Signs.RealtimeTest do destination: :riverside, stops_away: 0, seconds_until_arrival: -30, - seconds_until_departure: 60 + seconds_until_departure: 60, + trip_id: "1" ), prediction( destination: :riverside, stops_away: 0, seconds_until_arrival: -15, - seconds_until_departure: 75 + seconds_until_departure: 75, + trip_id: "2" ), prediction( destination: :boston_college, stops_away: 0, seconds_until_arrival: nil, - seconds_until_departure: 60 + seconds_until_departure: 60, + trip_id: "3" ) ] end) @@ -578,7 +542,96 @@ defmodule Signs.RealtimeTest do ]) expect_messages({"Wonderland BRD", ""}) - Signs.Realtime.handle_info(:run_loop, %{@sign | text_id: {"BBOW", "e"}}) + + Signs.Realtime.handle_info(:run_loop, %{ + @sign + | text_id: {"BBOW", "e"}, + source_config: %{@sign.source_config | sources: [%{@src | direction_id: 1}]} + }) + end + + test "doesn't announce arrivals if disabled in the config" do + expect(Engine.Predictions.Mock, :for_stop, fn _, _ -> + [prediction(destination: :alewife, arrival: 10)] + end) + + expect_messages({"Alewife ARR", ""}) + + Signs.Realtime.handle_info(:run_loop, %{ + @sign + | source_config: %{@sign.source_config | sources: [%{@src | announce_arriving?: false}]} + }) + end + + test "doesn't announce arrivals if already announced previously" do + expect(Engine.Predictions.Mock, :for_stop, fn _, _ -> + [prediction(destination: :alewife, arrival: 10, trip_id: "1")] + end) + + expect_messages({"Alewife ARR", ""}) + Signs.Realtime.handle_info(:run_loop, %{@sign | announced_arrivals: ["1"]}) + end + + test "announces approaching" do + expect(Engine.Predictions.Mock, :for_stop, fn _, _ -> + [prediction(destination: :ashmont, arrival: 45, trip_id: "1")] + end) + + expect_messages({"Ashmont 1 min", ""}) + expect_audios([{:canned, {"103", ["32127"], :audio_visual}}]) + assert {_, %{announced_approachings: ["1"]}} = Signs.Realtime.handle_info(:run_loop, @sign) + end + + test "doesn't announce approaching if already announced previously" do + expect(Engine.Predictions.Mock, :for_stop, fn _, _ -> + [prediction(destination: :alewife, arrival: 45, trip_id: "1")] + end) + + expect_messages({"Alewife 1 min", ""}) + Signs.Realtime.handle_info(:run_loop, %{@sign | announced_approachings: ["1"]}) + end + + test "doesn't announce approaching for light rail" do + expect(Engine.Predictions.Mock, :for_stop, fn _, _ -> + [prediction(destination: :cleveland_circle, arrival: 45)] + end) + + expect_messages({"Clvlnd Cir 1 min", ""}) + Signs.Realtime.handle_info(:run_loop, @sign) + end + + test "announces next prediction if we weren't showing any before" do + expect(Engine.Predictions.Mock, :for_stop, fn _, _ -> + [ + prediction(destination: :ashmont, arrival: 120), + prediction(destination: :ashmont, arrival: 240) + ] + end) + + expect_messages({"Ashmont 2 min", "Ashmont 4 min"}) + expect_audios([{:canned, {"90", ["4016", "503", "5002"], :audio}}]) + Signs.Realtime.handle_info(:run_loop, %{@sign | prev_prediction_keys: []}) + end + + test "doesn't announce ordinary predictions if we had some last time" do + expect(Engine.Predictions.Mock, :for_stop, fn _, _ -> + [prediction(destination: :ashmont, arrival: 120)] + end) + + expect_messages({"Ashmont 2 min", ""}) + Signs.Realtime.handle_info(:run_loop, %{@sign | prev_prediction_keys: [{"Red", 0}]}) + end + + test "announcements delay upcoming readouts" do + expect(Engine.Predictions.Mock, :for_stop, fn _, _ -> + [prediction(destination: :ashmont, arrival: 45, trip_id: "1")] + end) + + expect_messages({"Ashmont 1 min", ""}) + expect_audios([{:canned, {"103", ["32127"], :audio_visual}}]) + + assert {_, %{tick_read: 119}} = + Signs.Realtime.handle_info(:run_loop, %{@sign | tick_read: 20}) end test "Announce approaching with crowding when condfidence high" do diff --git a/test/signs/utilities/audio_test.exs b/test/signs/utilities/audio_test.exs index 3e6c7df4f..dd6c20155 100644 --- a/test/signs/utilities/audio_test.exs +++ b/test/signs/utilities/audio_test.exs @@ -39,232 +39,6 @@ defmodule Signs.Utilities.AudioTest do read_period_seconds: 100 } - describe "should_interrupting_read?/3" do - test "returns false if it's a numeric prediction" do - message = %Message.Predictions{destination: :alewife, minutes: 5} - refute should_interrupting_read?(message, {[@src]}, :top) - refute should_interrupting_read?(message, {[@src]}, :bottom) - end - - test "If it's ARR respects config's announce_arriving?, except on the bottom line of a single-source sign" do - message = %Message.Predictions{ - destination: :alewife, - minutes: :arriving, - stop_id: "1", - direction_id: 0 - } - - src = %{@src | announce_arriving?: false} - - refute should_interrupting_read?( - message, - %{@sign | source_config: %{sources: [src]}}, - :top - ) - - refute should_interrupting_read?( - message, - %{@sign | source_config: %{sources: [src]}}, - :bottom - ) - - src = %{@src | announce_arriving?: true} - - assert should_interrupting_read?( - message, - %{@sign | source_config: %{sources: [src]}}, - :top - ) - - assert should_interrupting_read?( - message, - %{@sign | source_config: {%{sources: [src]}, %{sources: [src]}}}, - :bottom - ) - - refute should_interrupting_read?( - message, - %{@sign | source_config: %{sources: [src]}}, - :bottom - ) - end - - test "If it's Approaching, respects config's announce_arriving? for heavy rail, except on the bottom line of a single-source sign" do - message = %Message.Predictions{ - destination: :alewife, - minutes: :approaching, - route_id: "Red", - direction_id: 0, - stop_id: "1" - } - - src = %{@src | announce_arriving?: false} - - refute should_interrupting_read?( - message, - %{@sign | source_config: %{sources: [src]}}, - :top - ) - - refute should_interrupting_read?( - message, - %{@sign | source_config: %{sources: [src]}}, - :bottom - ) - - src = %{@src | announce_arriving?: true} - - assert should_interrupting_read?( - message, - %{@sign | source_config: %{sources: [src]}}, - :top - ) - - assert should_interrupting_read?( - message, - %{@sign | source_config: {%{sources: [src]}, %{sources: [src]}}}, - :bottom - ) - - refute should_interrupting_read?( - message, - %{@sign | source_config: %{sources: [src]}}, - :bottom - ) - end - - test "If it's Approaching, does not interrupt for light rail" do - message = %Message.Predictions{ - destination: :riverside, - minutes: :approaching, - route_id: "Green-D" - } - - src = %{@src | announce_arriving?: true} - - refute should_interrupting_read?( - message, - %{@sign | source_config: %{sources: [src]}}, - :top - ) - end - - test "If it's Approaching, does not interrupt when announce_boarding?: true" do - message = %Message.Predictions{destination: :alewife, minutes: :approaching} - src = %{@src | announce_arriving?: false, announce_boarding?: true} - - refute should_interrupting_read?( - message, - %{@sign | source_config: %{sources: [src]}}, - :top - ) - end - - test "If it's BRD respects config's announce_boarding? when arrival was already announced" do - message = %Message.Predictions{ - destination: :alewife, - minutes: :boarding, - trip_id: "trip1", - stop_id: "1", - direction_id: 0 - } - - src = %{@src | announce_boarding?: false} - - refute should_interrupting_read?( - message, - %{@sign | source_config: %{sources: [src]}, announced_arrivals: ["trip1"]}, - :top - ) - - refute should_interrupting_read?( - message, - %{@sign | source_config: %{sources: [src]}, announced_arrivals: ["trip1"]}, - :bottom - ) - - src = %{@src | announce_boarding?: true} - - assert should_interrupting_read?( - message, - %{@sign | source_config: %{sources: [src]}, announced_arrivals: ["trip1"]}, - :top - ) - - assert should_interrupting_read?( - message, - %{@sign | source_config: %{sources: [src]}, announced_arrivals: ["trip1"]}, - :bottom - ) - end - - test "If it's BRD and announce_boarding? is false, but arrival for trip was not announced, still read" do - message = %Message.Predictions{destination: :alewife, minutes: :boarding, trip_id: "trip1"} - src = %{@src | announce_boarding?: false} - - log = - capture_log([level: :info], fn -> - assert should_interrupting_read?( - message, - %{@sign | source_config: %{sources: [src]}, announced_arrivals: []}, - :top - ) - end) - - assert log =~ "announced_brd_when_arr_skipped trip_id=\"trip1\" sign_id=\"sign_id\"" - end - - test "returns false if it's empty" do - refute should_interrupting_read?( - %Message.Empty{}, - %{@sign | source_config: %{sources: [@src]}}, - :top - ) - - refute should_interrupting_read?( - %Message.Empty{}, - %{@sign | source_config: %{sources: [@src]}}, - :bottom - ) - end - - test "returns false if it's the bottom line and a stopped train message" do - message = %Message.StoppedTrain{destination: :alewife, stops_away: 2} - - assert should_interrupting_read?( - message, - %{@sign | source_config: %{sources: [@src]}}, - :top - ) - - refute should_interrupting_read?( - message, - %{@sign | source_config: %{sources: [@src]}}, - :bottom - ) - end - - test "returns false for bottom headway message" do - message = %Message.Headways.Bottom{range: {1, 5}, prev_departure_mins: nil} - - refute should_interrupting_read?( - message, - %{@sign | source_config: %{sources: [@src]}}, - :top - ) - end - - test "returns true if it's a different kind of message" do - message = %Message.Headways.Top{destination: :alewife, vehicle_type: :train} - - assert should_interrupting_read?( - {@src, message}, - %{@sign | source_config: %{sources: [@src]}}, - :top - ) - end - end - describe "from_sign/1" do test "Station closure" do sign = %{ @@ -516,8 +290,8 @@ defmodule Signs.Utilities.AudioTest do } assert {[ - %Audio.TrainIsArriving{destination: :riverside}, - %Audio.NextTrainCountdown{destination: :lechmere, minutes: 3} + %Audio.NextTrainCountdown{destination: :lechmere, minutes: 3}, + %Audio.TrainIsArriving{destination: :riverside} ], ^sign} = from_sign(sign) end @@ -670,45 +444,6 @@ defmodule Signs.Utilities.AudioTest do assert log =~ "message_to_audio_error" end - test "announces arriving, then skips arriving for the same trip" do - sign = %{ - @sign - | current_content_top: %Message.Predictions{ - minutes: :arriving, - trip_id: "trip1", - destination: :alewife - }, - current_content_bottom: %Message.Empty{} - } - - {audio, new_sign} = from_sign(sign) - - assert [%Content.Audio.TrainIsArriving{}] = audio - assert new_sign.announced_arrivals == ["trip1"] - - assert {[], ^new_sign} = from_sign(new_sign) - end - - test "announces approaching, then skips approaching for the same trip" do - sign = %{ - @sign - | current_content_top: %Message.Predictions{ - minutes: :approaching, - trip_id: "trip1", - destination: :alewife, - route_id: "Red" - }, - current_content_bottom: %Message.Empty{} - } - - {audio, new_sign} = from_sign(sign) - - assert [%Content.Audio.Approaching{}] = audio - assert new_sign.announced_approachings == ["trip1"] - - assert {[], ^new_sign} = from_sign(new_sign) - end - test "Announces higher priority message first even on bottom of multi-source sign" do sign = %{ @sign @@ -727,8 +462,8 @@ defmodule Signs.Utilities.AudioTest do assert { [ - %Content.Audio.Approaching{destination: :ashmont}, - %Content.Audio.NextTrainCountdown{minutes: 5, destination: :alewife} + %Content.Audio.NextTrainCountdown{minutes: 5, destination: :alewife}, + %Content.Audio.Approaching{destination: :ashmont} ], ^sign } = from_sign(sign) diff --git a/test/signs/utilities/reader_test.exs b/test/signs/utilities/reader_test.exs deleted file mode 100644 index dd4a58100..000000000 --- a/test/signs/utilities/reader_test.exs +++ /dev/null @@ -1,111 +0,0 @@ -defmodule Signs.Utilities.ReaderTest do - use ExUnit.Case, async: true - - alias Content.Message.Custom - alias Content.Message.Empty - alias Content.Message.Predictions - alias Content.Audio.NextTrainCountdown - alias Signs.Utilities.Reader - - defmodule FakePredictions do - def for_stop(_stop_id, _direction_id), do: [] - def stopped_at?(_stop_id), do: false - end - - defmodule FakeUpdater do - def send_audio(audio_id, audio, priority, timeout, sign_id) do - send(self(), {:send_audio, audio_id, audio, priority, timeout, sign_id}) - end - end - - @src %Signs.Utilities.SourceConfig{ - stop_id: "1", - direction_id: 0, - platform: nil, - terminal?: false, - announce_arriving?: false, - announce_boarding?: false - } - - @sign %Signs.Realtime{ - id: "sign_id", - text_id: {"TEST", "x"}, - audio_id: {"TEST", ["x"]}, - source_config: %{sources: [@src]}, - current_content_top: %Predictions{destination: :alewife, minutes: 4}, - current_content_bottom: %Predictions{destination: :ashmont, minutes: 3}, - location_engine: Engine.Locations.Mock, - prediction_engine: FakePredictions, - headway_engine: FakeHeadways, - config_engine: Engine.Config, - alerts_engine: nil, - current_time_fn: nil, - sign_updater: FakeUpdater, - last_update: Timex.now(), - tick_read: 1, - read_period_seconds: 100 - } - - describe "read_sign/1" do - test "when the sign is not on a read interval, does not send next train announcements" do - sign = %{@sign | tick_read: 100} - - Reader.read_sign(sign) - - refute_received({:send_audio, _id, _, _p, _t}) - refute_received({:send_audio, _id, _, _p, _t}) - end - - test "when the sign is on a read interval, sends next train announcements" do - sign = %{@sign | tick_read: 0} - - Reader.read_sign(sign) - - assert_received({:send_audio, _id, _, _p, _t, _}) - end - - test "when the sign is on a read interval, sends a single-line custom announcement" do - sign = %{ - @sign - | tick_read: 0, - current_content_top: %Custom{line: :top, message: "Custom Top"}, - current_content_bottom: %Empty{} - } - - Reader.read_sign(sign) - - assert_received( - {:send_audio, _id, [%Content.Audio.Custom{}], _priority, _timeout, _sign_id} - ) - end - end - - describe "interrupting_read/1" do - test "does not send audio when tick read is 0, because it will be read by the read loop" do - sign = %{@sign | tick_read: 0} - - Reader.interrupting_read(sign) - - refute_received( - {:send_audio, _id, %NextTrainCountdown{destination: :alewife, minutes: 4, verb: :arrives}, - _p, _t} - ) - - refute_received( - {:send_audio, _id, %NextTrainCountdown{destination: :ashmont, minutes: 3, verb: :arrives}, - _p, _t} - ) - end - - test "bumps sign's read loop if interrupted with under 120 seconds to go" do - sign_under = %{@sign | tick_read: 119, read_period_seconds: 20} - sign_over = %{@sign | tick_read: 120, read_period_seconds: 20} - - new_sign_under = Reader.interrupting_read(sign_under) - new_sign_over = Reader.interrupting_read(sign_over) - - assert new_sign_under.tick_read == 139 - assert new_sign_over.tick_read == 120 - end - end -end diff --git a/test/signs/utilities/updater_test.exs b/test/signs/utilities/updater_test.exs index 1ef8f20ba..fa12cfb91 100644 --- a/test/signs/utilities/updater_test.exs +++ b/test/signs/utilities/updater_test.exs @@ -1,6 +1,5 @@ defmodule Signs.Utilities.UpdaterTest do use ExUnit.Case, async: true - import ExUnit.CaptureLog alias Content.Message.Predictions, as: P alias Signs.Utilities.Updater @@ -20,15 +19,6 @@ defmodule Signs.Utilities.UpdaterTest do end end - @src %Signs.Utilities.SourceConfig{ - stop_id: "1", - direction_id: 0, - platform: nil, - terminal?: false, - announce_arriving?: false, - announce_boarding?: false - } - @sign %Signs.Realtime{ id: "sign_id", text_id: {"TEST", "x"}, @@ -70,20 +60,5 @@ defmodule Signs.Utilities.UpdaterTest do assert_received({:update_sign, _id, %P{minutes: 3}, %P{minutes: 2}, _dur, _start, _sign_id}) assert sign.last_update == now end - - test "doesn't do an interrupting read if new top is same as old bottom and is a boarding message" do - src = %{@src | announce_boarding?: true} - - sign = %{ - @sign - | current_content_top: {src, %P{destination: :alewife, minutes: :boarding}}, - current_content_bottom: {src, %P{destination: :ashmont, minutes: :boarding}} - } - - diff_top = {src, %P{destination: :ashmont, minutes: :boarding}} - diff_bottom = {src, %P{destination: :alewife, minutes: 19}} - Updater.update_sign(sign, diff_top, diff_bottom, Timex.now()) - refute_received({:send_audio, _, _, _, _, _}) - end end end