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

[wip] adjust pkone for firmware 1.2 #1588

Draft
wants to merge 2 commits into
base: dev
Choose a base branch
from
Draft
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
6 changes: 6 additions & 0 deletions mpf/platforms/base_serial_communicator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
82 changes: 48 additions & 34 deletions mpf/platforms/pkone/pkone.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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.
Expand Down Expand Up @@ -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)

Expand All @@ -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)

Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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 <board_address_id> - <led> (simple LEDs only have 1 channel)
Expand All @@ -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."""
Expand All @@ -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 <board_address_id> - <led> (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)
Expand All @@ -586,9 +599,10 @@ def parse_light_number_to_channels(self, number: str, subtype: str):
# Normal LED number format: <board_address_id> - <group> - <led>
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 [
{
Expand Down Expand Up @@ -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)
50 changes: 29 additions & 21 deletions mpf/platforms/pkone/pkone_serial_communicator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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

Expand Down Expand Up @@ -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...')
Expand All @@ -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
Expand All @@ -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 '
Expand All @@ -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()
Expand All @@ -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

Expand All @@ -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')
4 changes: 2 additions & 2 deletions mpf/tests/test_PKONE.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down