From a5335adbc493b9479ed07beada416fb590fb0648 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Fri, 12 May 2017 02:51:56 -0700 Subject: [PATCH 1/2] Initial PJLink class 2 updates --- openlp/core/lib/projector/constants.py | 98 ++++++++++--------- openlp/core/lib/projector/pjlink1.py | 57 +++++++++-- .../test_projector_constants.py | 44 +++++++++ .../openlp_core_lib/test_projector_pjlink1.py | 15 +++ 4 files changed, 159 insertions(+), 55 deletions(-) create mode 100644 tests/functional/openlp_core_lib/test_projector_constants.py diff --git a/openlp/core/lib/projector/constants.py b/openlp/core/lib/projector/constants.py index 95ae9b2b2..af94ab71e 100644 --- a/openlp/core/lib/projector/constants.py +++ b/openlp/core/lib/projector/constants.py @@ -48,7 +48,8 @@ __all__ = ['S_OK', 'E_GENERAL', 'E_NOT_CONNECTED', 'E_FAN', 'E_LAMP', 'E_TEMP', 'S_INFO', 'S_NETWORK_SENDING', 'S_NETWORK_RECEIVED', 'ERROR_STRING', 'CR', 'LF', 'PJLINK_ERST_STATUS', 'PJLINK_POWR_STATUS', 'PJLINK_PORT', 'PJLINK_MAX_PACKET', 'TIMEOUT', 'ERROR_MSG', 'PJLINK_ERRORS', - 'STATUS_STRING', 'PJLINK_VALID_CMD', 'CONNECTION_ERRORS'] + 'STATUS_STRING', 'PJLINK_VALID_CMD', 'CONNECTION_ERRORS', + 'PJLINK_DEFAULT_SOURCES', 'PJLINK_DEFAULT_CODES', 'PJLINK_DEFAULT_ITEMS'] # Set common constants. CR = chr(0x0D) # \r @@ -321,53 +322,54 @@ PJLINK_DEFAULT_SOURCES = { '2': translate('OpenLP.DB', 'Video'), '3': translate('OpenLP.DB', 'Digital'), '4': translate('OpenLP.DB', 'Storage'), - '5': translate('OpenLP.DB', 'Network') + '5': translate('OpenLP.DB', 'Network'), + '6': translate('OpenLP.DB', 'Internal') } -PJLINK_DEFAULT_CODES = { - '11': translate('OpenLP.DB', 'RGB 1'), - '12': translate('OpenLP.DB', 'RGB 2'), - '13': translate('OpenLP.DB', 'RGB 3'), - '14': translate('OpenLP.DB', 'RGB 4'), - '15': translate('OpenLP.DB', 'RGB 5'), - '16': translate('OpenLP.DB', 'RGB 6'), - '17': translate('OpenLP.DB', 'RGB 7'), - '18': translate('OpenLP.DB', 'RGB 8'), - '19': translate('OpenLP.DB', 'RGB 9'), - '21': translate('OpenLP.DB', 'Video 1'), - '22': translate('OpenLP.DB', 'Video 2'), - '23': translate('OpenLP.DB', 'Video 3'), - '24': translate('OpenLP.DB', 'Video 4'), - '25': translate('OpenLP.DB', 'Video 5'), - '26': translate('OpenLP.DB', 'Video 6'), - '27': translate('OpenLP.DB', 'Video 7'), - '28': translate('OpenLP.DB', 'Video 8'), - '29': translate('OpenLP.DB', 'Video 9'), - '31': translate('OpenLP.DB', 'Digital 1'), - '32': translate('OpenLP.DB', 'Digital 2'), - '33': translate('OpenLP.DB', 'Digital 3'), - '34': translate('OpenLP.DB', 'Digital 4'), - '35': translate('OpenLP.DB', 'Digital 5'), - '36': translate('OpenLP.DB', 'Digital 6'), - '37': translate('OpenLP.DB', 'Digital 7'), - '38': translate('OpenLP.DB', 'Digital 8'), - '39': translate('OpenLP.DB', 'Digital 9'), - '41': translate('OpenLP.DB', 'Storage 1'), - '42': translate('OpenLP.DB', 'Storage 2'), - '43': translate('OpenLP.DB', 'Storage 3'), - '44': translate('OpenLP.DB', 'Storage 4'), - '45': translate('OpenLP.DB', 'Storage 5'), - '46': translate('OpenLP.DB', 'Storage 6'), - '47': translate('OpenLP.DB', 'Storage 7'), - '48': translate('OpenLP.DB', 'Storage 8'), - '49': translate('OpenLP.DB', 'Storage 9'), - '51': translate('OpenLP.DB', 'Network 1'), - '52': translate('OpenLP.DB', 'Network 2'), - '53': translate('OpenLP.DB', 'Network 3'), - '54': translate('OpenLP.DB', 'Network 4'), - '55': translate('OpenLP.DB', 'Network 5'), - '56': translate('OpenLP.DB', 'Network 6'), - '57': translate('OpenLP.DB', 'Network 7'), - '58': translate('OpenLP.DB', 'Network 8'), - '59': translate('OpenLP.DB', 'Network 9') +PJLINK_DEFAULT_ITEMS = { + '1': translate('OpenLP.DB', '1'), + '2': translate('OpenLP.DB', '2'), + '3': translate('OpenLP.DB', '3'), + '4': translate('OpenLP.DB', '4'), + '5': translate('OpenLP.DB', '5'), + '6': translate('OpenLP.DB', '6'), + '7': translate('OpenLP.DB', '7'), + '8': translate('OpenLP.DB', '8'), + '9': translate('OpenLP.DB', '9'), + 'A': translate('OpenLP.DB', 'A'), + 'B': translate('OpenLP.DB', 'B'), + 'C': translate('OpenLP.DB', 'C'), + 'D': translate('OpenLP.DB', 'D'), + 'E': translate('OpenLP.DB', 'E'), + 'F': translate('OpenLP.DB', 'F'), + 'G': translate('OpenLP.DB', 'G'), + 'H': translate('OpenLP.DB', 'H'), + 'I': translate('OpenLP.DB', 'I'), + 'J': translate('OpenLP.DB', 'J'), + 'K': translate('OpenLP.DB', 'K'), + 'L': translate('OpenLP.DB', 'L'), + 'M': translate('OpenLP.DB', 'M'), + 'N': translate('OpenLP.DB', 'N'), + 'O': translate('OpenLP.DB', 'O'), + 'P': translate('OpenLP.DB', 'P'), + 'Q': translate('OpenLP.DB', 'Q'), + 'R': translate('OpenLP.DB', 'R'), + 'S': translate('OpenLP.DB', 'S'), + 'T': translate('OpenLP.DB', 'T'), + 'U': translate('OpenLP.DB', 'U'), + 'V': translate('OpenLP.DB', 'V'), + 'W': translate('OpenLP.DB', 'W'), + 'X': translate('OpenLP.DB', 'X'), + 'Y': translate('OpenLP.DB', 'Y'), + 'Z': translate('OpenLP.DB', 'Z') } + +# Due to the expanded nature of PJLink class 2 video sources, +# translate the individual types then build the video source +# dictionary from the translations. +PJLINK_DEFAULT_CODES = dict() +for source in PJLINK_DEFAULT_SOURCES: + for item in PJLINK_DEFAULT_ITEMS: + label = "{source}{item}".format(source=source, item=item) + PJLINK_DEFAULT_CODES[label] = "{source} {item}".format(source=PJLINK_DEFAULT_SOURCES[source], + item=PJLINK_DEFAULT_ITEMS[item]) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index e13be94a9..61250a8e6 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -78,6 +78,33 @@ class PJLink1(QtNetwork.QTcpSocket): projectorNoAuthentication = QtCore.pyqtSignal(str) # PIN set and no authentication needed projectorReceivedData = QtCore.pyqtSignal() # Notify when received data finished processing projectorUpdateIcons = QtCore.pyqtSignal() # Update the status icons on toolbar + # New commands available in PJLink Class 2 + pjlink_future = [ + 'ACKN', # UDP Reply to 'SRCH' + 'FILT', # Get current filter usage time + 'FREZ', # Set freeze/unfreeze picture being projected + 'INNM', # Get Video source input terminal name + 'IRES', # Get Video source resolution + 'LKUP', # UPD Linkup status notification + 'MVOL', # Set microphone volume + 'RFIL', # Get replacement air filter model number + 'RLMP', # Get lamp replacement model number + 'RRES', # Get projector recommended video resolution + 'SNUM', # Get projector serial number + 'SRCH', # UDP broadcast search for available projectors on local network + 'SVER', # Get projector software version + 'SVOL', # Set speaker volume + 'TESTMEONLY' # For testing when other commands have been implemented + ] + + pjlink_udp_commands = [ + 'ACKN', + 'ERST', # Class 1 or 2 + 'INPT', # Class 1 or 2 + 'LKUP', + 'POWR', # Class 1 or 2 + 'SRCH' + ] def __init__(self, name=None, ip=None, port=PJLINK_PORT, pin=None, *args, **kwargs): """ @@ -403,7 +430,8 @@ class PJLink1(QtNetwork.QTcpSocket): return self.socket_timer.stop() self.projectorNetwork.emit(S_NETWORK_RECEIVED) - data_in = decode(read, 'ascii') + # NOTE: Class2 has changed to some values being UTF-8 + data_in = decode(read, 'utf-8') data = data_in.strip() if len(data) < 7: # Not enough data for a packet @@ -510,11 +538,12 @@ class PJLink1(QtNetwork.QTcpSocket): self._send_command() @QtCore.pyqtSlot() - def _send_command(self, data=None): + def _send_command(self, data=None, utf8=False): """ Socket interface to send data. If data=None, then check queue. :param data: Immediate data to send + :param utf8: Send as UTF-8 string otherwise send as ASCII string """ log.debug('({ip}) _send_string()'.format(ip=self.ip)) log.debug('({ip}) _send_string(): Connection status: {data}'.format(ip=self.ip, data=self.state())) @@ -542,7 +571,7 @@ class PJLink1(QtNetwork.QTcpSocket): log.debug('({ip}) _send_string(): Queue = {data}'.format(ip=self.ip, data=self.send_queue)) self.socket_timer.start() self.projectorNetwork.emit(S_NETWORK_SENDING) - sent = self.write(out.encode('ascii')) + sent = self.write(out.encode('{string_encoding}'.format(string_encoding='utf-8' if utf8 else 'ascii'))) self.waitForBytesWritten(2000) # 2 seconds should be enough if sent == -1: # Network error? @@ -556,7 +585,13 @@ class PJLink1(QtNetwork.QTcpSocket): :param cmd: Command to process :param data: Data being processed """ - log.debug('({ip}) Processing command "{data}"'.format(ip=self.ip, data=cmd)) + log.debug('({ip}) Processing command "{cmd}" with data "{data}"'.format(ip=self.ip, + cmd=cmd, + data=data)) + # Check if we have a future command not available yet + if cmd in self.pjlink_future: + self._not_implemented(cmd) + return if data in PJLINK_ERRORS: # Oops - projector error log.error('({ip}) Projector returned error "{data}"'.format(ip=self.ip, data=data)) @@ -568,9 +603,8 @@ class PJLink1(QtNetwork.QTcpSocket): self.projectorAuthentication.emit(self.name) elif data.upper() == 'ERR1': # Undefined command - self.change_status(E_UNDEFINED, '{error} "{data}"'.format(error=translate('OpenLP.PJLink1', - 'Undefined command:'), - data=cmd)) + self.change_status(E_UNDEFINED, '{error}: "{data}"'.format(error=ERROR_MSG[E_UNDEFINED], + data=cmd)) elif data.upper() == 'ERR2': # Invalid parameter self.change_status(E_PARAMETER) @@ -681,6 +715,7 @@ class PJLink1(QtNetwork.QTcpSocket): :param data: Currently selected source """ + # TODO: Class 2 change: verify input does not exceed 95 bytes self.source = data log.info('({ip}) Setting data source to "{data}"'.format(ip=self.ip, data=self.source)) return @@ -962,3 +997,11 @@ class PJLink1(QtNetwork.QTcpSocket): log.debug('({ip}) Setting AVMT to "10" (shutter open)'.format(ip=self.ip)) self.send_command(cmd='AVMT', opts='10') self.poll_loop() + + def _not_implemented(self, cmd): + """ + Log when a future PJLink command has not been implemented yet. + """ + log.warn("({ip}) Future command '{cmd}' has not been implemented yet".format(ip=self.ip, + cmd=cmd)) + return diff --git a/tests/functional/openlp_core_lib/test_projector_constants.py b/tests/functional/openlp_core_lib/test_projector_constants.py new file mode 100644 index 000000000..2b6ef948b --- /dev/null +++ b/tests/functional/openlp_core_lib/test_projector_constants.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2015 OpenLP Developers # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +Package to test the openlp.core.lib.projector.constants package. +""" +from unittest import TestCase, skip + + +class TestProjectorConstants(TestCase): + """ + Test specific functions in the projector constants module. + """ + @skip('Waiting for merge of ~alisonken1/openlp/pjlink2-resource-data') + def build_pjlink_video_label_test(self): + """ + Test building PJLINK_DEFAULT_CODES dictionary + """ + # GIVEN: Test data + from tests.resources.projector.data import TEST_VIDEO_CODES + + # WHEN: Import projector PJLINK_DEFAULT_CODES + from openlp.core.lib.projector.constants import PJLINK_DEFAULT_CODES + + # THEN: Verify dictionary was build correctly + self.assertEquals(PJLINK_DEFAULT_CODES, TEST_VIDEO_CODES, 'PJLink video strings should match') diff --git a/tests/functional/openlp_core_lib/test_projector_pjlink1.py b/tests/functional/openlp_core_lib/test_projector_pjlink1.py index 16b73c377..cce011c53 100644 --- a/tests/functional/openlp_core_lib/test_projector_pjlink1.py +++ b/tests/functional/openlp_core_lib/test_projector_pjlink1.py @@ -366,3 +366,18 @@ class TestPJLink(TestCase): # THEN: send_command should have the proper authentication self.assertEquals("{test}".format(test=mock_send_command.call_args), "call(data='{hash}%1CLSS ?\\r')".format(hash=TEST_HASH)) + + @patch.object(pjlink_test, '_not_implemented') + def not_implemented_test(self, mock_not_implemented): + """ + Test pjlink1._not_implemented method being called + """ + # GIVEN: test object + pjlink = pjlink_test + test_cmd = 'TESTMEONLY' + + # WHEN: A future command is called that is not implemented yet + pjlink.process_command(test_cmd, "Garbage data for test only") + + # THEN: pjlink1.__not_implemented should have been called with test_cmd + mock_not_implemented.assert_called_with(test_cmd) From 23047ae3547dcccd68754fc1979501e49817c534 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Mon, 15 May 2017 09:32:20 -0700 Subject: [PATCH 2/2] Remove extraneous TODO --- openlp/core/lib/projector/pjlink1.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 61250a8e6..b88369530 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -715,7 +715,6 @@ class PJLink1(QtNetwork.QTcpSocket): :param data: Currently selected source """ - # TODO: Class 2 change: verify input does not exceed 95 bytes self.source = data log.info('({ip}) Setting data source to "{data}"'.format(ip=self.ip, data=self.source)) return