Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions mpf/config_spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,9 @@ fast_switches:
fast_coils:
recycle_ms: single|ms|None
pwm2_ms: single|ms|None
max_eos_ms: single|ms|None
max_on_time_ms: single|ms|None
refresh_ms: single|ms|None
fast_servos:
max_us: single|int|2000
min_us: single|int|1000
Expand Down
21 changes: 12 additions & 9 deletions mpf/devices/flipper.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,19 +105,22 @@ def enable(self):

self.debug_log('Enabling flipper with config: %s', self.config)

# Apply the proper hardware rules for our config
# Apply the proper hardware rules for our config:

if self.config['activation_switch']:
# only add rules if we are using a switch
if self.config['use_eos']:
if self.config['activation_switch']: # only add rules if we are using a switch
if self.config['use_eos']: # eos is most significant factor for rule selection
self._enable_main_coil_eos_cutoff_rule()
elif self.config['hold_coil']:
if self.config['hold_coil']:
self._enable_hold_coil_rule()
return

# dual or single wound is second most important factor
if self.config['hold_coil']:
self._enable_main_coil_pulse_rule()
self._enable_hold_coil_rule()
else:
self._enable_single_coil_rule()

if self.config['hold_coil']:
self._enable_hold_coil_rule()

@event_handler(10)
def event_disable(self, **kwargs):
Expand Down Expand Up @@ -221,7 +224,7 @@ def _enable_hold_coil_rule(self):
self._active_rules.append(rule)

def _enable_main_coil_eos_cutoff_rule(self):
if self.config['hold_coil']:
if self.config['hold_coil']: # dual wound case, so main doesnt need hold-up behavior
self.debug_log('Enabling main coil EOS cutoff rule w/o hold')
rule = self.machine.platform_controller.set_pulse_on_hit_and_release_and_disable_rule(
SwitchRuleSettings(switch=self.config['activation_switch'], debounce=False, invert=False),
Expand All @@ -232,7 +235,7 @@ def _enable_main_coil_eos_cutoff_rule(self):
debounce_ms=self.config["eos_active_ms_before_repulse"])
)
self._active_rules.append(rule)
else:
else: # single wound case, so main rule must support holding behavior
self.debug_log('Enabling main coil EOS cutoff rule w/ hold')
rule = self.machine.platform_controller.set_pulse_on_hit_and_enable_and_release_and_disable_rule(
SwitchRuleSettings(switch=self.config['activation_switch'], debounce=False, invert=False),
Expand Down
4 changes: 2 additions & 2 deletions mpf/platforms/fast/fast.py
Original file line number Diff line number Diff line change
Expand Up @@ -957,7 +957,7 @@ def set_pulse_on_hit_and_release_rule(self, enable_switch, coil):
"""
# Force hold to None which is needed with this rule
coil.hold_settings = None
coil.hw_driver.set_hardware_rule('18', enable_switch, coil)
coil.hw_driver.set_hardware_rule('5D', enable_switch, coil)

def set_pulse_on_hit_and_enable_and_release_rule(self, enable_switch: SwitchSettings, coil: DriverSettings):
"""Set pulse on hit and enable and release rule on driver.
Expand Down Expand Up @@ -985,7 +985,7 @@ def set_pulse_on_hit_and_release_and_disable_rule(self, enable_switch: SwitchSet
# If enabled, set a switch rule to look for EOS being open and flipper button closed and manually pulse?
off_switch = eos_switch.hw_switch.hw_number

coil.hw_driver.set_hardware_rule('75', enable_switch, coil, off_switch=off_switch)
coil.hw_driver.set_hardware_rule('5E', enable_switch, coil, off_switch=off_switch)

def set_pulse_on_hit_and_enable_and_release_and_disable_rule(self, enable_switch: SwitchSettings,
eos_switch: SwitchSettings, coil: DriverSettings,
Expand Down
47 changes: 36 additions & 11 deletions mpf/platforms/fast/fast_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,21 @@ def __init__(self, communicator: FastSerialCommunicator, hw_number: int) -> None
self.current_driver_config = self.baseline_driver_config

self.mode_param_mapping = {
'10': ['pwm1_ms', 'pwm1_power', 'pwm2_ms', 'pwm2_power', 'recycle_ms'],
'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'],
'10': ['pwm1_ms', 'pwm1_power', 'pwm2_ms', 'pwm2_power', 'recycle_ms'], # pulse
'12': ['pwm1_ms', 'pwm1_power', 'pwm2_ms', 'pwm2_power', 'kick_ms'], # pulse + kick
'18': ['pwm1_ms', 'pwm1_power', 'pwm2_power', 'recycle_ms'], # pulse + hold
'20': ['off_switch', 'pwm1_ms', 'pwm1_power', 'pwm2_power', 'rest_ms'], # pulse + hold w cancel switch
'25': ['relay_on_report_ms', 'relay_off_report_ms'], # system 11 AC relay
'30': ['delay_ms_x10', 'pwm1_ms', 'pwm2_ms', 'pwm2_power', 'recycle_ms'], # delayed pulse
'70': ['pwm1_ms', 'pwm1_power', 'pwm2_ms_x100', 'pwm2_power', 'recycle_ms'], # long pulse
'75': ['off_switch', 'pwm1_ms', 'pwm2_ms_x100', 'pwm2_power', 'recycle_ms'], # pulse w cancel switch
'78': ['pwm1_ms', 'pwm1_power', 'hold_ms_x10', 'pwm2_power', 'rest_ms'], # pulse + hold w extension
'59': ['pwm1_ms', 'pwm1_power', 'pwm2_power'], # Flipper Hold (Dual PWM)
'5A': ['off_switch', 'pwm1_power', 'max_eos_ms', 'max_on_time_ms', 'refresh_ms'], # Flipper Max Dynamic Range
'5B': ['off_switch', 'pwm1_power', 'max_eos_ms', 'max_on_time_ms', 'refresh_ms'], # Flipper Max Dynamic Begin Range
'5D': ['pwm1_ms', 'pwm1_power', 'pwm2_power'], # Flipper Hold Direct
'5E': ['off_switch', 'pwm1_power', 'pwm2_power', 'max_eos_ms', 'refresh_ms'], # Flipper Main Direct
'5F': ['off_switch', 'pwm1_ms', 'pwm1_power', 'rest_ms'] # Flipper 3 Try
}

def set_initial_config(self, mpf_config: DriverConfig, platform_settings):
Expand Down Expand Up @@ -291,14 +298,32 @@ def set_hardware_rule(self, mode, switch, coil_settings, **kwargs): # noqa: MC0
else:
new_settings['recycle_ms'] = Util.int_to_hex_string(coil_settings.recycle)

for i in range(len(self.mode_param_mapping[mode]), 5):
setattr(self.current_driver_config, f'param{i + 1}', '00')

# Update the current_driver_config with any new settings
for idx, param in enumerate(self.mode_param_mapping[mode]):
if param is None or new_settings.get(param, None) is None:
if param is None:
continue

if new_settings.get(param, None) is None:
if self.platform_settings.get(param) is not None:
new_settings[param] = Util.int_to_hex_string(self.platform_settings[param])
elif param == 'max_eos_ms':
new_settings[param] = Util.int_to_hex_string(50)
elif param == 'max_on_time_ms':
new_settings[param] = Util.int_to_hex_string(255)
elif param == 'refresh_ms':
new_settings[param] = Util.int_to_hex_string(2)
else:
continue

if param == 'delay_ms_x10':
new_settings['delay_ms'] = Util.int_to_hex_string(int(new_settings[param], 16) // 10)
new_settings[param] = Util.int_to_hex_string(int(new_settings[param], 16) // 10)
elif param == 'pwm2_ms_x100':
new_settings['pwm2_ms'] = Util.int_to_hex_string(int(new_settings[param], 16) // 100)
new_settings[param] = Util.int_to_hex_string(int(new_settings[param], 16) // 100)
elif param == 'hold_ms_x10':
new_settings[param] = Util.int_to_hex_string(int(new_settings[param], 16) // 10)

param_name = f'param{idx + 1}'
if new_settings[param] != getattr(self.current_driver_config, param_name):
Expand Down
68 changes: 67 additions & 1 deletion mpf/tests/test_Fast_Neuron.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# mpf.tests.test_Fast_Neuron

from mpf.core.platform import SwitchConfig
from mpf.platforms.fast.fast_driver import FASTDriver
from mpf.tests.test_Fast import TestFastBase
from mpf.tests.MpfTestCase import MagicMock

Expand Down Expand Up @@ -959,4 +960,69 @@ def test_fast_game(self):
}

self.hit_and_release_switch("s_debounce_auto")
self.advance_time_and_run()
self.advance_time_and_run()

def test_neuron_driver_advanced_modes_and_padding(self):
"""Directly tests that advanced driver modes serialize parameters and pad safely."""

# Instantiate a naked driver object directly to bypass platform connection mock maps
mock_communicator = MagicMock()
hw_driver = FASTDriver(communicator=mock_communicator, hw_number=0)

# Mock up minimal switch and coil settings objects for the function signature
mock_switch = MagicMock()
mock_switch.hw_switch.hw_number = '01'
mock_switch.invert = False

mock_coil = MagicMock()
mock_coil.pulse_settings.duration = 23 # 0x17
mock_coil.pulse_settings.power = 1.0 # 0xFF
mock_coil.hold_settings.power = 0.25 # 0x40
mock_coil.hold_settings.duration = None
mock_coil.recycle = False

# Ensure platform_settings starts completely clean (no YAML overrides)
hw_driver.platform_settings = {}

# TEST 1: Short Mode 5D (Flipper Hold Direct) - Verify safe zero-padding
# Seed trailing slots with garbage data to prove the padding loop clears it
hw_driver.current_driver_config.param4 = 'AA'
hw_driver.current_driver_config.param5 = 'BB'

# Execute the hardware rule update for Mode 5D
hw_driver.set_hardware_rule(mode='5D', switch=mock_switch, coil_settings=mock_coil)

# Assertions for 3-parameter mode boundaries; tail params are zeroed
self.assertEqual(hw_driver.current_driver_config.mode, '5D')
self.assertEqual(hw_driver.current_driver_config.param1, '17') # pwm1_ms
self.assertEqual(hw_driver.current_driver_config.param2, 'FF') # pwm1_power
self.assertEqual(hw_driver.current_driver_config.param3, '88') # pwm2_power
self.assertEqual(hw_driver.current_driver_config.param4, '00')
self.assertEqual(hw_driver.current_driver_config.param5, '00')

# TEST 2: Mode 5A (Max Dynamic Range) - Verify name-checked defaults
hw_driver.set_hardware_rule(mode='5A', switch=mock_switch, coil_settings=mock_coil, off_switch='01')

self.assertEqual(hw_driver.current_driver_config.mode, '5A')
self.assertEqual(hw_driver.current_driver_config.param1, '01') # off_switch mapped from kwargs
self.assertEqual(hw_driver.current_driver_config.param2, 'FF') # pwm1_power

# Verify our explicit non-zero timing safety fallbacks kicked in
self.assertEqual(hw_driver.current_driver_config.param3, '32') # max_eos_ms default: 50 -> 0x32
self.assertEqual(hw_driver.current_driver_config.param4, 'FF') # max_on_time_ms default: 255 -> 0xFF
self.assertEqual(hw_driver.current_driver_config.param5, '02') # refresh_ms default: 2 -> 0x02

# TEST 3: Mode 5A with YAML overrides - Verify custom platform settings
# Populate platform_settings to mimic explicit options specified in a YAML profile
hw_driver.platform_settings = {
'max_eos_ms': 30, # 0x1E
'max_on_time_ms': 100, # 0x64
'refresh_ms': 10 # 0x0A
}

hw_driver.set_hardware_rule(mode='5A', switch=mock_switch, coil_settings=mock_coil)

# Verify that user-defined YAML attributes completely override the safe fallbacks
self.assertEqual(hw_driver.current_driver_config.param3, '1E')
self.assertEqual(hw_driver.current_driver_config.param4, '64')
self.assertEqual(hw_driver.current_driver_config.param5, '0A')
Loading