Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

FAST-0061 Stepper Controller #1855

Merged
merged 21 commits into from
Jan 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9580a38
Rough implementation of FAST Stepper platform device
avanwinkle Oct 9, 2024
4622077
Stepper config option 'home_on_startup'
avanwinkle Oct 9, 2024
24596df
FASTStepper commands for home and stop
avanwinkle Oct 9, 2024
fb42a4b
Support per-event movement speeds on steppers
avanwinkle Oct 11, 2024
c1680c8
Working poll and callback for MS: status requests
avanwinkle Oct 12, 2024
354484b
Polling and is_moving state to detect and async return when stepper s…
avanwinkle Oct 12, 2024
414ae54
Cleanup print and logging
avanwinkle Oct 12, 2024
e2503c2
Increment version 0.57.4.dev1
avanwinkle Oct 12, 2024
52460e8
Gracefully handle startup crashes without trigger downstream errors
avanwinkle Oct 12, 2024
b249f4b
Always send speed to FASTStepper, including an explicit default speed
avanwinkle Oct 12, 2024
f9cb10b
Use platform_settings to determine default stepper speed
avanwinkle Oct 12, 2024
b11dcc1
Faux implementation of move_vel_mode for software homing
avanwinkle Oct 12, 2024
628c8c9
Bugfix lagging MS response causing sequential moves to be dropped
avanwinkle Oct 12, 2024
f563b63
Bugfix speed as array in rel positions
avanwinkle Oct 13, 2024
352d3b6
Bugfix subsequent stepper speeds being lost on overlapping moves
avanwinkle Oct 13, 2024
a11c9ea
Merge branch 'dev' into fast-0061-stepper
avanwinkle Nov 16, 2024
27ff968
Linter fixes
avanwinkle Nov 16, 2024
38e412b
Merge branch 'dev' into fast-0061-stepper
avanwinkle Jan 5, 2025
e884591
Avoid empty set constructor as default arg
avanwinkle Jan 5, 2025
402b810
Don't use util int_to_hex_string because it has minimum 2 digits
avanwinkle Jan 5, 2025
43c9f2e
Remove unused import
avanwinkle Jan 5, 2025
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
12 changes: 10 additions & 2 deletions mpf/config_spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,9 @@ fast_servos:
min_us: single|int|1000
home_us: single|int|1500
max_runtime: single|ms|2000 # 65535 max
fast_stepper_settings:
__valid_in__: machine # todo add to validator
default_speed: single|int|600
file_shows:
__valid_in__: machine, mode # todo add to validator
__type__: config_dict
Expand Down Expand Up @@ -1941,23 +1944,28 @@ spinners:
steppers:
__valid_in__: machine
__type__: device
named_positions: dict|float:str|None
home_events: event_handler|event_handler:ms|None
home_on_startup: single|bool|true
homing_mode: single|enum(hardware,switch)|hardware
homing_switch: single|machine(switches)|None
homing_direction: single|enum(clockwise,counterclockwise)|clockwise
homing_speed: single|int|None
pos_min: single|int|0
pos_max: single|int|1000
ball_search_min: single|int|0
ball_search_max: single|int|1
ball_search_wait: single|ms|5s
include_in_ball_search: single|bool|true
relative_positions: dict|float:str|None
relative_positions: dict|float:subconfig(stepper_position_settings)|None
reset_position: single|int|0
reset_events: event_handler|event_handler:ms|machine_reset_phase_3, ball_starting, ball_will_end, service_mode_entered
named_positions: dict|float:subconfig(stepper_position_settings)|None
number: single|str|
platform: single|str|None
platform_settings: single|dict|None
stepper_position_settings:
event: single|str|
speed: single|int|None
spike_stepper_settings:
homing_speed: single|int|10
speed: single|int|20
Expand Down
6 changes: 4 additions & 2 deletions mpf/devices/servo.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,10 @@ def stop(self):

This should either home the servo or disable the output.
"""
self.debug_log("Stopping servo")
self.hw_servo.stop()
# A crash may occur during startup before hw_servo is instantiated
if self.hw_servo:
self.debug_log("Stopping servo")
self.hw_servo.stop()

@event_handler(5)
def _position_event(self, position, **kwargs):
Expand Down
51 changes: 36 additions & 15 deletions mpf/devices/stepper.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@ class Stepper(SystemWideDevice):
class_label = 'stepper'

__slots__ = ["hw_stepper", "_target_position", "_current_position", "_ball_search_started",
"_ball_search_old_target", "_is_homed", "_is_moving", "_move_task", "delay"]
"_ball_search_old_target", "_is_homed", "_is_moving", "_move_task", "delay",
"_target_speed"]

def __init__(self, machine, name):
"""Initialize 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._target_speed = None # in steps per second
self._ball_search_started = False
self._ball_search_old_target = 0
self._is_homed = False
Expand All @@ -51,14 +53,16 @@ async def _initialize(self):
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.machine.events.add_handler(self.config['named_positions'][position]['event'],
self.event_move_to_position,
position=position)
position=position,
speed=self.config['named_positions'][position]['speed'])

for position in self.config['relative_positions']:
self.machine.events.add_handler(self.config['relative_positions'][position],
self.machine.events.add_handler(self.config['relative_positions'][position]['event'],
self.event_move_to_position,
position=position,
speed=self.config['relative_positions'][position]['speed'],
is_relative=True)

if not self.platform.features['allow_empty_numbers'] and self.config['number'] is None:
Expand All @@ -82,6 +86,13 @@ async def _initialize(self):

def validate_and_parse_config(self, config, is_mode_config, debug_prefix: str = None):
"""Validate stepper config."""
# If positions are just strings, expand them into strings and speeds
# TODO: Figure out how to use express_config() to map this
for cfg in ('named_positions', 'relative_positions'):
if cfg in config:
for pos, value in config[cfg].items():
if isinstance(value, str):
config[cfg][pos] = {'event': value}
config = super().validate_and_parse_config(config, is_mode_config, debug_prefix)
platform = self.machine.get_platform_sections(
'stepper_controllers', getattr(config, "platform", None))
Expand All @@ -94,9 +105,13 @@ async def _run(self):
# wait for switches to be initialized
await self.machine.events.wait_for_event("init_phase_3")

# first home the stepper
self.info_log("Initializing stepper and homing.")
await self._home()
if self.config['home_on_startup']:
# first home the stepper
self.info_log("Initializing stepper and homing.")
await self._home()
else:
self.info_log("Initializing stepper but will not home.")
self._is_homed = True

# run the loop at least once
self._is_moving.set()
Expand All @@ -119,7 +134,10 @@ async def _run(self):
self.info_log("Stepper moving relative %s to hit target %s from %s",
delta, target_position, self._current_position)
# move stepper
self.hw_stepper.move_rel_pos(delta)
self.hw_stepper.move_rel_pos(delta, self._target_speed)
# Clear the speed override here, in case a subsequent move wants
# to set one before this one finishes.
self._target_speed = None
# wait for the move to complete
await self.hw_stepper.wait_for_move_completed()
else:
Expand All @@ -130,12 +148,13 @@ async def _run(self):
# post ready event
self._post_ready_event()

def _move_to_absolute_position(self, position):
def _move_to_absolute_position(self, position, speed=None):
"""Move stepper to position."""
self.info_log("Moving to absolute position %s. Current position: %s",
self.hw_stepper, position, self._current_position)
position, self._current_position)
if self.config['pos_min'] <= position <= self.config['pos_max']:
self._target_position = position
self._target_speed = speed
self._is_moving.set()
else:
raise ValueError("_move_to_absolute_position: position argument beyond limits")
Expand Down Expand Up @@ -176,7 +195,9 @@ def _post_ready_event(self):

def stop_device(self):
"""Stop motor."""
self.hw_stepper.stop()
# A crash during startup may not have initialized the hw_stepper yet
if self.hw_stepper:
self.hw_stepper.stop()
self._is_moving.clear()
if self._move_task:
self._move_task.cancel()
Expand All @@ -201,22 +222,22 @@ def reset(self):
self._move_to_absolute_position(self.config['reset_position'])

@event_handler(5)
def event_move_to_position(self, position=None, is_relative=False, **kwargs):
def event_move_to_position(self, position=None, speed=None, is_relative=False, **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, is_relative)
self.move_to_position(position, speed, is_relative)

def move_to_position(self, position, is_relative=False):
def move_to_position(self, position, speed=None, is_relative=False):
"""Move stepper to a position."""
self.info_log("Stepper at %s moving to %s position %s", self._current_position,
"relative" if is_relative else "absolute", position)
self._target_position = (self._current_position + position) if is_relative else position
if self._ball_search_started:
return
self._move_to_absolute_position(self._target_position)
self._move_to_absolute_position(self._target_position, speed)

def _ball_search_start(self, **kwargs):
del kwargs
Expand Down
23 changes: 22 additions & 1 deletion mpf/platforms/fast/communicators/exp.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""FAST Expansion Board Serial Communicator."""
# mpf/platforms/fast/communicators/exp.py

from functools import partial

from mpf.platforms.fast.fast_defines import EXPANSION_BOARD_FEATURES
from mpf.platforms.fast.fast_exp_board import FastExpansionBoard
from mpf.platforms.fast.communicators.base import FastSerialCommunicator
Expand All @@ -18,14 +20,16 @@ class FastExpCommunicator(FastSerialCommunicator):

IGNORED_MESSAGES = ['XX:F']

__slots__ = ["exp_boards_by_address", "active_board"]
__slots__ = ["exp_boards_by_address", "active_board", "_device_processors"]

def __init__(self, platform, processor, config):
"""Initialize the EXP communicator."""
super().__init__(platform, processor, config)

self.exp_boards_by_address = dict() # keys = board addresses, values = FastExpansionBoard objects
self._device_processors = dict()
self.active_board = None

self.message_processors['BR:'] = self._process_br

async def init(self):
Expand Down Expand Up @@ -104,3 +108,20 @@ def set_led_fade_rate(self, board_address: str, rate: int) -> None:

self.platform.debug_log("%s - Setting LED fade rate to %sms", self, rate)
self.send_and_forget(f'RF@{board_address}:{Util.int_to_hex_string(rate, True)}')

def register_processor(self, message_prefix, board_address, device_id, callback):
"""Register an exp board processor to handle messages."""
if message_prefix not in self.message_processors:
self.message_processors[message_prefix] = partial(self._process_device_msg, message_prefix)
self._device_processors[message_prefix] = dict()
if board_address not in self._device_processors[message_prefix]:
self._device_processors[message_prefix][board_address] = dict()
self._device_processors[message_prefix][board_address][device_id] = callback

def _process_device_msg(self, message_prefix, message):
# Commands like MS: currently don't include the EXP board in the response,
# so there's no way to know which board needs to be informed. Inform them
# all? If multiple boards are running concurrently, it'll get ugly.
device_id = message.split(",")[0]
for board_callback in self._device_processors[message_prefix].values():
board_callback[device_id](message)
39 changes: 38 additions & 1 deletion mpf/platforms/fast/fast.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from mpf.core.platform import (RgbDmdPlatform, DriverConfig, DriverSettings,
LightsPlatform, RepulseSettings,
SegmentDisplayPlatform, ServoPlatform,
StepperPlatform,
SwitchConfig, SwitchSettings)
from mpf.core.utility_functions import Util
from mpf.exceptions.config_file_error import ConfigFileError
Expand All @@ -24,14 +25,15 @@
from mpf.platforms.fast.fast_port_detector import FastPortDetector
from mpf.platforms.fast.fast_segment_display import FASTSegmentDisplay
from mpf.platforms.fast.fast_servo import FastServo
from mpf.platforms.fast.fast_stepper import FastStepper
from mpf.platforms.fast.fast_switch import FASTSwitch
# pylint: disable-msg=too-many-instance-attributes
from mpf.platforms.interfaces.light_platform_interface import LightPlatformInterface
from mpf.platforms.system11 import System11OverlayPlatform


class FastHardwarePlatform(ServoPlatform, LightsPlatform, RgbDmdPlatform,
SegmentDisplayPlatform,
SegmentDisplayPlatform, StepperPlatform,
System11OverlayPlatform):

"""Platform class for the FAST Pinball hardware."""
Expand Down Expand Up @@ -428,6 +430,41 @@ async def configure_servo(self, number: str, config: dict) -> FastServo:

return FastServo(brk_board, port, config)

async def configure_stepper(self, number: str, config: dict) -> FastStepper:
"""Configure a servo.

Args:
----
number: Number of stepper
config: Dict of config settings.

Returns: Stepper object.
"""
# TODO consolidate with similar code in configure_light()
number = number.lower()
parts = number.split("-")

exp_board = self.exp_boards_by_name[parts[0]]

try:
_, port = parts
breakout_id = '0'
except ValueError:
_, breakout_id, port = parts
breakout_id = breakout_id.strip('b')

brk_board = exp_board.breakouts[breakout_id]

# verify this board support servos
assert int(port) <= int(brk_board.features['stepper_ports']) # TODO should this be stored as an int?

return FastStepper(brk_board, port, config)

@classmethod
def get_stepper_config_section(cls):
"""Return config section."""
return "fast_stepper_settings"

def _parse_switch_number(self, number):
try:
board_str, switch_str = number.split("-")
Expand Down
11 changes: 11 additions & 0 deletions mpf/platforms/fast/fast_defines.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@
)

EXPANSION_BOARD_FEATURES = {
'FP-EXP-0061': {
'min_fw': '0.31',
'local_breakouts': ['FP-EXP-0061'],
'breakout_ports': 0,
'default_address': '90'
},
'FP-EXP-0071': {
'min_fw': '0.11',
'local_breakouts': ['FP-EXP-0071'],
Expand Down Expand Up @@ -54,6 +60,11 @@
}

BREAKOUT_FEATURES = {
'FP-EXP-0061': {
'min_fw': '0.33',
'led_ports': 4,
'stepper_ports': 2
},
'FP-EXP-0071': {
'min_fw': '0.11',
'led_ports': 4,
Expand Down
3 changes: 2 additions & 1 deletion mpf/platforms/fast/fast_servo.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Fast servo implementation."""

from mpf.platforms.interfaces.servo_platform_interface import ServoPlatformInterface


Expand All @@ -14,7 +15,7 @@ def __init__(self, breakout_board, port, config):
self.exp_connection = breakout_board.communicator

self.base_address = breakout_board.address
self.servo_index = str(int(port) - 1) # Servos are 0-indexed
self.servo_index = f"{(int(port) - 1):X}" # Servos are 0-indexed hex
self.max_runtime = f"{config['max_runtime']:02X}"

self.write_config_to_servo()
Expand Down
Loading
Loading