Skip to content

Commit

Permalink
Merge pull request #1638 from toomanybrians/fast-segment-throttling
Browse files Browse the repository at this point in the history
Fast segment display update hz
  • Loading branch information
toomanybrians authored May 23, 2022
2 parents da0ec7f + 1275490 commit c84b6df
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 40 deletions.
2 changes: 1 addition & 1 deletion mpf/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"""

__version__ = '0.56.0-dev.19'
__version__ = '0.56.0-dev.20'
'''The full version of MPF.'''

__short_version__ = '0.56'
Expand Down
2 changes: 1 addition & 1 deletion mpf/config_spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -620,11 +620,11 @@ fast:
net_buffer: single|int|10
rgb_buffer: single|int|3
dmd_buffer: single|int|3
segment_buffer: single|int|4
console_log: single|enum(none,basic,full)|none
file_log: single|enum(none,basic,full)|basic
firmware_updates: list|subconfig(fast_firmware_update)|None
ignore_rgb_crash: single|bool|false
segment_display_update_hz: single|int|20
fast_firmware_update:
type: single|enum(net,rgb)|
file: single|str|
Expand Down
2 changes: 1 addition & 1 deletion mpf/core/machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ async def reset(self) -> None:
These events are posted when MPF boots (after the init_phase events are
posted), and they're also posted subsequently when the machine is reset
(after existing the service mode, for example).
(after exiting the service mode, for example).
This is a queue event. The machine reset phase 3 will not be complete
until the queue is cleared.
Expand Down
63 changes: 48 additions & 15 deletions mpf/platforms/fast/fast.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import asyncio
import os
from copy import deepcopy
from distutils.version import StrictVersion
from packaging import version
from typing import Dict, Set, Optional
from serial import SerialException

Expand Down Expand Up @@ -43,7 +43,8 @@ class FastHardwarePlatform(ServoPlatform, LightsPlatform, DmdPlatform,

__slots__ = ["dmd_connection", "net_connection", "rgb_connection", "seg_connection", "is_retro",
"serial_connections", "fast_leds", "fast_commands", "config", "machine_type", "hw_switch_data",
"io_boards", "flag_led_tick_registered", "_watchdog_task", "_led_task"]
"io_boards", "flag_led_tick_registered", "_watchdog_task", "_led_task", "_seg_task",
"fast_segs"]

def __init__(self, machine):
"""Initialise fast hardware platform.
Expand Down Expand Up @@ -84,7 +85,9 @@ def __init__(self, machine):
self._led_task = None
self.serial_connections = set() # type: Set[FastSerialCommunicator]
self.fast_leds = {}
self.fast_segs = list()
self.flag_led_tick_registered = False
self._seg_task = None
self.hw_switch_data = None
self.io_boards = {} # type: Dict[int, FastIoBoard]

Expand Down Expand Up @@ -155,7 +158,7 @@ def _update_net(self) -> str:
max_firmware = self.net_connection.remote_firmware
update_config = None
for update in self.config['firmware_updates']:
if StrictVersion(update['version']) > StrictVersion(max_firmware) and update['type'] == "net":
if version.parse(update['version']) > version.parse(max_firmware) and update['type'] == "net":
update_config = update

if not update_config:
Expand Down Expand Up @@ -187,6 +190,9 @@ def stop(self):
if self._led_task:
self._led_task.cancel()
self._led_task = None
if self._seg_task:
self._seg_task.cancel()
self._seg_task = None
if self._watchdog_task:
self._watchdog_task.cancel()
self._watchdog_task = None
Expand Down Expand Up @@ -244,8 +250,10 @@ def register_io_board(self, board):

def _update_watchdog(self):
"""Send Watchdog command."""
if self.net_connection:
try:
self.net_connection.send('WD:' + str(hex(self.config['watchdog']))[2:])
except:
pass

def process_received_message(self, msg: str, remote_processor: str):
"""Send an incoming message from the FAST controller to the proper method for servicing.
Expand All @@ -268,7 +276,7 @@ def process_received_message(self, msg: str, remote_processor: str):
self.log.warning("Received malformed message: %s from %s", msg, remote_processor)
return

# Can't use try since it swallows too many errors for now
# Can't use try since it swallows too many errors for now #TODO
if cmd in self.fast_commands:
self.fast_commands[cmd](payload, remote_processor)
else: # pragma: no cover
Expand Down Expand Up @@ -335,22 +343,48 @@ def register_processor_connection(self, name: str, communicator):
self.net_connection = communicator
elif name == 'SEG':
self.seg_connection = communicator

if not self._seg_task:
# Need to wait until the segs are all set up
self.machine.events.add_handler('machine_reset_phase_3', self._start_seg_updates)

elif name == 'RGB':
self.rgb_connection = communicator
self.rgb_connection.send('RF:0')
self.rgb_connection.send('RA:000000') # turn off all LEDs
self.rgb_connection.send('RF:{}'.format(
Util.int_to_hex_string(self.config['hardware_led_fade_time'])))

def _start_seg_updates(self, **kwargs):

for s in self.machine.device_manager.collections["segment_displays"]:
self.fast_segs.append(s.hw_display)

self.fast_segs.sort(key=lambda x: x.number)

if self.fast_segs:
self._seg_task = self.machine.clock.schedule_interval(self._update_segs,
1 / self.machine.config['fast'][
'segment_display_update_hz'])

def _update_segs(self, **kwargs):

for s in self.fast_segs:

if s.next_text:
self.seg_connection.send(f'PA:{s.hex_id},{s.next_text.convert_to_str()[0:7]}')
s.next_text = None

if s.next_color:
self.seg_connection.send(('PC:{},{}').format(s.hex_id, s.next_color))
s.next_color = None

def update_leds(self):
"""Update all the LEDs connected to a FAST controller.
This is done once per game loop for efficiency (i.e. all LEDs are sent as a single
update rather than lots of individual ones).
Also, every LED is updated every loop, even if it doesn't change. This
is in case some interference causes a LED to change color. Since we
update every loop, it will only be the wrong color for one tick.
"""
dirty_leds = [led for led in self.fast_leds.values() if led.dirty]

Expand Down Expand Up @@ -774,7 +808,6 @@ async def configure_segment_display(self, number: str, display_size: int, platfo
"""Configure a segment display."""
self.debug_log("Configuring FAST segment display.")
del platform_settings
del display_size
if not self.seg_connection:
raise AssertionError("A request was made to configure a FAST "
"Segment Display but no connection is "
Expand All @@ -793,7 +826,7 @@ def get_switch_config_section(cls):
"""Return switch config section."""
return "fast_switches"

def _check_switch_coil_combincation(self, switch, coil):
def _check_switch_coil_combination(self, switch, coil):
# V2 hardware can write rules across node boards
if not self.net_connection.is_legacy:
return
Expand Down Expand Up @@ -824,7 +857,7 @@ def set_pulse_on_hit_and_release_rule(self, enable_switch, coil):
"Driver: %s", enable_switch.hw_switch.number,
coil.hw_driver.number)

self._check_switch_coil_combincation(enable_switch, coil)
self._check_switch_coil_combination(enable_switch, coil)

driver = coil.hw_driver

Expand Down Expand Up @@ -857,8 +890,8 @@ def set_pulse_on_hit_and_release_and_disable_rule(self, enable_switch: SwitchSet
"%s, Driver: %s", enable_switch.hw_switch.number,
coil.hw_driver.number)

self._check_switch_coil_combincation(enable_switch, coil)
self._check_switch_coil_combincation(eos_switch, coil)
self._check_switch_coil_combination(enable_switch, coil)
self._check_switch_coil_combination(eos_switch, coil)

driver = coil.hw_driver

Expand Down Expand Up @@ -890,7 +923,7 @@ def set_pulse_on_hit_rule(self, enable_switch: SwitchSettings, coil: DriverSetti
"Driver: %s", enable_switch.hw_switch.number,
coil.hw_driver.number)

self._check_switch_coil_combincation(enable_switch, coil)
self._check_switch_coil_combination(enable_switch, coil)

driver = coil.hw_driver

Expand All @@ -912,7 +945,7 @@ def set_pulse_on_hit_and_enable_and_release_rule(self, enable_switch: SwitchSett
"Switch: %s, Driver: %s",
enable_switch.hw_switch.number, coil.hw_driver.number)

self._check_switch_coil_combincation(enable_switch, coil)
self._check_switch_coil_combination(enable_switch, coil)

driver = coil.hw_driver

Expand Down
14 changes: 7 additions & 7 deletions mpf/platforms/fast/fast_segment_display.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,29 @@ class FASTSegmentDisplay(SegmentDisplayPlatformInterface):

"""FAST segment display."""

__slots__ = ["serial", "hex_id"]
__slots__ = ["serial", "hex_id", "next_color", "next_text"]

def __init__(self, index, communicator):
"""Initialise alpha numeric display."""
super().__init__(index)
self.serial = communicator
self.hex_id = Util.int_to_hex_string(index * 7)
self.next_color = None
self.next_text = None

def set_text(self, text: ColoredSegmentDisplayText, flashing: FlashingType, flash_mask: str) -> None:
"""Set digits to display."""
del flashing
del flash_mask
colors = text.get_colors()
self.serial.send(f'PA:{self.hex_id},{text.convert_to_str()[0:7]}')
self.next_text = text
if colors:
self._set_color(colors)

def _set_color(self, colors: List[RGBColor]) -> None:
"""Set display color."""
self.serial.platform.info_log("Color: {}".format(colors))
#self.serial.platform.info_log("Color: {}".format(colors))
if len(colors) == 1:
colors = (RGBColor(colors[0]).hex + ',') * 7
self.next_color = (RGBColor(colors[0]).hex + ',') * 7
else:
colors = ','.join([RGBColor(color).hex for color in colors])
self.serial.send(('PC:{},{}').format(
self.hex_id, colors))
self.next_color = ','.join([RGBColor(color).hex for color in colors]) + ','
31 changes: 16 additions & 15 deletions mpf/platforms/fast/fast_serial_communicator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Fast serial communicator."""
import asyncio
from distutils.version import StrictVersion
from packaging import version
from mpf.platforms.fast import fast_defines

from mpf.core.utility_functions import Util
Expand All @@ -12,14 +12,14 @@

# The following minimum firmware versions are to prevent breaking changes
# in MPF from running on boards that have not been updated.
DMD_MIN_FW = '0.88' # Minimum FW for a DMD
NET_MIN_FW = '2.0' # Minimum FW for a V2 controller
NET_LEGACY_MIN_FW = '0.88' # Minimum FW for a V1 controller
RGB_MIN_FW = '2.0' # Minimum FW for an RGB LED controller
RGB_LEGACY_MIN_FW = '0.87'
IO_MIN_FW = '1.09' # Minimum FW for an IO board linked to a V2 controller
IO_LEGACY_MIN_FW = '0.87' # Minimum FW for an IO board linked to a V1 controller
SEG_MIN_FW = '0.10' # Minimum FW for a Segment Display
DMD_MIN_FW = version.parse('0.88') # Minimum FW for a DMD
NET_MIN_FW = version.parse('2.0') # Minimum FW for a V2 controller
NET_LEGACY_MIN_FW = version.parse('0.88') # Minimum FW for a V1 controller
RGB_MIN_FW = version.parse('2.0') # Minimum FW for an RGB LED controller
RGB_LEGACY_MIN_FW = version.parse('0.87')
IO_MIN_FW = version.parse('1.09') # Minimum FW for an IO board linked to a V2 controller
IO_LEGACY_MIN_FW = version.parse('0.87') # Minimum FW for an IO board linked to a V1 controller
SEG_MIN_FW = version.parse('0.10') # Minimum FW for a Segment Display

LEGACY_ID = 'FP-CPU-0' # Start of an id for V1 controller
RETRO_ID = 'FP-SBI' # Start of an id for a Retro controller
Expand Down Expand Up @@ -143,7 +143,7 @@ async def _identify_connection(self):

if self.remote_model.startswith(RETRO_ID):
self.is_retro = True
elif StrictVersion(self.remote_firmware) < StrictVersion(V2_FW):
elif self.remote_firmware < V2_FW:
self.is_legacy = True

self.platform.log.info("Connected! Processor: %s, "
Expand Down Expand Up @@ -191,13 +191,11 @@ async def _identify_connection(self):
elif self.remote_processor == 'SEG':
min_version = SEG_MIN_FW
# latest_version = SEG_LATEST_FW
self.max_messages_in_flight = self.platform.config['segment_buffer']
self.platform.debug_log("Setting SEG buffer size: %s",
self.max_messages_in_flight)
self.max_messages_in_flight = 0 # SEG doesn't have ACK messages
else:
raise AttributeError(f"Unrecognized FAST processor type: {self.remote_processor}")

if StrictVersion(min_version) > StrictVersion(self.remote_firmware):
if version.parse(self.remote_firmware) < min_version:
raise AssertionError(f'Firmware version mismatch. MPF requires the {self.remote_processor} processor '
f'to be firmware {min_version}, but yours is {self.remote_firmware}')

Expand Down Expand Up @@ -315,7 +313,7 @@ async def query_fast_io_boards(self):
node_id, model, fw, int(sw, 16), int(dr, 16))

min_fw = IO_LEGACY_MIN_FW if self.is_legacy else IO_MIN_FW
if StrictVersion(min_fw) > str(fw):
if min_fw > version.parse(fw):
self.platform.log.critical("Firmware version mismatch. MPF requires the IO boards "
"to be firmware %s, but your Board %s (%s) is firmware %s",
min_fw, node_id, model, fw)
Expand Down Expand Up @@ -343,6 +341,9 @@ def _send(self, msg):
if debug and msg[0] != "W":
self.platform.log.debug("Send: %s", "".join(" 0x%02x" % b for b in msg))

elif not self.max_messages_in_flight: # For processors that don't use this
self.writer.write(msg.encode() + b'\r')
self.platform.log.debug("Sending without message flight tracking: %s", msg)
else:
self.messages_in_flight += 1
if self.messages_in_flight > self.max_messages_in_flight:
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
'pyserial-asyncio==0.5;platform_system!="Windows"',
'sortedcontainers==2.3.0',
'psutil==5.7.3',
'packaging',
],

extras_require={
Expand Down

0 comments on commit c84b6df

Please sign in to comment.