class BallSave(SystemWideDevice, ModeDevice): """Ball save device which will give back the ball within a certain time.""" config_section = 'ball_saves' collection = 'ball_saves' class_label = 'ball_save' def __init__(self, machine: "MachineController", name: str) -> None: """Initialise ball save.""" self.unlimited_saves = None # type: bool self.source_playfield = None # type: Playfield super().__init__(machine, name) self.delay = DelayManager(machine.delayRegistry) self.enabled = False self.timer_started = False self.saves_remaining = 0 self.early_saved = 0 self.state = 'disabled' self._scheduled_balls = 0 @asyncio.coroutine def _initialize(self) -> None: yield from super()._initialize() self.unlimited_saves = self.config['balls_to_save'] == -1 self.source_playfield = self.config['source_playfield'] @property def can_exist_outside_of_game(self) -> bool: """Return true if this device can exist outside of a game.""" return True def validate_and_parse_config(self, config: dict, is_mode_config: bool, debug_prefix: str = None) -> dict: """Make sure timer_start_events are not in enable_events.""" config = super().validate_and_parse_config(config, is_mode_config, debug_prefix) for event in config['timer_start_events']: if event in config['enable_events']: raise AssertionError( "{}: event {} in timer_start_events will not work because it is also in " "enable_events. Omit it!".format(event, str(self))) if config['delayed_eject_events'] and config['eject_delay']: raise AssertionError( "cannot use delayed_eject_events and eject_delay at the same time." ) return config @event_handler(10) def enable(self, **kwargs) -> None: """Enable ball save.""" del kwargs if self.enabled: return self.saves_remaining = self.config['balls_to_save'] self.early_saved = 0 self.enabled = True self.state = 'enabled' self.debug_log("Enabling. Auto launch: %s, Balls to save: %s", self.config['auto_launch'], self.config['balls_to_save']) # Enable shoot again self.machine.events.add_handler('ball_drain', self._ball_drain_while_active, priority=1000) if (self.config['active_time'] > 0 and not self.config['timer_start_events']): self.timer_start() self.machine.events.post('ball_save_{}_enabled'.format(self.name)) '''event: ball_save_(name)_enabled desc: The ball save called (name) has just been enabled. ''' @event_handler(1) def disable(self, **kwargs) -> None: """Disable ball save.""" del kwargs if not self.enabled: return self.enabled = False self.state = 'disabled' self.timer_started = False self.debug_log("Disabling...") self.machine.events.remove_handler(self._ball_drain_while_active) self.delay.remove('disable') self.delay.remove('hurry_up') self.delay.remove('grace_period') self.machine.events.post('ball_save_{}_disabled'.format(self.name)) '''event: ball_save_(name)_disabled desc: The ball save called (name) has just been disabled. ''' @event_handler(9) def timer_start(self, **kwargs) -> None: """Start the timer. This is usually called after the ball was ejected while the ball save may have been enabled earlier. """ del kwargs if self.timer_started or not self.enabled: return self.timer_started = True self.machine.events.post('ball_save_{}_timer_start'.format(self.name)) '''event: ball_save_(name)_timer_start desc: The ball save called (name) has just start its countdown timer. ''' if self.config['active_time'] > 0: self.debug_log('Starting ball save timer: %ss', self.config['active_time'] / 1000.0) self.delay.add(name='disable', ms=(self.config['active_time'] + self.config['grace_period']), callback=self.disable) self.delay.add(name='grace_period', ms=self.config['active_time'], callback=self._grace_period) self.delay.add(name='hurry_up', ms=(self.config['active_time'] - self.config['hurry_up_time']), callback=self._hurry_up) def _hurry_up(self) -> None: self.debug_log("Starting Hurry Up") self.state = 'hurry_up' self.machine.events.post('ball_save_{}_hurry_up'.format(self.name)) '''event: ball_save_(name)_hurry_up desc: The ball save called (name) has just entered its hurry up mode. ''' def _grace_period(self) -> None: self.debug_log("Starting Grace Period") self.state = 'grace_period' self.machine.events.post('ball_save_{}_grace_period'.format(self.name)) '''event: ball_save_(name)_grace_period desc: The ball save called (name) has just entered its grace period time. ''' def _get_number_of_balls_to_save(self, available_balls: int) -> int: no_balls_in_play = False try: if not self.machine.game.balls_in_play: no_balls_in_play = True if self.config[ 'only_last_ball'] and self.machine.game.balls_in_play > 1: self.debug_log("Will only save last ball but %s are in play.", self.machine.game.balls_in_play) return 0 except AttributeError: no_balls_in_play = True if no_balls_in_play: self.debug_log("Received request to save ball, but no balls are in" " play. Discarding request.") return 0 balls_to_save = available_balls if self.config['only_last_ball'] and balls_to_save > 1: balls_to_save = 1 if balls_to_save > self.machine.game.balls_in_play: balls_to_save = self.machine.game.balls_in_play if balls_to_save > self.saves_remaining and not self.unlimited_saves: balls_to_save = self.saves_remaining return balls_to_save def _reduce_remaining_saves_and_disable_if_zero( self, balls_to_save: int) -> None: if not self.unlimited_saves: self.saves_remaining -= balls_to_save self.debug_log("Saves remaining: %s", self.saves_remaining) else: self.debug_log("Unlimited saves remaining") if self.saves_remaining <= 0 and not self.unlimited_saves: self.debug_log("Disabling since there are no saves remaining") self.disable() def _ball_drain_while_active(self, balls: int, **kwargs) -> Optional[dict]: del kwargs if balls <= 0: return {} balls_to_save = self._get_number_of_balls_to_save(balls) self.debug_log( "Ball(s) drained while active. Requesting new one(s). " "Autolaunch: %s", self.config['auto_launch']) self.machine.events.post('ball_save_{}_saving_ball'.format(self.name), balls=balls_to_save, early_save=False) '''event: ball_save_(name)_saving_ball desc: The ball save called (name) has just saved one (or more) balls. args: balls: The number of balls this ball saver is saving. early_save: True if this is an early ball save. ''' self._schedule_balls(balls_to_save) self._reduce_remaining_saves_and_disable_if_zero(balls_to_save) return {'balls': balls - balls_to_save} @event_handler(8) def early_ball_save(self, **kwargs) -> None: """Perform early ball save if enabled.""" del kwargs if not self.enabled: return if not self._get_number_of_balls_to_save(1): return if self.early_saved > 0: self.debug_log( "Already performed an early ball save. Ball needs to drain first." ) return self.machine.events.post('ball_save_{}_saving_ball'.format(self.name), balls=1, early_save=True) # doc block above self.debug_log("Performing early ball save.") self.early_saved += 1 self._schedule_balls(1) self.machine.events.add_handler('ball_drain', self._early_ball_save_drain_handler, priority=1001) self._reduce_remaining_saves_and_disable_if_zero(1) def _early_ball_save_drain_handler(self, balls: int, **kwargs) -> dict: del kwargs if self.early_saved and balls > 0: balls -= 1 self.early_saved -= 1 self.debug_log("Early saved ball drained.") self.machine.events.remove_handler( self._early_ball_save_drain_handler) return {'balls': balls} else: return {} def _schedule_balls(self, balls_to_save: int) -> None: if self.config['eject_delay']: # schedule after delay. to add some drama self.delay.add(self.config['eject_delay'], self._add_balls, balls_to_save=balls_to_save) elif self.config['delayed_eject_events']: # unlimited delay. wait for event self._scheduled_balls += balls_to_save else: # default: no delay. just eject balls right now self._add_balls(balls_to_save) @event_handler(4) def delayed_eject(self, **kwargs): """Trigger eject of all scheduled balls.""" del kwargs self._add_balls(self._scheduled_balls) self._scheduled_balls = 0 def _add_balls(self, balls_to_save, **kwargs): del kwargs self.source_playfield.add_ball( balls=balls_to_save, player_controlled=self.config['auto_launch'] ^ 1) def device_removed_from_mode(self, mode: Mode) -> None: """Disable ball save when mode ends.""" del mode self.debug_log("Removing...") self.disable() if self.config['delayed_eject_events']: self.debug_log("Triggering delayed eject because mode ended.") self.delayed_eject()
class Stepper(SystemWideDevice): """Represents an stepper motor based axis in a pinball machine. Args: Same as the Device parent class. """ config_section = 'steppers' collection = 'steppers' class_label = 'stepper' def __init__(self, machine, name): """Initialise stepper.""" self.hw_stepper = None self.platform = None # type: Stepper self._cachedPosition = 0 # in user units self._ball_search_started = False self._min_pos = 0 self._max_pos = 1 self.positionMode = False self._cachedVelocity = 0 self._isHomed = False self._isMoving = False self._move_complete_pollrate = 100 # ms self._resetPosition = 0 self._position = None self._max_velocity = None self.delay = DelayManager(machine.delayRegistry) super().__init__(machine, name) def _initialize(self): self.platform = self.machine.get_platform_sections('stepper_controllers', self.config['platform']) for position in self.config['named_positions']: self.machine.events.add_handler(self.config['named_positions'][position], self._position_event, position=position) self.hw_stepper = self.platform.configure_stepper(self.config['number'], self.config) self._position = self.config['reset_position'] self._max_pos = self.config['pos_max'] self._min_pos = self.config['pos_min'] self._max_velocity = self.config['velocity_limit'] self._resetPosition = self.config['reset_position'] mode = self.config['mode'] if mode == 'position': self.positionMode = True elif mode == 'velocity': self.positionMode = False else: raise AssertionError("Operating Mode not defined") if self.config['include_in_ball_search']: self.machine.events.add_handler("ball_search_started", self._ball_search_start) self.machine.events.add_handler("ball_search_stopped", self._ball_search_stop) def current_position(self): """Return position in user units (vs microsteps).""" return self.hw_stepper.current_position() def move_abs_pos(self, position): """Move servo to position.""" if self._ball_search_started: return if not self.positionMode: raise RuntimeError("Cannot do a position move in velocity mode") if self._min_pos <= position <= self._max_pos: self.hw_stepper.move_abs_pos(position) if self._isMoving is False: # already moving, don't re-kickoff polling self._isMoving = True self._schedule_move_complete_check() else: raise ValueError("move_abs: position argument beyond limits") def home(self): """Home an axis, resetting 0 position.""" if self.positionMode: self.hw_stepper.home() self._isHomed = False if self._isMoving is False: # already moving, don't re-kickoff polling self._isMoving = True self._schedule_home_complete_check() else: raise RuntimeError("Cannot home in velocity mode") def move_rel_pos(self, delta): """Move axis to a relative position.""" start = self.current_position() self.move_abs_pos(start + delta) def move_vel_mode(self, velocity): """Move at a specific velocity indefinitely.""" if self.positionMode: raise RuntimeError("Cannot do a velocity move in position mode") if velocity <= self._max_velocity: self.hw_stepper.move_vel_mode(velocity) self._cachedVelocity = velocity else: raise ValueError("move_vel_mode: velocity argument is above limit") def stop(self): """Stop motor.""" self.hw_stepper.stop() self._isMoving = False self._cachedVelocity = 0.0 self.delay.remove('stepper_move_complete_check') self.delay.remove('stepper_home_complete_check') def _schedule_move_complete_check(self): self.delay.add(name='stepper_move_complete_check', ms=self._move_complete_pollrate, callback=self._check_mv_complete) def _check_mv_complete(self): # TODO add timeout that stops this with error event if it hasn't made it in some amount of time if not self._isMoving: return if self.hw_stepper.is_move_complete(): self._isMoving = False self._cachedPosition = self.current_position() self.machine.events.post('stepper_' + self.name + "_ready") '''event: stepper_(name)_ready''' else: # reschedule self._schedule_move_complete_check() def _schedule_home_complete_check(self): self.delay.add(name='stepper_home_complete_check', ms=self._move_complete_pollrate, callback=self._check_home_complete) def _check_home_complete(self): # TODO add timeout that stops this with error event if it hasn't made it in some amount of time if self._isHomed: return if self.hw_stepper.is_move_complete(): self._isMoving = False self._isHomed = True self.machine.events.post('stepper_' + self.name + "_ready") '''event: stepper_(name)_ready''' else: # reschedule self._schedule_home_complete_check() @event_handler(1) def reset(self, **kwargs): """Stop Motor.""" del kwargs self.stop() if self.positionMode: self.home() self.move_abs_pos(self._resetPosition) @event_handler(5) def _position_event(self, position, **kwargs): del kwargs self.move_abs_pos(position) def _ball_search_start(self, **kwargs): del kwargs # we do not touch self._position during ball search so we can reset to # it later self._ball_search_started = True self._ball_search_go_to_min() def _ball_search_go_to_min(self): self._move_abs_pos(self.config['ball_search_min']) self.delay.add(name="ball_search", callback=self._ball_search_go_to_max, ms=self.config['ball_search_wait']) def _ball_search_go_to_max(self): self._move_abs_pos(self.config['ball_search_max']) self.delay.add(name="ball_search", callback=self._ball_search_go_to_min, ms=self.config['ball_search_wait']) def _ball_search_stop(self, **kwargs): del kwargs # stop delay self.delay.remove("ball_search") self._ball_search_started = False # move to last commanded if self.positionMode: self.move_abs_pos(self._cachedPosition) else: self.move_vel_mode(self._cachedVelocity)
class Diverter(SystemWideDevice): """Represents a diverter in a pinball machine. Args: Same as the Device parent class. """ config_section = 'diverters' collection = 'diverters' class_label = 'diverter' def __init__(self, machine, name): """Initialise diverter.""" super().__init__(machine, name) self.delay = DelayManager(machine.delayRegistry) # Attributes self.active = False self.enabled = False self.platform = None self.diverting_ejects_count = 0 self.eject_state = False self.eject_attempt_queue = deque() def _initialize(self): # register for feeder device eject events for feeder_device in self.config['feeder_devices']: self.machine.events.add_handler( 'balldevice_' + feeder_device.name + '_ball_eject_attempt', self._feeder_eject_attempt) self.machine.events.add_handler( 'balldevice_' + feeder_device.name + '_ejecting_ball', self._feeder_ejecting) self.machine.events.add_handler( 'balldevice_' + feeder_device.name + '_ball_eject_failed', self._feeder_eject_count_decrease) self.machine.events.add_handler( 'balldevice_' + feeder_device.name + '_ball_eject_success', self._feeder_eject_count_decrease) self.machine.events.add_handler('init_phase_3', self._register_switches) self.platform = self.config['activation_coil'].platform if self.config['ball_search_order']: self.config['playfield'].ball_search.register( self.config['ball_search_order'], self._ball_search, self.name) def _register_switches(self, **kwargs): del kwargs # register for deactivation switches for switch in self.config['deactivation_switches']: self.machine.switch_controller.add_switch_handler( switch.name, self.deactivate) # register for disable switches: for switch in self.config['disable_switches']: self.machine.switch_controller.add_switch_handler( switch.name, self.disable) def reset(self, **kwargs): """Reset and deactivate the diverter.""" del kwargs self.deactivate() def enable(self, auto=False, **kwargs): """Enable this diverter. Args: auto: Boolean value which is used to indicate whether this diverter enabled itself automatically. This is passed to the event which is posted. **kwargs: unused If an 'activation_switches' is configured, then this method writes a hardware autofire rule to the pinball controller which fires the diverter coil when the switch is activated. If no `activation_switches` is specified, then the diverter is activated immediately. """ del kwargs self.enabled = True self.machine.events.post('diverter_' + self.name + '_enabling', auto=auto) '''event: diverter_(name)_enabling desc: The diverter called (name) is enabling itself. Note that if this diverter has ``activation_switches:`` configured, it will not physically activate until one of those switches is hit. Otherwise this diverter will activate immediately. args: auto: Boolean which indicates whether this diverter enabled itself automatically for the purpose of routing balls to their proper location(s). ''' if self.config['activation_switches']: self._enable_switches() else: self.activate() def disable(self, auto=False, **kwargs): """Disable this diverter. This method will remove the hardware rule if this diverter is activated via a hardware switch. Args: auto: Boolean value which is used to indicate whether this diverter disabled itself automatically. This is passed to the event which is posted. **kwargs: This is here because this disable method is called by whatever event the game programmer specifies in their machine configuration file, so we don't know what event that might be or whether it has random kwargs attached to it. """ del kwargs self.enabled = False self.machine.events.post('diverter_' + self.name + '_disabling', auto=auto) '''event: diverter_(name)_disabling desc: The diverter called (name) is disabling itself. Note that if this diverter has ``activation_switches:`` configured, it will not physically deactivate now, instead deactivating based on switch hits and timing. Otherwise this diverter will deactivate immediately. args: auto: Boolean which indicates whether this diverter disabled itself automatically for the purpose of routing balls to their proper location(s). ''' self.debug_log("Disabling Diverter") if self.config['activation_switches']: self._disable_switches() # if there is no deactivation way if not (self.config['activation_time'] or self.config['deactivation_switches'] or self.config['deactivate_events']): self.deactivate() def activate(self, **kwargs): """Physically activate this diverter's coil.""" del kwargs self.debug_log("Activating Diverter") self.active = True self.machine.events.post('diverter_' + self.name + '_activating') '''event: diverter_(name)_activating desc: The diverter called (name) is activating itself, which means it's physically pulsing or holding the coil to move. ''' if self.config['type'] == 'pulse': self.config['activation_coil'].pulse() elif self.config['type'] == 'hold': self.config['activation_coil'].enable() self.schedule_deactivation() def deactivate(self, **kwargs): """Deactivate this diverter. This method will disable the activation_coil, and (optionally) if it's configured with a deactivation coil, it will pulse it. """ del kwargs self.debug_log("Deactivating Diverter") self.active = False if self.config['activation_time']: self.delay.remove('deactivate_timed') self.machine.events.post('diverter_' + self.name + '_deactivating') '''event: diverter_(name)_deactivating desc: The diverter called (name) is deativating itself. ''' self.config['activation_coil'].disable() if self.config['deactivation_coil']: self.config['deactivation_coil'].pulse() def schedule_deactivation(self): """Schedule a delay to deactivate this diverter.""" if self.config['activation_time']: self.delay.add(name='deactivate_timed', ms=self.config['activation_time'], callback=self.deactivate) def _enable_switches(self): """Register switch handler on activation switches.""" self.debug_log("Enabling Diverter sw switches: %s", self.config['activation_switches']) for switch in self.config['activation_switches']: self.machine.switch_controller.add_switch_handler( switch_name=switch.name, callback=self.activate) def _disable_switches(self): """Deregister switch handlers for activation switches.""" self.debug_log("Disabling Diverter sw switches: %s", self.config['activation_switches']) for switch in self.config['activation_switches']: self.machine.switch_controller.remove_switch_handler( switch_name=switch.name, callback=self.activate) def _feeder_eject_count_decrease(self, target, **kwargs): del target del kwargs self.diverting_ejects_count -= 1 if self.diverting_ejects_count <= 0: self.diverting_ejects_count = 0 # If there are ejects waiting for the other target switch diverter if len(self.eject_attempt_queue) > 0: if not self.eject_state: self.eject_state = True self.debug_log( "Enabling diverter since eject target is on the " "active target list") self.enable() elif self.eject_state: self.eject_state = False self.debug_log( "Enabling diverter since eject target is on the " "inactive target list") self.disable() # And perform those ejects while len(self.eject_attempt_queue) > 0: self.diverting_ejects_count += 1 queue = self.eject_attempt_queue.pop() queue.clear() elif self.active and not self.config['activation_time']: # if diverter is active and no more ejects are ongoing self.deactivate() def _get_desired_state(self, target): desired_state = None if target in self.config['targets_when_active']: desired_state = True elif target in self.config['targets_when_inactive']: desired_state = False return desired_state def _feeder_eject_attempt(self, queue, target, **kwargs): # Event handler which is called when one of this diverter's feeder # devices attempts to eject a ball. This is what allows this diverter # to get itself in the right position to send the ball to where it needs # to go. # Since the 'target' kwarg is going to be an object, not a name, we need # to figure out if this object is one of the targets of this diverter. del kwargs self.debug_log("Feeder device eject attempt for target: %s", target) desired_state = self._get_desired_state(target) if desired_state is None: self.debug_log("Feeder device ejects to an unknown target: %s. " "Ignoring!", target.name) return if self.diverting_ejects_count > 0 and self.eject_state != desired_state: self.debug_log("Feeder devices tries to eject to a target which " "would require a state change. Postponing that " "because we have an eject to the other side") queue.wait() self.eject_attempt_queue.append(queue) return self.diverting_ejects_count += 1 self.eject_state = desired_state def _feeder_ejecting(self, target, **kwargs): """Enable or disable diverter on eject.""" del kwargs self.debug_log("Feeder device is ejecting for target: %s", target) desired_state = self._get_desired_state(target) if desired_state is None: self.debug_log("Feeder device ejects to an unknown target: %s. " "Ignoring!", target.name) return if desired_state: self.debug_log("Enabling diverter since eject target is on the " "active target list") self.enable() elif not desired_state: self.debug_log("Enabling diverter since eject target is on the " "inactive target list") self.disable() def _ball_search(self, phase, iteration): del phase del iteration self.activate() self.machine.delay.add(self.config['ball_search_hold_time'], self.deactivate, 'diverter_{}_ball_search'.format(self.name)) return True
class LogicBlock(SystemWideDevice, ModeDevice): """Parent class for each of the logic block classes.""" __slots__ = ["delay", "_state", "_start_enabled", "player_state_variable"] def __init__(self, machine: MachineController, name: str) -> None: """Initialize logic block.""" super().__init__(machine, name) self.delay = DelayManager(self.machine) self._state = None # type: Optional[LogicBlockState] self._start_enabled = None # type: Optional[bool] self.player_state_variable = "{}_state".format(self.name) '''player_var: (logic_block)_state config_section: counters, accruals, sequences desc: A dictionary that stores the internal state of the logic block with the name (logic_block). (In other words, a logic block called *mode1_hit_counter* will store its state in a player variable called ``mode1_hit_counter_state``). The state that's stored in this variable include whether the logic block is enabled and whether it's complete. ''' async def _initialize(self): await super()._initialize() if self.config['start_enabled'] is not None: self._start_enabled = self.config['start_enabled'] else: self._start_enabled = not self.config['enable_events'] def add_control_events_in_mode(self, mode: Mode) -> None: """Do not auto enable this device in modes.""" def validate_and_parse_config(self, config: dict, is_mode_config: bool, debug_prefix: str = None) -> dict: """Validate logic block config.""" del is_mode_config del debug_prefix if 'events_when_complete' not in config: config['events_when_complete'] = [ 'logicblock_' + self.name + '_complete' ] if 'events_when_hit' not in config: config['events_when_hit'] = ['logicblock_' + self.name + '_hit'] self.machine.config_validator.validate_config( self.config_section, config, self.name, ("device", "logic_blocks_common")) self._configure_device_logging(config) return config @property def can_exist_outside_of_game(self) -> bool: """Return true if persist_state is not set.""" return not bool(self.config['persist_state']) def get_start_value(self) -> Any: """Return the start value for this block.""" raise NotImplementedError("implement") async def device_added_system_wide(self): """Initialise internal state.""" self._state = LogicBlockState() self.value = self.get_start_value() await super().device_added_system_wide() if not self.config['enable_events']: self.enable() if self.config['persist_state']: self.raise_config_error( "Cannot set persist_state for system-wide logic_blocks", 1) self.post_update_event() def device_loaded_in_mode(self, mode: Mode, player: Player): """Restore internal state from player if persist_state is set or create new state.""" super().device_loaded_in_mode(mode, player) if self.config['persist_state']: if not player.is_player_var(self.player_state_variable): player[self.player_state_variable] = LogicBlockState() # enable device ONLY when we create a new entry in the player if self._start_enabled: mode.add_mode_event_handler( MODE_STARTING_EVENT_TEMPLATE.format(mode.name), self.event_enable, priority=mode.priority + 1) self._state = player[self.player_state_variable] self.value = self.get_start_value() else: self._state = player[self.player_state_variable] else: self._state = LogicBlockState() self.value = self.get_start_value() if self._start_enabled: mode.add_mode_event_handler( MODE_STARTING_EVENT_TEMPLATE.format(mode.name), self.event_enable, priority=mode.priority + 1) mode.add_mode_event_handler("mode_{}_starting".format(mode.name), self.post_update_event) def device_removed_from_mode(self, mode: Mode): """Unset internal state to prevent leakage.""" super().device_removed_from_mode(mode) self._state = None @property def value(self): """Return value or None if that is currently not possible.""" if self._state: return self._state.value return None @value.setter def value(self, value): """Set the value.""" self._state.value = value @property def enabled(self): """Return if enabled.""" return self._state and self._state.enabled @enabled.setter def enabled(self, value): """Set enable.""" self._state.enabled = value @property def completed(self): """Return if completed.""" return self._state and self._state.completed @completed.setter def completed(self, value): """Set if completed.""" self._state.completed = value def post_update_event(self, **kwargs): """Post an event to notify about changes.""" del kwargs value = self._state.value enabled = self._state.enabled self.machine.events.post("logicblock_{}_updated".format(self.name), value=value, enabled=enabled) '''event: logicblock_(name)_updated config_section: counters, accruals, sequences desc: The logic block called "name" has changed. This might happen when the block advanced, it was resetted or restored. args: value: The current value of this block. enabled: Whatever this block is enabled or not. ''' def enable(self): """Enable this logic block. Automatically called when one of the enable_event events is posted. Can also manually be called. """ super().enable() self.debug_log("Enabling") self.enabled = True self.post_update_event() self._logic_block_timer_start() def _post_hit_events(self, **kwargs): self.post_update_event() for event in self.config['events_when_hit']: self.machine.events.post(event, **kwargs) '''event: logicblock_(name)_hit config_section: counters, accruals, sequences desc: The logic block "name" was just hit. Note that this is the default hit event for logic blocks, but this can be changed in a logic block's "events_when_hit:" setting, so this might not be the actual event that's posted for all logic blocks in your machine. args: depend on the type ''' @event_handler(0) def event_disable(self, **kwargs): """Event handler for disable event.""" del kwargs self.disable() def disable(self): """Disable this logic block. Automatically called when one of the disable_event events is posted. Can also manually be called. """ self.debug_log("Disabling") self.enabled = False self.post_update_event() self.delay.remove("timeout") @event_handler(4) def event_reset(self, **kwargs): """Event handler for reset event.""" del kwargs self.reset() def reset(self): """Reset the progress towards completion of this logic block. Automatically called when one of the reset_event events is called. Can also be manually called. """ self.completed = False self.value = self.get_start_value() self.debug_log("Resetting") self.post_update_event() self._logic_block_timer_start() def _logic_block_timer_start(self): if self.config['logic_block_timeout']: self.debug_log("Setting up a logic block timer for %sms", self.config['logic_block_timeout']) self.delay.reset(name="timeout", ms=self.config['logic_block_timeout'], callback=self._logic_block_timeout) def _logic_block_timeout(self): """Reset the progress towards completion of this logic block when timer expires. Automatically called when one of the logic_block_timer_complete events is called. """ self.info_log("Logic Block timeouted") self.machine.events.post("{}_timeout".format(self.name)) '''event: (name)_timeout config_section: counters, accruals, sequences desc: The logic block called "name" has just timeouted. Timeouts are disabled by default but you can set logic_block_timeout to enable them. They will run from start of your logic block until it is stopped. ''' self.reset() @event_handler(5) def event_restart(self, **kwargs): """Event handler for restart event.""" del kwargs self.restart() def restart(self): """Restart this logic block by calling reset() and enable(). Automatically called when one of the restart_event events is called. Can also be manually called. """ self.debug_log("Restarting (resetting then enabling)") self.reset() self.enable() def complete(self): """Mark this logic block as complete. Posts the 'events_when_complete' events and optionally restarts this logic block or disables it, depending on this block's configuration settings. """ # if already completed do not complete again if self.completed: return # otherwise mark as completed self.completed = True self.delay.remove("timeout") self.debug_log("Complete") if self.config['events_when_complete']: for event in self.config['events_when_complete']: self.machine.events.post(event) '''event: logicblock_(name)_complete config_section: counters, accruals, sequences desc: The logic block called "name" has just been completed. Note that this is the default completion event for logic blocks, but this can be changed in a logic block's "events_when_complete:" setting, so this might not be the actual event that's posted for all logic blocks in your machine. ''' # call reset to reset completion if self.config['reset_on_complete']: self.reset() # disable block if self.config['disable_on_complete']: self.disable()
class BallSearch(object): """Ball search controller.""" def __init__(self, machine, playfield): """Initialise ball search.""" self.machine = machine self.playfield = playfield self.log = logging.getLogger("BallSearch " + playfield.name) self.delay = DelayManager(self.machine.delayRegistry) self.started = False self.enabled = False self.callbacks = [] self.iteration = False self.iterator = False self.phase = False # register for events self.machine.events.add_handler('request_to_start_game', self.request_to_start_game) self.machine.events.add_handler('cancel_ball_search', self.cancel_ball_search) '''event: cancel_ball_search desc: This event will cancel all running ball searches and mark the balls as lost. This is only a handler so all you have to do is to post the event.''' def request_to_start_game(self, **kwargs): """Method registered for the *request_to_start_game* event. Returns false if the ball search is running. """ del kwargs if self.started: return False else: return def register(self, priority, callback): """Register a callback for sequential ballsearch. Callbacks are called by priority. Ball search only waits if the callback returns true. Args: priority: priority of this callback in the ball search procedure callback: callback to call. ball search will wait before the next callback, if it returns true """ self.callbacks.append((priority, callback)) # sort by priority self.callbacks = sorted(self.callbacks, key=lambda entry: entry[0]) def enable(self): """Enable but do not start ball search. Ball search is started by a timeout. Enable also resets that timer. """ if self.playfield.config['enable_ball_search'] is False or ( not self.playfield.config['enable_ball_search'] and not self.machine.config['mpf']['default_ball_search']): return if not self.callbacks: raise AssertionError("No callbacks registered") self.log.debug("Enabling Ball Search") self.enabled = True self.reset_timer() def disable(self): """Disable ball search. Will stop the ball search if it is running. """ if self.started: self.machine.events.post('ball_search_stopped') '''event: ball_search_stopped desc: The ball search process has been disabled. This event is posted any time ball search stops, regardless of whether it found a ball or gave up. (If the ball search failed to find the ball, it will also post the *ball_search_failed* event.) ''' self.started = False self.enabled = False self.delay.remove('start') self.delay.remove('run') def reset_timer(self): """Reset the start timer. Called by playfield. """ if self.enabled and not self.started: self.delay.reset(name='start', callback=self.start, ms=self.playfield.config['ball_search_timeout']) def start(self): """Actually start ball search.""" if not self.enabled or self.started or not self.callbacks: return self.started = True self.iteration = 1 self.phase = 1 self.iterator = iter(self.callbacks) self.log.debug("Starting ball search") self.machine.events.post('ball_search_started') '''event: ball_search_started desc: The ball search process has been begun. ''' self.run() def run(self): """Run one iteration of the ball search. Will schedule itself for the next run. """ timeout = self.playfield.config['ball_search_interval'] # iterate until we are done with all callbacks while True: try: element = next(self.iterator) except StopIteration: self.iteration += 1 # give up at some point if self.iteration > self.playfield.config[ 'ball_search_phase_' + str(self.phase) + '_searches']: self.phase += 1 self.iteration = 1 if self.phase > 3: self.give_up() return self.log.debug("Ball Search Phase %s Iteratio %s", self.phase, self.iteration) self.iterator = iter(self.callbacks) element = next(self.iterator) timeout = self.playfield.config[ 'ball_search_wait_after_iteration'] (dummy_priority, callback) = element # if a callback returns True we wait for the next one if callback(self.phase, self.iteration): self.delay.add(name='run', callback=self.run, ms=timeout) return def cancel_ball_search(self, **kwargs): """Cancel the current ballsearch and mark the ball as missing.""" del kwargs if self.started: self.give_up() def give_up(self): """Give up the ball search. Did not find the missing ball. Execute the failed action which either adds a replacement ball or ends the game. """ self.log.warning("Ball Search failed to find ball. Giving up!") self.disable() self.machine.events.post('ball_search_failed') '''event: ball_search_failed desc: The ball search process has failed to locate a missing or stuck ball and has given up. This event will be posted immediately after the *ball_search_stopped* event. ''' lost_balls = self.playfield.balls self.machine.ball_controller.num_balls_known -= lost_balls self.playfield.balls = 0 self.playfield.available_balls = 0 self._compensate_lost_balls(lost_balls) def _compensate_lost_balls(self, lost_balls): if not self.machine.game: return if self.playfield.config['ball_search_failed_action'] == "new_ball": if self.machine.ball_controller.num_balls_known > 0: # we have at least one ball remaining self.log.debug("Adding %s replacement ball", lost_balls) for dummy_iterator in range(lost_balls): self.playfield.add_ball() else: self.log.debug("No more balls left. Ending game!") self.machine.game.game_ending() elif self.playfield.config['ball_search_failed_action'] == "end_game": if self.machine.game: self.log.debug("Ending the game") self.machine.game.game_ending() else: self.log.warning("There is no game. Doing nothing!") else: raise AssertionError( "Unknown action " + self.playfield.config['ball_search_failed_action'])
class ComboSwitch(SystemWideDevice, ModeDevice): """Combo Switch device.""" config_section = 'combo_switches' collection = 'combo_switches' class_label = 'combo_switch' __slots__ = [ "states", "_state", "_switches_1_active", "_switches_2_active", "delay" ] def __init__(self, machine, name): """Initialize Combo Switch.""" super().__init__(machine, name) self.states = ['inactive', 'both', 'one'] self._state = 'inactive' self._switches_1_active = False self._switches_2_active = False self.delay = DelayManager(self.machine) def validate_and_parse_config(self, config: dict, is_mode_config: bool, debug_prefix: str = None) -> dict: """Validate and parse config.""" config = super().validate_and_parse_config(config, is_mode_config, debug_prefix) for state in self.states: if not config['events_when_{}'.format(state)]: config['events_when_{}'.format(state)] = [ "{}_{}".format(self.name, state) ] for state in ["switches_1", "switches_2"]: if not config['events_when_{}'.format(state)]: config['events_when_{}'.format(state)] = [ "{}_{}".format(self.name, state) ] return config async def device_added_system_wide(self): """Add event handlers.""" await super().device_added_system_wide() self._add_switch_handlers() def device_loaded_in_mode(self, mode: Mode, player: Player): """Add event handlers.""" self._add_switch_handlers() def _add_switch_handlers(self): if self.config['tag_1']: for tag in self.config['tag_1']: for switch in self.machine.switches.items_tagged(tag): self.config['switches_1'].add(switch) if self.config['tag_2']: for tag in self.config['tag_2']: for switch in self.machine.switches.items_tagged(tag): self.config['switches_2'].add(switch) self._register_switch_handlers() @property def state(self): """Return current state.""" return self._state @property def can_exist_outside_of_game(self): """Return true if this device can exist outside of a game.""" return True def device_removed_from_mode(self, mode): """Mode ended. Args: mode: mode which stopped """ del mode self._remove_switch_handlers() self._kill_delays() def _register_switch_handlers(self): for switch in self.config['switches_1']: switch.add_handler(self._switch_1_went_active, state=1) switch.add_handler(self._switch_1_went_inactive, state=0) for switch in self.config['switches_2']: switch.add_handler(self._switch_2_went_active, state=1) switch.add_handler(self._switch_2_went_inactive, state=0) def _remove_switch_handlers(self): for switch in self.config['switches_1']: switch.remove_handler(self._switch_1_went_active, state=1) switch.remove_handler(self._switch_1_went_inactive, state=0) for switch in self.config['switches_2']: switch.remove_handler(self._switch_2_went_active, state=1) switch.remove_handler(self._switch_2_went_inactive, state=0) def _kill_delays(self): self.delay.clear() def _switch_1_went_active(self): self.debug_log('A switch from switches_1 just went active') self.delay.remove('switch_1_inactive') if self._switches_1_active: return if not self.config['hold_time']: self._activate_switches_1() else: self.delay.add_if_doesnt_exist(self.config['hold_time'], self._activate_switches_1, 'switch_1_active') def _switch_2_went_active(self): self.debug_log('A switch from switches_2 just went active') self.delay.remove('switch_2_inactive') if self._switches_2_active: return if not self.config['hold_time']: self._activate_switches_2() else: self.delay.add_if_doesnt_exist(self.config['hold_time'], self._activate_switches_2, 'switch_2_active') def _switch_1_went_inactive(self): self.debug_log('A switch from switches_1 just went inactive') for switch in self.config['switches_1']: if switch.state: # at least one switch is still active return self.delay.remove('switch_1_active') if not self.config['release_time']: self._release_switches_1() else: self.delay.add_if_doesnt_exist(self.config['release_time'], self._release_switches_1, 'switch_1_inactive') def _switch_2_went_inactive(self): self.debug_log('A switch from switches_2 just went inactive') for switch in self.config['switches_2']: if switch.state: # at least one switch is still active return self.delay.remove('switch_2_active') if not self.config['release_time']: self._release_switches_2() else: self.delay.add_if_doesnt_exist(self.config['release_time'], self._release_switches_2, 'switch_2_inactive') def _activate_switches_1(self): self.debug_log('Switches_1 has passed the hold time and is now ' 'active') self._switches_1_active = self.machine.clock.get_time() self.delay.remove("switch_2_only") if self._switches_2_active: if (self.config['max_offset_time'] >= 0 and (self._switches_1_active - self._switches_2_active > self.config['max_offset_time'])): self.debug_log( "Switches_2 is active, but the " "max_offset_time=%s which is largest than when " "a Switches_2 switch was first activated, so " "the state will not switch to 'both'", self.config['max_offset_time']) return self._switch_state('both') elif self.config['max_offset_time'] >= 0: self.delay.add_if_doesnt_exist(self.config['max_offset_time'] * 1000, self._post_only_one_active_event, "switch_1_only", number=1) def _activate_switches_2(self): self.debug_log('Switches_2 has passed the hold time and is now ' 'active') self._switches_2_active = self.machine.clock.get_time() self.delay.remove("switch_1_only") if self._switches_1_active: if (self.config['max_offset_time'] >= 0 and (self._switches_2_active - self._switches_1_active > self.config['max_offset_time'])): self.debug_log( "Switches_2 is active, but the " "max_offset_time=%s which is largest than when " "a Switches_2 switch was first activated, so " "the state will not switch to 'both'", self.config['max_offset_time']) return self._switch_state('both') elif self.config['max_offset_time'] >= 0: self.delay.add_if_doesnt_exist(self.config['max_offset_time'] * 1000, self._post_only_one_active_event, "switch_2_only", number=2) def _post_only_one_active_event(self, number): for event in self.config['events_when_switches_{}'.format(number)]: self.machine.events.post(event) def _release_switches_1(self): self.debug_log('Switches_1 has passed the release time and is now ' 'releases') self._switches_1_active = None if self._switches_2_active and self._state == 'both': self._switch_state('one') elif self._state == 'one': self._switch_state('inactive') def _release_switches_2(self): self.debug_log('Switches_2 has passed the release time and is now ' 'releases') self._switches_2_active = None if self._switches_1_active and self._state == 'both': self._switch_state('one') elif self._state == 'one': self._switch_state('inactive') def _switch_state(self, state): """Post events for current step.""" if state not in self.states: raise ValueError("Received invalid state: {}".format(state)) if state == self.state: return self._state = state self.debug_log("New State: %s", state) for event in self.config['events_when_{}'.format(state)]: self.machine.events.post(event) '''event: (combo_switch)_(state) desc: Combo switch (name) changed to state (state). Note that these events can be overridden in a combo switch's config. Valid states are: *inactive*, *both*, or *one*. ..rubric:: both A switch from group 1 and group 2 are both active at the same time, having been pressed within the ``max_offset_time:`` and being active for at least the ``hold_time:``. ..rubric:: one Either switch 1 or switch 2 has been released for at least the ``release_time:`` but the other switch is still active. ..rubric:: switches_1 Only switches_1 is active. max_offset_time has passed and this hit cannot become both later on. Only emmited when ``max_offset_time:`` is defined. ..rubric:: switches_2 Only switches_2 is active. max_offset_time has passed and this hit cannot become both later on. Only emmited when ``max_offset_time:`` is defined. ..rubric:: inactive Both switches are inactive. ''' '''event: flipper_cancel
class DropTargetBank(SystemWideDevice, ModeDevice): """A bank of drop targets in a pinball machine by grouping together multiple `DropTarget` class devices.""" config_section = 'drop_target_banks' collection = 'drop_target_banks' class_label = 'drop_target_bank' def __init__(self, machine: "MachineController", name: str) -> None: """Initialise drop target bank.""" super().__init__(machine, name) self.drop_targets = list() # type: List[DropTarget] self.reset_coil = None # type: Optional[Driver] self.reset_coils = set() # type: Set[Driver] self.complete = False self.down = 0 self.up = 0 self.delay = DelayManager(machine) self._ignore_switch_hits = False @property def can_exist_outside_of_game(self): """Return true if this device can exist outside of a game.""" return True async def _initialize(self): await super()._initialize() self.drop_targets = self.config['drop_targets'] self.reset_coil = self.config['reset_coil'] self.reset_coils = self.config['reset_coils'] def device_loaded_in_mode(self, mode: Mode, player: Player): """Add targets.""" self._add_targets_to_bank() async def device_added_system_wide(self): """Add targets.""" await super().device_added_system_wide() self._add_targets_to_bank() def _add_targets_to_bank(self): """Add targets to bank.""" for target in self.drop_targets: target.add_to_bank(self) self.member_target_change() self.debug_log('Drop Targets: %s', self.drop_targets) @event_handler(5) def event_reset(self, **kwargs): """Handle reset control event.""" del kwargs self.reset() def reset(self): """Reset this bank of drop targets. This method has some intelligence to figure out what coil(s) it should fire. It builds up a set by looking at its own reset_coil and reset_coils settings, and also scanning through all the member drop targets and collecting their coils. Then it pulses each of them. (This coil list is a "set" which means it only sends a single pulse to each coil, even if each drop target is configured with its own coil.) """ self.debug_log('Resetting') if self.down == 0: self.info_log('All targets are already up. Will not reset bank.') return self.info_log('%s targets are down. Will reset those.', self.down) # figure out all the coils we need to pulse coils = set() # type: Set[Driver] for drop_target in self.drop_targets: # add all reset coil for targets which are down if drop_target.reset_coil and drop_target.complete: coils.add(drop_target.reset_coil) # tell the drop target that we are going to reset it physically drop_target.external_reset_from_bank() for coil in self.reset_coils: coils.add(coil) if self.reset_coil: coils.add(self.reset_coil) if self.config['ignore_switch_ms']: self._ignore_switch_hits = True self.delay.add(ms=self.config['ignore_switch_ms'], callback=self._restore_switch_hits, name='ignore_hits') # now pulse them for coil in coils: self.debug_log('Pulsing reset coils: %s', coils) coil.pulse(max_wait_ms=self.config['reset_coil_max_wait_ms']) def _restore_switch_hits(self): self.machine.events.post('restore') self._ignore_switch_hits = False self.member_target_change() def member_target_change(self): """Handle that a member drop target has changed state. This method causes this group to update its down and up counts and complete status. """ if self._ignore_switch_hits: return self.down = 0 self.up = 0 for target in self.drop_targets: if target.complete: self.down += 1 else: self.up += 1 self.debug_log( 'Member drop target status change: Up: %s, Down: %s,' ' Total: %s', self.up, self.down, len(self.drop_targets)) if self.down == len(self.drop_targets): self._bank_down() elif not self.down: self._bank_up() else: self._bank_mixed() def _bank_down(self): self.complete = True self.debug_log('All targets are down') if self.config['reset_on_complete']: self.debug_log("Reset on complete after %s", self.config['reset_on_complete']) self.delay.add(self.config['reset_on_complete'], self.reset) self.machine.events.post('drop_target_bank_' + self.name + '_down') '''event: drop_target_bank_(name)_down desc: Every drop target in the drop target bank called (name) is now in the "down" state. This event is only posted once, when all the drop targets are down.''' def _bank_up(self): self.complete = False self.debug_log('All targets are up') self.machine.events.post('drop_target_bank_' + self.name + '_up') '''event: drop_target_bank_(name)_up desc: Every drop target in the drop target bank called (name) is now in the "up" state. This event is only posted once, when all the drop targets are up.''' def _bank_mixed(self): self.complete = False self.machine.events.post('drop_target_bank_' + self.name + '_mixed', down=self.down) '''event: drop_target_bank_(name)_mixed desc: The drop targets in the drop target bank (name) are in a "mixed" state, meaning that they're not all down or not all up. This event is posted every time a member drop target changes but the overall bank is not not complete.''' def device_removed_from_mode(self, mode): """Remove targets which were added in this mode.""" self.delay.remove('ignore_hits') for target in self.drop_targets: target.remove_from_bank(self)
class AutofireCoil(SystemWideDevice): """Autofire coils which fire based on switch hits with a hardware rule. Coils in the pinball machine which should fire automatically based on switch hits using defined hardware switch rules. Autofire coils work with rules written to the hardware pinball controller that allow them to respond "instantly" to switch hits versus waiting for the lag of USB and the host computer. Examples of Autofire Coils are pop bumpers, slingshots, and kicking targets. (Flippers use the same autofire rules under the hood, but flipper devices have their own device type in MPF. """ config_section = 'autofire_coils' collection = 'autofires' class_label = 'autofire' __slots__ = [ "_enabled", "_rule", "delay", "_ball_search_in_progress", "_timeout_watch_time", "_timeout_max_hits", "_timeout_disable_time", "_timeout_hits" ] def __init__(self, machine: "MachineController", name: str) -> None: """Initialise autofire.""" self._enabled = False self._rule = None # type: HardwareRule super().__init__(machine, name) self.delay = DelayManager(self.machine) self._ball_search_in_progress = False self._timeout_watch_time = None self._timeout_max_hits = None self._timeout_disable_time = None self._timeout_hits = [] # type: List[float] @asyncio.coroutine def _initialize(self): yield from super()._initialize() if self.config['ball_search_order']: self.config['playfield'].ball_search.register( self.config['ball_search_order'], self._ball_search, self.name) # pulse is handled via rule but add a handler so that we take notice anyway self.config['switch'].add_handler(self._hit) if self.config['timeout_watch_time']: self._timeout_watch_time = self.config['timeout_watch_time'] / 1000 self._timeout_max_hits = self.config['timeout_max_hits'] self._timeout_disable_time = self.config['timeout_disable_time'] if '{}_active'.format( self.config['playfield'].name) in self.config['switch'].tags: self.raise_config_error( "Autofire device '{}' uses switch '{}' which has a " "'{}_active' tag. This is handled internally by the device. Remove the " "redundant '{}_active' tag from that switch.".format( self.name, self.config['switch'].name, self.config['playfield'].name, self.config['playfield'].name), 1) @event_handler(1) def event_enable(self, **kwargs): """Handle enable control event. To prevent multiple rules at the same time we prioritize disable > enable. """ del kwargs self.enable() def enable(self): """Enable the autofire device. This causes the coil to respond to the switch hits. This is typically called when a ball starts to enable the slingshots, pops, etc. Note that there are several options for both the coil and the switch which can be incorporated into this rule, including recycle times, switch debounce, reversing the switch (fire the coil when the switch goes inactive), etc. These rules vary by hardware platform. See the user documentation for the hardware platform for details. Args: **kwargs: Not used, just included so this method can be used as an event callback. """ if self._enabled: return self._enabled = True self.debug_log("Enabling") recycle = True if self.config['coil_overwrite'].get( 'recycle', None) in (True, None) else False debounce = False if self.config['switch_overwrite'].get( 'debounce', None) in (None, "quick") else True self._rule = self.machine.platform_controller.set_pulse_on_hit_rule( SwitchRuleSettings(switch=self.config['switch'], debounce=debounce, invert=self.config['reverse_switch']), DriverRuleSettings(driver=self.config['coil'], recycle=recycle), PulseRuleSettings( duration=self.config['coil_overwrite'].get('pulse_ms', None), power=self.config['coil_overwrite'].get('pulse_power', None))) @event_handler(10) def event_disable(self, **kwargs): """Handle disable control event. To prevent multiple rules at the same time we prioritize disable > enable. """ del kwargs self.disable() def disable(self): """Disable the autofire device. This is typically called at the end of a ball and when a tilt event happens. Args: **kwargs: Not used, just included so this method can be used as an event callback. """ self.delay.remove("_timeout_enable_delay") if not self._enabled: return self._enabled = False self.debug_log("Disabling") self.machine.platform_controller.clear_hw_rule(self._rule) def _hit(self): """Rule was triggered.""" if not self._enabled: return if not self._ball_search_in_progress: self.config['playfield'].mark_playfield_active_from_device_action() if self._timeout_watch_time: current_time = self.machine.clock.get_time() self._timeout_hits = [ t for t in self._timeout_hits if t > current_time - self._timeout_watch_time / 1000.0 ] self._timeout_hits.append(current_time) if len(self._timeout_hits) >= self._timeout_max_hits: self.disable() self.delay.add(self._timeout_disable_time, self.enable, "_timeout_enable_delay") def _ball_search(self, phase, iteration): del phase del iteration self.delay.reset(ms=200, callback=self._ball_search_ignore_done, name="ball_search_ignore_done") self._ball_search_in_progress = True self.config['coil'].pulse() return True def _ball_search_ignore_done(self): """We no longer expect any fake hits.""" self._ball_search_in_progress = False
class DigitalOutput(SystemWideDevice): """A digital output on either a light or driver platform.""" config_section = 'digital_outputs' collection = 'digital_outputs' class_label = 'digital_output' __slots__ = ["hw_driver", "platform", "type", "__dict__"] def __init__(self, machine: MachineController, name: str) -> None: """Initialise digital output.""" self.hw_driver = None # type: Union[DriverPlatformInterface, LightPlatformInterface] self.platform = None # type: Union[DriverPlatform, LightsPlatform] self.type = None # type: str super().__init__(machine, name) self.delay = DelayManager(self.machine) @asyncio.coroutine def _initialize(self): """Initialise the hardware driver for this digital output.""" yield from super()._initialize() if self.config['type'] == "driver": self._initialize_driver() elif self.config['type'] == "light": self._initialize_light() else: raise AssertionError("Invalid type {}".format(self.config['type'])) def _initialize_light(self): """Configure a light as digital output.""" self.platform = self.machine.get_platform_sections( 'lights', self.config['platform']) self.type = "light" try: self.hw_driver = self.platform.configure_light( self.config['number'], self.config['light_subtype'], {}) except AssertionError as e: raise AssertionError( "Failed to configure light {} in platform. See error above". format(self.name)) from e def _initialize_driver(self): """Configure a driver as digital output.""" self.platform = self.machine.get_platform_sections( 'coils', self.config['platform']) self.type = "driver" config = DriverConfig(default_pulse_ms=255, default_pulse_power=1.0, default_hold_power=1.0, default_recycle=False, max_pulse_ms=255, max_pulse_power=1.0, max_hold_power=1.0) try: self.hw_driver = self.platform.configure_driver( config, self.config['number'], {}) except AssertionError as e: raise AssertionError( "Failed to configure driver {} in platform. See error above". format(self.name)) from e @staticmethod def _get_state(max_fade_ms: int, state: bool) -> Tuple[float, int, bool]: """Return the current state without any fade.""" del max_fade_ms if state: return 1.0, -1, True else: return 0.0, -1, True @event_handler(3) def event_pulse(self, pulse_ms, **kwargs): """Handle pulse control event.""" del kwargs self.pulse(pulse_ms) def pulse(self, pulse_ms): """Pulse digital output.""" if self.type == "driver": self.hw_driver.pulse(PulseSettings(power=1.0, duration=pulse_ms)) elif self.type == "light": self.hw_driver.set_fade(partial(self._get_state, state=True)) self.platform.light_sync() self.delay.reset(name='timed_disable', ms=pulse_ms, callback=self.disable) else: raise AssertionError("Invalid type {}".format(self.type)) @event_handler(2) def event_enable(self, **kwargs): """Handle enable control event.""" del kwargs self.enable() def enable(self): """Enable digital output.""" if self.type == "driver": self.hw_driver.enable(PulseSettings(power=1.0, duration=0), HoldSettings(power=1.0)) elif self.type == "light": self.hw_driver.set_fade(partial(self._get_state, state=True)) self.platform.light_sync() self.delay.remove(name='timed_disable') else: raise AssertionError("Invalid type {}".format(self.type)) @event_handler(1) def event_disable(self, **kwargs): """Handle disable control event.""" del kwargs self.disable() def disable(self): """Disable digital output.""" if self.type == "driver": self.hw_driver.disable() elif self.type == "light": self.hw_driver.set_fade(partial(self._get_state, state=False)) self.platform.light_sync() self.delay.remove(name='timed_disable') else: raise AssertionError("Invalid type {}".format(self.type))
class Servo(SystemWideDevice): """Represents a servo in a pinball machine. Args: Same as the Device parent class. """ config_section = 'servos' collection = 'servos' class_label = 'servo' def __init__(self, machine, name): """Initialise servo.""" self.hw_servo = None self.platform = None # type: ServoPlatform self._position = None self.speed_limit = None self.acceleration_limit = None self._ball_search_started = False self.delay = DelayManager(machine) super().__init__(machine, name) async def _initialize(self): await super()._initialize() self.platform = self.machine.get_platform_sections( 'servo_controllers', self.config['platform']) for position in self.config['positions']: self.machine.events.add_handler(self.config['positions'][position], self._position_event, position=position) if not self.platform.features[ 'allow_empty_numbers'] and self.config['number'] is None: self.raise_config_error("Servo must have a number.", 1) self.hw_servo = await self.platform.configure_servo( self.config['number']) self._position = self.config['reset_position'] self.speed_limit = self.config['speed_limit'] self.acceleration_limit = self.config['acceleration_limit'] if self.config['include_in_ball_search']: self.machine.events.add_handler("ball_search_started", self._ball_search_start) self.machine.events.add_handler("ball_search_stopped", self._ball_search_stop) self.set_speed_limit(self.speed_limit) self.set_acceleration_limit(self.acceleration_limit) @event_handler(1) def event_reset(self, **kwargs): """Event handler for reset event.""" del kwargs self.reset() def reset(self): """Go to reset position.""" self.go_to_position(self.config['reset_position']) @event_handler(5) def _position_event(self, position, **kwargs): del kwargs self.go_to_position(position) def go_to_position(self, position): """Move servo to position.""" self._position = position if self._ball_search_started: return self._go_to_position(position) def _go_to_position(self, position): # linearly interpolate between servo limits corrected_position = self.config['servo_min'] + position * ( self.config['servo_max'] - self.config['servo_min']) self.debug_log("Moving to position %s (corrected: %s)", position, corrected_position) # call platform with calculated position self.hw_servo.go_to_position(corrected_position) def set_speed_limit(self, speed_limit): """Set speed parameter.""" self.hw_servo.set_speed_limit(speed_limit) def set_acceleration_limit(self, acceleration_limit): """Set acceleration parameter.""" self.hw_servo.set_acceleration_limit(acceleration_limit) def _ball_search_start(self, **kwargs): del kwargs # we do not touch self._position during ball search so we can reset to # it later self._ball_search_started = True self._ball_search_go_to_min() def _ball_search_go_to_min(self): self._go_to_position(self.config['ball_search_min']) self.delay.add(name="ball_search", callback=self._ball_search_go_to_max, ms=self.config['ball_search_wait']) def _ball_search_go_to_max(self): self._go_to_position(self.config['ball_search_max']) self.delay.add(name="ball_search", callback=self._ball_search_go_to_min, ms=self.config['ball_search_wait']) def _ball_search_stop(self, **kwargs): del kwargs # stop delay self.delay.remove("ball_search") self._ball_search_started = False # move to last position set self._go_to_position(self._position)
class Driver(SystemWideDevice): """Generic class that holds driver objects. A 'driver' is any device controlled from a driver board which is typically the high-voltage stuff like coils and flashers. This class exposes the methods you should use on these driver types of devices. Each platform module (i.e. P-ROC, FAST, etc.) subclasses this class to actually communicate with the physical hardware and perform the actions. Args: Same as the Device parent class """ config_section = 'coils' collection = 'coils' class_label = 'coil' __slots__ = ["hw_driver", "delay", "platform", "__dict__", "_pulse_ms"] def __init__(self, machine: MachineController, name: str) -> None: """Initialise driver.""" self.hw_driver = None # type: Optional[DriverPlatformInterface] super().__init__(machine, name) self.delay = DelayManager(self.machine) self.platform = None # type: Optional[DriverPlatform] self._pulse_ms = None @classmethod def device_class_init(cls, machine: MachineController): """Register handler for duplicate coil number checks.""" machine.events.add_handler("init_phase_4", cls._check_duplicate_coil_numbers, machine=machine) @staticmethod def _check_duplicate_coil_numbers(machine, **kwargs): del kwargs check_set = set() for coil in machine.coils.values(): if not hasattr(coil, "hw_driver"): # skip dual wound and other special devices continue key = (coil.config['platform'], coil.hw_driver.number) if key in check_set: raise AssertionError( "Duplicate coil number {} for coil {}".format( coil.hw_driver.number, coil)) check_set.add(key) def validate_and_parse_config(self, config: dict, is_mode_config: bool, debug_prefix: str = None) -> dict: """Return the parsed and validated config. Args: ---- config: Config of device is_mode_config: Whether this device is loaded in a mode or system-wide debug_prefix: Prefix to use when logging. Returns: Validated config """ config = super().validate_and_parse_config(config, is_mode_config, debug_prefix) platform = self.machine.get_platform_sections( 'coils', getattr(config, "platform", None)) platform.assert_has_feature("drivers") config['platform_settings'] = platform.validate_coil_section( self, config.get('platform_settings', None)) return config def _calculate_pulse_ms_placeholder(self, *args): del args if self.config['default_pulse_ms'] is not None: self._pulse_ms, future = self.config[ 'default_pulse_ms'].evaluate_and_subscribe({}) future.add_done_callback(self._calculate_pulse_ms_placeholder) else: self._pulse_ms = self.machine.config['mpf']['default_pulse_ms'] async def _initialize(self): await super()._initialize() self.platform = self.machine.get_platform_sections( 'coils', self.config['platform']) self._calculate_pulse_ms_placeholder() config = DriverConfig( name=self.name, default_pulse_ms=self.get_and_verify_pulse_ms(None), default_pulse_power=self.get_and_verify_pulse_power(None), default_hold_power=self.get_and_verify_hold_power(None), default_recycle=self.config['default_recycle'], max_pulse_ms=self.config['max_pulse_ms'], max_pulse_power=self.config['max_pulse_power'], max_hold_power=self.config['max_hold_power']) platform_settings = dict( self.config['platform_settings'] ) if self.config['platform_settings'] else dict() if not self.platform.features[ 'allow_empty_numbers'] and self.config['number'] is None: self.raise_config_error("Driver must have a number.", 1) try: self.hw_driver = self.platform.configure_driver( config, self.config['number'], platform_settings) except AssertionError as e: raise AssertionError( "Failed to configure driver {} in platform. See error above". format(self.name)) from e def get_and_verify_pulse_power(self, pulse_power: Optional[float]) -> float: """Return the pulse power to use. If pulse_power is None it will use the default_pulse_power. Additionally it will verify the limits. """ if pulse_power is None: pulse_power = self.config['default_pulse_power'] if self.config[ 'default_pulse_power'] is not None else 1.0 if pulse_power and 0 > pulse_power > 1: raise AssertionError( "Pulse power has to be between 0 and 1 but is {}".format( pulse_power)) max_pulse_power = 0 if self.config['max_pulse_power']: max_pulse_power = self.config['max_pulse_power'] elif self.config['default_pulse_power']: max_pulse_power = self.config['default_pulse_power'] if pulse_power > max_pulse_power: raise DriverLimitsError( "Driver may {} not be pulsed with pulse_power {} because max_pulse_power is {}" .format(self.name, pulse_power, max_pulse_power)) return pulse_power def get_and_verify_hold_power(self, hold_power: Optional[float]) -> float: """Return the hold power to use. If hold_power is None it will use the default_hold_power. Additionally it will verify the limits. """ if hold_power is None and self.config['default_hold_power']: hold_power = self.config['default_hold_power'] if hold_power is None and self.config['max_hold_power']: hold_power = self.config['max_hold_power'] if hold_power is None and self.config['allow_enable']: hold_power = 1.0 if hold_power is None: hold_power = 0.0 if hold_power and 0 > hold_power > 1: raise AssertionError( "Hold_power has to be between 0 and 1 but is {}".format( hold_power)) max_hold_power = 0 # type: float if self.config['max_hold_power']: max_hold_power = self.config['max_hold_power'] elif self.config['allow_enable']: max_hold_power = 1.0 elif self.config['default_hold_power']: max_hold_power = self.config['default_hold_power'] if hold_power > max_hold_power: raise DriverLimitsError( "Driver {} may not be enabled with hold_power {} because max_hold_power is {}" .format(self.name, hold_power, max_hold_power)) return hold_power def get_and_verify_pulse_ms(self, pulse_ms: Optional[int]) -> int: """Return and verify pulse_ms to use. If pulse_ms is None return the default. """ assert self.platform is not None if pulse_ms is None: pulse_ms = self._pulse_ms if not isinstance(pulse_ms, int): raise AssertionError("Wrong type {}".format(pulse_ms)) if 0 > pulse_ms > self.platform.features['max_pulse']: raise AssertionError("Pulse_ms {} is not valid.".format(pulse_ms)) if self.config[ 'max_pulse_ms'] and pulse_ms > self.config['max_pulse_ms']: raise DriverLimitsError( "Driver {} may not be pulsed with pulse_ms {} because max_pulse_ms is {}" .format(self.name, pulse_ms, self.config['max_pulse_ms'])) return pulse_ms @event_handler(2) def event_enable(self, pulse_ms: int = None, pulse_power: float = None, hold_power: float = None, **kwargs): """Event handler for control enable.""" del kwargs self.enable(pulse_ms, pulse_power, hold_power) def enable(self, pulse_ms: int = None, pulse_power: float = None, hold_power: float = None, max_wait_ms: int = None): """Enable a driver by holding it 'on'. Args: ---- pulse_ms: The number of milliseconds the driver should be enabled for. If no value is provided, the driver will be enabled for the value specified in the config dictionary. pulse_power: The pulse power. A float between 0.0 and 1.0. hold_power: The pulse power. A float between 0.0 and 1.0. max_wait_ms: Maximum time this pulse may be delayed for PSU optimization. If this driver is configured with a holdpatter, then this method will use that holdpatter to pwm pulse the driver. If not, then this method will just enable the driver. As a safety precaution, if you want to enable() this driver without pwm, then you have to add the following option to this driver in your machine configuration files: allow_enable: True """ assert self.hw_driver is not None pulse_ms = self.get_and_verify_pulse_ms(pulse_ms) wait_ms = self._notify_psu_and_get_wait_ms(pulse_ms, max_wait_ms) pulse_power = self.get_and_verify_pulse_power(pulse_power) hold_power = self.get_and_verify_hold_power(hold_power) if hold_power == 0.0: raise DriverLimitsError("Cannot enable driver with hold_power 0.0") if wait_ms > 0: self.debug_log( "Delaying enable by %sms pulse_ms: %sms (%s pulse_power %s hold_power)", wait_ms, pulse_ms, pulse_power, hold_power) self.delay.add(wait_ms, self._enable_now, pulse_ms=pulse_ms, pulse_power=pulse_power, hold_power=hold_power) else: self._enable_now(pulse_ms, pulse_power, hold_power) def _enable_now(self, pulse_ms: int = None, pulse_power: float = None, hold_power: float = None): self.info_log( "Enabling Driver with power %s (pulse_ms %sms and pulse_power %s)", hold_power, pulse_ms, pulse_power) self.hw_driver.enable( PulseSettings(power=pulse_power, duration=pulse_ms), HoldSettings(power=hold_power)) if self.config['max_hold_duration']: self.delay.add_if_doesnt_exist( self.config['max_hold_duration'] * 1000, self._enable_limit_reached, "enable_limit_reached") # inform bcp clients self.machine.bcp.interface.send_driver_event( action="enable", name=self.name, number=self.config['number'], pulse_ms=pulse_ms, pulse_power=pulse_power, hold_power=hold_power) def _enable_limit_reached(self): """Disable driver and report service alert if max_hold_duration has been reached.""" self.log.warning( "Reached max_hold_duration for this coil. Will disable driver now to prevent damage!" ) self.disable() self.machine.service.add_technical_alert( self, "Reached max_hold_duration. Driver disabled to prevent damage!") @event_handler(1) def event_disable(self, **kwargs): """Event handler for disable control event.""" del kwargs self.disable() def disable(self): """Disable this driver.""" self.info_log("Disabling Driver") self.hw_driver.disable() self.delay.remove("enable_limit_reached") # inform bcp clients self.machine.bcp.interface.send_driver_event( action="disable", name=self.name, number=self.config['number']) def _notify_psu_and_get_wait_ms(self, pulse_ms: int, max_wait_ms: Optional[int]) -> int: """Determine if this pulse should be delayed.""" if max_wait_ms is None: self.config['psu'].notify_about_instant_pulse(pulse_ms=pulse_ms) return 0 return self.config['psu'].get_wait_time_for_pulse( pulse_ms=pulse_ms, max_wait_ms=max_wait_ms) def _pulse_now(self, pulse_ms: int, pulse_power: float) -> None: """Pulse this driver now.""" assert self.hw_driver is not None assert self.platform is not None if 0 < pulse_ms <= self.platform.features['max_pulse']: self.info_log("Pulsing Driver for %sms (%s pulse_power)", pulse_ms, pulse_power) self.hw_driver.pulse( PulseSettings(power=pulse_power, duration=pulse_ms)) else: self.info_log("Enabling Driver for %sms (%s pulse_power)", pulse_ms, pulse_power) self.delay.reset(name='timed_disable', ms=pulse_ms, callback=self.disable) self.hw_driver.enable(PulseSettings(power=pulse_power, duration=0), HoldSettings(power=pulse_power)) # inform bcp clients self.machine.bcp.interface.send_driver_event( action="pulse", name=self.name, number=self.config['number'], pulse_ms=pulse_ms, pulse_power=pulse_power) @event_handler(3) def event_pulse(self, pulse_ms: int = None, pulse_power: float = None, max_wait_ms: int = None, **kwargs) -> None: """Event handler for pulse control events.""" del kwargs self.pulse(pulse_ms, pulse_power, max_wait_ms) def pulse(self, pulse_ms: int = None, pulse_power: float = None, max_wait_ms: int = None) -> int: """Pulse this driver. Args: ---- pulse_ms: The number of milliseconds the driver should be enabled for. If no value is provided, the driver will be enabled for the value specified in the config dictionary. pulse_power: The pulse power. A float between 0.0 and 1.0. max_wait_ms: Maximum time this pulse may be delayed for PSU optimization. """ pulse_ms = self.get_and_verify_pulse_ms(pulse_ms) pulse_power = self.get_and_verify_pulse_power(pulse_power) wait_ms = self._notify_psu_and_get_wait_ms(pulse_ms, max_wait_ms) if wait_ms > 0: self.debug_log( "Delaying pulse by %sms pulse_ms: %sms (%s pulse_power)", wait_ms, pulse_ms, pulse_power) self.delay.add(wait_ms, self._pulse_now, pulse_ms=pulse_ms, pulse_power=pulse_power) else: self._pulse_now(pulse_ms, pulse_power) return wait_ms
class Stepper(SystemWideDevice): """Represents an stepper motor based axis in a pinball machine. Args: Same as the Device parent class. """ config_section = 'steppers' collection = 'steppers' class_label = 'stepper' __slots__ = ["hw_stepper", "platform", "_target_position", "_current_position", "_ball_search_started", "_ball_search_old_target", "_is_homed", "_is_moving", "_move_task", "delay"] def __init__(self, machine, name): """Initialise stepper.""" self.hw_stepper = None # type: Optional[StepperPlatformInterface] self.platform = None # type: Optional[Stepper] self._target_position = 0 # in user units self._current_position = 0 # in user units self._ball_search_started = False self._ball_search_old_target = 0 self._is_homed = False self._is_moving = asyncio.Event(loop=machine.clock.loop) self._move_task = None # type: Optional[asyncio.Task] self.delay = DelayManager(machine) super().__init__(machine, name) async def _initialize(self): await super()._initialize() self.platform = self.machine.get_platform_sections('stepper_controllers', self.config['platform']) # first target is the reset position but we might get an early target during startup via events self._target_position = self.config['reset_position'] for position in self.config['named_positions']: self.machine.events.add_handler(self.config['named_positions'][position], self.event_move_to_position, position=position) if not self.platform.features['allow_empty_numbers'] and self.config['number'] is None: self.raise_config_error("Stepper must have a number.", 2) self.hw_stepper = await self.platform.configure_stepper(self.config['number'], self.config['platform_settings']) if self.config['include_in_ball_search']: self.machine.events.add_handler("ball_search_started", self._ball_search_start) self.machine.events.add_handler("ball_search_stopped", self._ball_search_stop) if self.config['homing_mode'] == "switch" and not self.config['homing_switch']: self.raise_config_error("Cannot use homing_mode switch without a homing_switch. Please add homing_switch or" " use homing_mode hardware.", 1) self._move_task = self.machine.clock.loop.create_task(self._run()) self._move_task.add_done_callback(self._done) @staticmethod def _done(future): try: future.result() except asyncio.CancelledError: pass def validate_and_parse_config(self, config, is_mode_config, debug_prefix: str = None): """Validate stepper config.""" config = super().validate_and_parse_config(config, is_mode_config, debug_prefix) platform = self.machine.get_platform_sections( 'stepper_controllers', getattr(config, "platform", None)) config['platform_settings'] = platform.validate_stepper_section( self, config.get('platform_settings', None)) self._configure_device_logging(config) return config async def _run(self): # wait for switches to be initialised await self.machine.events.wait_for_event("init_phase_3") # first home the stepper self.debug_log("Homing stepper") await self._home() # run the loop at least once self._is_moving.set() while True: # wait until we should be moving await self._is_moving.wait() self._is_moving.clear() # store target position in local variable since it may change in the meantime target_position = self._target_position delta = target_position - self._current_position if delta != 0: self.debug_log("Got move command. Current position: %s Target position: %s Delta: %s", self._current_position, target_position, delta) # move stepper self.hw_stepper.move_rel_pos(delta) # wait for the move to complete await self.hw_stepper.wait_for_move_completed() else: self.debug_log("Got move command. Stepper already at target. Not moving.") # set current position self._current_position = target_position # post ready event self._post_ready_event() self.debug_log("Move completed") def _move_to_absolute_position(self, position): """Move servo to position.""" self.debug_log("Moving to position %s", position) if self.config['pos_min'] <= position <= self.config['pos_max']: self._target_position = position self._is_moving.set() else: raise ValueError("_move_to_absolute_position: position argument beyond limits") async def _home(self): """Home an axis, resetting 0 position.""" self._is_homed = False self._is_moving.set() if self.config['homing_mode'] == "hardware": self.hw_stepper.home(self.config['homing_direction']) await self.hw_stepper.wait_for_move_completed() else: # move the stepper manually if self.config['homing_direction'] == "clockwise": self.hw_stepper.move_vel_mode(1) else: self.hw_stepper.move_vel_mode(-1) # wait until home switch becomes active await self.machine.switch_controller.wait_for_switch(self.config['homing_switch'].name, only_on_change=False) self.hw_stepper.stop() self.hw_stepper.set_home_position() self._is_homed = True self._is_moving.clear() # home position is 0 self._current_position = 0 def _post_ready_event(self): if not self._ball_search_started: self.machine.events.post('stepper_' + self.name + "_ready", position=self._current_position) '''event: stepper_(name)_ready''' def stop(self): """Stop motor.""" self.hw_stepper.stop() self._is_moving.clear() if self._move_task: self._move_task.cancel() self._move_task = None @event_handler(1) def event_reset(self, **kwargs): """Event handler for reset event.""" del kwargs self.reset() def reset(self): """Move to reset position.""" self._move_to_absolute_position(self.config['reset_position']) @event_handler(5) def event_move_to_position(self, position=None, **kwargs): """Event handler for move_to_position event.""" del kwargs if position is None: raise AssertionError("move_to_position event is missing a position.") self.move_to_position(position) def move_to_position(self, position): """Move stepper to a position.""" self._target_position = position if self._ball_search_started: return self._move_to_absolute_position(position) def _ball_search_start(self, **kwargs): del kwargs # we do not touch self._position during ball search so we can reset to # it later self._ball_search_old_target = self._target_position self._ball_search_started = True self._ball_search_go_to_min() def _ball_search_go_to_min(self): self._move_to_absolute_position(self.config['ball_search_min']) self.delay.add(name="ball_search", callback=self._ball_search_go_to_max, ms=self.config['ball_search_wait']) def _ball_search_go_to_max(self): self._move_to_absolute_position(self.config['ball_search_max']) self.delay.add(name="ball_search", callback=self._ball_search_go_to_min, ms=self.config['ball_search_wait']) def _ball_search_stop(self, **kwargs): del kwargs # stop delay self.delay.remove("ball_search") self._ball_search_started = False # move to last position self._target_position = self._ball_search_old_target self._move_to_absolute_position(self._target_position)
class DigitalOutput(SystemWideDevice): """A digital output on either a light or driver platform.""" config_section = 'digital_outputs' collection = 'digital_outputs' class_label = 'digital_output' __slots__ = ["hw_driver", "platform", "type", "__dict__"] def __init__(self, machine: MachineController, name: str) -> None: """Initialise digital output.""" self.hw_driver = None # type: Optional[Union[DriverPlatformInterface, LightPlatformInterface]] self.platform = None # type: Optional[Union[DriverPlatform, LightsPlatform]] self.type = None # type: Optional[str] super().__init__(machine, name) self.delay = DelayManager(self.machine) async def _initialize(self): """Initialise the hardware driver for this digital output.""" await super()._initialize() if self.config['type'] == "driver": self._initialize_driver() elif self.config['type'] == "light": self._initialize_light() else: raise AssertionError("Invalid type {}".format(self.config['type'])) def _initialize_light(self): """Configure a light as digital output.""" self.platform = self.machine.get_platform_sections( 'lights', self.config['platform']) self.platform.assert_has_feature("lights") self.type = "light" if not self.platform.features[ 'allow_empty_numbers'] and self.config['number'] is None: self.raise_config_error("Digital Output must have a number.", 1) try: self.hw_driver = self.platform.configure_light( self.config['number'], self.config['light_subtype'], {}) except AssertionError as e: raise AssertionError( "Failed to configure light {} in platform. See error above". format(self.name)) from e def validate_and_parse_config(self, config: dict, is_mode_config: bool, debug_prefix: str = None) -> dict: """Return the parsed and validated config. Args: config: Config of device is_mode_config: Whether this device is loaded in a mode or system-wide debug_prefix: Prefix to use when logging. Returns: Validated config """ config = super().validate_and_parse_config(config, is_mode_config, debug_prefix) if config['type'] == "driver": platform = self.machine.get_platform_sections( 'coils', getattr(config, "platform", None)) platform.assert_has_feature("drivers") config['platform_settings'] = platform.validate_coil_section( self, config.get('platform_settings', None)) elif config['type'] == "light": pass else: raise AssertionError("Invalid type {}".format(config['type'])) return config def _initialize_driver(self): """Configure a driver as digital output.""" self.platform = self.machine.get_platform_sections( 'coils', self.config['platform']) self.platform.assert_has_feature("drivers") self.type = "driver" config = DriverConfig(default_pulse_ms=255, default_pulse_power=1.0, default_hold_power=1.0, default_recycle=False, max_pulse_ms=255, max_pulse_power=1.0, max_hold_power=1.0) if not self.platform.features[ 'allow_empty_numbers'] and self.config['number'] is None: self.raise_config_error("Digital Output must have a number.", 2) try: self.hw_driver = self.platform.configure_driver( config, self.config['number'], self.config['platform_settings']) except AssertionError as e: raise AssertionError( "Failed to configure driver {} in platform. See error above". format(self.name)) from e @event_handler(3) def event_pulse(self, pulse_ms, **kwargs): """Handle pulse control event.""" del kwargs self.pulse(pulse_ms) def pulse(self, pulse_ms): """Pulse digital output.""" if self.type == "driver": self.hw_driver.pulse(PulseSettings(power=1.0, duration=pulse_ms)) elif self.type == "light": self.hw_driver.set_fade(1.0, -1, 1.0, -1) self.platform.light_sync() self.delay.reset(name='timed_disable', ms=pulse_ms, callback=self.disable) else: raise AssertionError("Invalid type {}".format(self.type)) @event_handler(2) def event_enable(self, **kwargs): """Handle enable control event.""" del kwargs self.enable() def enable(self): """Enable digital output.""" if self.type == "driver": self.hw_driver.enable(PulseSettings(power=1.0, duration=0), HoldSettings(power=1.0)) elif self.type == "light": self.hw_driver.set_fade(1.0, -1, 1.0, -1) self.platform.light_sync() self.delay.remove(name='timed_disable') else: raise AssertionError("Invalid type {}".format(self.type)) @event_handler(1) def event_disable(self, **kwargs): """Handle disable control event.""" del kwargs self.disable() def disable(self): """Disable digital output.""" if self.type == "driver": self.hw_driver.disable() elif self.type == "light": self.hw_driver.set_fade(0.0, -1, 0.0, -1) self.platform.light_sync() self.delay.remove(name='timed_disable') else: raise AssertionError("Invalid type {}".format(self.type))
class TestEventManager(MpfFakeGameTestCase, MpfTestCase): def __init__(self, test_map): super().__init__(test_map) self._handler1_args = tuple() self._handler1_kwargs = dict() self._handler1_called = 0 self._handler2_args = tuple() self._handler2_kwargs = dict() self._handler2_called = 0 self._handler3_args = tuple() self._handler3_kwargs = dict() self._handler3_called = 0 self._handlers_called = list() self._handler_returns_false_args = tuple() self._handler_returns_false_kwargs = dict() self._handler_returns_false_called = 0 self._relay1_called = 0 self._relay2_called = 0 self._relay_callback_args = tuple() self._relay_callback_kwargs = dict() self._relay_callback_called = 0 self._callback_args = tuple() self._callback_kwargs = dict() self._callback_called = 0 self._queue = None self._queue_callback_args = tuple() self._queue_callback_kwargs = dict() self._queue_callback_called = 0 def getConfigFile(self): return 'test_event_manager.yaml' def getMachinePath(self): return 'tests/machine_files/event_manager/' def event_handler1(self, *args, **kwargs): self._handler1_args = args self._handler1_kwargs = kwargs self._handler1_called += 1 self._handlers_called.append(self.event_handler1) def event_handler2(self, *args, **kwargs): self._handler2_args = args self._handler2_kwargs = kwargs self._handler2_called += 1 self._handlers_called.append(self.event_handler2) def event_handler3(self, *args, **kwargs): self._handler3_args = args self._handler3_kwargs = kwargs self._handler3_called += 1 self._handlers_called.append(self.event_handler3) def event_handler_returns_false(self, *args, **kwargs): self._handler_returns_false_args = args self._handler_returns_false_kwargs = kwargs self._handler_returns_false_called += 1 self._handlers_called.append(self.event_handler_returns_false) return False def event_handler_relay1(self, relay_test, **kwargs): del kwargs self._relay1_called += 1 self._handlers_called.append(self.event_handler_relay1) return {'relay_test': relay_test} def event_handler_relay2(self, relay_test, **kwargs): del kwargs self._relay2_called += 1 self._handlers_called.append(self.event_handler_relay2) return {'relay_test': relay_test - 1} def callback(self, *args, **kwargs): self._callback_args = args self._callback_kwargs = kwargs self._callback_called += 1 self._handlers_called.append(self.callback) def relay_callback(self, *args, **kwargs): self._relay_callback_args = args self._relay_callback_kwargs = kwargs self._relay_callback_called += 1 self._handlers_called.append(self.relay_callback) def event_handler_calls_second_event(self, **kwargs): del kwargs self.machine.events.post('second_event') self._handlers_called.append(self.event_handler_calls_second_event) def event_handler_add_queue(self, queue, **kwargs): del kwargs self._handlers_called.append(self.event_handler_add_queue) self._queue = queue self._queue.wait() def event_handler_add_quick_queue(self, queue, **kwargs): del kwargs self._handlers_called.append(self.event_handler_add_quick_queue) self._queue = queue self._queue.wait() self._queue.clear() def event_handler_clear_queue(self, **kwargs): del kwargs self._handlers_called.append(self.event_handler_clear_queue) self._queue.clear() def queue_callback(self, *args, **kwargs): self._queue_callback_args = args self._queue_callback_kwargs = kwargs self._queue_callback_called += 1 self._handlers_called.append(self.queue_callback) def test_event(self): # tests that a handler responds to a regular event post self.machine.events.add_handler('test_event', self.event_handler1) self.advance_time_and_run(1) self.machine.events.post('test_event') self.advance_time_and_run(1) self.assertEqual(1, self._handler1_called) self.assertEqual(tuple(), self._handler1_args) self.assertEqual(dict(), self._handler1_kwargs) def test_event_with_kwargs(self): # test that a kwarg can be passed to a handler which is registered for # a regular event post self.machine.events.add_handler('test_event', self.event_handler1) self.advance_time_and_run(1) self.machine.events.post('test_event', test1='test1') self.advance_time_and_run(1) self.assertEqual(1, self._handler1_called) self.assertEqual(tuple(), self._handler1_args) self.assertEqual({'test1': 'test1'}, self._handler1_kwargs) def test_event_with_callback(self): # test that a callback is called when the event is done self.machine.events.add_handler('test_event', self.event_handler1) self.advance_time_and_run(1) self.machine.events.post('test_event', test1='test1', callback=self.callback) self.advance_time_and_run(1) self.assertEqual(1, self._callback_called) def test_nested_callbacks(self): # tests that an event handlers which posts another event has that event # handled before the first event's callback is called self.machine.events.add_handler('test_event', self.event_handler_calls_second_event) self.machine.events.add_handler('second_event', self.event_handler1) self.advance_time_and_run(1) self.machine.events.post('test_event', callback=self.callback) self.advance_time_and_run(1) self.assertEqual(self._handlers_called[0], self.event_handler_calls_second_event) self.assertEqual(self._handlers_called[1], self.event_handler1) self.assertEqual(self._handlers_called[2], self.callback) def test_event_handler_priorities(self): # tests that handler priorities work. The second handler should be # called first because it's a higher priority even though it's # registered second self.machine.events.add_handler('test_event', self.event_handler1, priority=100) self.machine.events.add_handler('test_event', self.event_handler2, priority=200) self.advance_time_and_run(1) self.machine.events.post('test_event') self.advance_time_and_run(1) self.assertEqual(1, self._handler1_called) self.assertEqual(tuple(), self._handler1_args) self.assertEqual(dict(), self._handler1_kwargs) self.assertEqual(1, self._handler2_called) self.assertEqual(tuple(), self._handler2_args) self.assertEqual(dict(), self._handler2_kwargs) self.assertEqual(self._handlers_called[0], self.event_handler2) self.assertEqual(self._handlers_called[1], self.event_handler1) def test_remove_handler_by_handler(self): # tests that a handler can be removed by passing the handler to remove self.machine.events.add_handler('test_event', self.event_handler1) self.advance_time_and_run(1) self.machine.events.post('test_event') self.advance_time_and_run(1) self.assertEqual(1, self._handler1_called) self.assertEqual(tuple(), self._handler1_args) self.assertEqual(dict(), self._handler1_kwargs) self.machine.events.remove_handler(self.event_handler1) self.machine.events.post('test_event') self.advance_time_and_run(1) self.assertEqual(1, self._handler1_called) self.assertEqual(tuple(), self._handler1_args) self.assertEqual(dict(), self._handler1_kwargs) def test_remove_handler_by_event(self): # tests that a handler can be removed by a handler/event combo, and # that only that handler/event combo is removed self.machine.events.add_handler('test_event1', self.event_handler1) self.machine.events.add_handler('test_event2', self.event_handler1) self.advance_time_and_run(1) self.machine.events.post('test_event1') self.advance_time_and_run(1) self.assertEqual(1, self._handler1_called) self.assertEqual(tuple(), self._handler1_args) self.assertEqual(dict(), self._handler1_kwargs) # should not remove handler since this is the wrong event self.machine.events.remove_handler_by_event('test_event3', self.event_handler1) self.machine.events.post('test_event1') self.advance_time_and_run(1) self.assertEqual(2, self._handler1_called) self.assertEqual(tuple(), self._handler1_args) self.assertEqual(dict(), self._handler1_kwargs) # remove handler for this event self.machine.events.remove_handler_by_event('test_event1', self.event_handler1) self.machine.events.post('test_event1') self.advance_time_and_run(1) # results should be the same as above since this handler was removed self.assertEqual(2, self._handler1_called) self.assertEqual(tuple(), self._handler1_args) self.assertEqual(dict(), self._handler1_kwargs) self.machine.events.post('test_event2') self.advance_time_and_run(1) # results should be the same as above since this handler was removed self.assertEqual(3, self._handler1_called) self.assertEqual(tuple(), self._handler1_args) self.assertEqual(dict(), self._handler1_kwargs) def test_remove_handler_by_key(self): # tests that a handler responds to a regular event post key = self.machine.events.add_handler('test_event', self.event_handler1) self.advance_time_and_run(1) self.machine.events.post('test_event') self.advance_time_and_run(1) self.assertEqual(1, self._handler1_called) self.assertEqual(tuple(), self._handler1_args) self.assertEqual(dict(), self._handler1_kwargs) self.machine.events.remove_handler_by_key(key) self.machine.events.post('test_event') self.advance_time_and_run(1) self.assertEqual(1, self._handler1_called) self.assertEqual(tuple(), self._handler1_args) self.assertEqual(dict(), self._handler1_kwargs) def test_remove_handlers_by_keys(self): # tests that multiple handlers can be removed by an iterable keys list keys = list() keys.append( self.machine.events.add_handler('test_event1', self.event_handler1)) keys.append( self.machine.events.add_handler('test_event2', self.event_handler2)) self.machine.events.post('test_event1') self.advance_time_and_run(1) self.assertEqual(1, self._handler1_called) self.assertEqual(tuple(), self._handler1_args) self.assertEqual(dict(), self._handler1_kwargs) self.machine.events.post('test_event2') self.advance_time_and_run(1) self.assertEqual(1, self._handler2_called) self.assertEqual(tuple(), self._handler2_args) self.assertEqual(dict(), self._handler2_kwargs) self.machine.events.remove_handlers_by_keys(keys) # post events again and handlers should not be called again self.machine.events.post('test_event1') self.advance_time_and_run(1) self.assertEqual(1, self._handler1_called) self.assertEqual(tuple(), self._handler1_args) self.assertEqual(dict(), self._handler1_kwargs) self.machine.events.post('test_event2') self.advance_time_and_run(1) self.assertEqual(1, self._handler2_called) self.assertEqual(tuple(), self._handler2_args) self.assertEqual(dict(), self._handler2_kwargs) def test_does_event_exist(self): self.machine.events.add_handler('test_event', self.event_handler1) self.assertEqual(True, self.machine.events.does_event_exist('test_event')) self.assertEqual(False, self.machine.events.does_event_exist('test_event1')) def test_regular_event_with_false_return(self): # tests that regular events process all handlers even if one returns # False self.machine.events.add_handler('test_event', self.event_handler1, priority=100) self.machine.events.add_handler('test_event', self.event_handler_returns_false, priority=200) self.advance_time_and_run(1) self.machine.events.post('test_event') self.advance_time_and_run(1) self.assertEqual(1, self._handler1_called) self.assertEqual(1, self._handler_returns_false_called) self.assertEqual(self._handlers_called[0], self.event_handler_returns_false) self.assertEqual(self._handlers_called[1], self.event_handler1) def test_post_boolean(self): # tests that a boolean event works self.machine.events.add_handler('test_event', self.event_handler1, priority=100) self.machine.events.add_handler('test_event', self.event_handler2, priority=200) self.advance_time_and_run(1) self.machine.events.post_boolean('test_event') self.advance_time_and_run(1) self.assertEqual(1, self._handler1_called) self.assertEqual(tuple(), self._handler1_args) self.assertEqual(dict(), self._handler1_kwargs) self.assertEqual(1, self._handler2_called) self.assertEqual(tuple(), self._handler2_args) self.assertEqual(dict(), self._handler2_kwargs) self.assertEqual(self._handlers_called[0], self.event_handler2) self.assertEqual(self._handlers_called[1], self.event_handler1) def test_boolean_event_with_false_return(self): # tests that regular events process all handlers even if one returns # False self.machine.events.add_handler('test_event', self.event_handler1, priority=100) self.machine.events.add_handler('test_event', self.event_handler_returns_false, priority=200) self.advance_time_and_run(1) self.machine.events.post_boolean('test_event') self.advance_time_and_run(1) self.assertEqual(0, self._handler1_called) self.assertEqual(1, self._handler_returns_false_called) self.assertEqual(self._handlers_called[0], self.event_handler_returns_false) self.assertEqual(1, len(self._handlers_called)) def test_relay_event(self): # tests that a relay event works by passing a value self.machine.events.add_handler('test_event', self.event_handler_relay1, priority=200) self.advance_time_and_run(1) self.machine.events.post_relay('test_event', relay_test=1, callback=self.relay_callback) self.advance_time_and_run(1) self.assertEqual(1, self._relay1_called) self.assertEqual(1, self._relay_callback_called) assert 'relay_test' in self._relay_callback_kwargs assert self._relay_callback_kwargs['relay_test'] == 1 def test_relay_event_handler_changes_value(self): # tests that a relay event works by passing a value to a handler that # changes that value self.machine.events.add_handler('test_event', self.event_handler_relay1, priority=200) self.machine.events.add_handler('test_event', self.event_handler_relay2, priority=100) self.advance_time_and_run(1) self.machine.events.post_relay('test_event', relay_test=1, callback=self.relay_callback) self.advance_time_and_run(1) self.assertEqual(1, self._relay1_called) self.assertEqual(1, self._relay2_called) self.assertEqual(1, self._relay_callback_called) assert 'relay_test' in self._relay_callback_kwargs self.assertEqual(self._relay_callback_kwargs['relay_test'], 0) def test_queue(self): # tests that a queue event works by registering and clearing a queue self.machine.events.add_handler('test_event', self.event_handler_add_queue) self.advance_time_and_run(1) self.machine.events.post_queue('test_event', callback=self.queue_callback) self.advance_time_and_run(1) self.assertEqual( self._handlers_called.count(self.event_handler_add_queue), 1) self.assertEqual(self._handlers_called.count(self.queue_callback), 0) self.assertEqual(False, self._queue.is_empty()) self.event_handler_clear_queue() self.advance_time_and_run() self.assertEqual(self._handlers_called.count(self.queue_callback), 1) self.assertEqual(True, self._queue.is_empty()) def test_queue_event_with_no_queue(self): # tests that a queue event works and the callback is called right away # if no handlers request a wait self.machine.events.add_handler('test_event', self.event_handler1) self.advance_time_and_run(1) self.machine.events.post_queue('test_event', callback=self.queue_callback) self.advance_time_and_run(1) self.assertEqual(self._handlers_called.count(self.event_handler1), 1) self.assertEqual(self._handlers_called.count(self.queue_callback), 1) def test_queue_event_with_quick_queue_clear(self): # tests that a queue event that quickly creates and clears a queue self.machine.events.add_handler('test_event', self.event_handler_add_quick_queue) self.advance_time_and_run(1) self.machine.events.post_queue('test_event', callback=self.queue_callback) self.advance_time_and_run(1) self.assertEqual( self._handlers_called.count(self.event_handler_add_quick_queue), 1) self.assertEqual(self._handlers_called.count(self.queue_callback), 1) self.assertEqual(True, self._queue.is_empty()) def test_queue_event_with_no_registered_handlers(self): # tests that a queue event callback is called works even if there are # not registered handlers for that event self.machine.events.post_queue('test_event', callback=self.queue_callback) self.advance_time_and_run(1) self.assertEqual(self._handlers_called.count(self.queue_callback), 1) self.assertIsNone(self._queue) def test_queue_event_with_double_quick_queue_clear(self): # tests that a queue event that quickly creates and clears a queue self.machine.events.add_handler('test_event', self.event_handler_add_quick_queue, priority=1) self.machine.events.add_handler('test_event', self.event_handler_add_quick_queue, priority=2) self.advance_time_and_run(1) self.machine.events.post_queue('test_event', callback=self.queue_callback) self.advance_time_and_run(1) self.assertEqual( self._handlers_called.count(self.event_handler_add_quick_queue), 2) self.assertEqual(self._handlers_called.count(self.queue_callback), 1) self.assertEqual(True, self._queue.is_empty()) def test_event_player(self): self.machine.events.add_handler('test_event_player1', self.event_handler1) self.machine.events.add_handler('test_event_player2', self.event_handler2) self.machine.events.add_handler('test_event_player3', self.event_handler3) self.advance_time_and_run(1) self.machine.events.post('test_event_player1', test="123") self.advance_time_and_run(1) self.assertEqual(1, self._handler1_called) self.assertEqual(1, self._handler2_called) self.assertEqual(1, self._handler3_called) self.assertEqual(tuple(), self._handler1_args) self.assertEqual(dict(test="123"), self._handler1_kwargs) self.assertEqual(tuple(), self._handler2_args) self.assertEqual(dict(priority=0), self._handler2_kwargs) self.assertEqual(tuple(), self._handler3_args) self.assertEqual(dict(priority=0), self._handler3_kwargs) def test_event_player_delay(self): self.mock_event('test_event_player2') self.mock_event('test_event_player3') self.machine.events.post('test_event_player_delayed') self.machine_run() self.assertEqual(0, self._events['test_event_player2']) self.assertEqual(0, self._events['test_event_player3']) self.advance_time_and_run(2) self.assertEqual(1, self._events['test_event_player2']) self.assertEqual(1, self._events['test_event_player3']) def test_random_event_player(self): self.machine.events.add_handler('test_random_event_player1', self.event_handler1) self.machine.events.add_handler('test_random_event_player2', self.event_handler2) self.machine.events.add_handler('test_random_event_player3', self.event_handler3) self.advance_time_and_run(1) with patch('random.randint', return_value=1) as mock_random: self.machine.events.post('test_random_event_player1', test="123") self.advance_time_and_run(1) mock_random.assert_called_once_with(1, 2) self.assertEqual(1, self._handler1_called) self.assertEqual(1, self._handler2_called) self.assertEqual(0, self._handler3_called) self.assertEqual(tuple(), self._handler1_args) self.assertEqual(dict(test="123"), self._handler1_kwargs) self.assertEqual(tuple(), self._handler2_args) self.assertEqual(dict(test="123"), self._handler2_kwargs) with patch('random.randint', return_value=1) as mock_random: self.machine.events.post('test_random_event_player1', test="123") self.advance_time_and_run(1) mock_random.assert_called_once_with(1, 1) self.assertEqual(2, self._handler1_called) self.assertEqual(1, self._handler2_called) self.assertEqual(1, self._handler3_called) self.assertEqual(tuple(), self._handler1_args) self.assertEqual(dict(test="123"), self._handler1_kwargs) self.assertEqual(tuple(), self._handler3_args) self.assertEqual(dict(test="123"), self._handler3_kwargs) def test_event_player_in_mode(self): self.machine.events.add_handler('test_event_player_mode1', self.event_handler1) self.machine.events.add_handler('test_event_player_mode2', self.event_handler2) self.machine.events.add_handler('test_event_player_mode3', self.event_handler3) self.advance_time_and_run(1) # mode not loaded. event should not be replayed self.machine.events.post('test_event_player_mode1', test="123") self.advance_time_and_run(1) self.assertEqual(1, self._handler1_called) self.assertEqual(0, self._handler2_called) self.assertEqual(0, self._handler3_called) # start mode self.machine.events.post('test_mode_start') self.advance_time_and_run(1) self.assertTrue(self.machine.mode_controller.is_active("test_mode")) # now the event should get replayed self.machine.events.post('test_event_player_mode1', test="123") self.advance_time_and_run(1) self.assertEqual(2, self._handler1_called) self.assertEqual(1, self._handler2_called) self.assertEqual(1, self._handler3_called) # stop mode self.machine.events.post('test_mode_end') self.advance_time_and_run(1) self.assertFalse(self.machine.mode_controller.is_active("test_mode")) # event should not longer get replayed self.machine.events.post('test_event_player_mode1', test="123") self.advance_time_and_run(1) self.assertEqual(3, self._handler1_called) self.assertEqual(1, self._handler2_called) self.assertEqual(1, self._handler3_called) def test_random_event_player_in_mode(self): self.machine.events.add_handler('test_random_event_player_mode1', self.event_handler1) self.machine.events.add_handler('test_random_event_player_mode2', self.event_handler2) self.machine.events.add_handler('test_random_event_player_mode3', self.event_handler3) self.advance_time_and_run(1) # mode not loaded. event should not be replayed self.machine.events.post('test_random_event_player_mode1', test="123") self.advance_time_and_run(1) self.assertEqual(1, self._handler1_called) self.assertEqual(0, self._handler2_called) self.assertEqual(0, self._handler3_called) # start mode self.machine.events.post('test_mode_start') self.advance_time_and_run(1) self.assertTrue(self.machine.mode_controller.is_active("test_mode")) # now the event should get replayed with patch('random.randint', return_value=1) as mock_random: self.machine.events.post('test_random_event_player_mode1', test="123") self.advance_time_and_run(1) mock_random.assert_called_once_with(1, 2) self.assertEqual(2, self._handler1_called) self.assertEqual(1, self._handler2_called) self.assertEqual(0, self._handler3_called) # stop mode self.machine.events.post('test_mode_end') self.advance_time_and_run(1) self.assertFalse(self.machine.mode_controller.is_active("test_mode")) # event should not longer get replayed self.machine.events.post('test_random_event_player_mode1', test="123") self.advance_time_and_run(1) self.assertEqual(3, self._handler1_called) self.assertEqual(1, self._handler2_called) self.assertEqual(0, self._handler3_called) def event_block(self, queue, **kwargs): del kwargs self.queue = queue queue.wait() def test_random_event_player_in_game_mode(self): self.start_game() self.machine.events.add_handler('out1', self.event_handler2) self.machine.events.add_handler('out2', self.event_handler3) self.advance_time_and_run(1) # mode not loaded. event should not be replayed self.machine.events.post('test_random_event_player_mode2', test="123") self.advance_time_and_run(1) self.assertEqual(0, self._handler2_called) self.assertEqual(0, self._handler3_called) # start mode self.machine.events.post('game_mode_start') self.advance_time_and_run(1) self.assertTrue(self.machine.mode_controller.is_active("game_mode")) # now the event should get replayed with patch('random.randint', return_value=1) as mock_random: self.machine.events.post('test_random_event_player_mode2', test="123") self.advance_time_and_run(1) mock_random.assert_called_once_with(1, 2) self.assertEqual(1, self._handler2_called) self.assertEqual(0, self._handler3_called) # block stop of mode self.machine.events.add_handler('mode_game_mode_stopping', self.event_block) # stop game self.machine.game.stop() self.advance_time_and_run() # event should still work (and not crash) self.machine.events.post('test_random_event_player_mode2', test="123") self.machine_run() self.assertEqual(1, self._handler2_called) self.assertEqual(1, self._handler3_called) # when the block ends game should end too self.queue.clear() self.machine_run() # but no longer after clear self.machine.events.post('test_random_event_player_mode2', test="123") self.machine_run() self.advance_time_and_run(1) self.assertFalse(self.machine.mode_controller.is_active("game_mode")) self.assertEqual(1, self._handler2_called) self.assertEqual(1, self._handler3_called) def delay1_cb(self, **kwargs): del kwargs self.machine.events.post("event1") def event1_cb(self, **kwargs): del kwargs self.delay.add(ms=100, callback=self.delay2_cb) def delay2_cb(self, **kwargs): del kwargs self.machine.events.post("event2") def event2_cb(self, **kwargs): del kwargs self.delay.add(ms=100, callback=self.delay3_cb) def delay3_cb(self, **kwargs): del kwargs self.machine.events.post("event3") def event3_cb(self, **kwargs): del kwargs self.correct = True def test_event_in_delay(self): self.machine.events.add_handler('event1', self.event1_cb) self.machine.events.add_handler('event2', self.event2_cb) self.machine.events.add_handler('event3', self.event3_cb) self.correct = False self.delay = DelayManager(self.machine.delayRegistry) self.machine.events.post("event1") self.advance_time_and_run(1) self.assertTrue(self.correct) def delay_first(self): self.called = True self.delay.remove("second") def delay_second(self): if not self.called: raise AssertionError("first has not been called") raise AssertionError("this should never be called") def test_delay_order(self): self.called = False self.delay = DelayManager(self.machine.delayRegistry) self.delay.add(ms=6001, name="second", callback=self.delay_second) self.delay.add(ms=6000, name="first", callback=self.delay_first) self.advance_time_and_run(10) def delay_zero_ms(self, start): self.delay.add(ms=0, name="second", callback=self.delay_zero_ms_next_frame, start=start) def delay_zero_ms_next_frame(self, start): self.assertLessEqual(self.machine.clock.get_time(), start) def test_zero_ms_delay(self): self.called = False self.delay = DelayManager(self.machine.delayRegistry) self.delay.add(ms=0, name="first", callback=self.delay_zero_ms, start=self.machine.clock.get_time()) self.advance_time_and_run(10) def _handler(self, **kwargs): del kwargs self._called += 1 def test_handler_with_condition(self): self._called = 0 self.machine.events.add_handler("test{param > 1 and a == True}", self._handler) self.post_event("test") self.assertEqual(0, self._called) self.post_event_with_params("test", param=3, a=False) self.assertEqual(0, self._called) self.post_event_with_params("test", param=3, a=True) self.assertEqual(1, self._called) def test_handler_with_settings_condition_invalid_setting(self): self._called = 0 self.machine.events.add_handler("test{settings.test == True}", self._handler) # invalid setting with self.assertRaises(AssertionError): self.post_event("test") self.assertEqual(0, self._called) # reset exception self._exception = None def test_handler_with_settings_condition(self): self._called = 0 self.machine.events.add_handler("test{settings.test == True}", self._handler) self.machine.settings._settings = {} self.machine.settings.add_setting( SettingEntry("test", "Test", 1, "test", "a", { False: "A (default)", True: "B" })) # setting false self.post_event("test") self.assertEqual(0, self._called) self.machine.settings.set_setting_value("test", True) # settings true self.post_event("test") self.assertEqual(1, self._called) def test_weighted(self): self.mock_event("out3") self.mock_event("out4") self.post_event("test_mode_start") self.advance_time_and_run() with patch('random.randint', return_value=1) as mock_random: self.machine.events.post('test_random_event_player_weighted') self.advance_time_and_run(1) mock_random.assert_called_once_with(1, 1001) self.assertEventCalled("out3") self.assertEventNotCalled("out4") self.mock_event("out3") self.mock_event("out4") with patch('random.randint', return_value=2) as mock_random: self.machine.events.post('test_random_event_player_weighted') self.advance_time_and_run(1) mock_random.assert_called_once_with(1, 1001) self.assertEventNotCalled("out3") self.assertEventCalled("out4") self.mock_event("out3") self.mock_event("out4") with patch('random.randint', return_value=500) as mock_random: self.machine.events.post('test_random_event_player_weighted') self.advance_time_and_run(1) mock_random.assert_called_once_with(1, 1001) self.assertEventNotCalled("out3") self.assertEventCalled("out4")
class SegmentDisplayPlayer(DeviceConfigPlayer): """Generates texts on segment displays.""" config_file_section = 'segment_display_player' show_section = 'segment_displays' machine_collection_name = 'segment_displays' __slots__ = ["delay"] def __init__(self, machine): """Initialise SegmentDisplayPlayer.""" super().__init__(machine) self.delay = DelayManager(self.machine.delayRegistry) def play(self, settings, context, calling_context, priority=0, **kwargs): """Show text on display.""" del kwargs instance_dict = self._get_instance_dict( context) # type: Dict[str, SegmentDisplay] full_context = self._get_full_context(context) for display, s in settings.items(): action = s['action'] if display not in instance_dict: instance_dict[display] = {} key = full_context + "." + display.name if s['key']: key += s['key'] if action == "add": # add text display.add_text(s['text'], priority + s['priority'], key) if s['expire']: instance_dict[display][key] = self.delay.add( s['expire'], self._remove, instance_dict=instance_dict, key=key, display=display) else: instance_dict[display][key] = True elif action == "remove": self._remove(instance_dict=instance_dict, key=key, display=display) elif action == "flash": display.set_flashing(True) elif action == "no_flash": display.set_flashing(False) else: raise AssertionError("Invalid action {}".format(action)) def _remove(self, instance_dict, key, display): if key in instance_dict[display]: display.remove_text_by_key(key) if instance_dict[display][key] is not True: self.delay.remove(instance_dict[display][key]) 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) self._reset_instance_dict(context) def get_express_config(self, value): """Parse express config.""" return dict(action="add", text=value)
class BallSearch(MpfController): """Implements Ball search for a playfield device. In MPF, the ball search functionality is attached to each playfield device, rather than being done at the global level. (In other words, each playfield is responsible for making sure no balls get stuck on it, and it leverages an instance of this BallSearch class to handle it.) """ def __init__(self, machine: MachineController, playfield: "Playfield") -> None: """Initialize ball search.""" self.module_name = 'BallSearch.' + playfield.name self.config_name = 'ball_search' super().__init__(machine) self.playfield = playfield """The playfield device this ball search instance is attached to.""" self.delay = DelayManager(self.machine) self.started = False """Is the ball search process started (running) now.""" self.enabled = False """Is ball search enabled.""" self.blocked = False """If True, ball search will be blocked and will not start.""" self.callbacks = [] # type: List[BallSearchCallback] self.iteration = False """Current iteration of the ball search, or ``False`` if ball search is not started.""" self.iterator = False self.phase = False """Current phase of the ball search, or ``False`` if ball search is not started.""" # register for events self.machine.events.add_handler('request_to_start_game', self.request_to_start_game) self.machine.events.add_handler('cancel_ball_search', self.cancel_ball_search) '''event: cancel_ball_search desc: This event will cancel all running ball searches and mark the balls as lost. This is only a handler so all you have to do is to post the event.''' def request_to_start_game(self, **kwargs): """Handle result of the *request_to_start_game* event. If ball search is running, this method will return *False* to prevent the game from starting while ball search is running. This method also posts the *ball_search_prevents_game_start* event if ball search is started. """ # todo we should enable ball search if a ball is missing on game start del kwargs if self.started: self.machine.events.post('ball_search_prevents_game_start') '''event: ball_search_prevents_game_start desc: A game start has been requested, but the ball search process is running and thus the game start has been blocked. This is a good event to use for a slide player to inform the player that the machine is looking for a missing ball.''' return False else: return True def register(self, priority, callback, name, *, restore_callback=None): """Register a callback for sequential ball search. Callbacks are called by priority. Ball search only waits if the callback returns true. Args: priority: priority of this callback in the ball search procedure callback: callback to call. ball search will wait before the next callback, if it returns true name: string name which is used for debugging & the logs restore_callback: optional callback to restore state of the device after ball search ended """ self.debug_log("Registering callback: {} (priority: {})".format( name, priority)) self.callbacks.append( BallSearchCallback(priority, callback, name, restore_callback)) # sort by priority self.callbacks = sorted(self.callbacks, key=lambda entry: entry.priority) def enable(self, **kwargs): """Enable the ball search for this playfield. Note that this method does *not* start the ball search process. Rather it just resets and starts the timeout timer, as well as resetting it when playfield switches are hit. """ if self.blocked: return del kwargs if self.playfield.config['enable_ball_search'] is False or ( not self.playfield.config['enable_ball_search'] and not self.machine.config['mpf']['default_ball_search']): return if not self.callbacks: raise AssertionError("No callbacks registered") self.debug_log("Enabling Ball Search") self.enabled = True self.reset_timer() def disable(self, **kwargs): """Disable ball search. This method will also stop the ball search if it is running. """ del kwargs self.stop() self.debug_log("Disabling Ball Search") self.enabled = False self.delay.remove('start') def block(self, **kwargs): """Block ball search for this playfield. Blocking will disable ball search if it's enabled or running, and will prevent ball search from enabling if it's disabled until ``ball_search_unblock()`` is called. """ del kwargs self.debug_log("Blocking ball search") self.disable() self.blocked = True def unblock(self, **kwargs): """Unblock ball search for this playfield. This will check to see if there are balls on the playfield, and if so, enable ball search. """ del kwargs self.debug_log("Unblocking ball search") self.blocked = False if self.playfield.balls: self.enable() def reset_timer(self): """Reset the timeout timer which starts ball search. This method will also cancel an actively running (started) ball search. This is called by the playfield anytime a playfield switch is hit. """ if self.started: self.stop() if self.enabled: self.debug_log("Resetting ball search timer") self.delay.reset(name='start', callback=self.start, ms=self.playfield.config['ball_search_timeout']) def start(self): """Start ball search the ball search process.""" if not self.enabled or self.started or not self.callbacks: return self.started = True self.iteration = 1 self.phase = 1 self.iterator = iter(self.callbacks) self.info_log("Starting ball search") self.machine.events.post('ball_search_started') '''event: ball_search_started desc: The ball search process has been begun. ''' self.machine.events.post('ball_search_phase_1', iteration=1) # see description below self._run() def stop(self): """Stop an actively running ball search.""" if not self.started: return self.info_log("Stopping ball search") self.started = False self.delay.remove('run') # restore all devices for callback in self.callbacks: if callback.restore_callback: callback.restore_callback() self.machine.events.post('ball_search_stopped') '''event: ball_search_stopped desc: The ball search process has been disabled. This event is posted any time ball search stops, regardless of whether it found a ball or gave up. (If the ball search failed to find the ball, it will also post the *ball_search_failed* event.) ''' def _run(self): # Runs one iteration of the ball search. # Will schedule itself for the next run. # check if we should skip this phase if not self.playfield.config['ball_search_phase_{}_searches'.format( self.phase)]: self.phase += 1 if self.phase > 3: # give up self.give_up() else: # go to the next phase self._run() return timeout = self.playfield.config['ball_search_interval'] # iterate until we are done with all callbacks while True: try: element = next(self.iterator) except StopIteration: self.iteration += 1 self.machine.events.post('ball_search_phase_{}'.format( self.phase), iteration=self.iteration) '''event: ball_search_phase_(num) desc: The ball search phase (num) has started. args: iteration: Current iteration of phase (num) ''' # give up at some point if self.iteration > self.playfield.config[ 'ball_search_phase_{}_searches'.format(self.phase)]: self.phase += 1 self.iteration = 1 if self.phase > 3: self.give_up() return self.iterator = iter(self.callbacks) element = next(self.iterator) timeout = self.playfield.config[ 'ball_search_wait_after_iteration'] # if a callback returns True we wait for the next one self.debug_log("Ball search: {} (phase: {} iteration: {})".format( element.name, self.phase, self.iteration)) if element.callback(self.phase, self.iteration): self.delay.add(name='run', callback=self._run, ms=timeout) return def cancel_ball_search(self, **kwargs): """Cancel the current ball search and mark the ball as missing.""" del kwargs if self.started: self.give_up() def give_up(self): """Give up the ball search. This method is called when the ball search process Did not find the missing ball. It executes the failed action which depending on the specification of *ball_search_failed_action*, either adds a replacement ball, ends the game, or ends the current ball. """ self.info_log("Ball Search failed to find ball. Giving up!") self.disable() self.machine.events.post('ball_search_failed') '''event: ball_search_failed desc: The ball search process has failed to locate a missing or stuck ball and has given up. This event will be posted immediately after the *ball_search_stopped* event. ''' lost_balls = self.playfield.balls self.machine.ball_controller.num_balls_known -= lost_balls self.playfield.balls = 0 self.playfield.available_balls = 0 self._compensate_lost_balls(lost_balls) def _compensate_lost_balls(self, lost_balls): if not self.machine.game: return if self.playfield.config['ball_search_failed_action'] == "new_ball": if self.machine.ball_controller.num_balls_known > 0: # we have at least one ball remaining self.info_log("Adding %s replacement ball", lost_balls) for dummy_iterator in range(lost_balls): self.playfield.add_ball() else: self.info_log("No more balls left. Ending game!") self.machine.game.end_game() elif self.playfield.config['ball_search_failed_action'] == "end_game": if self.machine.game: self.info_log("Ending the game") self.machine.game.end_game() else: self.warning_log("There is no game. Doing nothing!") elif self.playfield.config['ball_search_failed_action'] == "end_ball": self.info_log("Ending current ball") self.machine.game.end_ball() else: raise AssertionError( "Unknown action " + self.playfield.config['ball_search_failed_action'])
class Timer(ModeDevice): """Parent class for a mode timer. Args: ---- machine: The main MPF MachineController object. name: The string name of this timer. """ config_section = 'timers' collection = 'timers' class_label = 'timer' def __init__(self, machine: "MachineController", name: str) -> None: """Initialise mode timer.""" super().__init__(machine, name) self.machine = machine self.name = name self.running = False self.start_value = None # type: Optional[int] self.restart_on_complete = None # type: Optional[bool] self._ticks = 0 self.tick_var = None # type: Optional[str] self.tick_secs = None # type: Optional[float] self.player = None # type: Optional[Player] self.end_value = None # type: Optional[int] self.max_value = None # type: Optional[int] self.ticks_remaining = None # type: Optional[int] self.direction = None # type: Optional[str] self.timer = None # type: Optional[PeriodicTask] self.event_keys = list() # type: List[EventHandlerKey] self.delay = None # type: Optional[DelayManager] async def device_added_to_mode(self, mode: Mode) -> None: """Device added in mode.""" await super().device_added_to_mode(mode) self.tick_var = '{}_{}_tick'.format(mode.name, self.name) async def _initialize(self): await super()._initialize() self.ticks_remaining = 0 self.max_value = self.config['max_value'] self.direction = self.config['direction'] self.tick_secs = None self.timer = None self.event_keys = list() self.delay = DelayManager(self.machine) self.restart_on_complete = self.config['restart_on_complete'] self.end_value = None self.start_value = None self.ticks = None if self.config['debug']: self.configure_logging('Timer.' + self.name, 'full', 'full') else: self.configure_logging('Timer.' + self.name, self.config['console_log'], self.config['file_log']) self.debug_log("----------- Initial Values -----------") self.debug_log("running: %s", self.running) self.debug_log("start_value: %s", self.start_value) self.debug_log("restart_on_complete: %s", self.restart_on_complete) self.debug_log("_ticks: %s", self.ticks) self.debug_log("end_value: %s", self.end_value) self.debug_log("ticks_remaining: %s", self.ticks_remaining) self.debug_log("max_value: %s", self.max_value) self.debug_log("direction: %s", self.direction) self.debug_log("tick_secs: %s", self.tick_secs) self.debug_log("--------------------------------------") def device_loaded_in_mode(self, mode: Mode, player: Player): """Set up control events when mode is loaded.""" del mode self.tick_secs = self.config['tick_interval'].evaluate([]) try: self.end_value = self.config['end_value'].evaluate([]) except AttributeError: self.end_value = None if self.direction == 'down' and not self.end_value: self.end_value = 0 # need it to be 0 not None self.start_value = self.config['start_value'].evaluate([]) self.ticks = self.start_value self.player = player if self.config['control_events']: self._setup_control_events(self.config['control_events']) if self.config['start_running']: self.start() @property def ticks(self): """Return ticks.""" return self._ticks @ticks.setter def ticks(self, value): self._ticks = value try: self.player[self.tick_var] = value '''player_var: (mode)_(timer)_tick desc: Stores the current tick value for the timer from the mode (mode) with the time name (timer). For example, a timer called "my_timer" which is in the config for "mode1" will store its tick value in the player variable ``mode1_my_timer_tick``. ''' except TypeError: pass @property def can_exist_outside_of_game(self): """Timer can live outside of games.""" return True def _setup_control_events(self, event_list): self.debug_log("Setting up control events") kwargs = {} for entry in event_list: if entry['action'] in ('add', 'subtract', 'jump', 'pause', 'set_tick_interval'): handler = getattr(self, entry['action']) kwargs = {'timer_value': entry['value']} elif entry['action'] in ('start', 'stop', 'reset', 'restart'): handler = getattr(self, entry['action']) elif entry['action'] == 'change_tick_interval': handler = self.change_tick_interval kwargs = {'change': entry['value']} elif entry['action'] == 'set_tick_interval': handler = self.set_tick_interval kwargs = {'timer_value': entry['value']} elif entry['action'] == 'reset_tick_interval': handler = self.set_tick_interval kwargs = {'timer_value': self.config['tick_interval']} else: raise AssertionError( "Invalid control_event action {} in mode".format( entry['action']), self.name) self.event_keys.append( self.machine.events.add_handler(entry['event'], handler, **kwargs)) def _remove_control_events(self): self.debug_log("Removing control events") for key in self.event_keys: self.machine.events.remove_handler_by_key(key) def reset(self, **kwargs): """Reset this timer based to the starting value that's already been configured. Does not start or stop the timer. Args: ---- **kwargs: Not used in this method. Only exists since this method is often registered as an event handler which may contain additional keyword arguments. """ del kwargs self.debug_log("Resetting timer. New value: %s", self.start_value) self.jump(self.start_value) def start(self, **kwargs): """Start this timer based on the starting value that's already been configured. Use jump() if you want to set the starting time value. Args: ---- **kwargs: Not used in this method. Only exists since this method is often registered as an event handler which may contain additional keyword arguments. """ del kwargs # do not start if timer is already running if self.running: return self.info_log("Starting Timer.") if self._check_for_done(): return self.running = True self.delay.remove('pause') self._create_system_timer() self.machine.events.post('timer_' + self.name + '_started', ticks=self.ticks, ticks_remaining=self.ticks_remaining) '''event: timer_(name)_started desc: The timer named (name) has just started. args: ticks: The current tick number this timer is at. ticks_remaining: The number of ticks in this timer remaining. ''' self._post_tick_events() # since lots of slides and stuff are tied to the timer tick, we want # to post an initial tick event also that represents the starting # timer value. def restart(self, **kwargs): """Restart the timer by resetting it and then starting it. Essentially this is just a reset() then a start(). Args: ---- **kwargs: Not used in this method. Only exists since this method is often registered as an event handler which may contain additional keyword arguments. """ del kwargs self.reset() self.start() def stop(self, **kwargs): """Stop the timer and posts the 'timer_<name>_stopped' event. Args: ---- **kwargs: Not used in this method. Only exists since this method is often registered as an event handler which may contain additional keyword arguments. """ del kwargs self.info_log("Stopping Timer") self.delay.remove('pause') self.running = False self._remove_system_timer() self.machine.events.post('timer_' + self.name + '_stopped', ticks=self.ticks, ticks_remaining=self.ticks_remaining) '''event: timer_(name)_stopped desc: The timer named (name) has stopped. This event is posted any time the timer stops, whether it stops because it ended or because it was stopped early by some other event. args: ticks: The current tick number this timer is at. ticks_remaining: The number of ticks in this timer remaining. ''' def pause(self, timer_value=0, **kwargs): """Pause the timer and posts the 'timer_<name>_paused' event. Args: ---- timer_value: How many seconds you want to pause the timer for. Note that this pause time is real-world seconds and does not take into consideration this timer's tick interval. **kwargs: Not used in this method. Only exists since this method is often registered as an event handler which may contain additional keyword arguments. """ del kwargs if not timer_value: pause_ms = 0 # make sure it's not None, etc. else: pause_ms = self._get_timer_value( timer_value) * 1000 # delays happen in ms self.info_log("Pausing Timer for %s ms", pause_ms) self.running = False self._remove_system_timer() self.machine.events.post('timer_' + self.name + '_paused', ticks=self.ticks, ticks_remaining=self.ticks_remaining) '''event: timer_(name)_paused desc: The timer named (name) has paused. args: ticks: The current tick number this timer is at. ticks_remaining: The number of ticks in this timer remaining. ''' if pause_ms > 0: self.delay.add(name='pause', ms=pause_ms, callback=self.start) def timer_complete(self, **kwargs): """Automatically called when this timer completes. Posts the 'timer_<name>_complete' event. Can be manually called to mark this timer as complete. Args: ---- **kwargs: Not used in this method. Only exists since this method is often registered as an event handler which may contain additional keyword arguments. """ del kwargs self.info_log("Timer Complete") self.stop() self.machine.events.post('timer_' + self.name + '_complete', ticks=self.ticks, ticks_remaining=self.ticks_remaining) '''event: timer_(name)_complete desc: The timer named (name) has completed. Note that this timer may reset and start again after this event is posted, depending on its settings. args: ticks: The current tick number this timer is at. ticks_remaining: The number of ticks in this timer remaining. ''' if self.restart_on_complete: self.debug_log("Restart on complete: True") self.reset() self.start() def _timer_tick(self): # Automatically called by the core clock each tick if self._debug: self.debug_log("Timer Tick") if not self.running: if self._debug: self.debug_log("Timer is not running. Will remove.") self._remove_system_timer() return if self.direction == 'down': self.ticks -= 1 else: self.ticks += 1 self._post_tick_events() def _post_tick_events(self): if not self._check_for_done(): self.machine.events.post('timer_{}_tick'.format(self.name), ticks=self.ticks, ticks_remaining=self.ticks_remaining) '''event: timer_(name)_tick desc: The timer named (name) has just counted down (or up, depending on its settings). args: ticks: The new tick number this timer is at. ticks_remaining: The new number of ticks in this timer remaining. ''' if self._debug: self.debug_log("Ticks: %s, Remaining: %s", self.ticks, self.ticks_remaining) def add(self, timer_value, **kwargs): """Add ticks to this timer. Args: ---- timer_value: The number of ticks you want to add to this timer's current value. kwargs: Not used in this method. Only exists since this method is often registered as an event handler which may contain additional keyword arguments. """ del kwargs timer_value = self._get_timer_value(timer_value) ticks_added = timer_value new_value = self.ticks + ticks_added if self.max_value and new_value > self.max_value: new_value = self.max_value self.ticks = new_value ticks_added = new_value - timer_value self.machine.events.post('timer_' + self.name + '_time_added', ticks=self.ticks, ticks_added=ticks_added, ticks_remaining=self.ticks_remaining) '''event: timer_(name)_time_added desc: The timer named (name) has just had time added to it. args: ticks: The new tick number this timer is at. ticks_remaining: The new number of ticks in this timer remaining. ticks_added: How many ticks were just added. ''' self._check_for_done() def subtract(self, timer_value, **kwargs): """Subtract ticks from this timer. Args: ---- timer_value: The number of ticks you want to subtract from this timer's current value. **kwargs: Not used in this method. Only exists since this method is often registered as an event handler which may contain additional keyword arguments. """ del kwargs ticks_subtracted = self._get_timer_value(timer_value) self.ticks -= ticks_subtracted self.machine.events.post('timer_' + self.name + '_time_subtracted', ticks=self.ticks, ticks_subtracted=ticks_subtracted, ticks_remaining=self.ticks_remaining) '''event: timer_(name)_time_subtracted desc: The timer named (name) just had some ticks removed. args: ticks: The new current tick number this timer is at. ticks_remaining: The new number of ticks in this timer remaining. ticks_subtracted: How many ticks were just subtracted from this timer. (This number will be positive, indicating the ticks subtracted.) ''' self._check_for_done() def _check_for_done(self): # Checks to see if this timer is done. Automatically called anytime the # timer's value changes. if self._debug: self.debug_log( "Checking to see if timer is done. Ticks: %s, End " "Value: %s, Direction: %s", self.ticks, self.end_value, self.direction) if (self.direction == 'up' and self.end_value is not None and self.ticks >= self.end_value): self.timer_complete() return True if (self.direction == 'down' and self.ticks <= self.end_value): self.timer_complete() return True if self.end_value is not None: self.ticks_remaining = abs(self.end_value - self.ticks) if self._debug: self.debug_log("Timer is not done") return False def _create_system_timer(self): # Creates the clock event which drives this mode timer's tick method. self._remove_system_timer() self.timer = self.machine.clock.schedule_interval( self._timer_tick, self.tick_secs) def _remove_system_timer(self): # Removes the clock event associated with this mode timer. if self.timer: self.machine.clock.unschedule(self.timer) self.timer = None @staticmethod def _get_timer_value(timer_value): if hasattr(timer_value, "evaluate"): # Convert to int for ticks; config_spec must be float for change_tick_interval return int(timer_value.evaluate([])) return timer_value def change_tick_interval(self, change=0.0, **kwargs): """Change the interval for each "tick" of this timer. Args: ---- change: Float or int of the change you want to make to this timer's tick rate. Note this value is multiplied by the current tick interval: >1 will increase the tick interval (slow the timer) and <1 will decrease the tick interval (accelerate the timer). To set an absolute value, use the set_tick_interval() method. **kwargs: Not used in this method. Only exists since this method is often registered as an event handler which may contain additional keyword arguments. """ del kwargs self.tick_secs *= change.evaluate([]) self._create_system_timer() def set_tick_interval(self, timer_value, **kwargs): """Set the number of seconds between ticks for this timer. This is an absolute setting. To apply a change to the current value, use the change_tick_interval() method. Args: ---- timer_value: The new number of seconds between each tick of this timer. This value should always be positive. **kwargs: Not used in this method. Only exists since this method is often registered as an event handler which may contain additional keyword arguments. """ self.tick_secs = abs( self._get_timer_value(timer_value.evaluate(kwargs))) self._create_system_timer() def jump(self, timer_value, **kwargs): """Set the current amount of time of this timer. This value is expressed in "ticks" since the interval per tick can be something other than 1 second). Args: ---- timer_value: Integer of the current value you want this timer to be. **kwargs: Not used in this method. Only exists since this method is often registered as an event handler which may contain additional keyword arguments. """ del kwargs self.ticks = self._get_timer_value(timer_value) if self.max_value and self.ticks > self.max_value: self.ticks = self.max_value self._remove_system_timer() self._create_system_timer() self._check_for_done() def device_removed_from_mode(self, mode: Mode): """Stop this timer and also removes all the control events.""" self.stop() self._remove_control_events()
class Servo(SystemWideDevice): """Represents a servo in a pinball machine. Args: Same as the Device parent class. """ config_section = 'servos' collection = 'servos' class_label = 'servo' def __init__(self, machine, name): """Initialise servo.""" self.hw_servo = None self._position = None self._ball_search_started = False self.delay = DelayManager(machine.delayRegistry) super().__init__(machine, name) def _initialize(self): self.load_platform_section('servo_controllers') for position in self.config['positions']: self.machine.events.add_handler(self.config['positions'][position], self._position_event, position=position) self.hw_servo = self.platform.configure_servo(self.config) self._position = self.config['reset_position'] if self.config['include_in_ball_search']: self.machine.events.add_handler("ball_search_started", self._ball_search_start) self.machine.events.add_handler("ball_search_stopped", self._ball_search_stop) def reset(self, **kwargs): """Go to reset position.""" del kwargs self.go_to_position(self.config['reset_position']) def _position_event(self, position, **kwargs): del kwargs self.go_to_position(position) def go_to_position(self, position): """Move servo to position.""" self._position = position if self._ball_search_started: return self._go_to_position(position) def _go_to_position(self, position): # linearly interpolate between servo limits position = self.config['servo_min'] + position * ( self.config['servo_max'] - self.config['servo_min']) # call platform with calculated position self.hw_servo.go_to_position(position) def _ball_search_start(self, **kwargs): del kwargs # we do not touch self._position during ball search so we can reset to # it later self._ball_search_started = True self._ball_search_go_to_min() def _ball_search_go_to_min(self): self._go_to_position(self.config['ball_search_min']) self.delay.add(name="ball_search", callback=self._ball_search_go_to_max, ms=self.config['ball_search_wait']) def _ball_search_go_to_max(self): self._go_to_position(self.config['ball_search_max']) self.delay.add(name="ball_search", callback=self._ball_search_go_to_min, ms=self.config['ball_search_wait']) def _ball_search_stop(self, **kwargs): del kwargs # stop delay self.delay.remove("ball_search") self._ball_search_started = False # move to last position set self._go_to_position(self._position)
class SegmentDisplayPlayer(DeviceConfigPlayer): """Generates texts on segment displays.""" config_file_section = 'segment_display_player' show_section = 'segment_displays' machine_collection_name = 'segment_displays' __slots__ = ["delay"] def __init__(self, machine): """Initialise SegmentDisplayPlayer.""" super().__init__(machine) self.delay = DelayManager(self.machine) # pylint: disable=too-many-branches def play(self, settings, context, calling_context, priority=0, **kwargs): """Show text on display.""" del kwargs instance_dict = self._get_instance_dict( context) # type: Dict[str, SegmentDisplay] full_context = self._get_full_context(context) for display, s in settings.items(): action = s['action'] if display not in instance_dict: instance_dict[display] = {} key = full_context + "." + display.name if s['key']: key += s['key'] if action == "add": if key in instance_dict[display] and instance_dict[display][ key] is not True: self.delay.remove(instance_dict[display][key]) # add text s = TransitionManager.validate_config( s, self.machine.config_validator) display.add_text_entry(text=s['text'], color=s['color'], flashing=self._get_flashing_type(s), flash_mask=s['flash_mask'], transition=s['transition'], transition_out=s['transition_out'], priority=priority + s['priority'], key=key) if s['expire']: instance_dict[display][key] = self.delay.add( s['expire'], self._remove, instance_dict=instance_dict, key=key, display=display) else: instance_dict[display][key] = True elif action == "remove": self._remove(instance_dict=instance_dict, key=key, display=display) elif action == "flash": display.set_flashing(FlashingType.FLASH_ALL) elif action == "flash_match": display.set_flashing(FlashingType.FLASH_MATCH) elif action == "flash_mask": display.set_flashing(FlashingType.FLASH_MASK, s.get('flash_mask', "")) elif action == "no_flash": display.set_flashing(FlashingType.NO_FLASH) elif action == "set_color": if s['color']: display.set_color(s['color']) else: raise AssertionError("Invalid action {}".format(action)) @staticmethod def _get_flashing_type(config: dict): flashing = config.get('flashing', None) if flashing == "off": return FlashingType.NO_FLASH if flashing == "all": return FlashingType.FLASH_ALL if flashing == "match": return FlashingType.FLASH_MATCH if flashing == "mask": return FlashingType.FLASH_MASK return None def _remove(self, instance_dict, key, display): """Remove an instance by key.""" if key in instance_dict[display]: display.remove_text_by_key(key) if instance_dict[display][key] is not True: self.delay.remove(instance_dict[display][key]) 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) self._reset_instance_dict(context) def get_express_config(self, value): """Parse express config.""" return dict(action="add", text=value)