Skip to content

Commit

Permalink
Merge branch 'main' into cm/sl-headway-mode
Browse files Browse the repository at this point in the history
  • Loading branch information
cmaddox5 authored Jul 1, 2024
2 parents a87318b + bf977c2 commit 5c51975
Show file tree
Hide file tree
Showing 9 changed files with 488 additions and 165 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,7 @@ erl_crash.dump
*.ez

# local direnv configuration
/.envrc
/.envrc

/priv/tmp
/priv/output
45 changes: 15 additions & 30 deletions lib/engine/last_trip.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule Engine.LastTrip do

@recent_departures_table :recent_departures
@last_trips_table :last_trips
@hour_in_seconds 3600
@timezone "America/New_York"

@type state :: %{
recent_departures: :ets.tab(),
Expand Down Expand Up @@ -42,7 +42,7 @@ defmodule Engine.LastTrip do

@impl true
def init(_) do
schedule_clean(self())
schedule_loop(self())

state = %{
recent_departures: @recent_departures_table,
Expand Down Expand Up @@ -90,39 +90,24 @@ defmodule Engine.LastTrip do
end

@impl true
def handle_info(:clean_old_data, state) do
schedule_clean(self())
clean_last_trips(state)
clean_recent_departures(state)
def handle_info(:loop, state) do
schedule_loop(self())

{:noreply, state}
end

defp clean_last_trips(state) do
:ets.tab2list(state.last_trips)
|> Enum.each(fn {trip_id, timestamp} ->
if Timex.diff(Timex.now(), timestamp, :seconds) > @hour_in_seconds * 2 do
:ets.delete(state.last_trips, trip_id)
end
end)
end
{:ok, current_time_est} = DateTime.utc_now() |> DateTime.shift_zone(@timezone)

defp clean_recent_departures(state) do
current_time = Timex.now()
if current_time_est.hour == 3 and current_time_est.minute >= 30 do
clean_tables(state)
end

:ets.tab2list(state.recent_departures)
|> Enum.each(fn {key, departures} ->
departures_within_last_hour =
Map.filter(departures, fn {_, departed_time} ->
DateTime.to_unix(current_time) - DateTime.to_unix(departed_time) <=
@hour_in_seconds * 1.5
end)
{:noreply, state}
end

:ets.insert(state.recent_departures, {key, departures_within_last_hour})
end)
defp clean_tables(state) do
:ets.delete_all_objects(state.last_trips)
:ets.delete_all_objects(state.recent_departures)
end

defp schedule_clean(pid) do
Process.send_after(pid, :clean_old_data, 1_000)
defp schedule_loop(pid) do
Process.send_after(pid, :loop, 1_000)
end
end
9 changes: 6 additions & 3 deletions lib/pa_ess/utilities.ex
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,8 @@ defmodule PaEss.Utilities do
{"Waltham", ["Waltham"]},
{"Haymarket", ["Haymrkt", "Haymarkt", "Haymarket"]},
{"Silver Line Way", ["Slvr Ln Way"]},
{"Gallivan Blvd", ["Gallivn", "Gallivan"]}
{"Gallivan Blvd", ["Gallivn", "Gallivan"]},
{"Cobbs Corner Canton", ["Canton"]}
]

@spec headsign_abbreviations(String.t() | nil) :: [String.t()]
Expand Down Expand Up @@ -596,7 +597,8 @@ defmodule PaEss.Utilities do
{"Chelsea", "860"},
{"Gallivan Blvd", "881"},
{"Brookline Ave", "885"},
{"Brookline Village", "886"}
{"Brookline Village", "886"},
{"Cobbs Corner Canton", "887"}
]

@route_take_lookup %{
Expand Down Expand Up @@ -654,7 +656,8 @@ defmodule PaEss.Utilities do
"226" => "809",
"230" => "810",
"236" => "811",
"245" => "628"
"245" => "628",
"716" => "888"
}

@atom_take_lookup %{
Expand Down
175 changes: 162 additions & 13 deletions lib/signs/bus.ex
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ defmodule Signs.Bus do
{:ok, state}
end

@type content_values ::
{messages :: [Content.Message.value()], audios :: [Content.Audio.value()],
tts_audios :: [Content.Audio.tts_value()]}

@impl true
def handle_info(:run_loop, state) do
Process.send_after(self(), :run_loop, 1000)
Expand Down Expand Up @@ -167,10 +171,10 @@ defmodule Signs.Bus do
|> Enum.group_by(&{&1.stop_id, &1.route_id, &1.direction_id})

# Compute new sign text and audio
{[top, bottom], audios} =
{[top, bottom], audios, tts_audios} =
cond do
config_off?(config, all_route_ids) ->
{["", ""], []}
{_messages = ["", ""], _audios = [], _tts_audios = []}

match?({:static_text, _}, config) ->
static_text_content(
Expand Down Expand Up @@ -227,12 +231,16 @@ defmodule Signs.Bus do
end)
|> then(fn state ->
if should_read?(current_time, state) do
send_audio(audios, state)
send_audio(audios, tts_audios, state)
%{state | last_read_time: current_time}
else
if should_announce_drawbridge?(bridge_status, bridge_enabled?, current_time, state) do
bridge_audio(bridge_status, bridge_enabled?, current_time, state)
|> send_audio(state)
bridge_audios = bridge_audio(bridge_status, bridge_enabled?, current_time, state)

bridge_tts_audios =
bridge_tts_audio(bridge_status, bridge_enabled?, current_time, state)

send_audio(bridge_audios, bridge_tts_audios, state)
end

state
Expand Down Expand Up @@ -294,6 +302,15 @@ defmodule Signs.Bus do
end

# Static text mode. Just display the configured text, and possibly the bridge message.
@spec static_text_content(
Engine.Config.sign_config(),
term(),
boolean(),
DateTime.t(),
map(),
map(),
t()
) :: content_values()
defp static_text_content(
config,
bridge_status,
Expand Down Expand Up @@ -323,11 +340,15 @@ defmodule Signs.Bus do
[{:ad_hoc, {"#{line1} #{line2}", :audio}}]
|> Enum.concat(bridge_audio(bridge_status, bridge_enabled?, current_time, state))

{messages, audios}
tts_audios = [{"#{line1} #{line2}", nil}]

{messages, audios, tts_audios}
end

# Platform mode. Display one prediction per route, but if all the predictions are for the
# same route, then show a single page of two.
@spec platform_mode_content(map(), map(), map(), DateTime.t(), term(), boolean(), t()) ::
content_values()
defp platform_mode_content(
predictions_lookup,
route_alerts_lookup,
Expand Down Expand Up @@ -392,11 +413,30 @@ defmodule Signs.Bus do
|> paginate_audio()
|> Enum.concat(bridge_audio(bridge_status, bridge_enabled?, current_time, state))

{messages, audios}
tts_audios =
case audio_content do
[] ->
[]

[single] ->
[long_message_tts_audio(single, current_time, state)]

list ->
Enum.map(list, &message_tts_audio(&1, current_time, state))
|> Enum.join(" ")
|> add_tts_preamble()
|> List.wrap()
end
|> Enum.map(&{&1, nil})
|> Enum.concat(bridge_tts_audio(bridge_status, bridge_enabled?, current_time, state))

{messages, audios, tts_audios}
end
end

# Mezzanine mode. Display and paginate each line separately.
@spec mezzanine_mode_content(map(), map(), map(), DateTime.t(), term(), boolean(), t()) ::
content_values()
defp mezzanine_mode_content(
predictions_lookup,
route_alerts_lookup,
Expand Down Expand Up @@ -431,20 +471,38 @@ defmodule Signs.Bus do
|> paginate_audio()
|> Enum.concat(bridge_audio(bridge_status, bridge_enabled?, current_time, state))

{messages, audios}
tts_audios =
case top_content ++ bottom_content do
[] ->
[]

list ->
Enum.map(list, &message_tts_audio(&1, current_time, state))
|> Enum.join(" ")
|> add_tts_preamble()
|> List.wrap()
end
|> Enum.map(&{&1, nil})
|> Enum.concat(bridge_tts_audio(bridge_status, bridge_enabled?, current_time, state))

{messages, audios, tts_audios}
end
end

@spec special_harvard_content() :: content_values()
defp special_harvard_content() do
messages = ["Board routes 71", "and 73 on upper level"]
audios = paginate_audio([:board_routes_71_and_73_on_upper_level])
{messages, audios}
tts_audios = [{"Board routes 71 and 73 on upper level", nil}]
{messages, audios, tts_audios}
end

@spec no_service_content() :: content_values()
defp no_service_content do
messages = paginate_pairs([["No bus service", ""]])
audios = paginate_audio([:no_bus_service])
{messages, audios}
tts_audios = [{"No bus service", nil}]
{messages, audios, tts_audios}
end

defp configs_content(nil, _, _), do: []
Expand Down Expand Up @@ -614,6 +672,36 @@ defmodule Signs.Bus do
end
end

defp bridge_tts_audio(bridge_status, bridge_enabled?, current_time, state) do
%{chelsea_bridge: chelsea_bridge} = state

if bridge_enabled? && chelsea_bridge &&
bridge_status_raised?(bridge_status, current_time) do
{duration, duration_spanish} =
case bridge_status_minutes(bridge_status, current_time) do
minutes when minutes < 2 ->
{"We expect it to be lowered soon.", "Esperamos que se baje pronto."}

minutes ->
{"We expect this to last for at least #{minutes} more minutes.",
"Esperamos que esto dure al menos #{minutes} minutos más."}
end

english_text =
"The Chelsea Street bridge is raised. #{duration} SL3 buses may be delayed, detoured, or turned back."

spanish_text =
"El puente de Chelsea Street está levantado. #{duration_spanish} Los autobuses SL3 pueden sufrir retrasos, desvíos o dar marcha atrás."

[
{english_text, PaEss.Utilities.paginate_text(english_text)},
{spanish_text, PaEss.Utilities.paginate_text(spanish_text)}
]
else
[]
end
end

defp format_message(nil, _, _, _state), do: ""

defp format_message({:predictions, [first | _]}, other, current_time, _state) do
Expand Down Expand Up @@ -744,6 +832,8 @@ defmodule Signs.Bus do
defp add_preamble([]), do: []
defp add_preamble(items), do: [[:upcoming_departures] | items]

defp add_tts_preamble(str), do: "Upcoming departures: " <> str

# Returns a list of audio tokens describing the given message.
defp message_audio({:predictions, [prediction | _]}, current_time, _state) do
route =
Expand Down Expand Up @@ -776,6 +866,34 @@ defmodule Signs.Bus do
route ++ [{:headsign, headsign}, :no_service]
end

defp message_tts_audio({:predictions, [prediction | _]}, current_time, _state) do
route =
case PaEss.Utilities.prediction_route_name(prediction) do
nil -> ""
name -> "Route #{name}, "
end

time =
case prediction_minutes(prediction, current_time) do
0 -> "arriving"
1 -> "1 minute"
m -> "#{m} minutes"
end

"#{route}#{prediction.headsign}, #{time}."
end

defp message_tts_audio({:alert, config}, _, state) do
route =
case config_route_name(config) do
nil -> ""
name -> "Route #{name}, "
end

headsign = config_headsign(config, state)
"#{route}#{headsign}, no service."
end

# Returns a list of audio tokens representing the special "long form" description of
# the given prediction.
defp long_message_audio({:predictions, predictions}, current_time, _state) do
Expand Down Expand Up @@ -814,6 +932,38 @@ defmodule Signs.Bus do
[Enum.concat([[:there_is_no], route, [:bus_service_to, {:headsign, headsign}]])]
end

defp long_message_tts_audio({:predictions, predictions}, current_time, _state) do
Stream.take(predictions, 2)
|> Enum.zip_with(["next", "following"], fn prediction, next_or_following ->
route =
case PaEss.Utilities.prediction_route_name(prediction) do
nil -> ""
name -> "route #{name} "
end

time =
case prediction_minutes(prediction, current_time) do
0 -> "is now arriving"
1 -> "arrives in 1 minute"
m -> "arrives in #{m} minutes"
end

"The #{next_or_following} #{route}bus to #{prediction.headsign} #{time}."
end)
|> Enum.join(" ")
end

defp long_message_tts_audio({:alert, config}, _, state) do
route =
case config_route_name(config) do
nil -> ""
name -> "route #{name} "
end

headsign = config_headsign(config, state)
"There is no #{route}bus service to #{headsign}."
end

# Turns a list of audio tokens into a list of audio messages, chunking as needed to stay under
# the max var limit.
defp paginate_audio(items) do
Expand All @@ -830,15 +980,14 @@ defmodule Signs.Bus do
end)
end

defp send_audio(audios, state) do
defp send_audio(audios, tts_audios, state) do
%{audio_zones: audio_zones, sign_updater: sign_updater} = state

if audios != [] && audio_zones != [] do
sign_updater.play_message(
state,
audios,
# TODO: Implement TTS for bus audio
[],
tts_audios,
Enum.map(audios, fn _ -> [message_type: "Bus"] end)
)
end
Expand Down
Loading

0 comments on commit 5c51975

Please sign in to comment.