From ed144657bceb07a8b3433aba32936bb90ef2f9c1 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Sat, 13 May 2017 02:00:29 -0700 Subject: [PATCH] PJLink class 2 updates 2 --- openlp/core/lib/projector/constants.py | 43 ++++++---- openlp/core/lib/projector/pjlink1.py | 58 +++++++------ .../openlp_core_lib/test_projector_pjlink1.py | 83 ++++++++++++++++++- 3 files changed, 143 insertions(+), 41 deletions(-) diff --git a/openlp/core/lib/projector/constants.py b/openlp/core/lib/projector/constants.py index af94ab71e..5177aa691 100644 --- a/openlp/core/lib/projector/constants.py +++ b/openlp/core/lib/projector/constants.py @@ -57,20 +57,35 @@ LF = chr(0x0A) # \n PJLINK_PORT = 4352 TIMEOUT = 30.0 PJLINK_MAX_PACKET = 136 -PJLINK_VALID_CMD = {'1': ['PJLINK', # Initial connection - 'POWR', # Power option - 'INPT', # Video sources option - 'AVMT', # Shutter option - 'ERST', # Error status option - 'LAMP', # Lamp(s) query (Includes fans) - 'INST', # Input sources available query - 'NAME', # Projector name query - 'INF1', # Manufacturer name query - 'INF2', # Product name query - 'INFO', # Other information query - 'CLSS' # PJLink class support query - ]} - +# NOTE: Change format to account for some commands are both class 1 and 2 +PJLINK_VALID_CMD = { + 'ACKN': ['2', ], # UDP Reply to 'SRCH' + 'AVMT': ['1', ], # Shutter option + 'CLSS': ['1', ], # PJLink class support query + 'ERST': ['1', '2'], # Error status option + 'FILT': ['2', ], # Get current filter usage time + 'FREZ': ['2', ], # Set freeze/unfreeze picture being projected + 'INF1': ['1', ], # Manufacturer name query + 'INF2': ['1', ], # Product name query + 'INFO': ['1', ], # Other information query + 'INNM': ['2', ], # Get Video source input terminal name + 'INPT': ['1', ], # Video sources option + 'INST': ['1', ], # Input sources available query + 'IRES': ['2', ], # Get Video source resolution + 'LAMP': ['1', ], # Lamp(s) query (Includes fans) + 'LKUP': ['2', ], # UPD Linkup status notification + 'MVOL': ['2', ], # Set microphone volume + 'NAME': ['1', ], # Projector name query + 'PJLINK': ['1', ], # Initial connection + 'POWR': ['1', ], # Power option + 'RFIL': ['2', ], # Get replacement air filter model number + 'RLMP': ['2', ], # Get lamp replacement model number + 'RRES': ['2', ], # Get projector recommended video resolution + 'SNUM': ['2', ], # Get projector serial number + 'SRCH': ['2', ], # UDP broadcast search for available projectors on local network + 'SVER': ['2', ], # Get projector software version + 'SVOL': ['2', ] # Set speaker volume +} # Error and status codes S_OK = E_OK = 0 # E_OK included since I sometimes forget # Error codes. Start at 200 so we don't duplicate system error codes. diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 61250a8e6..3cda11827 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -53,16 +53,18 @@ from openlp.core.lib.projector.constants import CONNECTION_ERRORS, CR, ERROR_MSG E_AUTHENTICATION, E_CONNECTION_REFUSED, E_GENERAL, E_INVALID_DATA, E_NETWORK, E_NOT_CONNECTED, \ E_PARAMETER, E_PROJECTOR, E_SOCKET_TIMEOUT, E_UNAVAILABLE, E_UNDEFINED, PJLINK_ERRORS, \ PJLINK_ERST_STATUS, PJLINK_MAX_PACKET, PJLINK_PORT, PJLINK_POWR_STATUS, PJLINK_VALID_CMD, \ - STATUS_STRING, S_CONNECTED, S_CONNECTING, S_NETWORK_RECEIVED, S_NETWORK_SENDING, S_NOT_CONNECTED, \ - S_OFF, S_OK, S_ON, S_STATUS + PJLINK_DEFAULT_CODES, STATUS_STRING, S_CONNECTED, S_CONNECTING, S_NETWORK_RECEIVED, S_NETWORK_SENDING, \ + S_NOT_CONNECTED, S_OFF, S_OK, S_ON, S_STATUS # Shortcuts SocketError = QtNetwork.QAbstractSocket.SocketError SocketSTate = QtNetwork.QAbstractSocket.SocketState PJLINK_PREFIX = '%' -PJLINK_CLASS = '1' -PJLINK_HEADER = '{prefix}{linkclass}'.format(prefix=PJLINK_PREFIX, linkclass=PJLINK_CLASS) +PJLINK_CLASS = '1' # Default to class 1 until we query the projector +# Add prefix here, but defer linkclass expansion until later when we have the actual +# PJLink class for the command +PJLINK_HEADER = '{prefix}{{linkclass}}'.format(prefix=PJLINK_PREFIX) PJLINK_SUFFIX = CR @@ -271,8 +273,6 @@ class PJLink1(QtNetwork.QTcpSocket): self.send_command('INF2', queue=True) if self.pjlink_name is None: self.send_command('NAME', queue=True) - if self.power == S_ON and self.source_available is None: - self.send_command('INST', queue=True) def _get_status(self, status): """ @@ -349,7 +349,7 @@ class PJLink1(QtNetwork.QTcpSocket): elif len(read) < 8: log.warning('({ip}) Not enough data read)'.format(ip=self.ip)) return - data = decode(read, 'ascii') + data = decode(read, 'utf-8') # Possibility of extraneous data on input when reading. # Clean out extraneous characters in buffer. dontcare = self.readLine(self.max_size) @@ -436,20 +436,18 @@ class PJLink1(QtNetwork.QTcpSocket): if len(data) < 7: # Not enough data for a packet log.debug('({ip}) get_data(): Packet length < 7: "{data}"'.format(ip=self.ip, data=data)) - self.send_busy = False - self.projectorReceivedData.emit() - return - log.debug('({ip}) get_data(): Checking new data "{data}"'.format(ip=self.ip, data=data)) - if data.upper().startswith('PJLINK'): - # Reconnected from remote host disconnect ? - self.check_login(data) - self.send_busy = False - self.projectorReceivedData.emit() + self.receive_data_signal() return elif '=' not in data: log.warning('({ip}) get_data(): Invalid packet received'.format(ip=self.ip)) - self.send_busy = False - self.projectorReceivedData.emit() + self.receive_data_signal() + return + log.debug('({ip}) get_data(): Checking new data "{data}"'.format(ip=self.ip, data=data)) + # At this point, we should have something to work with + if data.upper().startswith('PJLINK'): + # Reconnected from remote host disconnect ? + self.check_login(data) + self.receive_data_signal() return data_split = data.split('=') try: @@ -458,15 +456,15 @@ class PJLink1(QtNetwork.QTcpSocket): log.warning('({ip}) get_data(): Invalid packet - expected header + command + data'.format(ip=self.ip)) log.warning('({ip}) get_data(): Received data: "{data}"'.format(ip=self.ip, data=data_in.strip())) self.change_status(E_INVALID_DATA) - self.send_busy = False - self.projectorReceivedData.emit() + self.receive_data_signal() return - - if not (self.pjlink_class in PJLINK_VALID_CMD and cmd in PJLINK_VALID_CMD[self.pjlink_class]): + if not (cmd in PJLINK_VALID_CMD and class_ in PJLINK_VALID_CMD[cmd]): log.warning('({ip}) get_data(): Invalid packet - unknown command "{data}"'.format(ip=self.ip, data=cmd)) - self.send_busy = False - self.projectorReceivedData.emit() + self.receive_data_signal() return + if int(self.pjlink_class) < int(class_): + log.warn('({ip}) get_data(): Projector returned class reply higher ' + 'than projector stated class'.format(ip=self.ip)) return self.process_command(cmd, data) @QtCore.pyqtSlot(QtNetwork.QAbstractSocket.SocketError) @@ -515,8 +513,10 @@ class PJLink1(QtNetwork.QTcpSocket): data=opts, salt='' if salt is None else ' with hash')) + # TODO: Check for class of command rather than default to projector PJLink class + header = PJLINK_HEADER.format(linkclass=self.pjlink_class) out = '{salt}{header}{command} {options}{suffix}'.format(salt="" if salt is None else salt, - header=PJLINK_HEADER, + header=header, command=cmd, options=opts, suffix=CR) @@ -998,6 +998,14 @@ class PJLink1(QtNetwork.QTcpSocket): self.send_command(cmd='AVMT', opts='10') self.poll_loop() + def receive_data_signal(self): + """ + Clear any busy flags and send data received signal + """ + self.send_busy = False + self.projectorReceivedData.emit() + return + def _not_implemented(self, cmd): """ Log when a future PJLink command has not been implemented yet. diff --git a/tests/functional/openlp_core_lib/test_projector_pjlink1.py b/tests/functional/openlp_core_lib/test_projector_pjlink1.py index cce011c53..fadd17bb7 100644 --- a/tests/functional/openlp_core_lib/test_projector_pjlink1.py +++ b/tests/functional/openlp_core_lib/test_projector_pjlink1.py @@ -23,10 +23,11 @@ Package to test the openlp.core.lib.projector.pjlink1 package. """ from unittest import TestCase -from unittest.mock import patch, MagicMock +from unittest.mock import call, patch, MagicMock from openlp.core.lib.projector.pjlink1 import PJLink1 -from openlp.core.lib.projector.constants import E_PARAMETER, ERROR_STRING, S_OFF, S_STANDBY, S_ON, PJLINK_POWR_STATUS +from openlp.core.lib.projector.constants import E_PARAMETER, ERROR_STRING, S_OFF, S_STANDBY, S_ON, \ + PJLINK_POWR_STATUS, S_CONNECTED from tests.resources.projector.data import TEST_PIN, TEST_SALT, TEST_CONNECT_AUTHENTICATE, TEST_HASH @@ -170,6 +171,7 @@ class TestPJLink(TestCase): # GIVEN: Test object and preset pjlink = pjlink_test pjlink.power = S_STANDBY + pjlink.socket_timer = MagicMock() # WHEN: Call process_command with turn power on command pjlink.process_command('POWR', PJLINK_POWR_STATUS[S_ON]) @@ -381,3 +383,80 @@ class TestPJLink(TestCase): # THEN: pjlink1.__not_implemented should have been called with test_cmd mock_not_implemented.assert_called_with(test_cmd) + + @patch.object(pjlink_test, 'disconnect_from_host') + def socket_abort_test(self, mock_disconnect): + """ + Test PJLink1.socket_abort calls disconnect_from_host + """ + # GIVEN: Test object + pjlink = pjlink_test + + # WHEN: Calling socket_abort + pjlink.socket_abort() + + # THEN: disconnect_from_host should be called + self.assertTrue(mock_disconnect.called, 'Should have called disconnect_from_host') + + def poll_loop_not_connected_test(self): + """ + Test PJLink1.poll_loop not connected return + """ + # GIVEN: Test object and mocks + pjlink = pjlink_test + pjlink.state = MagicMock() + pjlink.timer = MagicMock() + pjlink.state.return_value = False + pjlink.ConnectedState = True + + # WHEN: PJLink1.poll_loop called + pjlink.poll_loop() + + # THEN: poll_loop should exit without calling any other method + self.assertFalse(pjlink.timer.called, 'Should have returned without calling any other method') + + @patch.object(pjlink_test, 'send_command') + def poll_loop_start_test(self, mock_send_command): + """ + Test PJLink1.poll_loop makes correct calls + """ + # GIVEN: test object and test data + pjlink = pjlink_test + pjlink.state = MagicMock() + pjlink.timer = MagicMock() + pjlink.timer.interval = MagicMock() + pjlink.timer.setInterval = MagicMock() + pjlink.timer.start = MagicMock() + pjlink.poll_time = 20 + pjlink.power = S_ON + pjlink.source_available = None + pjlink.other_info = None + pjlink.manufacturer = None + pjlink.model = None + pjlink.pjlink_name = None + pjlink.ConnectedState = S_CONNECTED + pjlink.timer.interval.return_value = 10 + pjlink.state.return_value = S_CONNECTED + call_list = [ + call('POWR', queue=True), + call('ERST', queue=True), + call('LAMP', queue=True), + call('AVMT', queue=True), + call('INPT', queue=True), + call('INST', queue=True), + call('INFO', queue=True), + call('INF1', queue=True), + call('INF2', queue=True), + call('NAME', queue=True), + ] + + # WHEN: PJLink1.poll_loop is called + pjlink.poll_loop() + + # THEN: proper calls were made to retrieve projector data + # First, call to update the timer with the next interval + self.assertTrue(pjlink.timer.setInterval.called, 'Should have updated the timer') + # Next, should have called the timer to start + self.assertTrue(pjlink.timer.start.called, 'Should have started the timer') + # Finally, should have called send_command with a list of projetctor status checks + mock_send_command.assert_has_calls(call_list, 'Should have queued projector queries')