openlp/openlp/core/projectors/pjlink.py

1435 lines
64 KiB
Python
Raw Normal View History

2014-10-06 19:10:03 +00:00
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
2019-04-13 13:00:22 +00:00
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2019 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, either version 3 of the License, or #
# (at your option) any later version. #
# #
# 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, see <https://www.gnu.org/licenses/>. #
##########################################################################
2014-10-06 19:10:03 +00:00
"""
2017-10-07 07:05:07 +00:00
The :mod:`openlp.core.lib.projector.pjlink` module provides the necessary functions for connecting to a PJLink-capable
projector.
2014-10-06 19:10:03 +00:00
2017-10-07 07:05:07 +00:00
PJLink Class 1 Specifications
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2014-10-06 19:10:03 +00:00
2017-10-07 07:05:07 +00:00
Website: http://pjlink.jbmia.or.jp/english/dl_class1.html
2014-10-06 19:10:03 +00:00
2017-10-07 07:05:07 +00:00
- Section 5-1 PJLink Specifications
- Section 5-5 Guidelines for Input Terminals
PJLink Class 2 Specifications
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Website: http://pjlink.jbmia.or.jp/english/dl_class2.html
- Section 5-1 PJLink Specifications
- Section 5-5 Guidelines for Input Terminals
.. note:
Function names follow the following syntax::
2014-10-06 19:10:03 +00:00
2017-10-07 07:05:07 +00:00
def process_CCCC(...):
where ``CCCC`` is the PJLink command being processed
"""
import logging
2017-06-25 02:21:07 +00:00
import re
2014-10-17 17:28:12 +00:00
from codecs import decode
2014-10-06 19:10:03 +00:00
from PyQt5 import QtCore, QtNetwork
2014-10-06 19:10:03 +00:00
2017-10-07 07:05:07 +00:00
from openlp.core.common import qmd5_hash
from openlp.core.common.i18n import translate
2018-05-03 14:58:50 +00:00
from openlp.core.common.settings import Settings
2018-10-02 04:39:42 +00:00
from openlp.core.projectors.constants import CONNECTION_ERRORS, E_AUTHENTICATION, E_CONNECTION_REFUSED, E_GENERAL, \
E_NETWORK, E_NOT_CONNECTED, E_SOCKET_TIMEOUT, PJLINK_CLASS, PJLINK_DEFAULT_CODES, PJLINK_ERRORS, PJLINK_ERST_DATA, \
PJLINK_ERST_STATUS, PJLINK_MAX_PACKET, PJLINK_PORT, PJLINK_POWR_STATUS, PJLINK_PREFIX, PJLINK_SUFFIX, \
PJLINK_VALID_CMD, PROJECTOR_STATE, QSOCKET_STATE, S_CONNECTED, S_CONNECTING, S_NOT_CONNECTED, S_OFF, S_OK, S_ON, \
S_STANDBY, STATUS_CODE, STATUS_MSG
2014-10-06 19:10:03 +00:00
2017-10-07 07:05:07 +00:00
log = logging.getLogger(__name__)
log.debug('pjlink loaded')
2018-02-11 11:42:13 +00:00
__all__ = ['PJLink', 'PJLinkUDP']
2017-10-07 07:05:07 +00:00
2014-10-06 19:10:03 +00:00
# Shortcuts
SocketError = QtNetwork.QAbstractSocket.SocketError
SocketSTate = QtNetwork.QAbstractSocket.SocketState
2014-10-06 19:10:03 +00:00
2017-05-13 09:00:29 +00:00
# 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)
2014-10-06 19:10:03 +00:00
2017-09-22 12:03:28 +00:00
class PJLinkUDP(QtNetwork.QUdpSocket):
"""
Socket service for PJLink UDP socket.
"""
2018-05-03 14:58:50 +00:00
data_received = QtCore.pyqtSignal(QtNetwork.QHostAddress, int, str, name='udp_data') # host, port, data
def __init__(self, port=PJLINK_PORT):
2017-09-22 12:03:28 +00:00
"""
2018-02-11 11:42:13 +00:00
Socket services for PJLink UDP packets.
Since all UDP packets from any projector will come into the same
port, process UDP packets here then route to the appropriate
projector instance as needed.
2018-05-03 14:58:50 +00:00
:param port: UDP port to listen on
2017-09-22 12:03:28 +00:00
"""
2018-02-11 11:42:13 +00:00
super().__init__()
2017-09-22 12:03:28 +00:00
self.port = port
2018-02-11 11:42:13 +00:00
# Local defines
self.search_active = False
self.search_time = 30000 # 30 seconds for allowed time
self.search_timer = QtCore.QTimer()
2018-10-20 04:33:32 +00:00
self.udp_broadcast_listen_setting = False
log.debug('(UDP:{port}) PJLinkUDP() Initialized'.format(port=self.port))
if Settings().value('projector/udp broadcast listen'):
self.udp_start()
def udp_start(self):
"""
Start listening on UDP port
"""
log.debug('(UDP:{port}) Start called'.format(port=self.port))
2018-02-11 11:42:13 +00:00
self.readyRead.connect(self.get_datagram)
2018-10-20 04:33:32 +00:00
self.check_settings(checked=Settings().value('projector/udp broadcast listen'))
def udp_stop(self):
"""
Stop listening on UDP port
"""
log.debug('(UDP:{port}) Stopping listener'.format(port=self.port))
self.close()
self.readyRead.disconnect(self.get_datagram)
2018-02-11 11:42:13 +00:00
@QtCore.pyqtSlot()
def get_datagram(self):
"""
Retrieve packet and basic checks
"""
2018-10-20 04:33:32 +00:00
log.debug('(UDP:{port}) get_datagram() - Receiving data'.format(port=self.port))
2018-04-20 06:04:43 +00:00
read_size = self.pendingDatagramSize()
2018-05-03 14:58:50 +00:00
if -1 == read_size:
2018-10-20 04:33:32 +00:00
log.warning('(UDP:{port}) No data (-1)'.format(port=self.port))
2018-02-11 11:42:13 +00:00
return
2018-05-03 14:58:50 +00:00
elif 0 == read_size:
2018-10-20 04:33:32 +00:00
log.warning('(UDP:{port}) get_datagram() called when pending data size is 0'.format(port=self.port))
2018-02-11 11:42:13 +00:00
return
2018-05-03 14:58:50 +00:00
elif read_size > PJLINK_MAX_PACKET:
2018-10-20 04:33:32 +00:00
log.warning('(UDP:{port}) UDP Packet too large ({size} bytes)- ignoring'.format(size=read_size,
port=self.port))
2018-05-03 14:58:50 +00:00
return
data_in, peer_host, peer_port = self.readDatagram(read_size)
data = data_in.decode('utf-8') if isinstance(data_in, bytes) else data_in
2018-10-20 04:33:32 +00:00
log.debug('(UDP:{port}) {size} bytes received from {adx}'.format(size=len(data),
adx=peer_host.toString(),
port=self.port))
log.debug('(UDP:{port}) packet "{data}"'.format(data=data, port=self.port))
log.debug('(UDP:{port}) Sending data_received signal to projectors'.format(port=self.port))
2018-05-03 14:58:50 +00:00
self.data_received.emit(peer_host, self.localPort(), data)
2018-02-11 11:42:13 +00:00
return
def search_start(self):
"""
Start search for projectors on local network
"""
self.search_active = True
# TODO: Send SRCH packet here
self.search_timer.singleShot(self.search_time, self.search_stop)
@QtCore.pyqtSlot()
def search_stop(self):
"""
Stop search
"""
self.search_active = False
self.search_timer.stop()
2017-09-22 12:03:28 +00:00
2018-10-20 04:33:32 +00:00
def check_settings(self, checked):
"""
Update UDP listening state based on settings change.
NOTE: This method is called by projector settings tab and setup/removed by ProjectorManager
"""
if self.udp_broadcast_listen_setting == checked:
log.debug('(UDP:{port}) No change to status - skipping'.format(port=self.port))
return
self.udp_broadcast_listen_setting = checked
if self.udp_broadcast_listen_setting:
if self.state() == self.ListeningState:
log.debug('(UDP:{port}) Already listening - skipping')
return
self.bind(self.port)
log.debug('(UDP:{port}) Listening'.format(port=self.port))
else:
# Close socket
self.udp_stop()
2017-09-22 12:03:28 +00:00
2017-08-06 07:23:26 +00:00
class PJLinkCommands(object):
2014-10-06 19:10:03 +00:00
"""
2017-08-06 07:23:26 +00:00
Process replies from PJLink projector.
2014-10-06 19:10:03 +00:00
"""
2018-04-20 06:04:43 +00:00
# List of IP addresses and mac addresses found via UDP search command
ackn_list = []
2014-10-06 19:10:03 +00:00
2017-08-06 07:23:26 +00:00
def __init__(self, *args, **kwargs):
2014-10-06 19:10:03 +00:00
"""
2017-08-06 07:23:26 +00:00
Setup for the process commands
2014-10-06 19:10:03 +00:00
"""
2017-08-06 07:23:26 +00:00
log.debug('PJlinkCommands(args={args} kwargs={kwargs})'.format(args=args, kwargs=kwargs))
2017-05-30 23:26:37 +00:00
super().__init__()
2018-04-20 06:04:43 +00:00
# Map PJLink command to method and include pjlink class version for this instance
# Default initial pjlink class version is '1'
2017-05-20 05:51:58 +00:00
self.pjlink_functions = {
2018-04-20 06:04:43 +00:00
'ACKN': {"method": self.process_ackn, # Class 2 (command is SRCH)
"version": "2"},
'AVMT': {"method": self.process_avmt,
"version": "1"},
'CLSS': {"method": self.process_clss,
"version": "1"},
'ERST': {"method": self.process_erst,
"version": "1"},
'INFO': {"method": self.process_info,
"version": "1"},
'INF1': {"method": self.process_inf1,
"version": "1"},
'INF2': {"method": self.process_inf2,
"version": "1"},
'INPT': {"method": self.process_inpt,
"version": "1"},
'INST': {"method": self.process_inst,
"version": "1"},
'LAMP': {"method": self.process_lamp,
"version": "1"},
'LKUP': {"method": self.process_lkup, # Class 2 (reply only - no cmd)
"version": "2"},
'NAME': {"method": self.process_name,
"version": "1"},
'PJLINK': {"method": self.process_pjlink,
"version": "1"},
'POWR': {"method": self.process_powr,
"version": "1"},
'SNUM': {"method": self.process_snum,
"version": "1"},
'SRCH': {"method": self.process_srch, # Class 2 (reply is ACKN)
"version": "2"},
'SVER': {"method": self.process_sver,
"version": "1"},
'RFIL': {"method": self.process_rfil,
"version": "1"},
'RLMP': {"method": self.process_rlmp,
"version": "1"}
}
2014-10-06 19:10:03 +00:00
def reset_information(self):
2014-10-17 17:28:12 +00:00
"""
2017-08-06 07:23:26 +00:00
Initialize instance variables. Also used to reset projector-specific information to default.
2014-10-17 17:28:12 +00:00
"""
2017-12-25 08:44:30 +00:00
conn_state = STATUS_CODE[QSOCKET_STATE[self.state()]]
2018-01-13 05:41:42 +00:00
log.debug('({ip}) reset_information() connect status is {state}'.format(ip=self.entry.name,
2017-12-25 08:44:30 +00:00
state=conn_state))
2017-08-06 07:23:26 +00:00
self.fan = None # ERST
self.filter_time = None # FILT
self.lamp = None # LAMP
self.mac_adx_received = None # ACKN
self.manufacturer = None # INF1
self.model = None # INF2
self.model_filter = None # RFIL
self.model_lamp = None # RLMP
self.mute = None # AVMT
self.other_info = None # INFO
self.pjlink_name = None # NAME
self.power = S_OFF # POWR
self.serial_no = None # SNUM
2017-06-25 02:21:07 +00:00
self.serial_no_received = None
2017-08-06 07:23:26 +00:00
self.sw_version = None # SVER
2017-06-25 02:21:07 +00:00
self.sw_version_received = None
2017-08-06 07:23:26 +00:00
self.shutter = None # AVMT
self.source_available = None # INST
self.source = None # INPT
# These should be part of PJLink() class, but set here for convenience
2018-01-13 05:41:42 +00:00
if hasattr(self, 'poll_timer'):
log.debug('({ip}): Calling poll_timer.stop()'.format(ip=self.entry.name))
self.poll_timer.stop()
2014-10-17 02:28:51 +00:00
if hasattr(self, 'socket_timer'):
2018-01-13 05:41:42 +00:00
log.debug('({ip}): Calling socket_timer.stop()'.format(ip=self.entry.name))
2014-10-17 02:28:51 +00:00
self.socket_timer.stop()
2018-06-28 15:37:37 +00:00
if hasattr(self, 'status_timer'):
log.debug('({ip}): Calling status_timer.stop()'.format(ip=self.entry.name))
self.status_timer.stop()
self.status_timer_checks = {}
2014-10-17 02:28:51 +00:00
self.send_busy = False
2017-08-06 07:23:26 +00:00
self.send_queue = []
2017-12-04 00:24:47 +00:00
self.priority_queue = []
2018-04-20 06:04:43 +00:00
# Reset default version in command routing dict
for cmd in self.pjlink_functions:
self.pjlink_functions[cmd]["version"] = PJLINK_VALID_CMD[cmd]['default']
2018-05-03 14:58:50 +00:00
def process_command(self, cmd, data):
2014-10-06 19:10:03 +00:00
"""
2017-08-06 07:23:26 +00:00
Verifies any return error code. Calls the appropriate command handler.
2014-10-06 19:10:03 +00:00
2017-08-06 07:23:26 +00:00
:param cmd: Command to process
:param data: Data being processed
2014-10-06 19:10:03 +00:00
"""
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Processing command "{cmd}" with data "{data}"'.format(ip=self.entry.name,
2017-08-06 07:23:26 +00:00
cmd=cmd,
data=data))
2017-12-04 00:24:47 +00:00
# cmd should already be in uppercase, but data may be in mixed-case.
# Due to some replies should stay as mixed-case, validate using separate uppercase check
2017-08-06 23:33:53 +00:00
_data = data.upper()
2017-12-04 00:24:47 +00:00
# Check if we have a future command not available yet
2017-12-25 08:44:30 +00:00
if cmd not in self.pjlink_functions:
2018-01-13 05:41:42 +00:00
log.warning('({ip}) Unable to process command="{cmd}" (Future option?)'.format(ip=self.entry.name, cmd=cmd))
2017-08-06 07:23:26 +00:00
return
elif _data == 'OK':
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Command "{cmd}" returned OK'.format(ip=self.entry.name, cmd=cmd))
2017-12-04 00:24:47 +00:00
# A command returned successfully, so do a query on command to verify status
return self.send_command(cmd=cmd)
2017-08-06 23:33:53 +00:00
elif _data in PJLINK_ERRORS:
2017-08-06 07:23:26 +00:00
# Oops - projector error
2018-01-13 05:41:42 +00:00
log.error('({ip}) {cmd}: {err}'.format(ip=self.entry.name,
2017-12-25 08:44:30 +00:00
cmd=cmd,
err=STATUS_MSG[PJLINK_ERRORS[_data]]))
if PJLINK_ERRORS[_data] == E_AUTHENTICATION:
2017-08-06 07:23:26 +00:00
self.disconnect_from_host()
self.projectorAuthentication.emit(self.name)
2017-12-25 08:44:30 +00:00
return self.change_status(status=E_AUTHENTICATION)
2017-08-06 07:23:26 +00:00
# Command checks already passed
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Calling function for {cmd}'.format(ip=self.entry.name, cmd=cmd))
2018-05-03 14:58:50 +00:00
self.pjlink_functions[cmd]["method"](data=data)
2018-04-20 06:04:43 +00:00
2018-05-03 14:58:50 +00:00
def process_ackn(self, data):
2018-04-20 06:04:43 +00:00
"""
Process the ACKN command.
2014-10-06 19:10:03 +00:00
2018-04-20 06:04:43 +00:00
:param data: Data in packet
2018-05-03 14:58:50 +00:00
"""
# TODO: Have to rethink this one
pass
2018-04-20 06:04:43 +00:00
2018-05-03 14:58:50 +00:00
def process_avmt(self, data):
2014-10-17 02:28:51 +00:00
"""
2017-08-06 07:23:26 +00:00
Process shutter and speaker status. See PJLink specification for format.
Update self.mute (audio) and self.shutter (video shutter).
2018-06-28 15:37:37 +00:00
10 = Shutter open, audio unchanged
11 = Shutter closed, audio unchanged
2018-06-28 15:37:37 +00:00
20 = Shutter unchanged, Audio normal
21 = Shutter unchanged, Audio muted
2018-06-28 15:37:37 +00:00
30 = Shutter open, audio muted
31 = Shutter closed, audio normal
2014-10-17 02:28:51 +00:00
2017-08-06 07:23:26 +00:00
:param data: Shutter and audio status
2014-10-06 19:10:03 +00:00
"""
2018-06-28 15:37:37 +00:00
settings = {'10': {'shutter': False, 'mute': self.mute},
'11': {'shutter': True, 'mute': self.mute},
'20': {'shutter': self.shutter, 'mute': False},
2017-08-06 23:33:53 +00:00
'21': {'shutter': self.shutter, 'mute': True},
'30': {'shutter': False, 'mute': False},
'31': {'shutter': True, 'mute': True}
}
if data not in settings:
2018-01-13 05:41:42 +00:00
log.warning('({ip}) Invalid shutter response: {data}'.format(ip=self.entry.name, data=data))
return
shutter = settings[data]['shutter']
mute = settings[data]['mute']
# Check if we need to update the icons
update_icons = (shutter != self.shutter) or (mute != self.mute)
self.shutter = shutter
self.mute = mute
2017-08-06 07:23:26 +00:00
if update_icons:
2018-06-28 15:37:37 +00:00
if 'AVMT' in self.status_timer_checks:
self.status_timer_delete('AVMT')
2017-08-06 07:23:26 +00:00
self.projectorUpdateIcons.emit()
return
2017-06-25 02:21:07 +00:00
2018-05-03 14:58:50 +00:00
def process_clss(self, data):
2017-06-25 02:21:07 +00:00
"""
2017-08-06 07:23:26 +00:00
PJLink class that this projector supports. See PJLink specification for format.
Updates self.class.
:param data: Class that projector supports.
2017-06-25 02:21:07 +00:00
"""
2017-08-06 07:23:26 +00:00
# bug 1550891: Projector returns non-standard class response:
# : Expected: '%1CLSS=1'
# : Received: '%1CLSS=Class 1' (Optoma)
# : Received: '%1CLSS=Version1' (BenQ)
if len(data) > 1:
2018-01-13 05:41:42 +00:00
log.warning('({ip}) Non-standard CLSS reply: "{data}"'.format(ip=self.entry.name, data=data))
2017-08-06 07:23:26 +00:00
# Due to stupid projectors not following standards (Optoma, BenQ comes to mind),
# AND the different responses that can be received, the semi-permanent way to
# fix the class reply is to just remove all non-digit characters.
2018-07-02 20:38:47 +00:00
chk = re.findall(r'\d', data)
2018-04-20 06:04:43 +00:00
if len(chk) < 1:
2017-12-09 11:17:05 +00:00
log.error('({ip}) No numbers found in class version reply "{data}" - '
2018-01-13 05:41:42 +00:00
'defaulting to class "1"'.format(ip=self.entry.name, data=data))
2017-08-06 07:23:26 +00:00
clss = '1'
2018-04-20 06:04:43 +00:00
else:
clss = chk[0] # Should only be the first match
2017-08-06 07:23:26 +00:00
elif not data.isdigit():
2017-12-09 11:17:05 +00:00
log.error('({ip}) NAN CLSS version reply "{data}" - '
2018-01-13 05:41:42 +00:00
'defaulting to class "1"'.format(ip=self.entry.name, data=data))
2017-08-06 07:23:26 +00:00
clss = '1'
2017-06-25 02:21:07 +00:00
else:
2017-08-06 07:23:26 +00:00
clss = data
self.pjlink_class = clss
2018-02-11 11:42:13 +00:00
log.debug('({ip}) Setting pjlink_class for this projector '
'to "{data}"'.format(ip=self.entry.name,
data=self.pjlink_class))
2018-04-20 06:04:43 +00:00
# Update method class versions
for cmd in self.pjlink_functions:
if self.pjlink_class in PJLINK_VALID_CMD[cmd]['version']:
self.pjlink_functions[cmd]['version'] = self.pjlink_class
2018-01-13 05:41:42 +00:00
# Since we call this one on first connect, setup polling from here
if not self.no_poll:
log.debug('({ip}) process_pjlink(): Starting timer'.format(ip=self.entry.name))
self.poll_timer.setInterval(1000) # Set 1 second for initial information
self.poll_timer.start()
2017-08-06 07:23:26 +00:00
return
2017-06-25 02:21:07 +00:00
2018-05-03 14:58:50 +00:00
def process_erst(self, data):
2017-06-25 02:21:07 +00:00
"""
2017-08-06 07:23:26 +00:00
Error status. See PJLink Specifications for format.
Updates self.projector_errors
:param data: Error status
2017-06-25 02:21:07 +00:00
"""
if len(data) != PJLINK_ERST_DATA['DATA_LENGTH']:
count = PJLINK_ERST_DATA['DATA_LENGTH']
2018-02-11 11:42:13 +00:00
log.warning('({ip}) Invalid error status response "{data}": '
'length != {count}'.format(ip=self.entry.name,
data=data,
count=count))
return
2017-08-06 07:23:26 +00:00
try:
datacheck = int(data)
except ValueError:
# Bad data - ignore
2018-01-13 05:41:42 +00:00
log.warning('({ip}) Invalid error status response "{data}"'.format(ip=self.entry.name, data=data))
2017-08-06 07:23:26 +00:00
return
if datacheck == 0:
self.projector_errors = None
# No errors
return
# We have some sort of status error, so check out what it/they are
self.projector_errors = {}
fan, lamp, temp, cover, filt, other = (data[PJLINK_ERST_DATA['FAN']],
data[PJLINK_ERST_DATA['LAMP']],
data[PJLINK_ERST_DATA['TEMP']],
data[PJLINK_ERST_DATA['COVER']],
data[PJLINK_ERST_DATA['FILTER']],
data[PJLINK_ERST_DATA['OTHER']])
2017-12-25 08:44:30 +00:00
if fan != PJLINK_ERST_STATUS[S_OK]:
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Fan')] = \
PJLINK_ERST_STATUS[fan]
2017-12-25 08:44:30 +00:00
if lamp != PJLINK_ERST_STATUS[S_OK]:
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Lamp')] = \
PJLINK_ERST_STATUS[lamp]
2017-12-25 08:44:30 +00:00
if temp != PJLINK_ERST_STATUS[S_OK]:
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Temperature')] = \
PJLINK_ERST_STATUS[temp]
2017-12-25 08:44:30 +00:00
if cover != PJLINK_ERST_STATUS[S_OK]:
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Cover')] = \
PJLINK_ERST_STATUS[cover]
2017-12-25 08:44:30 +00:00
if filt != PJLINK_ERST_STATUS[S_OK]:
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Filter')] = \
PJLINK_ERST_STATUS[filt]
2017-12-25 08:44:30 +00:00
if other != PJLINK_ERST_STATUS[S_OK]:
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Other')] = \
PJLINK_ERST_STATUS[other]
2017-08-06 07:23:26 +00:00
return
2014-10-06 19:10:03 +00:00
2018-05-03 14:58:50 +00:00
def process_inf1(self, data):
2014-10-06 19:10:03 +00:00
"""
2017-08-06 07:23:26 +00:00
Manufacturer name set in projector.
Updates self.manufacturer
2014-10-17 17:28:12 +00:00
2017-08-06 07:23:26 +00:00
:param data: Projector manufacturer
2014-10-06 19:10:03 +00:00
"""
2017-08-06 07:23:26 +00:00
self.manufacturer = data
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Setting projector manufacturer data to "{data}"'.format(ip=self.entry.name,
data=self.manufacturer))
2017-08-06 07:23:26 +00:00
return
2014-10-06 19:10:03 +00:00
2018-05-03 14:58:50 +00:00
def process_inf2(self, data):
2014-10-06 19:10:03 +00:00
"""
2017-08-06 07:23:26 +00:00
Projector Model set in projector.
Updates self.model.
2014-10-17 17:28:12 +00:00
2017-08-06 07:23:26 +00:00
:param data: Model name
2014-10-06 19:10:03 +00:00
"""
2017-08-06 07:23:26 +00:00
self.model = data
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Setting projector model to "{data}"'.format(ip=self.entry.name, data=self.model))
2017-08-06 07:23:26 +00:00
return
2014-10-06 19:10:03 +00:00
2018-05-03 14:58:50 +00:00
def process_info(self, data):
2014-10-06 19:10:03 +00:00
"""
2017-08-06 07:23:26 +00:00
Any extra info set in projector.
Updates self.other_info.
2016-06-17 23:54:04 +00:00
2017-08-06 07:23:26 +00:00
:param data: Projector other info
2014-10-06 19:10:03 +00:00
"""
2017-08-06 07:23:26 +00:00
self.other_info = data
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Setting projector other_info to "{data}"'.format(ip=self.entry.name, data=self.other_info))
2017-08-06 07:23:26 +00:00
return
2014-10-06 19:10:03 +00:00
2018-05-03 14:58:50 +00:00
def process_inpt(self, data):
2017-07-07 23:43:50 +00:00
"""
2017-08-06 07:23:26 +00:00
Current source input selected. See PJLink specification for format.
Update self.source
:param data: Currently selected source
2017-07-07 23:43:50 +00:00
"""
2017-12-25 08:44:30 +00:00
# First, see if we have a valid input based on what is installed (if available)
if self.source_available is not None:
# We have available inputs, so verify it's in the list
if data not in self.source_available:
2018-01-13 05:41:42 +00:00
log.warn('({ip}) Input source not listed in available sources - ignoring'.format(ip=self.entry.name))
2017-12-25 08:44:30 +00:00
return
elif data not in PJLINK_DEFAULT_CODES:
# Hmm - no sources available yet, so check with PJLink defaults
2018-01-13 05:41:42 +00:00
log.warn('({ip}) Input source not listed as a PJLink available source '
'- ignoring'.format(ip=self.entry.name))
2017-12-25 08:44:30 +00:00
return
2017-08-06 07:23:26 +00:00
self.source = data
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Setting data source to "{data}"'.format(ip=self.entry.name, data=self.source))
2017-07-07 23:43:50 +00:00
return
2018-05-03 14:58:50 +00:00
def process_inst(self, data):
2014-10-06 19:10:03 +00:00
"""
2017-08-06 07:23:26 +00:00
Available source inputs. See PJLink specification for format.
Updates self.source_available
2014-10-06 19:10:03 +00:00
2017-08-06 07:23:26 +00:00
:param data: Sources list
2014-10-06 19:10:03 +00:00
"""
2017-08-06 07:23:26 +00:00
sources = []
check = data.split()
for source in check:
sources.append(source)
sources.sort()
self.source_available = sources
2019-03-08 15:19:57 +00:00
log.debug('({ip}) Setting projector source_available to "{data}"'.format(ip=self.entry.name,
data=self.source_available))
self.projectorUpdateIcons.emit()
2017-08-06 07:23:26 +00:00
return
2014-10-06 19:10:03 +00:00
2018-05-03 14:58:50 +00:00
def process_lamp(self, data):
2014-10-06 19:10:03 +00:00
"""
Lamp(s) status. See PJLink Specifications for format.
2014-10-17 17:28:12 +00:00
Data may have more than 1 lamp to process.
Update self.lamp dictionary with lamp status.
:param data: Lamp(s) status.
2014-10-06 19:10:03 +00:00
"""
lamps = []
2017-11-24 19:08:23 +00:00
lamp_list = data.split()
if len(lamp_list) < 2:
lamps.append({'Hours': int(lamp_list[0]), 'On': None})
2017-11-24 08:30:37 +00:00
else:
2017-11-24 19:08:23 +00:00
while lamp_list:
2017-11-24 08:30:37 +00:00
try:
2017-11-24 19:08:23 +00:00
fill = {'Hours': int(lamp_list[0]), 'On': False if lamp_list[1] == '0' else True}
2017-11-24 08:30:37 +00:00
except ValueError:
# In case of invalid entry
2018-01-13 05:41:42 +00:00
log.warning('({ip}) process_lamp(): Invalid data "{data}"'.format(ip=self.entry.name, data=data))
2017-11-24 08:30:37 +00:00
return
lamps.append(fill)
2017-11-24 19:08:23 +00:00
lamp_list.pop(0) # Remove lamp hours
lamp_list.pop(0) # Remove lamp on/off
2014-10-06 19:10:03 +00:00
self.lamp = lamps
return
2018-05-03 14:58:50 +00:00
def process_lkup(self, data):
2018-04-20 06:04:43 +00:00
"""
Process reply indicating remote is available for connection
:param data: Data packet from remote
"""
2018-05-03 14:58:50 +00:00
log.debug('({ip}) Processing LKUP command'.format(ip=self.entry.name))
2018-05-19 00:48:33 +00:00
if Settings().value('projector/connect when LKUP received'):
2018-05-03 14:58:50 +00:00
self.connect_to_host()
2018-04-20 06:04:43 +00:00
2018-05-03 14:58:50 +00:00
def process_name(self, data):
2017-08-06 07:23:26 +00:00
"""
Projector name set in projector.
Updates self.pjlink_name
:param data: Projector name
"""
self.pjlink_name = data
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Setting projector PJLink name to "{data}"'.format(ip=self.entry.name, data=self.pjlink_name))
2017-08-06 07:23:26 +00:00
return
2018-05-03 14:58:50 +00:00
def process_pjlink(self, data):
2017-12-04 00:24:47 +00:00
"""
Process initial socket connection to terminal.
:param data: Initial packet with authentication scheme
"""
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Processing PJLINK command'.format(ip=self.entry.name))
2017-12-09 11:17:05 +00:00
chk = data.split(' ')
2017-12-04 00:24:47 +00:00
if len(chk[0]) != 1:
# Invalid - after splitting, first field should be 1 character, either '0' or '1' only
2018-01-13 05:41:42 +00:00
log.error('({ip}) Invalid initial authentication scheme - aborting'.format(ip=self.entry.name))
2017-12-04 00:24:47 +00:00
return self.disconnect_from_host()
elif chk[0] == '0':
# Normal connection no authentication
if len(chk) > 1:
# Invalid data - there should be nothing after a normal authentication scheme
2018-01-13 05:41:42 +00:00
log.error('({ip}) Normal connection with extra information - aborting'.format(ip=self.entry.name))
2017-12-04 00:24:47 +00:00
return self.disconnect_from_host()
elif self.pin:
2018-01-13 05:41:42 +00:00
log.error('({ip}) Normal connection but PIN set - aborting'.format(ip=self.entry.name))
2017-12-04 00:24:47 +00:00
return self.disconnect_from_host()
else:
data_hash = None
elif chk[0] == '1':
if len(chk) < 2:
# Not enough information for authenticated connection
2018-01-13 05:41:42 +00:00
log.error('({ip}) Authenticated connection but not enough info - aborting'.format(ip=self.entry.name))
2017-12-04 00:24:47 +00:00
return self.disconnect_from_host()
elif not self.pin:
2018-01-13 05:41:42 +00:00
log.error('({ip}) Authenticate connection but no PIN - aborting'.format(ip=self.entry.name))
2017-12-04 00:24:47 +00:00
return self.disconnect_from_host()
else:
data_hash = str(qmd5_hash(salt=chk[1].encode('utf-8'), data=self.pin.encode('utf-8')),
encoding='ascii')
# Passed basic checks, so start connection
self.readyRead.connect(self.get_socket)
self.change_status(S_CONNECTED)
2018-01-13 05:41:42 +00:00
log.debug('({ip}) process_pjlink(): Sending "CLSS" initial command'.format(ip=self.entry.name))
2017-12-04 00:24:47 +00:00
# Since this is an initial connection, make it a priority just in case
return self.send_command(cmd="CLSS", salt=data_hash, priority=True)
2018-05-03 14:58:50 +00:00
def process_powr(self, data):
2014-10-06 19:10:03 +00:00
"""
Power status. See PJLink specification for format.
2014-10-17 17:28:12 +00:00
Update self.power with status. Update icons if change from previous setting.
:param data: Power status
2014-10-06 19:10:03 +00:00
"""
2018-01-13 05:41:42 +00:00
log.debug('({ip}: Processing POWR command'.format(ip=self.entry.name))
2014-10-06 19:10:03 +00:00
if data in PJLINK_POWR_STATUS:
2014-10-15 17:22:12 +00:00
power = PJLINK_POWR_STATUS[data]
update_icons = self.power != power
self.power = power
2014-10-06 19:10:03 +00:00
self.change_status(PJLINK_POWR_STATUS[data])
2014-10-15 17:22:12 +00:00
if update_icons:
self.projectorUpdateIcons.emit()
# Update the input sources available
if power == S_ON:
self.send_command('INST')
2014-10-06 19:10:03 +00:00
else:
# Log unknown status response
2018-01-13 05:41:42 +00:00
log.warning('({ip}) Unknown power response: "{data}"'.format(ip=self.entry.name, data=data))
2018-06-28 15:37:37 +00:00
if self.power in [S_ON, S_STANDBY, S_OFF] and 'POWR' in self.status_timer_checks:
self.status_timer_delete(cmd='POWR')
2014-10-06 19:10:03 +00:00
return
2018-05-03 14:58:50 +00:00
def process_rfil(self, data):
2014-10-06 19:10:03 +00:00
"""
2017-08-06 07:23:26 +00:00
Process replacement filter type
2014-10-06 19:10:03 +00:00
"""
2017-08-06 07:23:26 +00:00
if self.model_filter is None:
self.model_filter = data
2014-10-06 19:10:03 +00:00
else:
2018-01-13 05:41:42 +00:00
log.warning('({ip}) Filter model already set'.format(ip=self.entry.name))
log.warning('({ip}) Saved model: "{old}"'.format(ip=self.entry.name, old=self.model_filter))
log.warning('({ip}) New model: "{new}"'.format(ip=self.entry.name, new=data))
2014-10-06 19:10:03 +00:00
2018-05-03 14:58:50 +00:00
def process_rlmp(self, data):
2014-10-06 19:10:03 +00:00
"""
2017-08-06 07:23:26 +00:00
Process replacement lamp type
2014-10-06 19:10:03 +00:00
"""
2017-08-06 07:23:26 +00:00
if self.model_lamp is None:
self.model_lamp = data
else:
2018-01-13 05:41:42 +00:00
log.warning('({ip}) Lamp model already set'.format(ip=self.entry.name))
log.warning('({ip}) Saved lamp: "{old}"'.format(ip=self.entry.name, old=self.model_lamp))
log.warning('({ip}) New lamp: "{new}"'.format(ip=self.entry.name, new=data))
2014-10-06 19:10:03 +00:00
2018-05-03 14:58:50 +00:00
def process_snum(self, data):
2014-10-06 19:10:03 +00:00
"""
2017-08-06 07:23:26 +00:00
Serial number of projector.
2014-10-17 17:28:12 +00:00
2017-08-06 07:23:26 +00:00
:param data: Serial number from projector.
2014-10-06 19:10:03 +00:00
"""
2017-08-06 07:23:26 +00:00
if self.serial_no is None:
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Setting projector serial number to "{data}"'.format(ip=self.entry.name, data=data))
2017-08-06 07:23:26 +00:00
self.serial_no = data
self.db_update = False
else:
2017-08-06 07:23:26 +00:00
# Compare serial numbers and see if we got the same projector
if self.serial_no != data:
2018-01-13 05:41:42 +00:00
log.warning('({ip}) Projector serial number does not match saved serial '
'number'.format(ip=self.entry.name))
log.warning('({ip}) Saved: "{old}"'.format(ip=self.entry.name, old=self.serial_no))
log.warning('({ip}) Received: "{new}"'.format(ip=self.entry.name, new=data))
log.warning('({ip}) NOT saving serial number'.format(ip=self.entry.name))
2017-08-06 07:23:26 +00:00
self.serial_no_received = data
2014-10-06 19:10:03 +00:00
2018-05-03 14:58:50 +00:00
def process_srch(self, data):
2018-04-20 06:04:43 +00:00
"""
Process the SRCH command.
SRCH is processed by terminals so we ignore any packet.
:param data: Data in packet
"""
2018-05-03 14:58:50 +00:00
log.warning("({ip}) SRCH packet detected - ignoring".format(ip=self.entry.ip))
2018-04-20 06:04:43 +00:00
return
2018-05-03 14:58:50 +00:00
def process_sver(self, data):
2014-10-06 19:10:03 +00:00
"""
2017-08-06 07:23:26 +00:00
Software version of projector
"""
if len(data) > 32:
# Defined in specs max version is 32 characters
2017-12-09 11:17:05 +00:00
log.warning('Invalid software version - too long')
return
elif self.sw_version is None:
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Setting projector software version to "{data}"'.format(ip=self.entry.name, data=data))
2017-08-06 07:23:26 +00:00
else:
2017-12-25 08:44:30 +00:00
if self.sw_version != data:
2017-12-09 11:17:05 +00:00
log.warning('({ip}) Projector software version does not match saved '
2018-01-13 05:41:42 +00:00
'software version'.format(ip=self.entry.name))
log.warning('({ip}) Saved: "{old}"'.format(ip=self.entry.name, old=self.sw_version))
log.warning('({ip}) Received: "{new}"'.format(ip=self.entry.name, new=data))
log.warning('({ip}) Updating software version'.format(ip=self.entry.name))
2017-12-25 08:44:30 +00:00
self.sw_version = data
self.db_update = True
2014-10-17 17:28:12 +00:00
2017-08-06 07:23:26 +00:00
2017-10-23 22:09:57 +00:00
class PJLink(QtNetwork.QTcpSocket, PJLinkCommands):
2017-08-06 07:23:26 +00:00
"""
2018-02-11 11:42:13 +00:00
Socket services for PJLink TCP packets.
2017-08-06 07:23:26 +00:00
"""
# Signals sent by this module
changeStatus = QtCore.pyqtSignal(str, int, str)
projectorStatus = QtCore.pyqtSignal(int) # Status update
projectorAuthentication = QtCore.pyqtSignal(str) # Authentication error
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
2017-09-22 12:03:28 +00:00
def __init__(self, projector, *args, **kwargs):
2014-10-06 19:10:03 +00:00
"""
2017-08-06 07:23:26 +00:00
Setup for instance.
Options should be in kwargs except for port which does have a default.
2014-10-06 19:10:03 +00:00
2017-09-22 12:03:28 +00:00
:param projector: Database record of projector
2017-08-06 07:23:26 +00:00
Optional parameters
:param poll_time: Time (in seconds) to poll connected projector
:param socket_timeout: Time (in seconds) to abort the connection if no response
2014-10-06 19:10:03 +00:00
"""
2017-12-09 11:17:05 +00:00
log.debug('PJlink(projector="{projector}", args="{args}" kwargs="{kwargs}")'.format(projector=projector,
args=args,
kwargs=kwargs))
2017-08-06 07:23:26 +00:00
super().__init__()
2018-05-03 14:58:50 +00:00
self.settings_section = 'projector'
2017-09-22 12:03:28 +00:00
self.entry = projector
self.ip = self.entry.ip
2018-05-03 14:58:50 +00:00
self.qhost = QtNetwork.QHostAddress(self.ip)
2017-09-22 12:03:28 +00:00
self.location = self.entry.location
self.mac_adx = self.entry.mac_adx
self.name = self.entry.name
self.notes = self.entry.notes
self.pin = self.entry.pin
2018-05-03 14:58:50 +00:00
self.port = int(self.entry.port)
2018-01-13 05:41:42 +00:00
self.pjlink_class = PJLINK_CLASS if self.entry.pjlink_class is None else self.entry.pjlink_class
2018-04-20 06:04:43 +00:00
self.ackn_list = {} # Replies from online projectors (Class 2 option)
2017-08-06 07:23:26 +00:00
self.db_update = False # Use to check if db needs to be updated prior to exiting
# Poll time 20 seconds unless called with something else
self.poll_time = 20000 if 'poll_time' not in kwargs else kwargs['poll_time'] * 1000
2018-01-13 05:41:42 +00:00
# Socket timeout (in case of brain-dead projectors) 5 seconds unless called with something else
2017-08-06 07:23:26 +00:00
self.socket_timeout = 5000 if 'socket_timeout' not in kwargs else kwargs['socket_timeout'] * 1000
# In case we're called from somewhere that only wants information
self.no_poll = 'no_poll' in kwargs
self.status_connect = S_NOT_CONNECTED
self.last_command = ''
self.projector_status = S_NOT_CONNECTED
self.error_status = S_OK
# Socket information
# Add enough space to input buffer for extraneous \n \r
self.max_size = PJLINK_MAX_PACKET + 2
self.setReadBufferSize(self.max_size)
self.reset_information()
self.send_queue = []
2017-12-04 00:24:47 +00:00
self.priority_queue = []
2017-08-06 07:23:26 +00:00
self.send_busy = False
2018-01-13 05:41:42 +00:00
# Poll timer for status updates
self.poll_timer = QtCore.QTimer(self) # Timer that calls the poll_loop
self.poll_timer.setInterval(self.poll_time)
self.poll_timer.timeout.connect(self.poll_loop)
# Socket timer for some possible brain-dead projectors or network issues
self.socket_timer = QtCore.QTimer(self)
self.socket_timer.setInterval(self.socket_timeout)
self.socket_timer.timeout.connect(self.socket_abort)
2018-06-28 15:37:37 +00:00
# Timer for doing status updates for commands that change state and should update faster
self.status_timer_checks = {} # Keep track of events for the status timer
self.status_timer = QtCore.QTimer(self)
self.status_timer.setInterval(2000) # 2 second interval should be fast enough
self.status_timer.timeout.connect(self.status_timer_update)
2018-01-13 05:41:42 +00:00
# Socket status signals
2017-08-06 07:23:26 +00:00
self.connected.connect(self.check_login)
self.disconnected.connect(self.disconnect_from_host)
self.error.connect(self.get_error)
2017-12-04 00:24:47 +00:00
self.projectorReceivedData.connect(self._send_command)
2017-08-06 07:23:26 +00:00
def socket_abort(self):
"""
Aborts connection and closes socket in case of brain-dead projectors.
Should normally be called by socket_timer().
"""
2018-01-13 05:41:42 +00:00
log.debug('({ip}) socket_abort() - Killing connection'.format(ip=self.entry.name))
2017-08-06 07:23:26 +00:00
self.disconnect_from_host(abort=True)
def poll_loop(self):
"""
Retrieve information from projector that changes.
Normally called by timer().
"""
2017-12-25 08:44:30 +00:00
if QSOCKET_STATE[self.state()] != S_CONNECTED:
2018-01-13 05:41:42 +00:00
log.warning('({ip}) poll_loop(): Not connected - returning'.format(ip=self.entry.name))
# Stop timer just in case it's missed elsewhere
self.poll_timer.stop()
2017-08-06 07:23:26 +00:00
return
2018-01-13 05:41:42 +00:00
log.debug('({ip}) poll_loop(): Updating projector status'.format(ip=self.entry.name))
2017-08-06 07:23:26 +00:00
# The following commands do not change, so only check them once
2017-12-25 08:44:30 +00:00
# Call them first in case other functions rely on something here
2017-08-06 07:23:26 +00:00
if self.power == S_ON and self.source_available is None:
2017-12-04 00:24:47 +00:00
self.send_command('INST')
2017-08-06 07:23:26 +00:00
if self.other_info is None:
2017-12-04 00:24:47 +00:00
self.send_command('INFO')
2017-08-06 07:23:26 +00:00
if self.manufacturer is None:
2017-12-04 00:24:47 +00:00
self.send_command('INF1')
2017-08-06 07:23:26 +00:00
if self.model is None:
2017-12-04 00:24:47 +00:00
self.send_command('INF2')
2017-08-06 07:23:26 +00:00
if self.pjlink_name is None:
2017-12-04 00:24:47 +00:00
self.send_command('NAME')
2017-08-06 07:23:26 +00:00
if self.pjlink_class == '2':
# Class 2 specific checks
if self.serial_no is None:
2017-12-04 00:24:47 +00:00
self.send_command('SNUM')
2017-08-06 07:23:26 +00:00
if self.sw_version is None:
2017-12-04 00:24:47 +00:00
self.send_command('SVER')
2017-08-06 07:23:26 +00:00
if self.model_filter is None:
2017-12-04 00:24:47 +00:00
self.send_command('RFIL')
2017-08-06 07:23:26 +00:00
if self.model_lamp is None:
2017-12-04 00:24:47 +00:00
self.send_command('RLMP')
2017-12-25 08:44:30 +00:00
# These commands may change during connection
check_list = ['POWR', 'ERST', 'LAMP', 'AVMT', 'INPT']
if self.pjlink_class == '2':
check_list.extend(['FILT', 'FREZ'])
for command in check_list:
self.send_command(command)
2018-01-13 05:41:42 +00:00
# Reset the poll_timer for normal operations in case of initial connection
self.poll_timer.setInterval(self.poll_time)
2017-08-06 07:23:26 +00:00
def _get_status(self, status):
"""
Helper to retrieve status/error codes and convert to strings.
:param status: Status/Error code
2017-12-25 08:44:30 +00:00
:returns: tuple (-1 if code not INT, None)
:returns: tuple (string: code as string, None if no description)
:returns: tuple (string: code as string, string: Status/Error description)
2017-08-06 07:23:26 +00:00
"""
if not isinstance(status, int):
2017-12-25 08:44:30 +00:00
return -1, None
elif status not in STATUS_MSG:
return None, None
2017-08-06 07:23:26 +00:00
else:
2017-12-25 08:44:30 +00:00
return STATUS_CODE[status], STATUS_MSG[status]
2017-08-06 07:23:26 +00:00
def change_status(self, status, msg=None):
"""
Check connection/error status, set status for projector, then emit status change signal
for gui to allow changing the icons.
:param status: Status code
:param msg: Optional message
"""
2017-12-25 08:44:30 +00:00
if status in STATUS_CODE:
log.debug('({ip}) Changing status to {status} '
2018-01-13 05:41:42 +00:00
'"{msg}"'.format(ip=self.entry.name,
2017-12-25 08:44:30 +00:00
status=STATUS_CODE[status],
msg=msg if msg is not None else STATUS_MSG[status]))
else:
2018-01-13 05:41:42 +00:00
log.warning('({ip}) Unknown status change code: {code}'.format(ip=self.entry.name,
2017-12-25 08:44:30 +00:00
code=status))
return
2017-08-06 07:23:26 +00:00
if status in CONNECTION_ERRORS:
2017-12-25 08:44:30 +00:00
# Connection state error affects both socket and projector
self.error_status = status
self.status_connect = E_NOT_CONNECTED
elif status >= S_NOT_CONNECTED and status in QSOCKET_STATE:
# Socket connection status update
2017-08-06 07:23:26 +00:00
self.status_connect = status
2019-03-08 15:19:57 +00:00
# Check if we need to update error state as well
if self.error_status != S_OK and status != S_NOT_CONNECTED:
self.error_status = S_OK
2017-12-25 08:44:30 +00:00
elif status >= S_NOT_CONNECTED and status in PROJECTOR_STATE:
# Only affects the projector status
2017-08-06 07:23:26 +00:00
self.projector_status = status
2017-12-25 08:44:30 +00:00
# These log entries are for troubleshooting only
2017-08-06 07:23:26 +00:00
(status_code, status_message) = self._get_status(self.status_connect)
2018-01-13 05:41:42 +00:00
log.debug('({ip}) status_connect: {code}: "{message}"'.format(ip=self.entry.name,
2017-08-06 07:23:26 +00:00
code=status_code,
message=status_message if msg is None else msg))
(status_code, status_message) = self._get_status(self.projector_status)
2018-01-13 05:41:42 +00:00
log.debug('({ip}) projector_status: {code}: "{message}"'.format(ip=self.entry.name,
2017-08-06 07:23:26 +00:00
code=status_code,
message=status_message if msg is None else msg))
(status_code, status_message) = self._get_status(self.error_status)
2018-01-13 05:41:42 +00:00
log.debug('({ip}) error_status: {code}: "{message}"'.format(ip=self.entry.name,
2017-08-06 07:23:26 +00:00
code=status_code,
message=status_message if msg is None else msg))
2017-12-25 08:44:30 +00:00
# Now that we logged extra information for debugging, broadcast the original change/message
2019-03-08 15:19:57 +00:00
# Check for connection errors first
if self.error_status != S_OK:
log.debug('({ip}) Signalling error code'.format(ip=self.entry.name))
2019-03-09 03:53:20 +00:00
code, message = self._get_status(self.error_status)
2019-03-08 15:19:57 +00:00
status = self.error_status
else:
log.debug('({ip}) Signalling status code'.format(ip=self.entry.name))
2019-03-09 03:53:20 +00:00
code, message = self._get_status(status)
2017-12-25 08:44:30 +00:00
if msg is not None:
message = msg
elif message is None:
# No message for status code
message = translate('OpenLP.PJLink', 'No message') if msg is None else msg
2017-08-06 07:23:26 +00:00
self.changeStatus.emit(self.ip, status, message)
2017-12-04 00:24:47 +00:00
self.projectorUpdateIcons.emit()
2017-08-06 07:23:26 +00:00
@QtCore.pyqtSlot()
def check_login(self, data=None):
"""
2017-12-04 00:24:47 +00:00
Processes the initial connection and convert to a PJLink packet if valid initial connection
2017-08-06 07:23:26 +00:00
:param data: Optional data if called from another routine
"""
2018-01-13 05:41:42 +00:00
log.debug('({ip}) check_login(data="{data}")'.format(ip=self.entry.name, data=data))
2017-08-06 07:23:26 +00:00
if data is None:
# Reconnected setup?
if not self.waitForReadyRead(2000):
# Possible timeout issue
2018-01-13 05:41:42 +00:00
log.error('({ip}) Socket timeout waiting for login'.format(ip=self.entry.name))
2017-08-06 07:23:26 +00:00
self.change_status(E_SOCKET_TIMEOUT)
return
read = self.readLine(self.max_size)
2017-12-04 00:24:47 +00:00
self.readLine(self.max_size) # Clean out any trailing whitespace
2017-08-06 07:23:26 +00:00
if read is None:
2018-01-13 05:41:42 +00:00
log.warning('({ip}) read is None - socket error?'.format(ip=self.entry.name))
2017-08-06 07:23:26 +00:00
return
elif len(read) < 8:
2018-01-13 05:41:42 +00:00
log.warning('({ip}) Not enough data read - skipping'.format(ip=self.entry.name))
2017-08-06 07:23:26 +00:00
return
data = decode(read, 'utf-8')
# Possibility of extraneous data on input when reading.
# Clean out extraneous characters in buffer.
2017-12-26 04:14:39 +00:00
self.read(1024)
2018-01-13 05:41:42 +00:00
log.debug('({ip}) check_login() read "{data}"'.format(ip=self.entry.name, data=data.strip()))
2017-08-06 07:23:26 +00:00
# At this point, we should only have the initial login prompt with
# possible authentication
# PJLink initial login will be:
# 'PJLink 0' - Unauthenticated login - no extra steps required.
# 'PJLink 1 XXXXXX' Authenticated login - extra processing required.
2017-12-04 00:24:47 +00:00
if not data.startswith('PJLINK'):
# Invalid initial packet - close socket
2018-01-13 05:41:42 +00:00
log.error('({ip}) Invalid initial packet received - closing socket'.format(ip=self.entry.name))
2017-08-06 07:23:26 +00:00
return self.disconnect_from_host()
2017-12-25 08:44:30 +00:00
# Convert the initial login prompt with the expected PJLink normal command format for processing
2019-03-08 15:19:57 +00:00
log.debug('({ip}) check_login(): Formatting initial connection prompt '
2018-01-13 05:41:42 +00:00
'to PJLink packet'.format(ip=self.entry.name))
2017-12-09 11:17:05 +00:00
return self.get_data('{start}{clss}{data}'.format(start=PJLINK_PREFIX,
clss='1',
data=data.replace(' ', '=', 1)).encode('utf-8'))
2014-10-06 19:10:03 +00:00
2017-08-06 07:23:26 +00:00
def _trash_buffer(self, msg=None):
2014-10-06 19:10:03 +00:00
"""
2017-08-06 07:23:26 +00:00
Clean out extraneous stuff in the buffer.
2014-10-06 19:10:03 +00:00
"""
2018-04-20 06:04:43 +00:00
log.debug('({ip}) Cleaning buffer - msg = "{message}"'.format(ip=self.entry.name, message=msg))
if msg is None:
msg = 'Invalid packet'
log.warning('({ip}) {message}'.format(ip=self.entry.name, message=msg))
2017-08-06 07:23:26 +00:00
self.send_busy = False
trash_count = 0
while self.bytesAvailable() > 0:
trash = self.read(self.max_size)
trash_count += len(trash)
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Finished cleaning buffer - {count} bytes dropped'.format(ip=self.entry.name,
2017-08-06 07:23:26 +00:00
count=trash_count))
2014-10-06 19:10:03 +00:00
return
2018-05-03 14:58:50 +00:00
@QtCore.pyqtSlot(QtNetwork.QHostAddress, int, str, name='udp_data') # host, port, data
def get_buffer(self, host, port, data):
2017-09-22 12:03:28 +00:00
"""
Get data from somewhere other than TCP socket
2018-05-03 14:58:50 +00:00
:param host: QHostAddress of sender
:param port: Destination port
2017-09-22 12:03:28 +00:00
:param data: Data to process. buffer must be formatted as a proper PJLink packet.
"""
2018-05-03 14:58:50 +00:00
if (port == int(self.port)) and (host.isEqual(self.qhost)):
log.debug('({ip}) Received data from {host}'.format(ip=self.entry.name, host=host.toString()))
log.debug('({ip}) get_buffer(data="{buff}")'.format(ip=self.entry.name, buff=data))
return self.get_data(buff=data)
else:
log.debug('({ip}) Ignoring data for {host} - not me'.format(ip=self.entry.name, host=host.toString()))
2017-09-22 12:03:28 +00:00
2017-08-06 07:23:26 +00:00
@QtCore.pyqtSlot()
2017-09-22 12:03:28 +00:00
def get_socket(self):
2014-10-06 19:10:03 +00:00
"""
2017-09-22 12:03:28 +00:00
Get data from TCP socket.
2014-10-06 19:10:03 +00:00
"""
2018-01-13 05:41:42 +00:00
log.debug('({ip}) get_socket(): Reading data'.format(ip=self.entry.name))
2017-12-25 08:44:30 +00:00
if QSOCKET_STATE[self.state()] != S_CONNECTED:
2018-01-13 05:41:42 +00:00
log.debug('({ip}) get_socket(): Not connected - returning'.format(ip=self.entry.name))
2017-08-06 07:23:26 +00:00
self.send_busy = False
return
# Although we have a packet length limit, go ahead and use a larger buffer
read = self.readLine(1024)
2018-01-13 05:41:42 +00:00
log.debug('({ip}) get_socket(): "{buff}"'.format(ip=self.entry.name, buff=read))
2017-08-06 07:23:26 +00:00
if read == -1:
# No data available
2018-01-13 05:41:42 +00:00
log.debug('({ip}) get_socket(): No data available (-1)'.format(ip=self.entry.name))
2017-08-06 07:23:26 +00:00
return self.receive_data_signal()
self.socket_timer.stop()
2018-05-03 14:58:50 +00:00
return self.get_data(buff=read)
2017-09-22 12:03:28 +00:00
2018-05-03 14:58:50 +00:00
def get_data(self, buff, *args, **kwargs):
2017-09-22 12:03:28 +00:00
"""
Process received data
:param buff: Data to process.
"""
2018-05-03 14:58:50 +00:00
log.debug('({ip}) get_data(buffer="{buff}"'.format(ip=self.entry.name, buff=buff))
ignore_class = 'ignore_class' in kwargs
2017-08-06 07:23:26 +00:00
# NOTE: Class2 has changed to some values being UTF-8
2018-04-20 06:04:43 +00:00
if isinstance(buff, bytes):
data_in = decode(buff, 'utf-8')
else:
data_in = buff
2017-08-06 07:23:26 +00:00
data = data_in.strip()
2017-12-04 00:24:47 +00:00
# Initial packet checks
if (len(data) < 7):
2017-12-25 08:44:30 +00:00
self._trash_buffer(msg='get_data(): Invalid packet - length')
return self.receive_data_signal()
elif len(data) > self.max_size:
2018-04-20 06:04:43 +00:00
self._trash_buffer(msg='get_data(): Invalid packet - too long ({length} bytes)'.format(length=len(data)))
2017-12-25 08:44:30 +00:00
return self.receive_data_signal()
2017-12-04 00:24:47 +00:00
elif not data.startswith(PJLINK_PREFIX):
2017-12-25 08:44:30 +00:00
self._trash_buffer(msg='get_data(): Invalid packet - PJLink prefix missing')
return self.receive_data_signal()
2018-05-03 14:58:50 +00:00
elif data[6] != '=' and data[8] != '=':
# data[6] = standard command packet
# data[8] = initial PJLink connection (after mangling)
2017-12-25 08:44:30 +00:00
self._trash_buffer(msg='get_data(): Invalid reply - Does not have "="')
return self.receive_data_signal()
2018-01-13 05:41:42 +00:00
log.debug('({ip}) get_data(): Checking new data "{data}"'.format(ip=self.entry.name, data=data))
2017-08-06 07:23:26 +00:00
header, data = data.split('=')
2018-04-20 06:04:43 +00:00
log.debug('({ip}) get_data() header="{header}" data="{data}"'.format(ip=self.entry.name,
header=header, data=data))
2017-12-04 00:24:47 +00:00
# At this point, the header should contain:
# "PVCCCC"
# Where:
# P = PJLINK_PREFIX
# V = PJLink class or version
# C = PJLink command
2018-04-20 06:04:43 +00:00
version, cmd = header[1], header[2:].upper()
log.debug('({ip}) get_data() version="{version}" cmd="{cmd}"'.format(ip=self.entry.name,
version=version, cmd=cmd))
# TODO: Below commented for now since it seems to cause issues with testing some invalid data.
# Revisit after more refactoring is finished.
'''
2017-08-06 07:23:26 +00:00
try:
2017-12-04 00:24:47 +00:00
version, cmd = header[1], header[2:].upper()
2018-04-20 06:04:43 +00:00
log.debug('({ip}) get_data() version="{version}" cmd="{cmd}"'.format(ip=self.entry.name,
version=version, cmd=cmd))
2017-08-06 07:23:26 +00:00
except ValueError as e:
self.change_status(E_INVALID_DATA)
2018-01-13 05:41:42 +00:00
log.warning('({ip}) get_data(): Received data: "{data}"'.format(ip=self.entry.name, data=data_in))
2017-12-25 08:44:30 +00:00
self._trash_buffer('get_data(): Expected header + command + data')
return self.receive_data_signal()
2018-04-20 06:04:43 +00:00
'''
2017-08-06 07:23:26 +00:00
if cmd not in PJLINK_VALID_CMD:
2018-05-03 14:58:50 +00:00
self._trash_buffer('get_data(): Invalid packet - unknown command "{data}"'.format(data=cmd))
2017-12-25 08:44:30 +00:00
return self.receive_data_signal()
2018-04-20 06:04:43 +00:00
elif version not in PJLINK_VALID_CMD[cmd]['version']:
self._trash_buffer(msg='get_data() Command reply version does not match a valid command version')
return self.receive_data_signal()
elif int(self.pjlink_class) < int(version):
2018-05-03 14:58:50 +00:00
if not ignore_class:
log.warning('({ip}) get_data(): Projector returned class reply higher '
'than projector stated class'.format(ip=self.entry.name))
self.process_command(cmd, data)
2017-12-25 08:44:30 +00:00
return self.receive_data_signal()
2014-10-06 19:10:03 +00:00
2017-08-06 07:23:26 +00:00
@QtCore.pyqtSlot(QtNetwork.QAbstractSocket.SocketError)
def get_error(self, err):
2014-10-06 19:10:03 +00:00
"""
2017-08-06 07:23:26 +00:00
Process error from SocketError signal.
Remaps system error codes to projector error codes.
2014-10-17 17:28:12 +00:00
2017-08-06 07:23:26 +00:00
:param err: Error code
2014-10-06 19:10:03 +00:00
"""
2018-01-13 05:41:42 +00:00
log.debug('({ip}) get_error(err={error}): {data}'.format(ip=self.entry.name,
error=err,
data=self.errorString()))
2017-08-06 07:23:26 +00:00
if err <= 18:
# QSocket errors. Redefined in projector.constants so we don't mistake
# them for system errors
check = err + E_CONNECTION_REFUSED
2018-01-13 05:41:42 +00:00
self.poll_timer.stop()
2017-08-06 07:23:26 +00:00
else:
check = err
if check < E_GENERAL:
# Some system error?
self.change_status(err, self.errorString())
else:
self.change_status(E_NETWORK, self.errorString())
self.projectorUpdateIcons.emit()
2017-08-06 07:23:26 +00:00
if self.status_connect == E_NOT_CONNECTED:
self.abort()
self.reset_information()
2014-10-06 19:10:03 +00:00
return
2017-12-04 00:24:47 +00:00
def send_command(self, cmd, opts='?', salt=None, priority=False):
2014-10-06 19:10:03 +00:00
"""
2017-08-06 07:23:26 +00:00
Add command to output queue if not already in queue.
2014-10-17 17:28:12 +00:00
2017-08-06 07:23:26 +00:00
:param cmd: Command to send
:param opts: Command option (if any) - defaults to '?' (get information)
:param salt: Optional salt for md5 hash initial authentication
2017-12-04 00:24:47 +00:00
:param priority: Option to send packet now rather than queue it up
2014-10-06 19:10:03 +00:00
"""
2017-12-25 08:44:30 +00:00
if QSOCKET_STATE[self.state()] != S_CONNECTED:
2018-01-13 05:41:42 +00:00
log.warning('({ip}) send_command(): Not connected - returning'.format(ip=self.entry.name))
2017-12-04 00:24:47 +00:00
return self.reset_information()
2017-08-06 07:23:26 +00:00
if cmd not in PJLINK_VALID_CMD:
2018-01-13 05:41:42 +00:00
log.error('({ip}) send_command(): Invalid command requested - ignoring.'.format(ip=self.entry.name))
2017-08-06 07:23:26 +00:00
return
2018-01-13 05:41:42 +00:00
log.debug('({ip}) send_command(): Building cmd="{command}" opts="{data}"{salt}'.format(ip=self.entry.name,
2017-08-06 07:23:26 +00:00
command=cmd,
data=opts,
salt='' if salt is None
else ' with hash'))
2018-04-20 06:04:43 +00:00
header = PJLINK_HEADER.format(linkclass=self.pjlink_functions[cmd]["version"])
2017-08-06 07:23:26 +00:00
out = '{salt}{header}{command} {options}{suffix}'.format(salt="" if salt is None else salt,
header=header,
command=cmd,
options=opts,
2017-12-25 08:44:30 +00:00
suffix=PJLINK_SUFFIX)
2017-12-04 00:24:47 +00:00
if out in self.priority_queue:
2018-01-13 05:41:42 +00:00
log.debug('({ip}) send_command(): Already in priority queue - skipping'.format(ip=self.entry.name))
2017-12-04 00:24:47 +00:00
elif out in self.send_queue:
2018-01-13 05:41:42 +00:00
log.debug('({ip}) send_command(): Already in normal queue - skipping'.format(ip=self.entry.name))
2017-06-25 02:21:07 +00:00
else:
2017-12-04 00:24:47 +00:00
if priority:
2018-01-13 05:41:42 +00:00
log.debug('({ip}) send_command(): Adding to priority queue'.format(ip=self.entry.name))
2017-12-04 00:24:47 +00:00
self.priority_queue.append(out)
else:
2018-01-13 05:41:42 +00:00
log.debug('({ip}) send_command(): Adding to normal queue'.format(ip=self.entry.name))
2017-12-04 00:24:47 +00:00
self.send_queue.append(out)
if self.priority_queue or self.send_queue:
# May be some initial connection setup so make sure we send data
2017-08-06 07:23:26 +00:00
self._send_command()
2017-06-25 02:21:07 +00:00
2017-08-06 07:23:26 +00:00
@QtCore.pyqtSlot()
def _send_command(self, data=None, utf8=False):
2017-06-25 02:21:07 +00:00
"""
2017-08-06 07:23:26 +00:00
Socket interface to send data. If data=None, then check queue.
2018-05-03 14:58:50 +00:00
:param data: Immediate data to send (Optional)
2017-08-06 07:23:26 +00:00
:param utf8: Send as UTF-8 string otherwise send as ASCII string
2017-06-25 02:21:07 +00:00
"""
2018-05-03 14:58:50 +00:00
if not data and not self.priority_queue and not self.send_queue:
log.debug('({ip}) _send_command(): Nothing to send - returning'.format(ip=self.entry.name))
return
log.debug('({ip}) _send_command(data="{data}")'.format(ip=self.entry.name,
data=data.strip() if data else data))
log.debug('({ip}) _send_command(): priority_queue: {queue}'.format(ip=self.entry.name,
queue=self.priority_queue))
log.debug('({ip}) _send_command(): send_queue: {queue}'.format(ip=self.entry.name,
queue=self.send_queue))
2017-12-25 08:44:30 +00:00
conn_state = STATUS_CODE[QSOCKET_STATE[self.state()]]
2018-01-13 05:41:42 +00:00
log.debug('({ip}) _send_command(): Connection status: {data}'.format(ip=self.entry.name,
2017-12-25 08:44:30 +00:00
data=conn_state))
if QSOCKET_STATE[self.state()] != S_CONNECTED:
2018-01-13 05:41:42 +00:00
log.debug('({ip}) _send_command() Not connected - abort'.format(ip=self.entry.name))
2017-08-06 07:23:26 +00:00
self.send_busy = False
2017-12-04 00:24:47 +00:00
return self.disconnect_from_host()
if data and data not in self.priority_queue:
2018-01-13 05:41:42 +00:00
log.debug('({ip}) _send_command(): Priority packet - adding to priority queue'.format(ip=self.entry.name))
2017-12-04 00:24:47 +00:00
self.priority_queue.append(data)
2017-08-06 07:23:26 +00:00
if self.send_busy:
# Still waiting for response from last command sent
2018-01-13 05:41:42 +00:00
log.debug('({ip}) _send_command(): Still busy, returning'.format(ip=self.entry.name))
log.debug('({ip}) _send_command(): Priority queue = {data}'.format(ip=self.entry.name,
data=self.priority_queue))
log.debug('({ip}) _send_command(): Normal queue = {data}'.format(ip=self.entry.name, data=self.send_queue))
2017-08-06 07:23:26 +00:00
return
2017-12-04 00:24:47 +00:00
if len(self.priority_queue) != 0:
out = self.priority_queue.pop(0)
2018-01-13 05:41:42 +00:00
log.debug('({ip}) _send_command(): Getting priority queued packet'.format(ip=self.entry.name))
2017-08-06 07:23:26 +00:00
elif len(self.send_queue) != 0:
out = self.send_queue.pop(0)
2018-01-13 05:41:42 +00:00
log.debug('({ip}) _send_command(): Getting normal queued packet'.format(ip=self.entry.name))
2017-06-25 02:21:07 +00:00
else:
2017-08-06 07:23:26 +00:00
# No data to send
2018-01-13 05:41:42 +00:00
log.debug('({ip}) _send_command(): No data to send'.format(ip=self.entry.name))
2017-08-06 07:23:26 +00:00
self.send_busy = False
return
self.send_busy = True
2018-01-13 05:41:42 +00:00
log.debug('({ip}) _send_command(): Sending "{data}"'.format(ip=self.entry.name, data=out.strip()))
2017-08-06 07:23:26 +00:00
self.socket_timer.start()
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?
self.change_status(E_NETWORK,
translate('OpenLP.PJLink', 'Error while sending data to projector'))
2018-05-03 14:58:50 +00:00
log.warning('({ip}) _send_command(): -1 received - disconnecting from host'.format(ip=self.entry.name))
2017-12-04 00:24:47 +00:00
self.disconnect_from_host()
2017-06-25 02:21:07 +00:00
2014-10-06 19:10:03 +00:00
def connect_to_host(self):
"""
2014-10-17 17:28:12 +00:00
Initiate connection to projector.
2014-10-06 19:10:03 +00:00
"""
2018-01-13 05:41:42 +00:00
log.debug('({ip}) connect_to_host(): Starting connection'.format(ip=self.entry.name))
2017-12-25 08:44:30 +00:00
if QSOCKET_STATE[self.state()] == S_CONNECTED:
2018-01-13 05:41:42 +00:00
log.warning('({ip}) connect_to_host(): Already connected - returning'.format(ip=self.entry.name))
2014-10-06 19:10:03 +00:00
return
2017-12-25 08:44:30 +00:00
self.error_status = S_OK
2014-10-06 19:10:03 +00:00
self.change_status(S_CONNECTING)
2018-05-03 14:58:50 +00:00
self.connectToHost(self.ip, self.port)
2014-10-06 19:10:03 +00:00
@QtCore.pyqtSlot()
2014-10-17 02:28:51 +00:00
def disconnect_from_host(self, abort=False):
2014-10-06 19:10:03 +00:00
"""
Close socket and cleanup.
"""
2017-12-25 08:44:30 +00:00
if abort or QSOCKET_STATE[self.state()] != S_NOT_CONNECTED:
2014-10-17 02:28:51 +00:00
if abort:
2018-01-13 05:41:42 +00:00
log.warning('({ip}) disconnect_from_host(): Aborting connection'.format(ip=self.entry.name))
2017-12-25 08:44:30 +00:00
self.abort()
2014-10-17 02:28:51 +00:00
else:
2018-01-13 05:41:42 +00:00
log.warning('({ip}) disconnect_from_host(): Not connected'.format(ip=self.entry.name))
2014-10-06 19:10:03 +00:00
try:
2017-09-22 12:03:28 +00:00
self.readyRead.disconnect(self.get_socket)
2014-10-06 19:10:03 +00:00
except TypeError:
2018-05-03 14:58:50 +00:00
# Since we already know what's happening, just log it for reference.
log.debug('({ip}) disconnect_from_host(): Issue detected with '
'readyRead.disconnect'.format(ip=self.entry.name))
log.debug('({ip}) disconnect_from_host(): '
2018-01-13 05:41:42 +00:00
'Current status {data}'.format(ip=self.entry.name, data=self._get_status(self.status_connect)[0]))
2018-05-03 14:58:50 +00:00
self.disconnectFromHost()
2014-10-17 02:28:51 +00:00
if abort:
self.change_status(E_NOT_CONNECTED)
else:
2017-12-04 00:24:47 +00:00
self.change_status(S_NOT_CONNECTED)
2014-10-13 16:40:58 +00:00
self.reset_information()
2014-10-06 19:10:03 +00:00
2018-06-28 15:37:37 +00:00
def get_av_mute_status(self, priority=False):
"""
Send command to retrieve shutter status.
"""
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Sending AVMT command'.format(ip=self.entry.name))
2018-06-28 15:37:37 +00:00
return self.send_command(cmd='AVMT', priority=priority)
2014-10-06 19:10:03 +00:00
def get_available_inputs(self):
"""
Send command to retrieve available source inputs.
"""
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Sending INST command'.format(ip=self.entry.name))
2014-10-06 19:10:03 +00:00
return self.send_command(cmd='INST')
def get_error_status(self):
"""
Send command to retrieve currently known errors.
"""
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Sending ERST command'.format(ip=self.entry.name))
2014-10-06 19:10:03 +00:00
return self.send_command(cmd='ERST')
def get_input_source(self):
"""
Send command to retrieve currently selected source input.
"""
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Sending INPT command'.format(ip=self.entry.name))
2014-10-06 19:10:03 +00:00
return self.send_command(cmd='INPT')
def get_lamp_status(self):
"""
Send command to return the lap status.
"""
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Sending LAMP command'.format(ip=self.entry.name))
2014-10-06 19:10:03 +00:00
return self.send_command(cmd='LAMP')
def get_manufacturer(self):
"""
Send command to retrieve manufacturer name.
"""
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Sending INF1 command'.format(ip=self.entry.name))
2014-10-06 19:10:03 +00:00
return self.send_command(cmd='INF1')
def get_model(self):
"""
Send command to retrieve the model name.
"""
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Sending INF2 command'.format(ip=self.entry.name))
2014-10-06 19:10:03 +00:00
return self.send_command(cmd='INF2')
def get_name(self):
"""
Send command to retrieve name as set by end-user (if set).
"""
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Sending NAME command'.format(ip=self.entry.name))
2014-10-06 19:10:03 +00:00
return self.send_command(cmd='NAME')
def get_other_info(self):
"""
Send command to retrieve extra info set by manufacturer.
"""
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Sending INFO command'.format(ip=self.entry.name))
2014-10-06 19:10:03 +00:00
return self.send_command(cmd='INFO')
2018-06-28 15:37:37 +00:00
def get_power_status(self, priority=False):
2014-10-06 19:10:03 +00:00
"""
Send command to retrieve power status.
2018-06-28 15:37:37 +00:00
:param priority: (OPTIONAL) Send in priority queue
2014-10-06 19:10:03 +00:00
"""
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Sending POWR command'.format(ip=self.entry.name))
2018-06-28 15:37:37 +00:00
return self.send_command(cmd='POWR', priority=priority)
2014-10-06 19:10:03 +00:00
def set_input_source(self, src=None):
"""
Verify input source available as listed in 'INST' command,
then send the command to select the input source.
2014-10-17 17:28:12 +00:00
:param src: Video source to select in projector
2014-10-06 19:10:03 +00:00
"""
2018-01-13 05:41:42 +00:00
log.debug('({ip}) set_input_source(src="{data}")'.format(ip=self.entry.name, data=src))
2014-10-06 19:10:03 +00:00
if self.source_available is None:
return
elif src not in self.source_available:
return
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Setting input source to "{data}"'.format(ip=self.entry.name, data=src))
2017-12-25 08:44:30 +00:00
self.send_command(cmd='INPT', opts=src, priority=True)
2014-10-15 17:29:15 +00:00
self.poll_loop()
2014-10-06 19:10:03 +00:00
def set_power_on(self):
"""
Send command to turn power to on.
"""
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Setting POWR to 1 (on)'.format(ip=self.entry.name))
2017-12-25 08:44:30 +00:00
self.send_command(cmd='POWR', opts='1', priority=True)
2018-06-28 15:37:37 +00:00
self.status_timer_add(cmd='POWR', callback=self.get_power_status)
2014-10-15 17:29:15 +00:00
self.poll_loop()
2014-10-06 19:10:03 +00:00
def set_power_off(self):
"""
Send command to turn power to standby.
"""
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Setting POWR to 0 (standby)'.format(ip=self.entry.name))
2017-12-25 08:44:30 +00:00
self.send_command(cmd='POWR', opts='0', priority=True)
2018-06-28 15:37:37 +00:00
self.status_timer_add(cmd='POWR', callback=self.get_power_status)
2014-10-15 17:29:15 +00:00
self.poll_loop()
2014-10-06 19:10:03 +00:00
def set_shutter_closed(self):
"""
Send command to set shutter to closed position.
"""
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Setting AVMT to 11 (shutter closed)'.format(ip=self.entry.name))
2017-12-25 08:44:30 +00:00
self.send_command(cmd='AVMT', opts='11', priority=True)
2018-06-28 15:37:37 +00:00
self.status_timer_add('AVMT', self.get_av_mute_status)
2014-10-15 17:29:15 +00:00
self.poll_loop()
2014-10-06 19:10:03 +00:00
def set_shutter_open(self):
"""
Send command to set shutter to open position.
"""
2018-01-13 05:41:42 +00:00
log.debug('({ip}) Setting AVMT to "10" (shutter open)'.format(ip=self.entry.name))
2017-12-25 08:44:30 +00:00
self.send_command(cmd='AVMT', opts='10', priority=True)
2018-06-28 15:37:37 +00:00
self.status_timer_add('AVMT', self.get_av_mute_status)
2014-10-15 17:29:15 +00:00
self.poll_loop()
2018-06-28 15:37:37 +00:00
def status_timer_add(self, cmd, callback):
"""
Add a callback to the status timer.
:param cmd: PJLink command associated with callback
:param callback: Method to call
"""
if cmd in self.status_timer_checks:
log.warning('({ip}) "{cmd}" already in checks - returning'.format(ip=self.entry.name, cmd=cmd))
return
log.debug('({ip}) Adding "{cmd}" callback for status timer'.format(ip=self.entry.name, cmd=cmd))
if not self.status_timer.isActive():
self.status_timer.start()
self.status_timer_checks[cmd] = callback
def status_timer_delete(self, cmd):
"""
Delete a callback from the status timer.
:param cmd: PJLink command associated with callback
:param callback: Method to call
"""
if cmd not in self.status_timer_checks:
log.warning('({ip}) "{cmd}" not listed in status timer - returning'.format(ip=self.entry.name, cmd=cmd))
return
log.debug('({ip}) Removing "{cmd}" from status timer'.format(ip=self.entry.name, cmd=cmd))
self.status_timer_checks.pop(cmd)
if not self.status_timer_checks:
self.status_timer.stop()
def status_timer_update(self):
"""
Call methods defined in status_timer_checks for updates
"""
if not self.status_timer_checks:
log.warning('({ip}) status_timer_update() called when no callbacks - '
'Race condition?'.format(ip=self.entry.name))
self.status_timer.stop()
return
for cmd, callback in self.status_timer_checks.items():
log.debug('({ip}) Status update call for {cmd}'.format(ip=self.entry.name, cmd=cmd))
callback(priority=True)
2017-05-12 09:51:56 +00:00
2017-05-13 09:00:29 +00:00
def receive_data_signal(self):
"""
Clear any busy flags and send data received signal
"""
self.send_busy = False
self.projectorReceivedData.emit()
return