diff --git a/mpf/platforms/base_serial_communicator.py b/mpf/platforms/base_serial_communicator.py index 5fb0e2cfb..5226605d3 100644 --- a/mpf/platforms/base_serial_communicator.py +++ b/mpf/platforms/base_serial_communicator.py @@ -83,6 +83,12 @@ async def _connect_to_hardware(self, port, baud, xonxoff=False): await self._identify_connection() + def reset_input_buffer(self): + """Clear buffer.""" + assert self.reader + # pylint: disable-msg=protected-access + self.reader._buffer = bytearray() + async def start_read_loop(self): """Start the read loop.""" self.read_task = self.machine.clock.loop.create_task(self._socket_reader()) diff --git a/mpf/platforms/pkone/pkone.py b/mpf/platforms/pkone/pkone.py index 7adcf2fc8..fea5be3d4 100644 --- a/mpf/platforms/pkone/pkone.py +++ b/mpf/platforms/pkone/pkone.py @@ -198,23 +198,24 @@ def _parse_coil_number(self, number: str) -> PKONECoilNumber: try: board_id_str, coil_num_str = number.split("-") except ValueError: - raise AssertionError("Invalid coil number {}".format(number)) + self.raise_config_error("Invalid coil number {}".format(number), 2) + raise board_id = int(board_id_str) coil_num = int(coil_num_str) if board_id not in self.pkone_extensions: - raise AssertionError("PKONE Extension {} does not exist for coil {}".format(board_id, number)) + self.raise_config_error("PKONE Extension {} does not exist for coil {}".format(board_id, number), 3) if coil_num == 0: - raise AssertionError("PKONE coil numbering begins with 1. Coil: {}".format(number)) + self.raise_config_error("PKONE coil numbering begins with 1. Coil: {}".format(number), 4) coil_count = self.pkone_extensions[board_id].coil_count if coil_count < coil_num or coil_num < 1: - raise AssertionError( + self.raise_config_error( "PKONE Extension {board_id} only has {coil_count} coils " "({first_coil} - {last_coil}). Coil: {number}".format( - board_id=board_id, coil_count=coil_count, first_coil=1, last_coil=coil_count, number=number)) + board_id=board_id, coil_count=coil_count, first_coil=1, last_coil=coil_count, number=number), 5) return PKONECoilNumber(board_id, coil_num) @@ -233,22 +234,21 @@ def configure_driver(self, config: DriverConfig, number: str, platform_settings: platform_settings = deepcopy(platform_settings) if not self.controller_connection: - raise AssertionError('A request was made to configure a PKONE coil, but no ' - 'connection to a PKONE controller is available') + self.raise_config_error('A request was made to configure a PKONE coil, but no ' + 'connection to a PKONE controller is available', 6) if not number: - raise AssertionError("Coil number is required") + self.raise_config_error("Coil number is required", 7) coil_number = self._parse_coil_number(str(number)) return PKONECoil(config, self, coil_number, platform_settings) - @staticmethod - def _check_coil_switch_combination(coil: DriverSettings, switch: SwitchSettings): + def _check_coil_switch_combination(self, coil: DriverSettings, switch: SwitchSettings): """Check to see if the coil/switch combination is legal for hardware rules.""" # coil and switch must be on the same extension board (same board address id) if switch.hw_switch.number.board_address_id != coil.hw_driver.number.board_address_id: - raise AssertionError("Coil {} and switch {} are on different boards. Cannot apply hardware rule!".format( - coil.hw_driver.number, switch.hw_switch.number)) + self.raise_config_error("Coil {} and switch {} are on different boards. Cannot apply hardware rule!".format( + coil.hw_driver.number, switch.hw_switch.number), 8) def clear_hw_rule(self, switch: SwitchSettings, coil: DriverSettings): """Clear a hardware rule. @@ -341,21 +341,23 @@ def _parse_servo_number(self, number: str) -> PKONEServoNumber: try: board_id_str, servo_num_str = number.split("-") except ValueError: - raise AssertionError("Invalid servo number {}".format(number)) + self.raise_config_error("Invalid servo number {}".format(number), 9) + raise board_id = int(board_id_str) servo_num = int(servo_num_str) if board_id not in self.pkone_extensions: - raise AssertionError("PKONE Extension {} does not exist for servo {}".format(board_id, number)) + self.raise_config_error("PKONE Extension {} does not exist for servo {}".format(board_id, number), 10) # Servos are numbered in sequence immediately after the highest coil number driver_count = self.pkone_extensions[board_id].coil_count servo_count = self.pkone_extensions[board_id].servo_count if servo_num <= driver_count or servo_num > driver_count + servo_count: - raise AssertionError("PKONE Extension {} supports {} servos ({} - {}). " - "Servo: {} is not a valid number.".format( - board_id, servo_count, driver_count + 1, driver_count + servo_count, number)) + self.raise_config_error("PKONE Extension {} supports {} servos ({} - {}). " + "Servo: {} is not a valid number.".format( + board_id, servo_count, driver_count + 1, driver_count + servo_count, number), + 11) return PKONEServoNumber(board_id, servo_num) @@ -373,20 +375,21 @@ def _parse_switch_number(self, number: str) -> PKONESwitchNumber: try: board_id_str, switch_num_str = number.split("-") except ValueError: - raise AssertionError("Invalid switch number {}".format(number)) + self.raise_config_error("Invalid switch number {}".format(number), 12) + raise board_id = int(board_id_str) switch_num = int(switch_num_str) if board_id not in self.pkone_extensions: - raise AssertionError("PKONE Extension {} does not exist for switch {}".format(board_id, number)) + self.raise_config_error("PKONE Extension {} does not exist for switch {}".format(board_id, number), 13) if switch_num == 0: - raise AssertionError("PKONE switch numbering begins with 1. Switch: {}".format(number)) + self.raise_config_error("PKONE switch numbering begins with 1. Switch: {}".format(number), 14) if self.pkone_extensions[board_id].switch_count < switch_num: - raise AssertionError("PKONE Extension {} only has {} switches. Switch: {}".format( - board_id, self.pkone_extensions[board_id].switch_count, number)) + self.raise_config_error("PKONE Extension {} only has {} switches. Switch: {}".format( + board_id, self.pkone_extensions[board_id].switch_count, number), 15) return PKONESwitchNumber(board_id, switch_num) @@ -403,18 +406,19 @@ def configure_switch(self, number: str, config: SwitchConfig, platform_config: d """ del platform_config if not number: - raise AssertionError("Switch requires a number") + self.raise_config_error("Switch requires a number", 16) if not self.controller_connection: - raise AssertionError("A request was made to configure a PKONE switch, but no " - "connection to PKONE controller is available") + self.raise_config_error("A request was made to configure a PKONE switch, but no " + "connection to PKONE controller is available", 17) try: switch_number = self._parse_switch_number(number) except ValueError: - raise AssertionError("Could not parse switch number {}/{}. Seems " - "to be not a valid switch number for the" - "PKONE platform.".format(config.name, number)) + self.raise_config_error("Could not parse switch number {}/{}. Seems " + "to be not a valid switch number for the" + "PKONE platform.".format(config.name, number), 18) + raise self.debug_log("PKONE Switch: %s (%s)", number, config.name) return PKONESwitch(config, switch_number, self) @@ -425,6 +429,7 @@ async def get_hw_switch_states(self) -> Dict[str, bool]: def receive_all_switches(self, msg): """Process the all switch states message.""" + # TODO: move this to the init part # The PSA message contains the following information: # [PSA opcode] + [[board address id] + 0 or 1 for each switch on the board] + E self.debug_log("Received all switch states (PSA): %s", msg) @@ -444,9 +449,9 @@ def receive_switch(self, msg): """Process a single switch state change.""" # The PSW message contains the following information: # [PSW opcode] + [board address id] + switch number + switch state (0 or 1) + E - self.debug_log("Received switch state change (PSW): %s", msg) switch_number = PKONESwitchNumber(int(msg[0]), int(msg[1:3])) switch_state = int(msg[-1]) + self.debug_log("Received switch %s state change to %s", switch_number, switch_state) self.machine.switch_controller.process_switch_by_num(state=switch_state, num=switch_number, platform=self) @@ -527,13 +532,14 @@ async def _send_multiple_light_update(self, sequential_brightness_list: List[Tup "".join("%03d" % (b[1] * 255) for b in sequential_brightness_list)) self.controller_connection.send(cmd) + # pylint: disable-msg=inconsistent-return-statements def configure_light(self, number, subtype, config, platform_settings): """Configure light in platform.""" del platform_settings if not self.controller_connection: - raise AssertionError("A request was made to configure a PKONE light, but no " - "connection to PKONE controller is available") + self.raise_config_error("A request was made to configure a PKONE light, but no " + "connection to PKONE controller is available", 19) if subtype == "simple": # simple LEDs use the format - (simple LEDs only have 1 channel) @@ -549,7 +555,7 @@ def configure_light(self, number, subtype, config, platform_settings): self._light_system.mark_dirty(led_channel) return led_channel - raise AssertionError("Unknown subtype {}".format(subtype)) + self.raise_config_error("Unknown subtype {}".format(subtype), 20) def _led_is_hardware_aligned(self, led_name) -> bool: """Determine whether the specified LED is hardware aligned.""" @@ -571,11 +577,18 @@ def _initialize_led_hw_driver_alignment(self): for channel in lightshow.get_all_channel_hw_drivers(): channel.set_hardware_aligned(self._led_is_hardware_aligned(channel.config.name)) + def _assert_is_light_board(self, board_address_id): + """Make sure that this id is connected to a light board.""" + if int(board_address_id) not in self.pkone_lightshows: + self.raise_config_error("Board {} is not a lightboard.".format(board_address_id), 1) + + # pylint: disable-msg=inconsistent-return-statements def parse_light_number_to_channels(self, number: str, subtype: str): """Parse light channels from number string.""" if subtype == "simple": # simple LEDs use the format - (simple LEDs only have 1 channel) board_address_id, index = number.split('-') + self._assert_is_light_board(board_address_id) return [ { "number": "{}-{}".format(board_address_id, index) @@ -586,9 +599,10 @@ def parse_light_number_to_channels(self, number: str, subtype: str): # Normal LED number format: - - board_address_id, group, number_str = str(number).split('-') index = int(number_str) + self._assert_is_light_board(board_address_id) # Determine if there are 3 or 4 channels depending upon firmware on board - if self.pkone_lightshows[board_address_id].rgbw_firmware: + if self.pkone_lightshows[int(board_address_id)].rgbw_firmware: # rgbw uses 4 channels per led return [ { @@ -618,4 +632,4 @@ def parse_light_number_to_channels(self, number: str, subtype: str): }, ] - raise AssertionError("Unknown light subtype {}".format(subtype)) + self.raise_config_error("Unknown light subtype {}".format(subtype), 21) diff --git a/mpf/platforms/pkone/pkone_serial_communicator.py b/mpf/platforms/pkone/pkone_serial_communicator.py index 8a7b9b8f4..3212ef606 100644 --- a/mpf/platforms/pkone/pkone_serial_communicator.py +++ b/mpf/platforms/pkone/pkone_serial_communicator.py @@ -43,9 +43,6 @@ def __init__(self, platform: "PKONEHardwarePlatform", port, baud) -> None: self.max_messages_in_flight = 10 self.messages_in_flight = 0 - self.send_ready = asyncio.Event() - self.send_ready.set() - super().__init__(platform, port, baud) async def _read_with_timeout(self, timeout): @@ -57,14 +54,17 @@ async def _read_with_timeout(self, timeout): async def _identify_connection(self): """Identify which controller this serial connection is talking to.""" + self.writer.transport.serial.dtr = False + await asyncio.sleep(.1) + count = 0 while True: if (count % 10) == 0: self.platform.debug_log("Sending 'PCN' command to port '%s'", self.port) count += 1 - self.writer.write('PCNE'.encode('ascii', 'replace')) - msg = await self._read_with_timeout(.5) + self.send('PCN') + msg = await self._read_with_timeout(.1) if msg.startswith('PCN'): break @@ -121,15 +121,19 @@ async def reset_controller(self): self.platform.debug_log('Resetting controller.') # this command returns several responses (one from each board, starting with the Nano controller) - self.writer.write('PRSE'.encode()) + self.send('PRS') msg = '' - while msg != 'PRSE' and not msg.startswith('PXX'): + while msg != 'PRSNE' and not msg.startswith('PXX'): msg = (await self.readuntil(b'E')).decode() self.platform.debug_log("Got: {}".format(msg)) if msg.startswith('PXX'): raise AssertionError('Received an error while resetting the controller: {}'.format(msg)) + # wait a bit and then discard everything we got. boards will send us some PSW here which we do not parse + await asyncio.sleep(.1) + self.reset_input_buffer() + async def query_pkone_boards(self): """Query the NANO processor to discover which additional boards are connected.""" self.platform.debug_log('Querying PKONE boards...') @@ -140,15 +144,26 @@ async def query_pkone_boards(self): # Lightshow board - PCB01LF10H1RGBW = PCB[board number 0-3]LF[firmware rev]H[hardware rev][firmware_type] # No board at the address: PCB[board number 0-7]N for address_id in range(8): - self.writer.write('PCB{}E'.format(address_id).encode('ascii', 'replace')) - msg = await self._read_with_timeout(.5) - if msg == 'PCB{}NE'.format(address_id): + self.send('PCB{}'.format(address_id)) + while True: + msg = await self._read_with_timeout(.1) + if not msg or msg.startswith("PCB"): + break + self.platform.log.warning("Ignoring unexpected msg: {}".format(msg)) + continue + + if not msg: self.platform.log.debug("No board at address ID {}".format(address_id)) continue - match = re.fullmatch('PCB([0-7])([XLN])F([0-9]+)H([0-9]+)(P[YN])?(RGB|RGBW)?E', msg) + match = re.fullmatch('PCB([0-7])([XLN])F([0-9]+)H([0-9]+)(P?)(Y|N)?(RGB|RGBW)?E', msg) if not match: self.platform.log.warning("Received unexpected message from PKONE: {}".format(msg)) + continue + + if match.group(1) != str(address_id): + raise AssertionError("Invalid ID {} in response: {} for board {}".format( + match.group(1), msg, address_id)) if match.group(2) == "X": # Extension board @@ -171,7 +186,7 @@ async def query_pkone_boards(self): # Lightshow board firmware = match.group(3)[:-1] + '.' + match.group(3)[-1] hardware_rev = match.group(4) - rgbw_firmware = match.group(6) == 'RGBW' + rgbw_firmware = match.group(7) == 'RGBW' if StrictVersion(LIGHTSHOW_MIN_FW) > StrictVersion(firmware): raise AssertionError('Firmware version mismatch. MPF requires ' @@ -197,7 +212,7 @@ async def read_all_switches(self): """Read the current state of all switches from the hardware.""" self.platform.debug_log('Reading all switches.') for address_id in self.platform.pkone_extensions: - self.writer.write('PSA{}E'.format(address_id).encode()) + self.send('PSA{}'.format(address_id)) msg = '' while not msg.startswith('PSA'): msg = (await self.readuntil(b'E')).decode() @@ -219,13 +234,6 @@ def _parse_msg(self, msg): msg = self.received_msg[:pos] self.received_msg = self.received_msg[pos + 1:] - self.messages_in_flight -= 1 - if self.messages_in_flight <= self.max_messages_in_flight or not self.read_task: - self.send_ready.set() - if self.messages_in_flight < 0: - self.log.warning("Received more messages than were sent! Resetting!") - self.messages_in_flight = 0 - if not msg: continue @@ -240,6 +248,6 @@ def send(self, msg): msg: Bytes of the message you want to send. """ if self.debug: - self.log.debug("%s sending: %s", self, msg) + self.log.debug("%s sending: %sE", self, msg) self.writer.write(msg.encode() + b'E') diff --git a/mpf/tests/test_PKONE.py b/mpf/tests/test_PKONE.py index 0b4b48d60..065573ec6 100644 --- a/mpf/tests/test_PKONE.py +++ b/mpf/tests/test_PKONE.py @@ -113,12 +113,12 @@ def setUp(self): 'PCB0': 'PCB0XF11H2PY', # Extension board at ID 0 (firmware 1.1, hardware rev 2, high power on) 'PCB1': 'PCB1XF11H2PN', # Extension board at ID 1 (firmware 1.1, hardware rev 2, high power off) 'PCB2': 'PCB2LF10H1RGB', # Lightshow board at ID 2 (RGB firmware 1.0, hardware rev 1) - 'PCB3': 'PCB2LF10H1RGBW', # Lightshow board at ID 3 (RGBW firmware 1.0, hardware rev 1) + 'PCB3': 'PCB3LF10H1RGBW', # Lightshow board at ID 3 (RGBW firmware 1.0, hardware rev 1) 'PCB4': 'PCB4N', 'PCB5': 'PCB5N', 'PCB6': 'PCB6N', 'PCB7': 'PCB7N', - 'PRS': 'PRS', + 'PRS': 'PRSN', 'PWS1000': 'PWS', 'PSA0': 'PSA011000000000000000000000000000000000E', 'PSA1': 'PSA100110000000000000000000000000000000E',