openlp-core team mailing list archive
-
openlp-core team
-
Mailing list archive
-
Message #31508
[Merge] lp:~alisonken1/openlp/pjlink2-b into lp:openlp
Ken Roberts has proposed merging lp:~alisonken1/openlp/pjlink2-b into lp:openlp with lp:~alisonken1/openlp/pjlink2-a as a prerequisite.
Commit message:
PJLink Class 2 update 2
Requested reviews:
OpenLP Core (openlp-core)
For more details, see:
https://code.launchpad.net/~alisonken1/openlp/pjlink2-b/+merge/324019
- Added PJLINK_DEFAULT_CODES to pjink1 imports
- Refactor command class checks and methods
- Update PJLink1.get_data for UTF-8
- Added method to clear busy flags and send received data signals
- Added class check on command reply data v. stated projector class compatibility
- Added test for PJLink1.socket_abort
- Added test for PJLink1.poll_loop
- Fix regression in test_projector_process_power_on
--------------------------------
lp:~alisonken1/openlp/pjlink2-b (revision 2735)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/2002/
[SUCCESS] https://ci.openlp.io/job/Branch-02-Functional-Tests/1912/
[SUCCESS] https://ci.openlp.io/job/Branch-03-Interface-Tests/1848/
[SUCCESS] https://ci.openlp.io/job/Branch-04a-Code_Analysis/1228/
[SUCCESS] https://ci.openlp.io/job/Branch-04b-Test_Coverage/1090/
[SUCCESS] https://ci.openlp.io/job/Branch-04c-Code_Analysis2/219/
[FAILURE] https://ci.openlp.io/job/Branch-05-AppVeyor-Tests/67/
--
Your team OpenLP Core is requested to review the proposed merge of lp:~alisonken1/openlp/pjlink2-b into lp:openlp.
=== modified file 'openlp/core/lib/projector/constants.py'
--- openlp/core/lib/projector/constants.py 2017-05-13 09:08:16 +0000
+++ openlp/core/lib/projector/constants.py 2017-05-13 09:08:16 +0000
@@ -57,20 +57,35 @@
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.
=== modified file 'openlp/core/lib/projector/pjlink1.py'
--- openlp/core/lib/projector/pjlink1.py 2017-05-13 09:08:16 +0000
+++ openlp/core/lib/projector/pjlink1.py 2017-05-13 09:08:16 +0000
@@ -53,16 +53,18 @@
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 @@
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 @@
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 @@
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()
+ self.receive_data_signal()
+ return
+ elif '=' not in data:
+ log.warning('({ip}) get_data(): Invalid packet received'.format(ip=self.ip))
+ 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.send_busy = False
- self.projectorReceivedData.emit()
- 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
data_split = data.split('=')
try:
@@ -458,15 +456,15 @@
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 @@
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 @@
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.
=== modified file 'tests/functional/openlp_core_lib/test_projector_pjlink1.py'
--- tests/functional/openlp_core_lib/test_projector_pjlink1.py 2017-05-13 09:08:16 +0000
+++ tests/functional/openlp_core_lib/test_projector_pjlink1.py 2017-05-13 09:08:16 +0000
@@ -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 @@
# 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 @@
# 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')
Follow ups