Skip to content

Commit

Permalink
Lots of FAST platform cleanup, syntax, etc. 0.57.0.dev13
Browse files Browse the repository at this point in the history
  • Loading branch information
toomanybrians committed Sep 27, 2023
1 parent 39bb7c8 commit 16bbf71
Show file tree
Hide file tree
Showing 9 changed files with 83 additions and 112 deletions.
2 changes: 1 addition & 1 deletion mpf/_version.py
Expand Up @@ -10,7 +10,7 @@
"""

__version__ = '0.57.0.dev12'
__version__ = '0.57.0.dev13'
'''The full version of MPF.'''

__short_version__ = '0.57'
Expand Down
4 changes: 2 additions & 2 deletions mpf/platforms/fast/TODO.md
Expand Up @@ -7,13 +7,12 @@
* RGBW LEDs on EXP bus (Eli will add functionality to the EXP boards that will take RGB colors and convert them to RGBW, so all we'll have to add is a way to specify which LEDs are RGBW so we can send the correct config commands.)
* Add ability to not probe the serial ports for platforms where the order does not change (e.g. everything but windows.) This will speed up the initial load time. (though only by about 100ms) We will still use the USB PID/VID to identify which serial ports are connected to which devices, we just won't actually probe them to verify their IDs.

# Things to verify / check / confirm
## Things to verify / check / confirm

* Digital driver, confirm? Initial config?
* Verify driver enable for neuron, with disable later
* Find all the TODOs


## Tests Needed

* Auto port detection
Expand All @@ -34,6 +33,7 @@
* Move FAST devices on EXP and breakout boards to mixin classes. e.g. LEds are a mixin class, servos are a mixin class, so the config for an EXP-0071 would pull in those two mixin classes rather than having device specific code in the base class.
* Figure out the config file errors / error logs to FAST site / etc. Currently there is no consistency, need to figure out which errors we link to the web, come up with a template and URLs to display them, etc.
* Single LED update task. Currently the LED update task is per EXP board which means they can be off by a few ms. We should have a single LED update task that runs at the same time for all boards.
* Change EXP `led_hz` setting to `led_update_ms` or something (specify in ms and not hertz since that's how the hardware works. Also enforce multiples of 32ms since that's the hw update speed.)

## MPF general

Expand Down
56 changes: 37 additions & 19 deletions mpf/platforms/fast/communicators/base.py
Expand Up @@ -129,7 +129,6 @@ async def clear_board_serial_buffer(self):
"""Clear out the serial buffer."""

self.write_to_port(b'\r\r\r\r')
# await asyncio.sleep(.5)

async def init(self):
raise NotImplementedError(f"{self.__class__.__name__} does not implement init()")
Expand All @@ -138,12 +137,6 @@ def _process_xx(self, msg):
"""Process the XX response."""
self.log.warning(f"Received XX response:{msg}") # what are we going to do here? TODO

def process_pass_message(self, msg):
if msg == 'P':
return
else:
self.log.warning(f"Received unexpected pass message:{msg}")

def _process_id(self, msg):
"""Process the ID response."""
processor, self.remote_model, self.remote_firmware = msg.split()
Expand All @@ -165,13 +158,6 @@ def _processor_mismatch(self, processor):
print(f"PORT CONFIG ERROR: You config lists port '{self.port}' for the {self.remote_processor} connection, but the ID: response shows that port is the {processor} connection. Update your config.")
self.machine.stop('FAST Serial port mismatch')

async def _read_with_timeout(self, timeout):
try:
msg_raw = await asyncio.wait_for(self.readuntil(b'\r'), timeout=timeout)
except asyncio.TimeoutError:
return ""
return msg_raw.decode()

# pylint: disable-msg=inconsistent-return-statements
async def readuntil(self, separator, min_chars: int = 0):
"""Read until separator.
Expand Down Expand Up @@ -201,6 +187,7 @@ def start_tasks(self):
def stopping(self):
"""The serial connection is about to stop. This is called before stop() and allows you
to do things that need to go out before the connection is closed. A 100ms delay to allow for this happens after this is called."""
pass

def stop(self):
"""Stop and shut down this serial connection."""
Expand All @@ -225,11 +212,32 @@ def stop(self):
self.writer = None

async def send_and_wait_for_response(self, msg, pause_sending_until, log_msg=None):
"""Sends a message and awaits until the response is received.
Args:
msg (_type_): Message to send
pause_sending_until (_type_): Response to wait for before sending the next message
log_msg (_type_, optional): Optional version of the message that will be used in logs. Typically used with binary messages so the longs
can contain human readable versions. Defaults to None which means the actual msg will be used in the logs.
"""
await self.no_response_waiting.wait()
self.no_response_waiting.clear()
self.send_with_confirmation(msg, pause_sending_until, log_msg)

async def send_and_wait_for_response_processed(self, msg, pause_sending_until, timeout=1, max_retries=0, log_msg=None):
"""Send a message and wait for the response to be processed. Unlike send_and_wait_for_response(), this method will not release the wait when the response is received.
Instead, the wait must manually be released by calling done_processing_msg_response(). This is useful for messages that require multiple responses, or for messages that
require real processing where you don't want the next messages to be sent until the processing is complete.
Args:
msg (_type_): Message to send
pause_sending_until (_type_): Response to wait for before sending the next message
timeout (int, optional): The time (in seconds) this communicator will wait for a response. If a response is not received by then
(based on the pause_sending_until), the message will be resent. Defaults to 1.
max_retries (int, optional): How many times the message will be resent if the response is not received by the timeout. -1 means unlimited retries. Defaults to 0.
log_msg (_type_, optional): Optional version of the message that will be used in logs. Typically used with binary messages so the longs
can contain human readable versions. Defaults to None which means the actual msg will be used in the logs.
"""
self.done_waiting.clear()

retries = 0
Expand All @@ -245,15 +253,25 @@ async def send_and_wait_for_response_processed(self, msg, pause_sending_until, t
await self.done_waiting.wait()

def done_processing_msg_response(self):
"""Releases the wait for the response to be processed. This is used in conjunction with send_and_wait_for_response_processed().
May be called safely if there's no wait to release."""
self.done_waiting.set()

def send_with_confirmation(self, msg, pause_sending_until, log_msg=None):
"""Sends a message without blocking (returns immediately), but will hold future messages in the queue until the response is received.
Args:
msg (_type_): _description_
pause_sending_until (_type_): _description_
log_msg (_type_, optional): _description_. Defaults to None.
"""
if log_msg:
self.send_queue.put_nowait((f'{msg}\r'.encode(), pause_sending_until, log_msg))
else:
self.send_queue.put_nowait((f'{msg}\r'.encode(), pause_sending_until, msg))

def send_and_forget(self, msg, log_msg=None):
"""Sends a message and returns immediately without waiting for a response."""
if log_msg:
self.send_queue.put_nowait((f'{msg}\r'.encode(), None, log_msg))
else:
Expand Down Expand Up @@ -293,10 +311,10 @@ def parse_incoming_raw_bytes(self, msg):
if self.port_debug:
self.log.info(f"<<<< {msg}")

self.dispatch_incoming_msg(msg)

def dispatch_incoming_msg(self, msg):
self._dispatch_incoming_msg(msg)

def _dispatch_incoming_msg(self, msg):
# Figures out what to do with incoming messages
if msg in self.IGNORED_MESSAGES:
return

Expand All @@ -307,13 +325,13 @@ def dispatch_incoming_msg(self, msg):

# if the msg_header matches the first chars of the self.pause_sending_until, unpause sending
if self.pause_sending_flag.is_set() and self.pause_sending_until.startswith(msg_header):
self.resume_sending()
self._resume_sending()

def pause_sending(self, msg_header):
self.pause_sending_until = msg_header
self.pause_sending_flag.set()

def resume_sending(self):
def _resume_sending(self):
self.pause_sending_until = None
self.pause_sending_flag.clear()

Expand Down
29 changes: 6 additions & 23 deletions mpf/platforms/fast/communicators/net_neuron.py
Expand Up @@ -125,14 +125,12 @@ async def reset_drivers(self):
self.platform.drivers_initialized = True

def _process_ch(self, msg):
if msg == 'F:':
return # TODO?
if msg == 'F':
raise AssertionError("Could not configure hardware. Check your FAST config.")
self.done_processing_msg_response()

async def query_io_boards(self):
"""Query the NET processor to see if any FAST I/O boards are connected.
If so, queries the I/O boards to log them and make sure they're the proper firmware version.
"""Walks through the I/O boards in the config to verify the physical boards match the config.
"""

if 'switches' not in self.machine.config and 'coils' not in self.machine.config:
Expand All @@ -151,7 +149,7 @@ def _process_nn(self, msg):
firmware_ok = True

if msg == 'F': # NN:F
return
return # TODO now what?

node_id, model, fw, dr, sw, _, _, _, _, _, _ = msg.split(',')

Expand Down Expand Up @@ -196,23 +194,14 @@ def _process_nn(self, msg):
if not firmware_ok:
raise AssertionError("Exiting due to I/O board firmware mismatch")

# self.no_response_waiting.set()

# If this is the last I/O board, signal that we're done
# if node_id == len(self.config['io_loop']) - 1:
self.done_processing_msg_response()

def process_driver_config_msg(self, msg):
# Got an incoming DL/DN: message
if msg == 'P':
self.done_processing_msg_response()
return

try:
int(msg, 16) # received an DL:L switch count response
return
except ValueError:
pass

# From here down we're processing driver config data, 9 fields, one byte each
# <driver_id>,<trigger>,<switch_id>,<mode>,<param_1>,<param_2>,<param_3>,<param_4>,<param_5>
# https://fastpinball.com/fast-serial-protocol/net/dl/
Expand All @@ -222,7 +211,7 @@ def process_driver_config_msg(self, msg):
try:
driver_obj = self.drivers[int(current_hw_driver_config.number, 16)]
except IndexError:
return # we always get data for 48 drivers, no worries if we don't have that many
return # we might get a config for a driver that doesn't exist in MPF, no worries

if driver_obj.current_driver_config != current_hw_driver_config:
driver_obj.send_config_to_driver()
Expand All @@ -237,12 +226,6 @@ def process_switch_config_msg(self, msg):
self.done_processing_msg_response()
return

try:
int(msg, 16) # received an SL:L switch count response
return
except ValueError:
pass

# From here down we're processing switch config data
# '00,02,01,02' = switch number, mode, debounce close, debounce open
# https://fastpinball.com/fast-serial-protocol/net/sl/
Expand Down
2 changes: 1 addition & 1 deletion mpf/platforms/fast/communicators/rgb.py
Expand Up @@ -39,7 +39,7 @@ def update_leds(self):
update rather than lots of individual ones).
"""
dirty_leds = [led for led in self.platform.fast_leds.values() if led.dirty]
dirty_leds = [led for led in self.platform.fast_rgb_leds.values() if led.dirty]

if dirty_leds:
msg = 'RS:' + ','.join(["%s%s" % (led.number, led.current_color) for led in dirty_leds])
Expand Down

0 comments on commit 16bbf71

Please sign in to comment.