Repository: missionpinball/mpf

Issue:
System 11 A/C select relay handling

[snux.zip](https://github.com/missionpinball/mpf/files/2730432/snux.zip)

Add 2 parameters to the snux: platform in config_spec.yaml... (the last 2 in this list)

```
snux:
    __valid_in__: machine
    flipper_enable_driver: single|machine(coils)|
    diag_led_driver: single|machine(coils)|
    platform: single|str|None
    console_log: single|enum(none,basic,full)|none
    file_log: single|enum(none,basic,full)|basic
    prefer_a_side_event: single|str|game_ended
    prefer_c_side_event: single|str|game_will_start
```

Modify the snux.py platform code to know what the A/C relay should do when at rest.  By default during game play it will now rest in the 'C' side (so relay is energised).  Typically flasher lamps are on the C side so they will get a quick response instead of having to wait for the relay to switch.  When the game is over the relay will prefer the 'A' side (so de-energised).  I've attached the snux.py code and the change to config_spec.yaml is mentioned above.  

I also need to update the docs to state that care should be taken if processing a lot of drives to the A and C sides at the same time.  That message applies regardless of this change.

Discussion:

Patch:
diff --git a/mpf/config_spec.yaml b/mpf/config_spec.yaml
index afe7a653e..9772cb31c 100644
--- a/mpf/config_spec.yaml
+++ b/mpf/config_spec.yaml
@@ -1232,6 +1232,8 @@ snux:
     platform: single|str|None
     console_log: single|enum(none,basic,full)|none
     file_log: single|enum(none,basic,full)|basic
+    prefer_a_side_event: single|str|game_ended
+    prefer_c_side_event: single|str|game_will_start
 smartmatrix:
     __valid_in__: machine
     port: single|str|
diff --git a/mpf/platforms/snux.py b/mpf/platforms/snux.py
index d92e3c81a..ce17d8026 100644
--- a/mpf/platforms/snux.py
+++ b/mpf/platforms/snux.py
@@ -47,6 +47,11 @@ class SnuxHardwarePlatform(DriverPlatform):
         self.c_side_enabled = False
 
         self.ac_relay_in_transition = False
+        # 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.
+        # At game over though, it should prefer the 'A' side so that the relay isn't permanently energised.
+        self.prefer_a_side = True
 
     def stop(self):
         """Stop the overlay. Nothing to do here because stop is also called on parent platform."""
@@ -62,6 +67,16 @@ class SnuxHardwarePlatform(DriverPlatform):
         """Return if C side cannot be switches off right away."""
         return self.drivers_holding_c_side or self.c_side_done_time > self.machine.clock.get_time()
 
+    @property
+    def c_side_busy(self):
+        """Return if C side cannot be switches off right away."""
+        return self.drivers_holding_c_side or self.c_side_done_time > self.machine.clock.get_time() or self.c_side_queue
+
+    @property
+    def a_side_active(self):
+        """Return if A side cannot be switches off right away."""
+        return self.drivers_holding_a_side or self.a_side_done_time > self.machine.clock.get_time()
+
     def _null_log_handler(self, *args, **kwargs):
         pass
 
@@ -105,6 +120,22 @@ class SnuxHardwarePlatform(DriverPlatform):
         self.machine.events.add_handler('init_phase_5',
                                         self._initialize_phase_2)
 
+        self.machine.events.add_handler(self.snux_config['prefer_a_side_event'], self._prefer_a_side)
+        self.log.info("Configuring Snux driver to prefer A side on event %s", self.snux_config['prefer_a_side_event'])
+
+        self.machine.events.add_handler(self.snux_config['prefer_c_side_event'], self._prefer_c_side)
+        self.log.info("Configuring Snux driver to prefer C side on event %s", self.snux_config['prefer_c_side_event'])
+
+    def _prefer_a_side(self, **kwargs):
+        del kwargs
+        self.prefer_a_side = True
+        self._enable_a_side()
+
+    def _prefer_c_side(self, **kwargs):
+        del kwargs
+        self.prefer_a_side = False
+        self._enable_c_side()
+
     def _initialize_phase_2(self, **kwargs):
         del kwargs
         self.machine.clock.schedule_interval(self._flash_diag_led, 0.5)
@@ -121,12 +152,20 @@ class SnuxHardwarePlatform(DriverPlatform):
 
         Called based on the timer_tick event
         """
-        if self.a_side_queue:
-            self._service_a_side()
-        elif self.c_side_queue:
-            self._service_c_side()
-        elif self.c_side_enabled and not self.c_side_active:
-            self._enable_a_side()
+        if self.prefer_a_side:
+            if self.a_side_queue:
+                self._service_a_side()
+            elif self.c_side_queue:
+                self._service_c_side()
+            elif self.c_side_enabled and not self.c_side_active:
+                self._enable_a_side()
+        else:
+            if self.c_side_queue:
+                self._service_c_side()
+            elif self.a_side_queue:
+                self._service_a_side()
+            elif self.a_side_enabled and not self.a_side_active:
+                self._enable_c_side()
 
     def _flash_diag_led(self):
         """Flash diagnosis LED."""
@@ -216,13 +255,22 @@ class SnuxHardwarePlatform(DriverPlatform):
 
         This action will be serviced immediately if it can, or ASAP otherwise.
         """
-        if driver in self.a_drivers:
-            self.a_side_queue.add((driver, pulse_settings, hold_settings))
-            self._service_a_side()
-        elif driver in self.c_drivers:
-            self.c_side_queue.add((driver, pulse_settings, hold_settings))
-            if not self.ac_relay_in_transition and not self.a_side_busy:
+        if self.prefer_a_side:
+            if driver in self.a_drivers:
+                self.a_side_queue.add((driver, pulse_settings, hold_settings))
+                self._service_a_side()
+            elif driver in self.c_drivers:
+                self.c_side_queue.add((driver, pulse_settings, hold_settings))
+                if not self.ac_relay_in_transition and not self.a_side_busy:
+                    self._service_c_side()
+        else:
+            if driver in self.c_drivers:
+                self.c_side_queue.add((driver, pulse_settings, hold_settings))
                 self._service_c_side()
+            elif driver in self.a_drivers:
+                self.a_side_queue.add((driver, pulse_settings, hold_settings))
+                if not self.ac_relay_in_transition and not self.c_side_busy:
+                    self._service_a_side()
 
     def _enable_ac_relay(self):
         self.system11_config['ac_relay_driver'].enable()
@@ -245,27 +293,46 @@ class SnuxHardwarePlatform(DriverPlatform):
     # -------------------------------- A SIDE ---------------------------------
 
     def _enable_a_side(self):
-        if not self.a_side_enabled and not self.ac_relay_in_transition:
-
-            if self.c_side_active:
-                self._disable_all_c_side_drivers()
+        if self.prefer_a_side:
+            if not self.a_side_enabled and not self.ac_relay_in_transition:
+
+                if self.c_side_active:
+                    self._disable_all_c_side_drivers()
+                    self._disable_ac_relay()
+                    self.delay.add(ms=self.system11_config['ac_relay_delay_ms'],
+                                   callback=self._enable_a_side,
+                                   name='enable_a_side')
+                    return
+
+                elif self.c_side_enabled:
+                    self._disable_ac_relay()
+
+                else:
+                    self._a_side_enabled()
+        else:
+            if (not self.ac_relay_in_transition and
+                    not self.a_side_enabled and
+                    not self.c_side_busy):
                 self._disable_ac_relay()
-                self.delay.add(ms=self.system11_config['ac_relay_delay_ms'],
-                               callback=self._enable_a_side,
-                               name='enable_a_side')
-                return
 
-            elif self.c_side_enabled:
-                self._disable_ac_relay()
-
-            else:
-                self._a_side_enabled()
+            elif self.a_side_enabled and self.a_side_queue:
+                self._service_a_side()
 
     def _a_side_enabled(self):
         self.ac_relay_in_transition = False
-        self.a_side_enabled = True
-        self.c_side_enabled = False
-        self._service_a_side()
+        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
+
+            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:
@@ -300,24 +367,47 @@ class SnuxHardwarePlatform(DriverPlatform):
     # -------------------------------- C SIDE ---------------------------------
 
     def _enable_c_side(self):
-        if (not self.ac_relay_in_transition and
-                not self.c_side_enabled and
-                not self.a_side_busy):
-            self._enable_ac_relay()
+        if self.prefer_a_side:
+            if not self.c_side_enabled and not self.ac_relay_in_transition:
+
+                if self.a_side_active:
+                    self._disable_all_a_side_drivers()
+                    self._enable_ac_relay()
+                    self.delay.add(ms=self.system11_config['ac_relay_delay_ms'],
+                                   callback=self._enable_c_side,
+                                   name='enable_c_side')
+                    return
+
+                elif self.a_side_enabled:
+                    self._enable_ac_relay()
+
+                else:
+                    self._c_side_enabled()
+        else:
+            if (not self.ac_relay_in_transition and
+                    not self.c_side_enabled and
+                    not self.a_side_busy):
+                self._enable_ac_relay()
 
-        elif self.c_side_enabled and self.c_side_queue:
-            self._service_c_side()
+            elif self.c_side_enabled and self.c_side_queue:
+                self._service_c_side()
 
     def _c_side_enabled(self):
         self.ac_relay_in_transition = False
 
-        if self.a_side_queue:
-            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:
 
-        self.a_side_enabled = False
-        self.c_side_enabled = True
-        self._service_c_side()
+            if self.a_side_queue:
+                self._enable_a_side()
+                return
+
+            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:
@@ -359,6 +449,14 @@ class SnuxHardwarePlatform(DriverPlatform):
             self.c_side_done_time = 0
             self.c_side_enabled = False
 
+    def _disable_all_a_side_drivers(self):
+        if self.a_side_active:
+            for driver in self.drivers_holding_a_side:
+                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."""
         return self.platform.validate_coil_section(driver, config)


Test patch:
diff --git a/mpf/tests/test_Snux.py b/mpf/tests/test_Snux.py
index f21149f55..46bda22aa 100644
--- a/mpf/tests/test_Snux.py
+++ b/mpf/tests/test_Snux.py
@@ -1,10 +1,9 @@
 """Test snux platform."""
+from mpf.tests.MpfFakeGameTestCase import MpfFakeGameTestCase, MagicMock
 from mpf.platforms.interfaces.driver_platform_interface import PulseSettings, HoldSettings
 
-from mpf.tests.MpfTestCase import MpfTestCase, MagicMock
 
-
-class TestSnux(MpfTestCase):
+class TestSnux(MpfFakeGameTestCase):
     def getConfigFile(self):
         return 'config.yaml'
 
@@ -30,6 +29,22 @@ class TestSnux(MpfTestCase):
                 return driver
         return False
 
+    def test_ac_relay_default(self):
+        # outside game it should be default off
+        c_ac_relay = self.machine.coils["c_ac_relay"]
+        self.assertEqual("disabled", c_ac_relay.hw_driver.state)
+
+        self.start_game()
+        # during a game it should be default on to allow fast flashers
+        self.assertEqual("enabled", c_ac_relay.hw_driver.state)
+
+        # after the game ended it should turn back to default off
+        self.drain_all_balls()
+        self.drain_all_balls()
+        self.drain_all_balls()
+        self.assertGameIsNotRunning()
+        self.assertEqual("disabled", c_ac_relay.hw_driver.state)
+
     def test_ac_switch_and_pulse(self):
         # test diag led flashing. otherwise snux is not running
         c_diag_led_driver = self.machine.coils.c_diag_led_driver