Skip to content

Commit

Permalink
chore: rework "static" screen parameters
Browse files Browse the repository at this point in the history
The central/motivating change here is a change to `screens-config-lib`
that removes most audio-related parameters from the per-screen configs,
since we'd like them to be the same for all screens of a given type.

This creates a `Static` struct to encode/organize "static" parameters
for each screen type — including candidate generators, refresh rate,
periodic audio readout interval, and now various other audio settings.
  • Loading branch information
digitalcora committed Oct 21, 2024
1 parent a924faa commit 7a201f7
Show file tree
Hide file tree
Showing 11 changed files with 358 additions and 310 deletions.
90 changes: 23 additions & 67 deletions lib/screens/v2/screen_audio_data.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@ defmodule Screens.V2.ScreenAudioData do
@moduledoc false

alias Screens.Config.Cache
alias Screens.Util
alias Screens.V2.ScreenData
alias Screens.V2.ScreenData.Parameters
alias Screens.V2.WidgetInstance
alias ScreensConfig.Screen
alias ScreensConfig.V2.{Audio, BusShelter, Busway, GlEink, PreFare}

@type screen_id :: String.t()

@spec by_screen_id(screen_id()) :: list({module(), map()}) | :error
@spec by_screen_id(screen_id()) :: list({module(), map()})
def by_screen_id(
screen_id,
get_config_fn \\ &Cache.screen/1,
Expand All @@ -20,32 +17,25 @@ defmodule Screens.V2.ScreenAudioData do
now \\ DateTime.utc_now()
) do
config = get_config_fn.(screen_id)
{:ok, now} = DateTime.shift_zone(now, "America/New_York")

case config do
%Screen{app_params: %app{}} when app not in [BusShelter, PreFare, GlEink, Busway] ->
:error

%Screen{app_params: %_app{audio: audio}} ->
if date_in_range?(audio, now) do
visual_widgets_with_audio_equivalence =
config
|> generate_layout_fn.()
|> elem(1)
|> Map.values()
|> Enum.filter(&WidgetInstance.audio_valid_candidate?/1)

audio_only_widgets =
visual_widgets_with_audio_equivalence
|> get_audio_only_instances_fn.(config)
|> Enum.filter(&WidgetInstance.audio_valid_candidate?/1)

(visual_widgets_with_audio_equivalence ++ audio_only_widgets)
|> Enum.sort_by(&WidgetInstance.audio_sort_key/1)
|> Enum.map(&{WidgetInstance.audio_view(&1), WidgetInstance.audio_serialize(&1)})
else
[]
end
if Parameters.audio_enabled?(config, now) do
visual_widgets_with_audio_equivalence =
config
|> generate_layout_fn.()
|> elem(1)
|> Map.values()
|> Enum.filter(&WidgetInstance.audio_valid_candidate?/1)

audio_only_widgets =
visual_widgets_with_audio_equivalence
|> get_audio_only_instances_fn.(config)
|> Enum.filter(&WidgetInstance.audio_valid_candidate?/1)

(visual_widgets_with_audio_equivalence ++ audio_only_widgets)
|> Enum.sort_by(&WidgetInstance.audio_sort_key/1)
|> Enum.map(&{WidgetInstance.audio_view(&1), WidgetInstance.audio_serialize(&1)})
else
[]
end
end

Expand All @@ -55,50 +45,16 @@ defmodule Screens.V2.ScreenAudioData do
get_config_fn \\ &Cache.screen/1,
now \\ DateTime.utc_now()
) do
config = get_config_fn.(screen_id)
{:ok, now} = DateTime.shift_zone(now, "America/New_York")

case config do
%Screen{app_params: %app{}} when app not in [BusShelter] ->
:error

%Screen{app_params: %_app{audio: audio}} ->
{:ok, get_volume(audio, now)}
case screen_id |> get_config_fn.() |> Parameters.audio_volume(now) do
nil -> :error
volume -> {:ok, volume}
end
end

defp get_audio_only_instances(visual_widgets_with_audio_equivalence, config) do
candidate_generator = Parameters.get_candidate_generator(config)

candidate_generator.audio_only_instances(
Parameters.candidate_generator(config).audio_only_instances(
visual_widgets_with_audio_equivalence,
config
)
end

defp get_volume(
%Audio{
daytime_start_time: daytime_start_time,
daytime_stop_time: daytime_stop_time,
daytime_volume: daytime_volume,
nighttime_volume: nighttime_volume
},
now
) do
if Util.time_in_range?(now, daytime_start_time, daytime_stop_time),
do: daytime_volume,
else: nighttime_volume
end

defp date_in_range?(
%Audio{
start_time: start_time,
stop_time: stop_time,
days_active: days_active
},
dt
) do
Date.day_of_week(dt) in days_active and
Util.time_in_range?(DateTime.to_time(dt), start_time, stop_time)
end
end
6 changes: 3 additions & 3 deletions lib/screens/v2/screen_data.ex
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ defmodule Screens.V2.ScreenData do
selected_variant = Keyword.get(opts, :generator_variant)

if Keyword.get(opts, :run_all_variants?, false) do
other_variants = List.delete([nil | @parameters.get_variants(config)], selected_variant)
other_variants = List.delete([nil | @parameters.variants(config)], selected_variant)

Enum.each(other_variants, fn variant ->
{:ok, _pid} =
Expand All @@ -87,7 +87,7 @@ defmodule Screens.V2.ScreenData do

ParallelRunSupervisor
|> Task.Supervisor.async_stream(
[nil | @parameters.get_variants(config)],
[nil | @parameters.variants(config)],
fn variant ->
{variant, config |> Layout.generate(variant) |> then_fn.(config)}
end
Expand All @@ -114,7 +114,7 @@ defmodule Screens.V2.ScreenData do
end

defp resolve_paging(layout, config),
do: Layout.resolve_paging(layout, @parameters.get_refresh_rate(config))
do: Layout.resolve_paging(layout, @parameters.refresh_rate(config))

@spec serialize(Layout.non_paged()) :: map() | nil
def serialize({layout, instance_map, paging_metadata}) do
Expand Down
2 changes: 1 addition & 1 deletion lib/screens/v2/screen_data/layout.ex
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ defmodule Screens.V2.ScreenData.Layout do
@spec generate(Screen.t()) :: t()
@spec generate(Screen.t(), String.t() | nil) :: t()
def generate(config, variant \\ nil) do
candidate_generator = @parameters.get_candidate_generator(config, variant)
candidate_generator = @parameters.candidate_generator(config, variant)
screen_template = candidate_generator.screen_template()

config
Expand Down
174 changes: 112 additions & 62 deletions lib/screens/v2/screen_data/parameters.ex
Original file line number Diff line number Diff line change
@@ -1,88 +1,138 @@
defmodule Screens.V2.ScreenData.Parameters do
@moduledoc false

alias Screens.Util
alias Screens.V2.CandidateGenerator
alias Screens.V2.ScreenData.Static
alias Screens.V2.ScreenData.Static.PeriodicAudio
alias ScreensConfig.Screen

@app_id_to_candidate_generators %{
bus_eink_v2: CandidateGenerator.BusEink,
bus_shelter_v2: CandidateGenerator.BusShelter,
gl_eink_v2: CandidateGenerator.GlEink,
busway_v2: CandidateGenerator.Busway,
solari_large_v2: CandidateGenerator.SolariLarge,
pre_fare_v2: CandidateGenerator.PreFare,
dup_v2: {
CandidateGenerator.Dup,
%{"new_departures" => CandidateGenerator.DupNew}
@all_times {~T[00:00:00], ~T[23:59:59]}

@static_params %{
bus_eink_v2: %Static{
candidate_generator: CandidateGenerator.BusEink,
refresh_rate: 30
},
bus_shelter_v2: %Static{
audio_active_time: {~T[04:45:00], ~T[01:45:00]},
candidate_generator: CandidateGenerator.BusShelter,
periodic_audio: %PeriodicAudio{
day_volume: 1.0,
interval_minutes: 5,
night_time: {~T[21:00:00], ~T[07:00:00]},
night_volume: 0.5
},
refresh_rate: 20
},
busway_v2: %Static{
audio_active_time: @all_times,
candidate_generator: CandidateGenerator.Busway,
refresh_rate: 15
},
dup_v2: %Static{
candidate_generator: CandidateGenerator.Dup,
refresh_rate: 30,
variants: %{"new_departures" => CandidateGenerator.DupNew}
},
elevator_v2: %Static{
candidate_generator: CandidateGenerator.Elevator,
refresh_rate: 30
},
gl_eink_v2: %Static{
audio_active_time: @all_times,
candidate_generator: CandidateGenerator.GlEink,
refresh_rate: 30
},
pre_fare_v2: %Static{
audio_active_time: {~T[04:45:00], ~T[01:45:00]},
candidate_generator: CandidateGenerator.PreFare,
refresh_rate: 20
},
elevator_v2: CandidateGenerator.Elevator
solari_large_v2: %Static{
candidate_generator: CandidateGenerator.SolariLarge,
refresh_rate: 15
}
}

@app_id_to_refresh_rate %{
bus_eink_v2: 30,
bus_shelter_v2: 20,
gl_eink_v2: 30,
busway_v2: 15,
solari_large_v2: 15,
pre_fare_v2: 20,
dup_v2: 30,
elevator_v2: 30
}
@typep static_params :: %{Screen.app_id() => Static.t()}

@app_id_to_audio_readout_interval %{
bus_eink_v2: 0,
bus_shelter_v2: 5,
gl_eink_v2: 0,
busway_v2: 0,
solari_large_v2: 0,
pre_fare_v2: 0,
dup_v2: 0,
elevator_v2: 0
}
@spec audio_enabled?(Screen.t(), DateTime.t()) :: boolean()
@spec audio_enabled?(Screen.t(), DateTime.t(), static_params()) :: boolean()
def audio_enabled?(%Screen{app_id: app_id}, now, static_params \\ @static_params) do
case Map.fetch!(static_params, app_id) do
%Static{audio_active_time: nil} ->
false

@callback get_candidate_generator(ScreensConfig.Screen.t()) :: module()
@callback get_candidate_generator(ScreensConfig.Screen.t(), String.t() | nil) :: module()
def get_candidate_generator(%ScreensConfig.Screen{app_id: app_id}, variant \\ nil) do
case Map.get(@app_id_to_candidate_generators, app_id) do
{default, _variants} when is_nil(variant) -> default
{_default, variants} -> Map.fetch!(variants, variant)
default -> default
%Static{audio_active_time: {start_time, end_time}} ->
Util.time_in_range?(now, start_time, end_time)
end
end

@callback get_variants(ScreensConfig.Screen.t()) :: [String.t()]
def get_variants(%ScreensConfig.Screen{app_id: app_id}) do
case Map.get(@app_id_to_candidate_generators, app_id) do
{_default, variants} -> Map.keys(variants)
_default -> []
@spec audio_interval_minutes(Screen.app_id()) :: pos_integer() | nil
@spec audio_interval_minutes(Screen.app_id(), static_params()) :: pos_integer() | nil
def audio_interval_minutes(app_id, static_params \\ @static_params) do
case Map.fetch!(static_params, app_id) do
%Static{periodic_audio: nil} -> nil
%Static{periodic_audio: %PeriodicAudio{interval_minutes: interval}} -> interval
end
end

@callback get_refresh_rate(ScreensConfig.Screen.t() | atom()) :: pos_integer() | nil
def get_refresh_rate(%ScreensConfig.Screen{app_id: app_id}) do
get_refresh_rate(app_id)
@spec audio_interval_offset_seconds(Screen.t()) :: pos_integer() | nil
def audio_interval_offset_seconds(%Screen{
app_params: %ScreensConfig.V2.BusShelter{
audio: %ScreensConfig.V2.Audio{interval_offset_seconds: interval_offset_seconds}
}
}) do
interval_offset_seconds
end

def get_refresh_rate(app_id) do
Map.get(@app_id_to_refresh_rate, app_id)
end
def audio_interval_offset_seconds(_screen), do: nil

@spec audio_volume(Screen.t(), DateTime.t()) :: float() | nil
@spec audio_volume(Screen.t(), DateTime.t(), static_params()) :: float() | nil
def audio_volume(%Screen{app_id: app_id}, now, static_params \\ @static_params) do
case Map.fetch!(static_params, app_id) do
%Static{periodic_audio: nil} ->
nil

@spec get_audio_readout_interval(ScreensConfig.Screen.t() | atom()) :: pos_integer() | nil
def get_audio_readout_interval(%ScreensConfig.Screen{app_id: app_id}) do
get_refresh_rate(app_id)
%Static{
periodic_audio: %PeriodicAudio{
day_volume: day_volume,
night_time: {night_start, night_end},
night_volume: night_volume
}
} ->
{:ok, now} = DateTime.shift_zone(now, "America/New_York")
if Util.time_in_range?(now, night_start, night_end), do: night_volume, else: day_volume
end
end

def get_audio_readout_interval(app_id) do
Map.get(@app_id_to_audio_readout_interval, app_id)
@callback candidate_generator(Screen.t()) :: module()
@callback candidate_generator(Screen.t(), String.t() | nil) :: module()
@callback candidate_generator(Screen.t(), String.t() | nil, static_params()) :: module()
def candidate_generator(
%Screen{app_id: app_id},
variant \\ nil,
static_params \\ @static_params
) do
case Map.fetch!(static_params, app_id) do
%Static{candidate_generator: default} when is_nil(variant) -> default
%Static{variants: %{^variant => variant}} -> variant
end
end

@spec get_audio_interval_offset_seconds(ScreensConfig.Screen.t()) :: pos_integer()
def get_audio_interval_offset_seconds(%ScreensConfig.Screen{
app_params: %ScreensConfig.V2.BusShelter{
audio: %ScreensConfig.V2.Audio{interval_offset_seconds: interval_offset_seconds}
}
}) do
interval_offset_seconds
@callback refresh_rate(Screen.app_id()) :: pos_integer() | nil
@callback refresh_rate(Screen.app_id(), static_params()) :: pos_integer() | nil
def refresh_rate(app_id, static_params \\ @static_params) do
%Static{refresh_rate: refresh_rate} = Map.fetch!(static_params, app_id)
refresh_rate
end

def get_audio_interval_offset_seconds(_screen), do: 0
@callback variants(Screen.t()) :: [String.t()]
@callback variants(Screen.t(), static_params()) :: [String.t()]
def variants(%Screen{app_id: app_id}, static_params \\ @static_params) do
%Static{variants: variants} = Map.fetch!(static_params, app_id)
Map.keys(variants)
end
end
37 changes: 37 additions & 0 deletions lib/screens/v2/screen_data/static.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
defmodule Screens.V2.ScreenData.Static do
@moduledoc "Encodes static configuration that is the same across all screens of a given type."

@type time_range :: {start :: Time.t(), stop :: Time.t()}

defmodule PeriodicAudio do
@moduledoc """
Static configuration for screens that read out audio periodically, without rider action.
Currently, screens that support periodic readouts happen to also be the only screens where we
can control the volume of readouts, so this configuration is bundled together.
"""

alias Screens.V2.ScreenData.Static

@type t :: %__MODULE__{
day_volume: float(),
interval_minutes: pos_integer(),
night_time: Static.time_range(),
night_volume: float()
}

@enforce_keys ~w[day_volume interval_minutes night_time night_volume]a
defstruct @enforce_keys
end

@type t :: %__MODULE__{
audio_active_time: time_range() | nil,
candidate_generator: module(),
periodic_audio: PeriodicAudio.t() | nil,
refresh_rate: pos_integer(),
variants: %{String.t() => module()}
}

@enforce_keys ~w[candidate_generator refresh_rate]a
defstruct @enforce_keys ++ [audio_active_time: nil, periodic_audio: nil, variants: %{}]
end
Loading

0 comments on commit 7a201f7

Please sign in to comment.