Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial Implementation for display_segment update_method replace #1820

Open
wants to merge 7 commits into
base: dev
Choose a base branch
from
23 changes: 17 additions & 6 deletions mpf/config_players/segment_display_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,23 @@ def _remove(self, instance_dict, key, display):
del instance_dict[display][key]

def clear_context(self, context):
"""Remove all texts."""
instance_dict = self._get_instance_dict(context)
for display, keys in instance_dict.items():
for key in dict(keys).keys():
self._remove(instance_dict=instance_dict,
key=key, display=display)

##############
# Remove all texts. Ignore what keys are available, that will be checked later in the segment display code.
# Especially important for update_method replace since there are no keys.
##############

instance_dict = self._get_instance_dict(context) # key of the dict is the display, the value is another dict

for display, keys_dict in instance_dict.items(): # keys_dict key is the show key, the value is a boolean (with yet unknown usage)
if(keys_dict): #depending on the situation the keys_dict might be empty, still need to clear the display
for key in dict(keys_dict).keys():
display.clear_segment_display(key)
if instance_dict[display][key] is not True:
self.delay.remove(instance_dict[display][key])
del instance_dict[display][key]
else:
display.clear_segment_display(None)

self._reset_instance_dict(context)

Expand Down
2 changes: 1 addition & 1 deletion mpf/config_spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1498,7 +1498,7 @@ segment_display_player:
action: single|enum(add,remove,flash,no_flash,flash_match,flash_mask,set_color)|add
transition: ignore
transition_out: ignore
flashing: single|enum(off,all,match,mask,not_set)|not_set
flashing: single|enum(off,all,match,mask,not_set)|off
flash_mask: single|str|None
key: single|str|None
expire: single|ms_or_token|None
Expand Down
90 changes: 74 additions & 16 deletions mpf/devices/segment_display/segment_display.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class SegmentDisplay(SystemWideDevice):

__slots__ = ["hw_display", "size", "virtual_connector", "_text_stack", "_current_placeholder",
"_current_text_stack_entry", "_transition_update_task", "_current_transition", "_default_color",
"_current_state", "_current_placeholder_future"]
"_current_state", "_current_placeholder_future", "_previous_text", "_previous_color", "_previous_transition_out"]

config_section = 'segment_displays'
collection = 'segment_displays'
Expand All @@ -73,6 +73,9 @@ def __init__(self, machine, name: str) -> None:
self._current_transition = None # type: Optional[TransitionRunner]
self._default_color = None # type: Optional[RGBColor]
self._current_state = None # type: Optional[SegmentDisplayState]
self._previous_text = None # Last text entry for transitions if update_method = replace
self._previous_color = None # Last color for transitions if update_method = replace
self._previous_transition_out = None # Last transistion_out if update_method = replace

async def _initialize(self):
"""Initialize display."""
Expand All @@ -86,8 +89,14 @@ async def _initialize(self):

self.size = self.config['size']
self._default_color = [RGBColor(color) for color in self.config["default_color"][0:self.size]]
if len(self._default_color) < self.size:
self._default_color += [RGBColor("white")] * (self.size - len(self._default_color))

if (len(self._default_color)) == 1:
self._default_color = self._default_color * self.size
elif len(self._default_color) != self.size:
self.warning_log("The amount of colors you specified for your text has to be either equal to "
"the amount of digits in your display or equals 1. Your display has a size of %s and the "
"amount of colors specified is %s. All display colors will be set to white.", self.size, len(self._default_color))
self._default_color = [RGBColor("white")] * self.size

# configure hardware
try:
Expand Down Expand Up @@ -145,7 +154,12 @@ def add_text_entry(self, text, color, flashing, flash_mask, transition, transiti

This will replace texts with the same key.
"""
if not color:
color = self._current_state.text.get_colors()

if self.config['update_method'] == "stack":


self._text_stack[key] = TextStackEntry(
text, color, flashing, flash_mask, transition, transition_out, priority, key)
self._update_stack()
Expand All @@ -155,11 +169,46 @@ def add_text_entry(self, text, color, flashing, flash_mask, transition, transiti
raise ValueError(f"Unknown update_method '{self.config['update_method']}' for segment display {self.name}")

# For the replace-text update method, skip the stack and write straight to the display
new_text = TextTemplate(self.machine, text).evaluate({})
text = SegmentDisplayText.from_str(new_text, self.size, self.config['integrated_dots'],
self.config['integrated_commas'], self.config['use_dots_for_commas'],
color)
self._update_display(SegmentDisplayState(text, flashing, flash_mask))

###############################
#Store current color and text as previous text/color of next run even if no transition in this step,
#the next step might have a transition, that the old text/color needs to be included into that transition
###############################

# Handle new and previous text
previous_text = self._previous_text or ""
self._previous_text = text # Save the new text as the next previous text

# Handle new and previous color
previous_color = self._previous_color or self._default_color
self._previous_color = color # Save the new color as the next previous color

if transition or self._previous_transition_out:
transition_conf = TransitionManager.get_transition(self.size,
self.config['integrated_dots'],
self.config['integrated_commas'],
self.config['use_dots_for_commas'],
transition or self._previous_transition_out)

#start transition
self._start_transition(transition_conf, previous_text, text,
previous_color, color,
self.config['default_transition_update_hz'], flashing, flash_mask)

else: #No transition configured
if transition_out: #in case transition_out is set we need to preserve it for the next step
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gotcha here is that if there is no transition_out on this transition but the previous transition did have an out, then self._previous_transition_out will still have the value of the last transition out. Best to be explicit and not use an if statement.

self._previous_transition_out = transition_out or None

Which is also in the above if block, so it could be called just once at the end of this method outside of the if/else block.

self._previous_transition_out = transition_out
new_text = TextTemplate(self.machine, text).evaluate({})
text = SegmentDisplayText.from_str(new_text, self.size, self.config['integrated_dots'],
self.config['integrated_commas'], self.config['use_dots_for_commas'],
color)
self._update_display(SegmentDisplayState(text, flashing, flash_mask))

###############################
# Once the transistion_out is played, removed it that is not played in the next step again, but in case transition_out is set in the current step
# then we need to preserve it for the next step but only after the previous step's transition_out is in this step's config (or the transition of the current step)
###############################
self._previous_transition_out = transition_out or None

def add_text(self, text: str, priority: int = 0, key: str = None) -> None:
"""Add text to display stack.
Expand All @@ -170,21 +219,30 @@ def add_text(self, text: str, priority: int = 0, key: str = None) -> None:

def remove_text_by_key(self, key: Optional[str]):
"""Remove entry from text stack."""
if self.config['update_method'] != "stack":
self.info_log("Segment display 'remove' action is TBD.")
return
if self.config['update_method'] == "stack":
if key in self._text_stack:
del self._text_stack[key]
self._update_stack()
else: # must be update_method replace, send empyt text since no key in that case
self.add_text_entry("", self._previous_color, FlashingType.NO_FLASH, "", None, None, 100, key)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good use of an existing method to clear out a display, but the key part is misleading because the argument is passed to the method to define which key to remove, but it's actually adding empty text with that key.

I would suggest instead that this behavior check the current text to see if it has the requested key, and if so remove the text and pass an empty key, and if not do nothing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would tend to disagree on this point. But maybe I misunderstood something
here. I tried to change the code as little as possible that is why we have
this early return (which I would usually try to avoid), maybe that is
misleading. Initially that function was only implemented for the stack case
(and that implementation has not changed).

For the replace case I would claim neither key nor priority is relevant since
we don't have a stack where we can remove something based on a key (or sort
based on a priority). Of course a key is passed as well for the replace case,
but there is nothing you can really do with it (in the replace case). So
removal in that case means rather emptying the display. Especially since that
remove method is as well called when the context is being removed (when a show
gets the stop command).

On a side note: I am working on one fix for the show stop since if you stop a
show then the current transition is still being played (in the replace case)
and a transition_out would be as well played. But it still empties the display.

If you really believe that the key is relevant for the replace case then please rephrase your feedback. Happy to understand it better and to improve.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a use case I'm picturing where key and priority would be helpful, let me know what you think.

A player starts a non-exclusive hurry-up mode (priority 1000) with a jackpot on the left loop, and that hurry-up mode sends the text "HIT LEFT LOOP" to a display. Instead of going for the hurry-up, the player instead starts a multi ball (priority 5000) and the multi ball mode sends the text "JACKPOT: 5M" to the display. During the multi ball, the hurry-up mode times out and ends.

By default, the hurry-up mode is set to remove the "HIT LEFT LOOP" text from the display, so it will send a command to the displays to replace the text with empty string. Without a key, the display doesn't know that the text being shown is not the text that's expected to be removed, and will instead remove the "JACKPOT: 5M" text from the display even though the multi ball is still running. As a result, the multi ball will have a blank display.

Reverse the situation and start the multi ball before the hurry-up. The multi ball mode is higher priority so its display text should have precedence, and the hurry-up display text should be ignored. Otherwise the multi ball "JACKPOT: 5M" will turn into "HIT LEFT LOOP" and then disappear after the hurry-up, again leaving the multi ball with a blank display.

I realize that this type of use case is exactly what the stack mode is meant to do, but I believe there is value in replace also respecting keys and priorities because that creates more consistent behavior in the game. Designers will have the option to override the behavior by specifying keys and priorities if they want to, but regardless the behavior will be the same every time. Ignoring keys and priorities will yield different displays based on timing, which is not ideal.

Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The described use case is very valid and is very much a use case for the stack case. The initial implementation of replace

# For the replace-text update method, skip the stack and write straight to the display

indicates that the text is written straight to the display. Can you describe in words what would be the logic of replace if it includes key and priority and where this behavior differs from the stack case? I simply don't see the logic of such an implementation.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, I'm overthinking it and trying to keep stack-like functionality where it doesn't belong. The concept of replace doesn't need to have any considerations for priority or key or what's currently on the display, and should always overwrite when called.



def clear_segment_display(self, key: Optional[str]):
"""Clear segment dispaly if context is removed from player."""

if self.config['update_method'] == "replace":
self._stop_transition()
self._previous_transition_out = None

self.remove_text_by_key(key)

if key in self._text_stack:
del self._text_stack[key]
self._update_stack()

# pylint: disable=too-many-arguments
def _start_transition(self, transition: TransitionBase, current_text: str, new_text: str,
current_colors: List[RGBColor], new_colors: List[RGBColor],
update_hz: float, flashing, flash_mask):

"""Start the specified transition."""
current_colors = self._expand_colors(current_colors, len(current_text))
new_colors = self._expand_colors(new_colors, len(new_text))
if self._current_transition:
self._stop_transition()
self._current_transition = TransitionRunner(self.machine, transition, current_text, new_text,
Expand Down
32 changes: 17 additions & 15 deletions mpf/devices/segment_display/segment_display_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,30 +74,32 @@ def _embed_dots_and_commas(cls, text: str, collapse_dots: bool, collapse_commas:
def _create_characters(cls, text: str, display_size: int, collapse_dots: bool, collapse_commas: bool,
use_dots_for_commas: bool, colors: List[Optional[RGBColor]]) -> List[DisplayCharacter]:
"""Create characters from text and color them.

- Colors are used from the left to the right (starting with the first character).
- If colors are shorter than text the last color is repeated for text.
- The first color is used to pad the text to the left if text is shorter than the display - thus text is right
aligned.
- Dots and commas are embedded on the fly.
- Text will be right aligned on the display, thus if text is shorter than display spaces will be padded before the text
- If list of colors is less than the display size then all white will be used, if only one color is given that will be used for the full display
"""
char_list = []
left_pad_color = colors[0] if colors else None
default_right_color = colors[len(colors) - 1] if colors else None
uncolored_chars = cls._embed_dots_and_commas(text, collapse_dots, collapse_commas, use_dots_for_commas)
colors = colors[-len(uncolored_chars):]
for char_code, char_has_dot, char_has_comma in uncolored_chars:
color = colors.pop(0) if colors else default_right_color
char_list.append(DisplayCharacter(char_code, char_has_dot, char_has_comma, color))

# ensure list is the same size as the segment display (cut off on left or right justify characters)
current_length = len(char_list)
# Adujust the color array if needed
if (len(colors)) == 1:
colors = colors * display_size
elif len(colors) != display_size:
#TODO: Log that colors were adjusted to white as default
colors = [RGBColor("white")] * display_size

# ensure list is the same size as the segment display (cut off on left if too long or right justify characters if too short)
current_length = len(uncolored_chars)
if current_length > display_size:
for _ in range(current_length - display_size):
char_list.pop(0)
uncolored_chars.pop(0) # remove very left char of array if too long
elif current_length < display_size:
for _ in range(display_size - current_length):
char_list.insert(0, DisplayCharacter(SPACE_CODE, False, False, left_pad_color))
uncolored_chars.insert(0, (SPACE_CODE, False, False))

for i, char in enumerate(uncolored_chars):
color = colors[i]
char_list.append(DisplayCharacter(char[0], char[1], char[2], color)) #0: char code 1: char_has_dot 2: char_has_comma

return char_list

Expand Down
Loading