Merge lp:~alisonken1/openlp/pjlink2-r into lp:openlp

Proposed by Ken Roberts
Status: Superseded
Proposed branch: lp:~alisonken1/openlp/pjlink2-r
Merge into: lp:openlp
Diff against target: 856 lines (+259/-133)
8 files modified
openlp/core/common/settings.py (+1/-0)
openlp/core/projectors/manager.py (+25/-2)
openlp/core/projectors/pjlink.py (+93/-87)
openlp/core/projectors/tab.py (+8/-0)
tests/openlp_core/projectors/test_projector_pjlink_base_01.py (+2/-12)
tests/openlp_core/projectors/test_projector_pjlink_base_02.py (+101/-0)
tests/openlp_core/projectors/test_projector_pjlink_cmd_routing.py (+14/-4)
tests/openlp_core/projectors/test_projector_pjlink_udp.py (+15/-28)
To merge this branch: bzr merge lp:~alisonken1/openlp/pjlink2-r
Reviewer Review Type Date Requested Status
Phill Needs Fixing
Tim Bentley Pending
Review via email: mp+344798@code.launchpad.net

This proposal supersedes a proposal from 2018-04-28.

This proposal has been superseded by a proposal from 2018-05-03.

Commit message

PJLink2 update R

Description of the change

- Convert UDP packet to send pyqtSignal and let individual projectors decide who it's for
- Remove unneccessary *args **kwargs from commands
- Remove projector_list and projector block search since switching to signals
- pass on ACKN, and SRCH commands until I think about it some more
- Cleanup extraneous args on process_* commands since switching to signals
- Add UDP socket listener(s) for PJLink2 UDP options
- Added process_lkup method code
- Add projector configure option to connect when LKUP packet received
- Fix _send_queue to ignore call if no data to send
- Rename tests/openlp_core/projectors/test_projector_pjlink_base.py to test_projector_pjlink_base_01.py
- Added tests/openlp_core/projectors/test_projector_pjlink_base_02.py
- Added test for PJLink._send_command
- Removed unused test_projector_pjlink_base_02.pjlink_test from module
- Cleanup tests/openlp_core/projectors/test_projector_pjlink_base_01 imports
- Fix test_projector_pjlink_cmd_routing.test_get_data_unknown_command
- Fix invalid delete projector signal disconnect call in manager
- Move try block under if block when disconnecting signal from deleted projector in manager
- Minor cleanups

--------------------------------------------------------------------------------
lp:~alisonken1/openlp/pjlink2-r (revision 2819)
https://ci.openlp.io/job/Branch-01-Pull/2512/ [SUCCESS]
https://ci.openlp.io/job/Branch-02a-Linux-Tests/2413/ [SUCCESS]
https://ci.openlp.io/job/Branch-02b-macOS-Tests/199/ [FAILURE]
https://ci.openlp.io/job/Branch-03a-Build-Source/109/ [SUCCESS]
https://ci.openlp.io/job/Branch-03b-Build-macOS/102/ [SUCCESS]
https://ci.openlp.io/job/Branch-04a-Code-Analysis/1571/ [SUCCESS]
https://ci.openlp.io/job/Branch-04b-Test-Coverage/1384/ [SUCCESS]
https://ci.openlp.io/job/Branch-05-AppVeyor-Tests/313/ [FAILURE]

To post a comment you must log in.
Revision history for this message
Tim Bentley (trb143) wrote : Posted in a previous version of this proposal

A question in the code

In future can you put the test results at the bottom of the request as it makes the history easier to read.

review: Needs Fixing
Revision history for this message
Phill (phill-ridout) wrote : Posted in a previous version of this proposal

Ran out of time to complete a review, but a few in lines to start.

review: Needs Fixing
Revision history for this message
Ken Roberts (alisonken1) wrote : Posted in a previous version of this proposal
Download full text (7.6 KiB)

On Sat, Apr 28, 2018 at 12:15 AM, Phill <email address hidden> wrote:
> Review: Needs Fixing
>
> Ran out of time to complete a review, but a few in lines to start.
>
> Diff comments:
>
>>
>> === modified file 'openlp/core/projectors/manager.py'
>> --- openlp/core/projectors/manager.py 2018-04-20 06:04:43 +0000
>> +++ openlp/core/projectors/manager.py 2018-04-28 02:41:38 +0000
>> if read_size < 0:
>> log.warning('(UDP) No data (-1)')
>> return
>> - if read_size < 1:
>> + elif read_size < 1:
>
> given the < 0 condition above, this condition will only be True when read_size == 0, which to me is a bit readable / obvious at a quick glance.

-1 indicates a socket problem - which is distinct from no data (0 or
empty packet)

>> @@ -654,10 +651,10 @@
>> :param host: IP address of sending host
>> :param port: Port received on
>> """
>> - log.warning('(UDP) SRCH packet received from {host} - ignoring'.format(host=host))
>> + log.warning("({ip}) I don't do SRCH packets - ignoring".format(ip=self.ip))
>
> Why this change? the original is more understandable to me!

This method is only called from within a projector instance - not the
UDP monitor.
However, just noticed it's using the wrong option for {ip} - should be
ip=self.entry.name

>
>> return
>>
>> - def process_sver(self, data, *args, **kwargs):
>> + def process_sver(self, data):
>> """
>> Software version of projector
>> """
>> @@ -1181,7 +1187,7 @@
>> try:
>> self.readyRead.disconnect(self.get_socket)
>> except TypeError:
>> - pass
>> + log.debug('({ip}) disconnect_from_host(): TypeError detected'.format(ip=self.entry.name))
>
> You're just disconnecting a slot here right? Do we need to log that?

Since this is in the projector instance, I would like to know if
there's a type issue - if so, then I would like to see it in the log.

>
>> log.debug('({ip}) disconnect_from_host() '
>> 'Current status {data}'.format(ip=self.entry.name, data=self._get_status(self.status_connect)[0]))
>> if abort:
>
>
> --
> https://code.launchpad.net/~alisonken1/openlp/pjlink2-r/+merge/344795
> You are the owner of lp:~alisonken1/openlp/pjlink2-r.

--
- Ken
Registered Linux user 296561
Slackin' since 1993
Slackware Linux (http://www.slackware.com)
OpenLP - Church Projection Software
Empower Your Church http://openlp.org

On Sat, Apr 28, 2018 at 12:15 AM, Phill <email address hidden> wrote:
> Review: Needs Fixing
>
> Ran out of time to complete a review, but a few in lines to start.
>
> Diff comments:
>
>>
>> === modified file 'openlp/core/projectors/manager.py'
>> --- openlp/core/projectors/manager.py 2018-04-20 06:04:43 +0000
>> +++ openlp/core/projectors/manager.py 2018-04-28 02:41:38 +0000
>> @@ -513,6 +519,18 @@
>> projector.socket_timer.timeout.disconnect(projector.link.socket_abort)
>> except (AttributeError, TypeError):
>> pass
>> + # Disconnect signals from projector being deleted
>> + try:
>> + self.pjlink_udp.data_received.di...

Read more...

Revision history for this message
Phill (phill-ridout) wrote : Posted in a previous version of this proposal

See inlines please.

Revision history for this message
Phill (phill-ridout) : Posted in a previous version of this proposal
review: Needs Fixing
Revision history for this message
Phill (phill-ridout) wrote :

Sorry, reviewed a previous version.

review: Needs Fixing
lp:~alisonken1/openlp/pjlink2-r updated
2819. By Ken Roberts

PJLink2 update R

2820. By Ken Roberts

Fix settings calls

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'openlp/core/common/settings.py'
2--- openlp/core/common/settings.py 2018-02-02 21:33:41 +0000
3+++ openlp/core/common/settings.py 2018-05-03 14:59:14 +0000
4@@ -199,6 +199,7 @@
5 'projector/db database': '',
6 'projector/enable': True,
7 'projector/connect on start': False,
8+ 'projector/connect when LKUP received': True, # PJLink v2: Projector sends LKUP command after it powers up
9 'projector/last directory import': None,
10 'projector/last directory export': None,
11 'projector/poll time': 20, # PJLink timeout is 30 seconds
12
13=== modified file 'openlp/core/projectors/manager.py'
14--- openlp/core/projectors/manager.py 2018-04-20 06:04:43 +0000
15+++ openlp/core/projectors/manager.py 2018-05-03 14:59:14 +0000
16@@ -37,7 +37,7 @@
17 from openlp.core.projectors import DialogSourceStyle
18 from openlp.core.projectors.constants import E_AUTHENTICATION, E_ERROR, E_NETWORK, E_NOT_CONNECTED, \
19 E_UNKNOWN_SOCKET_ERROR, S_CONNECTED, S_CONNECTING, S_COOLDOWN, S_INITIALIZE, S_NOT_CONNECTED, S_OFF, S_ON, \
20- S_STANDBY, S_WARMUP, STATUS_CODE, STATUS_MSG, QSOCKET_STATE
21+ S_STANDBY, S_WARMUP, PJLINK_PORT, STATUS_CODE, STATUS_MSG, QSOCKET_STATE
22
23 from openlp.core.projectors.db import ProjectorDB
24 from openlp.core.projectors.editform import ProjectorEditForm
25@@ -294,6 +294,9 @@
26 self.projectordb = projectordb
27 self.projector_list = []
28 self.source_select_form = None
29+ # Dictionary of PJLinkUDP objects to listen for UDP broadcasts from PJLink 2+ projectors.
30+ # Key is port number that projectors use
31+ self.pjlink_udp = {}
32
33 def bootstrap_initialise(self):
34 """
35@@ -307,12 +310,15 @@
36 else:
37 log.debug('Using existing ProjectorDB() instance')
38 self.get_settings()
39- self.pjlink_udp = PJLinkUDP(self.projector_list)
40
41 def bootstrap_post_set_up(self):
42 """
43 Post-initialize setups.
44 """
45+ # Default PJLink port UDP socket
46+ log.debug('Creating PJLinkUDP listener for default port {port}'.format(port=PJLINK_PORT))
47+ self.pjlink_udp = {PJLINK_PORT: PJLinkUDP(port=PJLINK_PORT)}
48+ self.pjlink_udp[PJLINK_PORT].bind(PJLINK_PORT)
49 # Set 1.5 second delay before loading all projectors
50 if self.autostart:
51 log.debug('Delaying 1.5 seconds before loading all projectors')
52@@ -513,6 +519,14 @@
53 projector.socket_timer.timeout.disconnect(projector.link.socket_abort)
54 except (AttributeError, TypeError):
55 pass
56+ # Disconnect signals from projector being deleted
57+ if self.pjlink_udp[projector.port]:
58+ try:
59+ self.pjlink_udp[projector.port].data_received.disconnect(projector.get_buffer)
60+ except (AttributeError, TypeError):
61+ pass
62+
63+ # Rebuild projector list
64 new_list = []
65 for item in self.projector_list:
66 if item.link.db_item.id == projector.link.db_item.id:
67@@ -726,6 +740,15 @@
68 item.link.projectorAuthentication.connect(self.authentication_error)
69 item.link.projectorNoAuthentication.connect(self.no_authentication_error)
70 item.link.projectorUpdateIcons.connect(self.update_icons)
71+ # Connect UDP signal to projector instances with same port
72+ if item.link.port not in self.pjlink_udp:
73+ log.debug('Adding new PJLinkUDP listener fo port {port}'.format(port=item.link.port))
74+ self.pjlink_udp[item.link.port] = PJLinkUDP(port=item.link.port)
75+ self.pjlink_udp[item.link.port].bind(item.link.port)
76+ log.debug('Connecting PJLinkUDP port {port} signal to "{item}"'.format(port=item.link.port,
77+ item=item.link.name))
78+ self.pjlink_udp[item.link.port].data_received.connect(item.link.get_buffer)
79+
80 self.projector_list.append(item)
81 if start:
82 item.link.connect_to_host()
83
84=== modified file 'openlp/core/projectors/pjlink.py'
85--- openlp/core/projectors/pjlink.py 2018-04-20 06:04:43 +0000
86+++ openlp/core/projectors/pjlink.py 2018-05-03 14:59:14 +0000
87@@ -54,6 +54,7 @@
88
89 from openlp.core.common import qmd5_hash
90 from openlp.core.common.i18n import translate
91+from openlp.core.common.settings import Settings
92 from openlp.core.projectors.constants import CONNECTION_ERRORS, PJLINK_CLASS, PJLINK_DEFAULT_CODES, PJLINK_ERRORS, \
93 PJLINK_ERST_DATA, PJLINK_ERST_STATUS, PJLINK_MAX_PACKET, PJLINK_PREFIX, PJLINK_PORT, PJLINK_POWR_STATUS, \
94 PJLINK_SUFFIX, PJLINK_VALID_CMD, PROJECTOR_STATE, STATUS_CODE, STATUS_MSG, QSOCKET_STATE, \
95@@ -78,25 +79,27 @@
96 """
97 Socket service for PJLink UDP socket.
98 """
99- def __init__(self, projector_list, port=PJLINK_PORT):
100+
101+ data_received = QtCore.pyqtSignal(QtNetwork.QHostAddress, int, str, name='udp_data') # host, port, data
102+
103+ def __init__(self, port=PJLINK_PORT):
104 """
105 Socket services for PJLink UDP packets.
106
107 Since all UDP packets from any projector will come into the same
108 port, process UDP packets here then route to the appropriate
109 projector instance as needed.
110+
111+ :param port: UDP port to listen on
112 """
113- # Keep track of currently defined projectors so we can route
114- # inbound packets to the correct instance
115 super().__init__()
116- self.projector_list = projector_list
117 self.port = port
118 # Local defines
119 self.search_active = False
120 self.search_time = 30000 # 30 seconds for allowed time
121 self.search_timer = QtCore.QTimer()
122 self.readyRead.connect(self.get_datagram)
123- log.debug('(UDP) PJLinkUDP() Initialized')
124+ log.debug('(UDP) PJLinkUDP() Initialized for port {port}'.format(port=self.port))
125
126 @QtCore.pyqtSlot()
127 def get_datagram(self):
128@@ -105,25 +108,23 @@
129 """
130 log.debug('(UDP) get_datagram() - Receiving data')
131 read_size = self.pendingDatagramSize()
132- if read_size < 0:
133+ if -1 == read_size:
134 log.warning('(UDP) No data (-1)')
135 return
136- if read_size < 1:
137+ elif 0 == read_size:
138 log.warning('(UDP) get_datagram() called when pending data size is 0')
139 return
140- data, peer_address, peer_port = self.readDatagram(self.pendingDatagramSize())
141+ elif read_size > PJLINK_MAX_PACKET:
142+ log.warning('(UDP) UDP Packet too large ({size} bytes)- ignoring'.format(size=read_size))
143+ return
144+ data_in, peer_host, peer_port = self.readDatagram(read_size)
145+ data = data_in.decode('utf-8') if isinstance(data_in, bytes) else data_in
146 log.debug('(UDP) {size} bytes received from {adx} on port {port}'.format(size=len(data),
147- adx=peer_address,
148- port=peer_port))
149+ adx=peer_host.toString(),
150+ port=self.port))
151 log.debug('(UDP) packet "{data}"'.format(data=data))
152- # Send to appropriate instance to process packet
153- log.debug('(UDP) Checking projector list for ip {host} to process'.format(host=peer_address))
154- for projector in self.projector_list:
155- if peer_address == projector.ip:
156- # Dispatch packet to appropriate remote instance
157- log.debug('(UDP) Dispatching packet to {host}'.format(host=projector.entry.name))
158- return projector.get_data(buff=data, ip=peer_address, host=peer_address, port=peer_port)
159- log.warning('(UDP) Could not find projector with ip {ip} to process packet'.format(ip=peer_address))
160+ log.debug('(UDP) Sending data_received signal to projectors')
161+ self.data_received.emit(peer_host, self.localPort(), data)
162 return
163
164 def search_start(self):
165@@ -131,7 +132,6 @@
166 Start search for projectors on local network
167 """
168 self.search_active = True
169- self.ackn_list = {}
170 # TODO: Send SRCH packet here
171 self.search_timer.singleShot(self.search_time, self.search_stop)
172
173@@ -240,7 +240,7 @@
174 for cmd in self.pjlink_functions:
175 self.pjlink_functions[cmd]["version"] = PJLINK_VALID_CMD[cmd]['default']
176
177- def process_command(self, cmd, data, *args, **kwargs):
178+ def process_command(self, cmd, data):
179 """
180 Verifies any return error code. Calls the appropriate command handler.
181
182@@ -272,25 +272,18 @@
183 return self.change_status(status=E_AUTHENTICATION)
184 # Command checks already passed
185 log.debug('({ip}) Calling function for {cmd}'.format(ip=self.entry.name, cmd=cmd))
186- self.pjlink_functions[cmd]["method"](data=data, *args, **kwargs)
187+ self.pjlink_functions[cmd]["method"](data=data)
188
189- def process_ackn(self, data, host, port):
190+ def process_ackn(self, data):
191 """
192 Process the ACKN command.
193
194 :param data: Data in packet
195- :param host: IP address of sending host
196- :param port: Port received on
197 """
198- log.debug('({ip}) Processing ACKN packet'.format(ip=self.entry.name))
199- if host not in self.ackn_list:
200- log.debug('({ip}) Adding {host} to ACKN list'.format(ip=self.entry.name, host=host))
201- self.ackn_list[host] = {'data': data,
202- 'port': port}
203- else:
204- log.warning('({ip}) Host {host} already replied - ignoring'.format(ip=self.entry.name, host=host))
205+ # TODO: Have to rethink this one
206+ pass
207
208- def process_avmt(self, data, *args, **kwargs):
209+ def process_avmt(self, data):
210 """
211 Process shutter and speaker status. See PJLink specification for format.
212 Update self.mute (audio) and self.shutter (video shutter).
213@@ -319,7 +312,7 @@
214 self.projectorUpdateIcons.emit()
215 return
216
217- def process_clss(self, data, *args, **kwargs):
218+ def process_clss(self, data):
219 """
220 PJLink class that this projector supports. See PJLink specification for format.
221 Updates self.class.
222@@ -365,7 +358,7 @@
223
224 return
225
226- def process_erst(self, data, *args, **kwargs):
227+ def process_erst(self, data):
228 """
229 Error status. See PJLink Specifications for format.
230 Updates self.projector_errors
231@@ -417,7 +410,7 @@
232 PJLINK_ERST_STATUS[other]
233 return
234
235- def process_inf1(self, data, *args, **kwargs):
236+ def process_inf1(self, data):
237 """
238 Manufacturer name set in projector.
239 Updates self.manufacturer
240@@ -429,7 +422,7 @@
241 data=self.manufacturer))
242 return
243
244- def process_inf2(self, data, *args, **kwargs):
245+ def process_inf2(self, data):
246 """
247 Projector Model set in projector.
248 Updates self.model.
249@@ -440,7 +433,7 @@
250 log.debug('({ip}) Setting projector model to "{data}"'.format(ip=self.entry.name, data=self.model))
251 return
252
253- def process_info(self, data, *args, **kwargs):
254+ def process_info(self, data):
255 """
256 Any extra info set in projector.
257 Updates self.other_info.
258@@ -451,7 +444,7 @@
259 log.debug('({ip}) Setting projector other_info to "{data}"'.format(ip=self.entry.name, data=self.other_info))
260 return
261
262- def process_inpt(self, data, *args, **kwargs):
263+ def process_inpt(self, data):
264 """
265 Current source input selected. See PJLink specification for format.
266 Update self.source
267@@ -473,7 +466,7 @@
268 log.debug('({ip}) Setting data source to "{data}"'.format(ip=self.entry.name, data=self.source))
269 return
270
271- def process_inst(self, data, *args, **kwargs):
272+ def process_inst(self, data):
273 """
274 Available source inputs. See PJLink specification for format.
275 Updates self.source_available
276@@ -490,7 +483,7 @@
277 data=self.source_available))
278 return
279
280- def process_lamp(self, data, *args, **kwargs):
281+ def process_lamp(self, data):
282 """
283 Lamp(s) status. See PJLink Specifications for format.
284 Data may have more than 1 lamp to process.
285@@ -516,18 +509,22 @@
286 self.lamp = lamps
287 return
288
289- def process_lkup(self, data, host, port):
290+ def process_lkup(self, data):
291 """
292 Process reply indicating remote is available for connection
293
294 :param data: Data packet from remote
295- :param host: Remote IP address
296- :param port: Local port packet received on
297 """
298- # TODO: Check if autoconnect is enabled and connect?
299- pass
300+ log.debug('({ip}) Processing LKUP command'.format(ip=self.entry.name))
301+ settings = Settings()
302+ settings.beginGroup(self.settings_section)
303+ autostart = settings.value('connect when LKUP received')
304+ settings.endGroup()
305+ del settings
306+ if autostart:
307+ self.connect_to_host()
308
309- def process_name(self, data, *args, **kwargs):
310+ def process_name(self, data):
311 """
312 Projector name set in projector.
313 Updates self.pjlink_name
314@@ -538,7 +535,7 @@
315 log.debug('({ip}) Setting projector PJLink name to "{data}"'.format(ip=self.entry.name, data=self.pjlink_name))
316 return
317
318- def process_pjlink(self, data, *args, **kwargs):
319+ def process_pjlink(self, data):
320 """
321 Process initial socket connection to terminal.
322
323@@ -579,7 +576,7 @@
324 # Since this is an initial connection, make it a priority just in case
325 return self.send_command(cmd="CLSS", salt=data_hash, priority=True)
326
327- def process_powr(self, data, *args, **kwargs):
328+ def process_powr(self, data):
329 """
330 Power status. See PJLink specification for format.
331 Update self.power with status. Update icons if change from previous setting.
332@@ -602,7 +599,7 @@
333 log.warning('({ip}) Unknown power response: "{data}"'.format(ip=self.entry.name, data=data))
334 return
335
336- def process_rfil(self, data, *args, **kwargs):
337+ def process_rfil(self, data):
338 """
339 Process replacement filter type
340 """
341@@ -613,7 +610,7 @@
342 log.warning('({ip}) Saved model: "{old}"'.format(ip=self.entry.name, old=self.model_filter))
343 log.warning('({ip}) New model: "{new}"'.format(ip=self.entry.name, new=data))
344
345- def process_rlmp(self, data, *args, **kwargs):
346+ def process_rlmp(self, data):
347 """
348 Process replacement lamp type
349 """
350@@ -624,7 +621,7 @@
351 log.warning('({ip}) Saved lamp: "{old}"'.format(ip=self.entry.name, old=self.model_lamp))
352 log.warning('({ip}) New lamp: "{new}"'.format(ip=self.entry.name, new=data))
353
354- def process_snum(self, data, *args, **kwargs):
355+ def process_snum(self, data):
356 """
357 Serial number of projector.
358
359@@ -644,20 +641,18 @@
360 log.warning('({ip}) NOT saving serial number'.format(ip=self.entry.name))
361 self.serial_no_received = data
362
363- def process_srch(self, data, host, port):
364+ def process_srch(self, data):
365 """
366 Process the SRCH command.
367
368 SRCH is processed by terminals so we ignore any packet.
369
370 :param data: Data in packet
371- :param host: IP address of sending host
372- :param port: Port received on
373 """
374- log.warning('(UDP) SRCH packet received from {host} - ignoring'.format(host=host))
375+ log.warning("({ip}) SRCH packet detected - ignoring".format(ip=self.entry.ip))
376 return
377
378- def process_sver(self, data, *args, **kwargs):
379+ def process_sver(self, data):
380 """
381 Software version of projector
382 """
383@@ -705,14 +700,16 @@
384 args=args,
385 kwargs=kwargs))
386 super().__init__()
387+ self.settings_section = 'projector'
388 self.entry = projector
389 self.ip = self.entry.ip
390+ self.qhost = QtNetwork.QHostAddress(self.ip)
391 self.location = self.entry.location
392 self.mac_adx = self.entry.mac_adx
393 self.name = self.entry.name
394 self.notes = self.entry.notes
395 self.pin = self.entry.pin
396- self.port = self.entry.port
397+ self.port = int(self.entry.port)
398 self.pjlink_class = PJLINK_CLASS if self.entry.pjlink_class is None else self.entry.pjlink_class
399 self.ackn_list = {} # Replies from online projectors (Class 2 option)
400 self.db_update = False # Use to check if db needs to be updated prior to exiting
401@@ -928,19 +925,21 @@
402 count=trash_count))
403 return
404
405- @QtCore.pyqtSlot(str, str)
406- def get_buffer(self, data, ip):
407+ @QtCore.pyqtSlot(QtNetwork.QHostAddress, int, str, name='udp_data') # host, port, data
408+ def get_buffer(self, host, port, data):
409 """
410 Get data from somewhere other than TCP socket
411
412+ :param host: QHostAddress of sender
413+ :param port: Destination port
414 :param data: Data to process. buffer must be formatted as a proper PJLink packet.
415- :param ip: Destination IP for buffer.
416 """
417- log.debug('({ip}) get_buffer(data="{buff}" ip="{ip_in}"'.format(ip=self.entry.name, buff=data, ip_in=ip))
418- if ip is None:
419- log.debug("({ip}) get_buffer() Don't know who data is for - exiting".format(ip=self.entry.name))
420- return
421- return self.get_data(buff=data, ip=ip)
422+ if (port == int(self.port)) and (host.isEqual(self.qhost)):
423+ log.debug('({ip}) Received data from {host}'.format(ip=self.entry.name, host=host.toString()))
424+ log.debug('({ip}) get_buffer(data="{buff}")'.format(ip=self.entry.name, buff=data))
425+ return self.get_data(buff=data)
426+ else:
427+ log.debug('({ip}) Ignoring data for {host} - not me'.format(ip=self.entry.name, host=host.toString()))
428
429 @QtCore.pyqtSlot()
430 def get_socket(self):
431@@ -960,20 +959,16 @@
432 log.debug('({ip}) get_socket(): No data available (-1)'.format(ip=self.entry.name))
433 return self.receive_data_signal()
434 self.socket_timer.stop()
435- return self.get_data(buff=read, ip=self.ip)
436+ return self.get_data(buff=read)
437
438- def get_data(self, buff, ip=None, *args, **kwargs):
439+ def get_data(self, buff, *args, **kwargs):
440 """
441 Process received data
442
443 :param buff: Data to process.
444- :param ip: (optional) Destination IP.
445 """
446- # Since "self" is not available to options and the "ip" keyword is a "maybe I'll use in the future",
447- # set to default here
448- if ip is None:
449- ip = self.ip
450- log.debug('({ip}) get_data(ip="{ip_in}" buffer="{buff}"'.format(ip=self.entry.name, ip_in=ip, buff=buff))
451+ log.debug('({ip}) get_data(buffer="{buff}"'.format(ip=self.entry.name, buff=buff))
452+ ignore_class = 'ignore_class' in kwargs
453 # NOTE: Class2 has changed to some values being UTF-8
454 if isinstance(buff, bytes):
455 data_in = decode(buff, 'utf-8')
456@@ -990,7 +985,9 @@
457 elif not data.startswith(PJLINK_PREFIX):
458 self._trash_buffer(msg='get_data(): Invalid packet - PJLink prefix missing')
459 return self.receive_data_signal()
460- elif data[6] != '=':
461+ elif data[6] != '=' and data[8] != '=':
462+ # data[6] = standard command packet
463+ # data[8] = initial PJLink connection (after mangling)
464 self._trash_buffer(msg='get_data(): Invalid reply - Does not have "="')
465 return self.receive_data_signal()
466 log.debug('({ip}) get_data(): Checking new data "{data}"'.format(ip=self.entry.name, data=data))
467@@ -1020,16 +1017,16 @@
468 return self.receive_data_signal()
469 '''
470 if cmd not in PJLINK_VALID_CMD:
471- self._trash_buffer('get_data(): Invalid packet - unknown command "{data}"'.format(ip=self.entry.name,
472- data=cmd))
473+ self._trash_buffer('get_data(): Invalid packet - unknown command "{data}"'.format(data=cmd))
474 return self.receive_data_signal()
475 elif version not in PJLINK_VALID_CMD[cmd]['version']:
476 self._trash_buffer(msg='get_data() Command reply version does not match a valid command version')
477 return self.receive_data_signal()
478 elif int(self.pjlink_class) < int(version):
479- log.warning('({ip}) get_data(): Projector returned class reply higher '
480- 'than projector stated class'.format(ip=self.entry.name))
481- self.process_command(cmd, data, *args, **kwargs)
482+ if not ignore_class:
483+ log.warning('({ip}) get_data(): Projector returned class reply higher '
484+ 'than projector stated class'.format(ip=self.entry.name))
485+ self.process_command(cmd, data)
486 return self.receive_data_signal()
487
488 @QtCore.pyqtSlot(QtNetwork.QAbstractSocket.SocketError)
489@@ -1107,11 +1104,18 @@
490 """
491 Socket interface to send data. If data=None, then check queue.
492
493- :param data: Immediate data to send
494+ :param data: Immediate data to send (Optional)
495 :param utf8: Send as UTF-8 string otherwise send as ASCII string
496 """
497- # Funny looking data check, but it's a quick check for data=None
498- log.debug('({ip}) _send_command(data="{data}")'.format(ip=self.entry.name, data=data.strip() if data else data))
499+ if not data and not self.priority_queue and not self.send_queue:
500+ log.debug('({ip}) _send_command(): Nothing to send - returning'.format(ip=self.entry.name))
501+ return
502+ log.debug('({ip}) _send_command(data="{data}")'.format(ip=self.entry.name,
503+ data=data.strip() if data else data))
504+ log.debug('({ip}) _send_command(): priority_queue: {queue}'.format(ip=self.entry.name,
505+ queue=self.priority_queue))
506+ log.debug('({ip}) _send_command(): send_queue: {queue}'.format(ip=self.entry.name,
507+ queue=self.send_queue))
508 conn_state = STATUS_CODE[QSOCKET_STATE[self.state()]]
509 log.debug('({ip}) _send_command(): Connection status: {data}'.format(ip=self.entry.name,
510 data=conn_state))
511@@ -1149,9 +1153,9 @@
512 self.waitForBytesWritten(2000) # 2 seconds should be enough
513 if sent == -1:
514 # Network error?
515- log.warning('({ip}) _send_command(): -1 received - disconnecting from host'.format(ip=self.entry.name))
516 self.change_status(E_NETWORK,
517 translate('OpenLP.PJLink', 'Error while sending data to projector'))
518+ log.warning('({ip}) _send_command(): -1 received - disconnecting from host'.format(ip=self.entry.name))
519 self.disconnect_from_host()
520
521 def connect_to_host(self):
522@@ -1164,7 +1168,7 @@
523 return
524 self.error_status = S_OK
525 self.change_status(S_CONNECTING)
526- self.connectToHost(self.ip, self.port if isinstance(self.port, int) else int(self.port))
527+ self.connectToHost(self.ip, self.port)
528
529 @QtCore.pyqtSlot()
530 def disconnect_from_host(self, abort=False):
531@@ -1177,13 +1181,15 @@
532 self.abort()
533 else:
534 log.warning('({ip}) disconnect_from_host(): Not connected'.format(ip=self.entry.name))
535- self.disconnectFromHost()
536 try:
537 self.readyRead.disconnect(self.get_socket)
538 except TypeError:
539- pass
540- log.debug('({ip}) disconnect_from_host() '
541+ # Since we already know what's happening, just log it for reference.
542+ log.debug('({ip}) disconnect_from_host(): Issue detected with '
543+ 'readyRead.disconnect'.format(ip=self.entry.name))
544+ log.debug('({ip}) disconnect_from_host(): '
545 'Current status {data}'.format(ip=self.entry.name, data=self._get_status(self.status_connect)[0]))
546+ self.disconnectFromHost()
547 if abort:
548 self.change_status(E_NOT_CONNECTED)
549 else:
550
551=== modified file 'openlp/core/projectors/tab.py'
552--- openlp/core/projectors/tab.py 2017-12-29 09:15:48 +0000
553+++ openlp/core/projectors/tab.py 2018-05-03 14:59:14 +0000
554@@ -89,6 +89,10 @@
555 self.connect_box_layout.addRow(self.dialog_type_label, self.dialog_type_combo_box)
556 self.left_layout.addStretch()
557 self.dialog_type_combo_box.activated.connect(self.on_dialog_type_combo_box_changed)
558+ # Connect on LKUP packet received (PJLink v2+ only)
559+ self.connect_on_linkup = QtWidgets.QCheckBox(self.connect_box)
560+ self.connect_on_linkup.setObjectName('connect_on_linkup')
561+ self.connect_box_layout.addRow(self.connect_on_linkup)
562
563 def retranslateUi(self):
564 """
565@@ -109,6 +113,8 @@
566 translate('OpenLP.ProjectorTab', 'Tabbed dialog box'))
567 self.dialog_type_combo_box.setItemText(DialogSourceStyle.Single,
568 translate('OpenLP.ProjectorTab', 'Single dialog box'))
569+ self.connect_on_linkup.setText(
570+ translate('OpenLP.ProjectorTab', 'Connect to projector when LINKUP received (v2 only)'))
571
572 def load(self):
573 """
574@@ -120,6 +126,7 @@
575 self.socket_timeout_spin_box.setValue(settings.value('socket timeout'))
576 self.socket_poll_spin_box.setValue(settings.value('poll time'))
577 self.dialog_type_combo_box.setCurrentIndex(settings.value('source dialog type'))
578+ self.connect_on_linkup.setChecked(settings.value('connect when LKUP received'))
579 settings.endGroup()
580
581 def save(self):
582@@ -132,6 +139,7 @@
583 settings.setValue('socket timeout', self.socket_timeout_spin_box.value())
584 settings.setValue('poll time', self.socket_poll_spin_box.value())
585 settings.setValue('source dialog type', self.dialog_type_combo_box.currentIndex())
586+ settings.setValue('connect when LKUP received', self.connect_on_linkup.isChecked())
587 settings.endGroup()
588
589 def on_dialog_type_combo_box_changed(self):
590
591=== renamed file 'tests/openlp_core/projectors/test_projector_pjlink_base.py' => 'tests/openlp_core/projectors/test_projector_pjlink_base_01.py'
592--- tests/openlp_core/projectors/test_projector_pjlink_base.py 2018-01-13 05:41:42 +0000
593+++ tests/openlp_core/projectors/test_projector_pjlink_base_01.py 2018-05-03 14:59:14 +0000
594@@ -27,23 +27,13 @@
595
596 import openlp.core.projectors.pjlink
597 from openlp.core.projectors.constants import \
598- E_NOT_CONNECTED, \
599- E_PARAMETER, \
600- E_UNKNOWN_SOCKET_ERROR, \
601- STATUS_CODE, \
602- STATUS_MSG, \
603- S_CONNECTED, \
604- S_CONNECTING, \
605- S_NOT_CONNECTED, \
606- S_OK, \
607- S_ON, \
608+ E_NOT_CONNECTED, E_PARAMETER, E_UNKNOWN_SOCKET_ERROR, STATUS_CODE, STATUS_MSG, \
609+ S_CONNECTED, S_CONNECTING, S_NOT_CONNECTED, S_OK, S_ON, \
610 QSOCKET_STATE
611 from openlp.core.projectors.db import Projector
612 from openlp.core.projectors.pjlink import PJLink
613 from tests.resources.projector.data import TEST1_DATA
614
615-pjlink_test = PJLink(Projector(**TEST1_DATA), no_poll=True)
616-
617
618 class TestPJLinkBase(TestCase):
619 """
620
621=== added file 'tests/openlp_core/projectors/test_projector_pjlink_base_02.py'
622--- tests/openlp_core/projectors/test_projector_pjlink_base_02.py 1970-01-01 00:00:00 +0000
623+++ tests/openlp_core/projectors/test_projector_pjlink_base_02.py 2018-05-03 14:59:14 +0000
624@@ -0,0 +1,101 @@
625+# -*- coding: utf-8 -*-
626+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
627+
628+###############################################################################
629+# OpenLP - Open Source Lyrics Projection #
630+# --------------------------------------------------------------------------- #
631+# Copyright (c) 2008-2015 OpenLP Developers #
632+# --------------------------------------------------------------------------- #
633+# This program is free software; you can redistribute it and/or modify it #
634+# under the terms of the GNU General Public License as published by the Free #
635+# Software Foundation; version 2 of the License. #
636+# #
637+# This program is distributed in the hope that it will be useful, but WITHOUT #
638+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
639+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
640+# more details. #
641+# #
642+# You should have received a copy of the GNU General Public License along #
643+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
644+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
645+###############################################################################
646+"""
647+Package to test the openlp.core.projectors.pjlink base package.
648+"""
649+from unittest import TestCase
650+from unittest.mock import call, patch
651+
652+import openlp.core.projectors.pjlink
653+
654+from openlp.core.projectors.constants import S_NOT_CONNECTED
655+from openlp.core.projectors.db import Projector
656+from openlp.core.projectors.pjlink import PJLink
657+from tests.resources.projector.data import TEST1_DATA
658+
659+
660+class TestPJLinkBase(TestCase):
661+ """
662+ Tests for the PJLink module
663+ """
664+ @patch.object(openlp.core.projectors.pjlink.PJLink, 'state')
665+ @patch.object(openlp.core.projectors.pjlink.PJLink, 'reset_information')
666+ @patch.object(openlp.core.projectors.pjlink.PJLink, '_send_command')
667+ @patch.object(openlp.core.projectors.pjlink, 'log')
668+ def test_send_command_no_data(self, mock_log, mock_send_command, mock_reset, mock_state):
669+ """
670+ Test _send_command with no data to send
671+ """
672+ # GIVEN: Test object
673+ log_warning_calls = [call('({ip}) send_command(): Not connected - returning'.format(ip=TEST1_DATA['name']))]
674+
675+ log_debug_calls = [call('PJlink(projector="< Projector(id="None", ip="111.111.111.111", port="1111", '
676+ 'mac_adx="11:11:11:11:11:11", pin="1111", name="___TEST_ONE___", '
677+ 'location="location one", notes="notes one", pjlink_name="None", '
678+ 'pjlink_class="None", manufacturer="None", model="None", '
679+ 'serial_no="Serial Number 1", other="None", sources="None", source_list="[]", '
680+ 'model_filter="Filter type 1", model_lamp="Lamp type 1", '
681+ 'sw_version="Version 1") >", args="()" kwargs="{\'no_poll\': True}")'),
682+ call('PJlinkCommands(args=() kwargs={})')]
683+ mock_state.return_value = S_NOT_CONNECTED
684+ pjlink = PJLink(Projector(**TEST1_DATA), no_poll=True)
685+ pjlink.send_queue = []
686+ pjlink.priority_queue = []
687+
688+ # WHEN: _send_command called with no data and queue's empty
689+ pjlink.send_command(cmd='DONTCARE')
690+
691+ # THEN:
692+ mock_log.debug.assert_has_calls(log_debug_calls)
693+ mock_log.warning.assert_has_calls(log_warning_calls)
694+ assert mock_reset.called is True
695+ assert mock_reset.called is True
696+
697+ @patch.object(openlp.core.projectors.pjlink, 'log')
698+ def test_local_send_command_no_data(self, mock_log):
699+ """
700+ Test _send_command with no data to send
701+ """
702+ # GIVEN: Test object
703+ log_debug_calls = [call('PJlink(projector="< Projector(id="None", ip="111.111.111.111", port="1111", '
704+ 'mac_adx="11:11:11:11:11:11", pin="1111", name="___TEST_ONE___", '
705+ 'location="location one", notes="notes one", pjlink_name="None", '
706+ 'pjlink_class="None", manufacturer="None", model="None", '
707+ 'serial_no="Serial Number 1", other="None", sources="None", source_list="[]", '
708+ 'model_filter="Filter type 1", model_lamp="Lamp type 1", '
709+ 'sw_version="Version 1") >", args="()" kwargs="{\'no_poll\': True}")'),
710+ call('PJlinkCommands(args=() kwargs={})'),
711+ call('(___TEST_ONE___) reset_information() connect status is S_NOT_CONNECTED'),
712+ call('(___TEST_ONE___) _send_command(): Nothing to send - returning')]
713+
714+ pjlink = PJLink(Projector(**TEST1_DATA), no_poll=True)
715+ pjlink.send_queue = []
716+ pjlink.priority_queue = []
717+
718+ # WHEN: _send_command called with no data and queue's emtpy
719+ # Patch here since pjlink does not have socket_timer until after instantiation
720+ with patch.object(pjlink, 'socket_timer') as mock_timer:
721+ pjlink._send_command(data=None, utf8=False)
722+
723+ # THEN:
724+ mock_log.debug.assert_has_calls(log_debug_calls)
725+ assert mock_timer.called is False
726
727=== modified file 'tests/openlp_core/projectors/test_projector_pjlink_cmd_routing.py'
728--- tests/openlp_core/projectors/test_projector_pjlink_cmd_routing.py 2018-04-20 06:04:43 +0000
729+++ tests/openlp_core/projectors/test_projector_pjlink_cmd_routing.py 2018-05-03 14:59:14 +0000
730@@ -49,13 +49,23 @@
731 pjlink.pjlink_functions = MagicMock()
732 log_warning_text = [call('({ip}) get_data(): Invalid packet - '
733 'unknown command "UNKN"'.format(ip=pjlink.name))]
734- log_debug_text = [call('(___TEST_ONE___) get_data(ip="111.111.111.111" buffer="%1UNKN=Huh?"'),
735+ log_debug_text = [call('PJlink(projector="< Projector(id="None", ip="111.111.111.111", port="1111", '
736+ 'mac_adx="11:11:11:11:11:11", pin="1111", name="___TEST_ONE___", '
737+ 'location="location one", notes="notes one", pjlink_name="None", '
738+ 'pjlink_class="None", manufacturer="None", model="None", serial_no="Serial Number 1", '
739+ 'other="None", sources="None", source_list="[]", model_filter="Filter type 1", '
740+ 'model_lamp="Lamp type 1", sw_version="Version 1") >", '
741+ 'args="()" kwargs="{\'no_poll\': True}")'),
742+ call('PJlinkCommands(args=() kwargs={})'),
743+ call('(___TEST_ONE___) reset_information() connect status is S_NOT_CONNECTED'),
744+ call('(___TEST_ONE___) get_data(buffer="%1UNKN=Huh?"'),
745 call('(___TEST_ONE___) get_data(): Checking new data "%1UNKN=Huh?"'),
746 call('(___TEST_ONE___) get_data() header="%1UNKN" data="Huh?"'),
747 call('(___TEST_ONE___) get_data() version="1" cmd="UNKN"'),
748- call('(___TEST_ONE___) Cleaning buffer - msg = "get_data(): '
749- 'Invalid packet - unknown command "UNKN""'),
750- call('(___TEST_ONE___) Finished cleaning buffer - 0 bytes dropped')]
751+ call('(___TEST_ONE___) Cleaning buffer - msg = "get_data(): Invalid packet - '
752+ 'unknown command "UNKN""'),
753+ call('(___TEST_ONE___) Finished cleaning buffer - 0 bytes dropped'),
754+ call('(___TEST_ONE___) _send_command(): Nothing to send - returning')]
755
756 # WHEN: get_data called with an unknown command
757 pjlink.get_data(buff='{prefix}1UNKN=Huh?'.format(prefix=PJLINK_PREFIX))
758
759=== modified file 'tests/openlp_core/projectors/test_projector_pjlink_udp.py'
760--- tests/openlp_core/projectors/test_projector_pjlink_udp.py 2018-04-20 06:04:43 +0000
761+++ tests/openlp_core/projectors/test_projector_pjlink_udp.py 2018-05-03 14:59:14 +0000
762@@ -30,37 +30,23 @@
763 import openlp.core.projectors.pjlink
764 from openlp.core.projectors.constants import PJLINK_PORT
765
766-from openlp.core.projectors.db import Projector
767-from openlp.core.projectors.pjlink import PJLinkUDP, PJLink
768-from tests.resources.projector.data import TEST1_DATA, TEST2_DATA
769+from openlp.core.projectors.pjlink import PJLinkUDP
770+from tests.resources.projector.data import TEST1_DATA
771
772
773 class TestPJLinkBase(TestCase):
774 """
775 Tests for the PJLinkUDP class
776 """
777- def setUp(self):
778- """
779- Setup generic test conditions
780- """
781- self.test_list = [PJLink(projector=Projector(**TEST1_DATA)),
782- PJLink(projector=Projector(**TEST2_DATA))]
783-
784- def tearDown(self):
785- """
786- Close generic test condidtions
787- """
788- self.test_list = None
789-
790 @patch.object(openlp.core.projectors.pjlink, 'log')
791 def test_get_datagram_data_negative_zero_length(self, mock_log):
792 """
793 Test get_datagram when pendingDatagramSize = 0
794 """
795 # GIVEN: Test setup
796- pjlink_udp = PJLinkUDP(projector_list=self.test_list)
797- log_warn_calls = [call('(UDP) No data (-1)')]
798- log_debug_calls = [call('(UDP) PJLinkUDP() Initialized'),
799+ pjlink_udp = PJLinkUDP()
800+ log_warning_calls = [call('(UDP) No data (-1)')]
801+ log_debug_calls = [call('(UDP) PJLinkUDP() Initialized for port 4352'),
802 call('(UDP) get_datagram() - Receiving data')]
803 with patch.object(pjlink_udp, 'pendingDatagramSize') as mock_datagram, \
804 patch.object(pjlink_udp, 'readDatagram') as mock_read:
805@@ -71,7 +57,7 @@
806 pjlink_udp.get_datagram()
807
808 # THEN: Log entries should be made and method returns
809- mock_log.warning.assert_has_calls(log_warn_calls)
810+ mock_log.warning.assert_has_calls(log_warning_calls)
811 mock_log.debug.assert_has_calls(log_debug_calls)
812
813 @patch.object(openlp.core.projectors.pjlink, 'log')
814@@ -80,9 +66,10 @@
815 Test get_datagram when data length = 0
816 """
817 # GIVEN: Test setup
818- pjlink_udp = PJLinkUDP(projector_list=self.test_list)
819- log_warn_calls = [call('(UDP) get_datagram() called when pending data size is 0')]
820- log_debug_calls = [call('(UDP) get_datagram() - Receiving data')]
821+ pjlink_udp = PJLinkUDP()
822+ log_warning_calls = [call('(UDP) get_datagram() called when pending data size is 0')]
823+ log_debug_calls = [call('(UDP) PJLinkUDP() Initialized for port 4352'),
824+ call('(UDP) get_datagram() - Receiving data')]
825 with patch.object(pjlink_udp, 'pendingDatagramSize') as mock_datagram, \
826 patch.object(pjlink_udp, 'readDatagram') as mock_read:
827 mock_datagram.return_value = 0
828@@ -92,7 +79,7 @@
829 pjlink_udp.get_datagram()
830
831 # THEN: Log entries should be made and method returns
832- mock_log.warning.assert_has_calls(log_warn_calls)
833+ mock_log.warning.assert_has_calls(log_warning_calls)
834 mock_log.debug.assert_has_calls(log_debug_calls)
835
836 @patch.object(openlp.core.projectors.pjlink, 'log')
837@@ -101,9 +88,9 @@
838 Test get_datagram when pendingDatagramSize = 0
839 """
840 # GIVEN: Test setup
841- pjlink_udp = PJLinkUDP(projector_list=self.test_list)
842- log_warn_calls = [call('(UDP) get_datagram() called when pending data size is 0')]
843- log_debug_calls = [call('(UDP) PJLinkUDP() Initialized'),
844+ pjlink_udp = PJLinkUDP()
845+ log_warning_calls = [call('(UDP) get_datagram() called when pending data size is 0')]
846+ log_debug_calls = [call('(UDP) PJLinkUDP() Initialized for port 4352'),
847 call('(UDP) get_datagram() - Receiving data')]
848 with patch.object(pjlink_udp, 'pendingDatagramSize') as mock_datagram:
849 mock_datagram.return_value = 0
850@@ -112,5 +99,5 @@
851 pjlink_udp.get_datagram()
852
853 # THEN: Log entries should be made and method returns
854- mock_log.warning.assert_has_calls(log_warn_calls)
855+ mock_log.warning.assert_has_calls(log_warning_calls)
856 mock_log.debug.assert_has_calls(log_debug_calls)