diff --git a/mpf/devices/driver.py b/mpf/devices/driver.py index bfc644bb4..1e81b4fbf 100644 --- a/mpf/devices/driver.py +++ b/mpf/devices/driver.py @@ -382,6 +382,8 @@ def timed_enable(self, timed_enable_ms: int = None, hold_power: float = None, pu # Let the PSU wait for both the pulse _and_ the timed enable wait_ms = self._notify_psu_and_get_wait_ms(pulse_duration + hold_duration, max_wait_ms) + self.info_log("Pulsing Driver for %sms (%s pulse_power) with timed enable for %sms (%s hold_power)", + pulse_duration, pulse_power, hold_duration, hold_power) # TODO: Detect a NotImplementedError and simulate a timed_enable # with a software timer and enable+disable self.hw_driver.timed_enable(PulseSettings(pulse_power, pulse_duration), diff --git a/mpf/platforms/fast/communicators/base.py b/mpf/platforms/fast/communicators/base.py index f29d9efbf..acd007533 100644 --- a/mpf/platforms/fast/communicators/base.py +++ b/mpf/platforms/fast/communicators/base.py @@ -60,7 +60,7 @@ def __init__(self, platform, processor, config): else: self.watchdog_cmd = None - self.configure_logging(logger=f'[{self.remote_processor}]', console_level=config['debug'], + self.configure_logging(logger=f'FAST [{self.remote_processor}]', console_level=config['debug'], file_level=config['debug'], url_base='https://fastpinball.com/mpf/error') # TODO change these to not be hardcoded # TODO do something with the URL endpoint @@ -349,10 +349,14 @@ def _dispatch_incoming_msg(self, msg): self.no_response_waiting.set() # if the msg_header matches the first chars of the self.pause_sending_until, unpause sending + # Note that the msg_header includes the colon (e.g. "DL:", "SA:") and therefore + # pause_sending_until must also include the colon. if self.pause_sending_flag.is_set() and self.pause_sending_until.startswith(msg_header): self._resume_sending() def pause_sending(self, msg_header): + if __debug__: + assert len(msg_header) >= 3, f"Confirmation headers should be at least three characters, received '{msg_header}'" self.pause_sending_until = msg_header self.pause_sending_flag.set() @@ -409,7 +413,8 @@ async def _socket_writer(self): if self.pause_sending_flag.is_set(): await self.pause_sending_flag.wait() - except: + except Exception as e: + self.log.error(e) return # TODO better way to catch shutting down? def write_to_port(self, msg, log_msg=None): diff --git a/mpf/platforms/fast/communicators/net_neuron.py b/mpf/platforms/fast/communicators/net_neuron.py index 2afe447f1..77f22513b 100644 --- a/mpf/platforms/fast/communicators/net_neuron.py +++ b/mpf/platforms/fast/communicators/net_neuron.py @@ -116,7 +116,7 @@ async def reset_drivers(self): # self.drivers contains a list of all drivers, not just ones defined in the config for driver in self.drivers: - await self.send_and_wait_for_response_processed(f'{self.DRIVER_CMD}:{Util.int_to_hex_string(driver.number)}', self.DRIVER_CMD) + await self.send_and_wait_for_response_processed(f'{self.DRIVER_CMD}:{Util.int_to_hex_string(driver.number)}', f"{self.DRIVER_CMD}:") self.platform.drivers_initialized = True diff --git a/mpf/platforms/fast/fast.py b/mpf/platforms/fast/fast.py index 9486269a8..1f86a9281 100644 --- a/mpf/platforms/fast/fast.py +++ b/mpf/platforms/fast/fast.py @@ -79,8 +79,10 @@ def __init__(self, machine): else: self.raise_config_error(f'Unknown machine_type "{self.machine_type}" configured fast.', 6) + # Even though System11 uses ticks, that's handled on the Overlay and not needed here. + self.features['tickless'] = True # Most FAST platforms don't use ticks, but System11 does - self.features['tickless'] = self.machine_type != 'sys11' + #self.features['tickless'] = self.machine_type != 'sys11' self.features['max_pulse'] = 25500 self.serial_connections = dict() diff --git a/mpf/platforms/fast/fast_defines.py b/mpf/platforms/fast/fast_defines.py index c208099fe..22856ae93 100644 --- a/mpf/platforms/fast/fast_defines.py +++ b/mpf/platforms/fast/fast_defines.py @@ -12,6 +12,7 @@ (11914, 4156): ('aud', 'FAST Audio Interface'), (11914, 4157): ('exp', 'FAST Expansion Board'), (11914, 4158): ('dsp', 'FAST Display Controller'), + (5824, 1163): ('net', 'FAST Retro Controller'), # Teensyduino (1027, 24593): ('net', 'FAST Nano Controller'), # FTDI Quad RS232-HS } diff --git a/mpf/platforms/fast/fast_driver.py b/mpf/platforms/fast/fast_driver.py index c3c854b92..c81d4ea7b 100644 --- a/mpf/platforms/fast/fast_driver.py +++ b/mpf/platforms/fast/fast_driver.py @@ -31,7 +31,7 @@ class FASTDriver: """Base class for drivers connected to a FAST Controller.""" __slots__ = ["log", "communicator", "number", "hw_number", "autofire_config", "baseline_driver_config", - "current_driver_config", "mode_param_mapping", "platform_settings"] + "config", "current_driver_config", "mode_param_mapping", "platform_settings"] def __init__(self, communicator: FastSerialCommunicator, hw_number: int) -> None: """Initialize the driver object. @@ -42,6 +42,7 @@ def __init__(self, communicator: FastSerialCommunicator, hw_number: int) -> None self.communicator = communicator self.number = hw_number # must be int to work with the rest of MPF self.hw_number = Util.int_to_hex_string(hw_number) # hex version the FAST hw actually uses + self.config = None self.autofire_config = None self.platform_settings = dict() @@ -56,6 +57,7 @@ def __init__(self, communicator: FastSerialCommunicator, hw_number: int) -> None '12': ['pwm1_ms', 'pwm1_power', 'pwm2_ms', 'pwm2_power', 'kick_ms'], '18': ['pwm1_ms', 'pwm1_power', 'pwm2_power', 'recycle_ms', None], '20': ['off_switch', 'pwm1_ms', 'pwm1_power', 'pwm2_power', 'rest_ms'], + '25': ['relay_on_report_ms', 'relay_off_report_ms'], '30': ['delay_ms_x10', 'pwm1_ms', 'pwm2_ms', 'pwm2_power', 'recycle_ms'], '70': ['pwm1_ms', 'pwm1_power', 'pwm2_ms_x100', 'pwm2_power', 'recycle_ms'], '75': ['off_switch', 'pwm1_ms', 'pwm2_ms_x100', 'pwm2_power', 'recycle_ms'], @@ -73,7 +75,7 @@ def set_initial_config(self, mpf_config: DriverConfig, platform_settings): This will not be called for drivers that are not in the MPF config. """ - + self.config = mpf_config self.platform_settings = platform_settings self.current_driver_config = self.convert_mpf_config_to_fast(mpf_config, platform_settings) self.baseline_driver_config = copy(self.current_driver_config) @@ -144,8 +146,8 @@ def clear_bit(self, hex_string, bit): return Util.int_to_hex_string(num) def send_config_to_driver(self, one_shot: bool = False, wait_to_confirm: bool = False): - self.log.debug("Sending config to driver %s. one_shot: %s. wait_to_confirm: %s", - self.number, one_shot, wait_to_confirm) + self.log.debug("Sending config to driver %s (0x%s). one_shot: %s. wait_to_confirm: %s", + self.number, self.hw_number, one_shot, wait_to_confirm) if one_shot: trigger = self.set_bit(self.current_driver_config.trigger, 3) @@ -157,7 +159,7 @@ def send_config_to_driver(self, one_shot: bool = False, wait_to_confirm: bool = f'{self.current_driver_config.param2},{self.current_driver_config.param3},{self.current_driver_config.param4},' f'{self.current_driver_config.param5}') if wait_to_confirm: - self.communicator.send_with_confirmation(msg, f'{self.communicator.DRIVER_CMD}') + self.communicator.send_with_confirmation(msg, f'{self.communicator.DRIVER_CMD}:') else: self.communicator.send_and_forget(msg) @@ -354,17 +356,38 @@ def clear_autofire(self): self.autofire_config = None self.communicator.send_and_forget(f'{self.communicator.TRIGGER_CMD}:{self.hw_number},02') + def set_relay(self, relay_switch, debounce_closed_ms, debounce_open_ms): + """Set an AC Relay rule with virtual switch.""" + + self.log.debug("Setting A/C Relay for driver %s (0x%s) and switch %s (0x%s)", + self.number, self.hw_number, relay_switch.number, relay_switch.hw_number) + self.current_driver_config = FastDriverConfig(number=self.hw_number, trigger='81', + switch_id=relay_switch.hw_number, + mode='25', + param1=Util.int_to_hex_string(debounce_closed_ms), + param2=Util.int_to_hex_string(debounce_open_ms), + param3='00', + param4='00', + param5='00') + self.send_config_to_driver(wait_to_confirm=True) + def enable(self, pulse_settings: PulseSettings, hold_settings: HoldSettings): """Enable (turn on) this driver.""" - self.log.debug("Enabling (turning on) driver %s with pulse_settings: %s and hold_settings: %s.", - self.number, pulse_settings, hold_settings) + self.log.debug("Enabling (turning on) driver %s (0x%s) mode %s with pulse_settings: %s and hold_settings: %s.", + self.number, self.hw_number, self.current_driver_config.mode, pulse_settings, hold_settings) self._check_and_clear_delay() reconfigured = False mode = self.current_driver_config.mode + # AC Relays have special behavior + if mode == '25': + self.log.debug(" - A/C Relay activating!") + self.communicator.send_and_forget(f'{self.communicator.TRIGGER_CMD}:{self.hw_number},03') + return + pwm1_ms = Util.int_to_hex_string(pulse_settings.duration) pwm1_power = Util.float_to_pwm8_hex_string(pulse_settings.power) pwm2_power = Util.float_to_pwm8_hex_string(hold_settings.power) diff --git a/mpf/platforms/fast/fast_port_detector.py b/mpf/platforms/fast/fast_port_detector.py index 4c111fd43..e56931f35 100644 --- a/mpf/platforms/fast/fast_port_detector.py +++ b/mpf/platforms/fast/fast_port_detector.py @@ -70,18 +70,14 @@ async def _connect_task(self, port, baud): total_attempts = 5 attempts = 0 - while True: + while attempts < total_attempts: writer.write(b'ID:\r') # Wait for a response with 1-second timeout try: data = await asyncio.wait_for(reader.read(100), timeout=1.0) except asyncio.TimeoutError: - attempts += 1 - if attempts < total_attempts: - continue # retry - else: - self.platform.debug_log("Unable to get ID: on port %s after %s retries.", port, total_attempts) + pass if data: data = data.decode('utf-8', errors='ignore') @@ -91,6 +87,9 @@ async def _connect_task(self, port, baud): self._report_success(processor, port) writer.close() return + attempts += 1 + self.platform.debug_log("Unable to get ID: on port %s after %s retries.", port, total_attempts) + return def _report_success(self, processor, port): self.results[processor] = port diff --git a/mpf/platforms/system11.py b/mpf/platforms/system11.py index 549085dd6..b94982d9f 100644 --- a/mpf/platforms/system11.py +++ b/mpf/platforms/system11.py @@ -25,7 +25,7 @@ class System11OverlayPlatform(DriverPlatform, SwitchPlatform): __slots__ = ["delay", "platform", "system11_config", "a_side_queue", "c_side_queue", "debounce_secs", "a_side_done_time", "c_side_done_time", "drivers_holding_a_side", "drivers_holding_c_side", - "a_side_enabled", "c_side_enabled", "ac_relay_in_transition", "prefer_a_side", "drivers", + "_a_side_enabled", "_c_side_enabled", "ac_relay_in_transition", "prefer_a_side", "drivers", "relay_switch"] def __init__(self, machine: MachineController) -> None: @@ -48,11 +48,13 @@ def __init__(self, machine: MachineController) -> None: self.debounce_secs = 0 self.drivers_holding_a_side = set() # type: Set[DriverPlatformInterface] self.drivers_holding_c_side = set() # type: Set[DriverPlatformInterface] - self.a_side_enabled = True - self.c_side_enabled = False + # Internal tracker for which side is enabled, for platforms that don't have switches + self._a_side_enabled = True + self._c_side_enabled = False self.drivers = {} # type: Dict[str, DriverPlatformInterface] self.ac_relay_in_transition = False + self.relay_switch = None # Specify whether the AC relay should favour the A or C side when at rest. # Typically during a game the 'C' side should be preferred, since that is # normally where the flashers are which need a quick response without having to wait on the relay. @@ -72,6 +74,14 @@ def a_side_busy(self): """Return if A side cannot be switched off right away.""" return self.a_side_active or self.a_side_queue + @property + def a_side_enabled(self): + if self.ac_relay_in_transition: + return False + if self.relay_switch: + return not self.relay_switch.state + return self._a_side_enabled + @property def c_side_active(self): """Return if C side cannot be switched off right away.""" @@ -82,6 +92,12 @@ def c_side_busy(self): """Return if C side cannot be switched off right away.""" return self.c_side_active or self.c_side_queue + @property + def c_side_enabled(self): + if self.relay_switch: + # Never enabled if the relay is in transition + return not self.ac_relay_in_transition and self.relay_switch.state + return self._c_side_enabled async def initialize(self): """Automatically called by the Platform class after all the core modules are loaded.""" @@ -109,12 +125,22 @@ def _initialize(self, **kwargs): self.system11_config['ac_relay_driver']) self.system11_config['ac_relay_driver'].get_and_verify_hold_power(1.0) - - if self.system11_config['ac_relay_switch']: - self.log.debug("Configuring A/C Select Relay switch %s", self.system11_config['ac_relay_switch']) - self.system11_config['ac_relay_switch'].add_handler(state=0, callback=self._a_side_enabled) - self.system11_config['ac_relay_switch'].add_handler(state=1, callback=self._c_side_enabled) - + self.relay_switch = self.system11_config['ac_relay_switch'] + + if self.relay_switch: + self.log.debug("Configuring A/C Select Relay switch %s", self.relay_switch) + self.relay_switch.add_handler(state=0, callback=self._on_a_side_enabled) + self.relay_switch.add_handler(state=1, callback=self._on_c_side_enabled) + + # If the platform does not have a physical switch for the AC Relay, a virtual + # switch may be implemented with a special driver configuration if that + # driver class has a set_relay() method defined. + if hasattr(self.system11_config['ac_relay_driver'].hw_driver, 'set_relay'): + self.system11_config['ac_relay_driver'].hw_driver.set_relay( + self.relay_switch.hw_switch, + 20, # Ms to delay before reporting closed + 20 # Ms to delay before reporting open + ) self.debounce_secs = self.system11_config['ac_relay_debounce_ms'] / 1000.0 self.log.debug("Configuring A/C Select Relay transition delay for %sms and debounce for %s", self.system11_config['ac_relay_delay_ms'], @@ -296,14 +322,18 @@ def driver_action(self, driver, pulse_settings: Optional[PulseSettings], hold_se else: if side == "C": # Sometimes it doesn't make sense to queue the C side (flashers) and play them after - # switching to the A side (coils) and back. In which case, just ignore this driver action. - if not self.c_side_enabled and not self.system11_config['queue_c_side_while_preferred']: + # switching to the A side (coils) and back. If we are on A side or have a queue on + # the A side, ignore this C side request. + if (self.a_side_queue or not self.c_side_enabled) and not self.system11_config['queue_c_side_while_preferred']: return self.c_side_queue.add((driver, pulse_settings, hold_settings, timed_enable)) if not self.ac_relay_in_transition: self._service_c_side() elif side == "A": self.a_side_queue.add((driver, pulse_settings, hold_settings, timed_enable)) + # Clear the C-side queue to prioritize A-side and get it switched over faster + if not self.system11_config['queue_c_side_while_preferred']: + self.c_side_queue.clear() if not self.ac_relay_in_transition and not self.c_side_busy: self._service_a_side() else: @@ -312,25 +342,25 @@ def driver_action(self, driver, pulse_settings: Optional[PulseSettings], hold_se def _enable_ac_relay(self): self.system11_config['ac_relay_driver'].enable() self.ac_relay_in_transition = True - self.a_side_enabled = False - self.c_side_enabled = False + self._a_side_enabled = False + self._c_side_enabled = False # Without a relay switch, use a delay to wait for the relay to enable - if not self.system11_config['ac_relay_switch']: + if not self.relay_switch: self.delay.add(ms=self.system11_config['ac_relay_delay_ms'], - callback=self._c_side_enabled, + callback=self._on_c_side_enabled, name='enable_ac_relay') def _disable_ac_relay(self): self.system11_config['ac_relay_driver'].disable() self.ac_relay_in_transition = True - self.a_side_enabled = False - self.c_side_enabled = False + self._a_side_enabled = False + self._c_side_enabled = False # Clear out the C side queue if we don't want to hold onto it for later if not self.system11_config['queue_c_side_while_preferred']: self.c_side_queue.clear() - if not self.system11_config['ac_relay_switch']: + if not self.relay_switch: self.delay.add(ms=self.system11_config['ac_relay_delay_ms'], - callback=self._a_side_enabled, + callback=self._on_a_side_enabled, name='disable_ac_relay') # -------------------------------- A SIDE --------------------------------- @@ -348,7 +378,7 @@ def _enable_a_side(self): self._disable_ac_relay() else: - self._a_side_enabled() + self._on_a_side_enabled() else: if (not self.ac_relay_in_transition and not self.a_side_enabled and @@ -358,21 +388,16 @@ def _enable_a_side(self): elif self.a_side_enabled and self.a_side_queue: self._service_a_side() - def _a_side_enabled(self): + def _on_a_side_enabled(self): self.ac_relay_in_transition = False - if self.prefer_a_side: - self.a_side_enabled = True - self.c_side_enabled = False - self._service_a_side() - else: - - if self.c_side_queue: - self._enable_c_side() - return + # If A side has no queue and is not preferred, return to C side + if not self.a_side_queue and not self.prefer_a_side: + self._enable_c_side() + return - self.c_side_enabled = False - self.a_side_enabled = True - self._service_a_side() + self._c_side_enabled = False + self._a_side_enabled = True + self._service_a_side() def _service_a_side(self): if not self.a_side_queue: @@ -421,7 +446,7 @@ def _enable_c_side(self): self._enable_ac_relay() else: - self._c_side_enabled() + self._on_c_side_enabled() else: if (not self.ac_relay_in_transition and not self.c_side_enabled and @@ -431,22 +456,16 @@ def _enable_c_side(self): elif self.c_side_enabled and self.c_side_queue: self._service_c_side() - def _c_side_enabled(self): + def _on_c_side_enabled(self): self.ac_relay_in_transition = False + # If C side is not preferred and has no queue, return to A side + if not self.c_side_queue and self.prefer_a_side: + self._enable_a_side() + return - if self.prefer_a_side: - self.c_side_enabled = True - self.a_side_enabled = False - self._service_c_side() - else: - - if self.a_side_queue: - self._enable_a_side() - return - - self.a_side_enabled = False - self.c_side_enabled = True - self._service_c_side() + self._a_side_enabled = False + self._c_side_enabled = True + self._service_c_side() def _service_c_side(self): if not self.c_side_queue: @@ -488,7 +507,6 @@ def _disable_all_c_side_drivers(self): driver.disable() self.drivers_holding_c_side = set() self.c_side_done_time = 0 - self.c_side_enabled = False def _disable_all_a_side_drivers(self): if self.a_side_active: @@ -496,7 +514,6 @@ def _disable_all_a_side_drivers(self): driver.disable() self.drivers_holding_a_side = set() self.a_side_done_time = 0 - self.a_side_enabled = False def validate_coil_section(self, driver, config): """Validate coil config for platform."""