forked from openlp/openlp
963 lines
39 KiB
Python
963 lines
39 KiB
Python
# -*- coding: utf-8 -*-
|
|
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
|
|
|
###############################################################################
|
|
# OpenLP - Open Source Lyrics Projection #
|
|
# --------------------------------------------------------------------------- #
|
|
# Copyright (c) 2008-2017 OpenLP Developers #
|
|
# --------------------------------------------------------------------------- #
|
|
# This program is free software; you can redistribute it and/or modify it #
|
|
# under the terms of the GNU General Public License as published by the Free #
|
|
# Software Foundation; version 2 of the License. #
|
|
# #
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
|
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
|
# more details. #
|
|
# #
|
|
# You should have received a copy of the GNU General Public License along #
|
|
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
|
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
|
###############################################################################
|
|
"""
|
|
:mod:`openlp.core.lib.projector.pjlink1` module
|
|
Provides the necessary functions for connecting to a PJLink-capable projector.
|
|
|
|
See PJLink Class 1 Specifications for details.
|
|
http://pjlink.jbmia.or.jp/english/dl.html
|
|
|
|
Section 5-1 PJLink Specifications
|
|
|
|
Section 5-5 Guidelines for Input Terminals
|
|
|
|
NOTE:
|
|
Function names follow the following syntax:
|
|
def process_CCCC(...):
|
|
WHERE:
|
|
CCCC = PJLink command being processed.
|
|
"""
|
|
|
|
import logging
|
|
log = logging.getLogger(__name__)
|
|
|
|
log.debug('pjlink1 loaded')
|
|
|
|
__all__ = ['PJLink1']
|
|
|
|
from codecs import decode
|
|
|
|
from PyQt5.QtCore import pyqtSignal, pyqtSlot
|
|
from PyQt5.QtNetwork import QAbstractSocket, QTcpSocket
|
|
|
|
from openlp.core.common import translate, md5_hash
|
|
from openlp.core.lib.projector.constants import *
|
|
|
|
# Shortcuts
|
|
SocketError = QAbstractSocket.SocketError
|
|
SocketSTate = QAbstractSocket.SocketState
|
|
|
|
PJLINK_PREFIX = '%'
|
|
PJLINK_CLASS = '1'
|
|
PJLINK_HEADER = '{prefix}{linkclass}'.format(prefix=PJLINK_PREFIX, linkclass=PJLINK_CLASS)
|
|
PJLINK_SUFFIX = CR
|
|
|
|
|
|
class PJLink1(QTcpSocket):
|
|
"""
|
|
Socket service for connecting to a PJLink-capable projector.
|
|
"""
|
|
# Signals sent by this module
|
|
changeStatus = pyqtSignal(str, int, str)
|
|
projectorNetwork = pyqtSignal(str, int) # Projector network activity
|
|
projectorStatus = pyqtSignal(int) # Status update
|
|
projectorAuthentication = pyqtSignal(str) # Authentication error
|
|
projectorNoAuthentication = pyqtSignal(str) # PIN set and no authentication needed
|
|
projectorReceivedData = pyqtSignal() # Notify when received data finished processing
|
|
projectorUpdateIcons = pyqtSignal() # Update the status icons on toolbar
|
|
|
|
def __init__(self, name=None, ip=None, port=PJLINK_PORT, pin=None, *args, **kwargs):
|
|
"""
|
|
Setup for instance.
|
|
|
|
:param name: Display name
|
|
:param ip: IP address to connect to
|
|
:param port: Port to use. Default to PJLINK_PORT
|
|
:param pin: Access pin (if needed)
|
|
|
|
Optional parameters
|
|
:param dbid: Database ID number
|
|
:param location: Location where projector is physically located
|
|
:param notes: Extra notes about the projector
|
|
:param poll_time: Time (in seconds) to poll connected projector
|
|
:param socket_timeout: Time (in seconds) to abort the connection if no response
|
|
"""
|
|
log.debug('PJlink(args={args} kwargs={kwargs})'.format(args=args, kwargs=kwargs))
|
|
self.name = name
|
|
self.ip = ip
|
|
self.port = port
|
|
self.pin = pin
|
|
super(PJLink1, self).__init__()
|
|
self.dbid = None
|
|
self.location = None
|
|
self.notes = None
|
|
self.dbid = None if 'dbid' not in kwargs else kwargs['dbid']
|
|
self.location = None if 'location' not in kwargs else kwargs['location']
|
|
self.notes = None if 'notes' not in kwargs else kwargs['notes']
|
|
# Poll time 20 seconds unless called with something else
|
|
self.poll_time = 20000 if 'poll_time' not in kwargs else kwargs['poll_time'] * 1000
|
|
# Timeout 5 seconds unless called with something else
|
|
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.i_am_running = False
|
|
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.maxSize = PJLINK_MAX_PACKET + 2
|
|
self.setReadBufferSize(self.maxSize)
|
|
# PJLink information
|
|
self.pjlink_class = '1' # Default class
|
|
self.reset_information()
|
|
# Set from ProjectorManager.add_projector()
|
|
self.widget = None # QListBox entry
|
|
self.timer = None # Timer that calls the poll_loop
|
|
self.send_queue = []
|
|
self.send_busy = False
|
|
# Socket timer for some possible brain-dead projectors or network cable pulled
|
|
self.socket_timer = None
|
|
# Map command to function
|
|
self.PJLINK1_FUNC = {'AVMT': self.process_avmt,
|
|
'CLSS': self.process_clss,
|
|
'ERST': self.process_erst,
|
|
'INFO': self.process_info,
|
|
'INF1': self.process_inf1,
|
|
'INF2': self.process_inf2,
|
|
'INPT': self.process_inpt,
|
|
'INST': self.process_inst,
|
|
'LAMP': self.process_lamp,
|
|
'NAME': self.process_name,
|
|
'PJLINK': self.check_login,
|
|
'POWR': self.process_powr
|
|
}
|
|
|
|
def reset_information(self):
|
|
"""
|
|
Reset projector-specific information to default
|
|
"""
|
|
log.debug('({ip}) reset_information() connect status is {state}'.format(ip=self.ip, state=self.state()))
|
|
self.power = S_OFF
|
|
self.pjlink_name = None
|
|
self.manufacturer = None
|
|
self.model = None
|
|
self.shutter = None
|
|
self.mute = None
|
|
self.lamp = None
|
|
self.fan = None
|
|
self.source_available = None
|
|
self.source = None
|
|
self.other_info = None
|
|
if hasattr(self, 'timer'):
|
|
log.debug('({ip}): Calling timer.stop()'.format(ip=self.ip))
|
|
self.timer.stop()
|
|
if hasattr(self, 'socket_timer'):
|
|
log.debug('({ip}): Calling socket_timer.stop()'.format(ip=self.ip))
|
|
self.socket_timer.stop()
|
|
self.send_queue = []
|
|
self.send_busy = False
|
|
|
|
def thread_started(self):
|
|
"""
|
|
Connects signals to methods when thread is started.
|
|
"""
|
|
log.debug('({ip}) Thread starting'.format(ip=self.ip))
|
|
self.i_am_running = True
|
|
self.connected.connect(self.check_login)
|
|
self.disconnected.connect(self.disconnect_from_host)
|
|
self.error.connect(self.get_error)
|
|
|
|
def thread_stopped(self):
|
|
"""
|
|
Cleanups when thread is stopped.
|
|
"""
|
|
log.debug('({ip}) Thread stopped'.format(ip=self.ip))
|
|
try:
|
|
self.connected.disconnect(self.check_login)
|
|
except TypeError:
|
|
pass
|
|
try:
|
|
self.disconnected.disconnect(self.disconnect_from_host)
|
|
except TypeError:
|
|
pass
|
|
try:
|
|
self.error.disconnect(self.get_error)
|
|
except TypeError:
|
|
pass
|
|
try:
|
|
self.projectorReceivedData.disconnect(self._send_command)
|
|
except TypeError:
|
|
pass
|
|
self.disconnect_from_host()
|
|
self.deleteLater()
|
|
self.i_am_running = False
|
|
|
|
def socket_abort(self):
|
|
"""
|
|
Aborts connection and closes socket in case of brain-dead projectors.
|
|
Should normally be called by socket_timer().
|
|
"""
|
|
log.debug('({ip}) socket_abort() - Killing connection'.format(ip=self.ip))
|
|
self.disconnect_from_host(abort=True)
|
|
|
|
def poll_loop(self):
|
|
"""
|
|
Retrieve information from projector that changes.
|
|
Normally called by timer().
|
|
"""
|
|
if self.state() != self.ConnectedState:
|
|
return
|
|
log.debug('({ip}) Updating projector status'.format(ip=self.ip))
|
|
# Reset timer in case we were called from a set command
|
|
if self.timer.interval() < self.poll_time:
|
|
# Reset timer to 5 seconds
|
|
self.timer.setInterval(self.poll_time)
|
|
# Restart timer
|
|
self.timer.start()
|
|
# These commands may change during connetion
|
|
for command in ['POWR', 'ERST', 'LAMP', 'AVMT', 'INPT']:
|
|
self.send_command(command, queue=True)
|
|
# The following commands do not change, so only check them once
|
|
if self.power == S_ON and self.source_available is None:
|
|
self.send_command('INST', queue=True)
|
|
if self.other_info is None:
|
|
self.send_command('INFO', queue=True)
|
|
if self.manufacturer is None:
|
|
self.send_command('INF1', queue=True)
|
|
if self.model is None:
|
|
self.send_command('INF2', queue=True)
|
|
if self.pjlink_name is None:
|
|
self.send_command('NAME', queue=True)
|
|
if self.power == S_ON and self.source_available is None:
|
|
self.send_command('INST', queue=True)
|
|
|
|
def _get_status(self, status):
|
|
"""
|
|
Helper to retrieve status/error codes and convert to strings.
|
|
|
|
:param status: Status/Error code
|
|
:returns: (Status/Error code, String)
|
|
"""
|
|
if status in ERROR_STRING:
|
|
return ERROR_STRING[status], ERROR_MSG[status]
|
|
elif status in STATUS_STRING:
|
|
return STATUS_STRING[status], ERROR_MSG[status]
|
|
else:
|
|
return status, translate('OpenLP.PJLink1', 'Unknown status')
|
|
|
|
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
|
|
"""
|
|
message = translate('OpenLP.PJLink1', 'No message') if msg is None else msg
|
|
(code, message) = self._get_status(status)
|
|
if msg is not None:
|
|
message = msg
|
|
if status in CONNECTION_ERRORS:
|
|
# Projector, connection state
|
|
self.projector_status = self.error_status = self.status_connect = E_NOT_CONNECTED
|
|
elif status >= S_NOT_CONNECTED and status < S_STATUS:
|
|
self.status_connect = status
|
|
self.projector_status = S_NOT_CONNECTED
|
|
elif status < S_NETWORK_SENDING:
|
|
self.status_connect = S_CONNECTED
|
|
self.projector_status = status
|
|
(status_code, status_message) = self._get_status(self.status_connect)
|
|
log.debug('({ip}) status_connect: {code}: "{message}"'.format(ip=self.ip,
|
|
code=status_code,
|
|
message=status_message if msg is None else msg))
|
|
(status_code, status_message) = self._get_status(self.projector_status)
|
|
log.debug('({ip}) projector_status: {code}: "{message}"'.format(ip=self.ip,
|
|
code=status_code,
|
|
message=status_message if msg is None else msg))
|
|
(status_code, status_message) = self._get_status(self.error_status)
|
|
log.debug('({ip}) error_status: {code}: "{message}"'.format(ip=self.ip,
|
|
code=status_code,
|
|
message=status_message if msg is None else msg))
|
|
self.changeStatus.emit(self.ip, status, message)
|
|
|
|
@pyqtSlot()
|
|
def check_login(self, data=None):
|
|
"""
|
|
Processes the initial connection and authentication (if needed).
|
|
Starts poll timer if connection is established.
|
|
|
|
NOTE: Qt md5 hash function doesn't work with projector authentication. Use the python md5 hash function.
|
|
|
|
:param data: Optional data if called from another routine
|
|
"""
|
|
log.debug('({ip}) check_login(data="{data}")'.format(ip=self.ip, data=data))
|
|
if data is None:
|
|
# Reconnected setup?
|
|
if not self.waitForReadyRead(2000):
|
|
# Possible timeout issue
|
|
log.error('({ip}) Socket timeout waiting for login'.format(ip=self.ip))
|
|
self.change_status(E_SOCKET_TIMEOUT)
|
|
return
|
|
read = self.readLine(self.maxSize)
|
|
dontcare = self.readLine(self.maxSize) # Clean out the trailing \r\n
|
|
if read is None:
|
|
log.warning('({ip}) read is None - socket error?'.format(ip=self.ip))
|
|
return
|
|
elif len(read) < 8:
|
|
log.warning('({ip}) Not enough data read)'.format(ip=self.ip))
|
|
return
|
|
data = decode(read, 'ascii')
|
|
# Possibility of extraneous data on input when reading.
|
|
# Clean out extraneous characters in buffer.
|
|
dontcare = self.readLine(self.maxSize)
|
|
log.debug('({ip}) check_login() read "{data}"'.format(ip=self.ip, data=data.strip()))
|
|
# 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.
|
|
if not data.upper().startswith('PJLINK'):
|
|
# Invalid response
|
|
return self.disconnect_from_host()
|
|
if '=' in data:
|
|
# Processing a login reply
|
|
data_check = data.strip().split('=')
|
|
else:
|
|
# Process initial connection
|
|
data_check = data.strip().split(' ')
|
|
log.debug('({ip}) data_check="{data}"'.format(ip=self.ip, data=data_check))
|
|
# Check for projector reporting an error
|
|
if data_check[1].upper() == 'ERRA':
|
|
# Authentication error
|
|
self.disconnect_from_host()
|
|
self.change_status(E_AUTHENTICATION)
|
|
log.debug('({ip}) emitting projectorAuthentication() signal'.format(ip=self.name))
|
|
return
|
|
elif data_check[1] == '0' and self.pin is not None:
|
|
# Pin set and no authentication needed
|
|
log.warning('({ip}) Regular connection but PIN set'.format(ip=self.name))
|
|
self.disconnect_from_host()
|
|
self.change_status(E_AUTHENTICATION)
|
|
log.debug('({ip}) Emitting projectorNoAuthentication() signal'.format(ip=self.name))
|
|
self.projectorNoAuthentication.emit(self.name)
|
|
return
|
|
elif data_check[1] == '1':
|
|
# Authenticated login with salt
|
|
if self.pin is None:
|
|
log.warning('({ip}) Authenticated connection but no pin set'.format(ip=self.name))
|
|
self.disconnect_from_host()
|
|
self.change_status(E_AUTHENTICATION)
|
|
log.debug('({ip}) Emitting projectorAuthentication() signal'.format(ip=self.name))
|
|
self.projectorAuthentication.emit(self.name)
|
|
return
|
|
else:
|
|
log.debug('({ip}) Setting hash with salt="{data}"'.format(ip=self.ip, data=data_check[2]))
|
|
log.debug('({ip}) pin="{data}"'.format(ip=self.ip, data=self.pin))
|
|
salt = md5_hash(salt=data_check[2].encode('ascii'), data=self.pin.encode('ascii'))
|
|
else:
|
|
salt = None
|
|
# We're connected at this point, so go ahead and do regular I/O
|
|
self.readyRead.connect(self.get_data)
|
|
self.projectorReceivedData.connect(self._send_command)
|
|
# Initial data we should know about
|
|
self.send_command(cmd='CLSS', salt=salt)
|
|
self.waitForReadyRead()
|
|
if (not self.no_poll) and (self.state() == self.ConnectedState):
|
|
log.debug('({ip}) Starting timer'.format(ip=self.ip))
|
|
self.timer.setInterval(2000) # Set 2 seconds for initial information
|
|
self.timer.start()
|
|
|
|
@pyqtSlot()
|
|
def get_data(self):
|
|
"""
|
|
Socket interface to retrieve data.
|
|
"""
|
|
log.debug('({ip}) get_data(): Reading data'.format(ip=self.ip))
|
|
if self.state() != self.ConnectedState:
|
|
log.debug('({ip}) get_data(): Not connected - returning'.format(ip=self.ip))
|
|
self.send_busy = False
|
|
return
|
|
read = self.readLine(self.maxSize)
|
|
if read == -1:
|
|
# No data available
|
|
log.debug('({ip}) get_data(): No data available (-1)'.format(ip=self.ip))
|
|
self.send_busy = False
|
|
self.projectorReceivedData.emit()
|
|
return
|
|
self.socket_timer.stop()
|
|
self.projectorNetwork.emit(self.ip, S_NETWORK_RECEIVED)
|
|
data_in = decode(read, 'ascii')
|
|
data = data_in.strip()
|
|
if len(data) < 7:
|
|
# Not enough data for a packet
|
|
log.debug('({ip}) get_data(): Packet length < 7: "{data}"'.format(ip=self.ip, data=data))
|
|
self.send_busy = False
|
|
self.projectorReceivedData.emit()
|
|
return
|
|
log.debug('({ip}) get_data(): Checking new data "{data}"'.format(ip=self.ip, data=data))
|
|
if data.upper().startswith('PJLINK'):
|
|
# Reconnected from remote host disconnect ?
|
|
self.check_login(data)
|
|
self.send_busy = False
|
|
self.projectorReceivedData.emit()
|
|
return
|
|
elif '=' not in data:
|
|
log.warning('({ip}) get_data(): Invalid packet received'.format(ip=self.ip))
|
|
self.send_busy = False
|
|
self.projectorReceivedData.emit()
|
|
return
|
|
data_split = data.split('=')
|
|
try:
|
|
(prefix, class_, cmd, data) = (data_split[0][0], data_split[0][1], data_split[0][2:], data_split[1])
|
|
except ValueError as e:
|
|
log.warning('({ip}) get_data(): Invalid packet - expected header + command + data'.format(ip=self.ip))
|
|
log.warning('({ip}) get_data(): Received data: "{data}"'.format(ip=self.ip, data=data_in.strip()))
|
|
self.change_status(E_INVALID_DATA)
|
|
self.send_busy = False
|
|
self.projectorReceivedData.emit()
|
|
return
|
|
|
|
if not (self.pjlink_class in PJLINK_VALID_CMD and cmd in PJLINK_VALID_CMD[self.pjlink_class]):
|
|
log.warning('({ip}) get_data(): Invalid packet - unknown command "{data}"'.format(ip=self.ip, data=cmd))
|
|
self.send_busy = False
|
|
self.projectorReceivedData.emit()
|
|
return
|
|
return self.process_command(cmd, data)
|
|
|
|
@pyqtSlot(QAbstractSocket.SocketError)
|
|
def get_error(self, err):
|
|
"""
|
|
Process error from SocketError signal.
|
|
Remaps system error codes to projector error codes.
|
|
|
|
:param err: Error code
|
|
"""
|
|
log.debug('({ip}) get_error(err={error}): {data}'.format(ip=self.ip, error=err, data=self.errorString()))
|
|
if err <= 18:
|
|
# QSocket errors. Redefined in projector.constants so we don't mistake
|
|
# them for system errors
|
|
check = err + E_CONNECTION_REFUSED
|
|
self.timer.stop()
|
|
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()
|
|
if self.status_connect == E_NOT_CONNECTED:
|
|
self.abort()
|
|
self.reset_information()
|
|
return
|
|
|
|
def send_command(self, cmd, opts='?', salt=None, queue=False):
|
|
"""
|
|
Add command to output queue if not already in queue.
|
|
|
|
:param cmd: Command to send
|
|
:param opts: Command option (if any) - defaults to '?' (get information)
|
|
:param salt: Optional salt for md5 hash initial authentication
|
|
:param queue: Option to force add to queue rather than sending directly
|
|
"""
|
|
if self.state() != self.ConnectedState:
|
|
log.warning('({ip}) send_command(): Not connected - returning'.format(ip=self.ip))
|
|
self.send_queue = []
|
|
return
|
|
self.projectorNetwork.emit(self.ip, S_NETWORK_SENDING)
|
|
log.debug('(%s) send_command(): Building cmd="%s" opts="%s" %s' % (self.ip,
|
|
cmd,
|
|
opts,
|
|
'' if salt is None else 'with hash'))
|
|
if salt is None:
|
|
out = '%s%s %s%s' % (PJLINK_HEADER, cmd, opts, CR)
|
|
else:
|
|
out = '%s%s%s %s%s' % (salt, PJLINK_HEADER, cmd, opts, CR)
|
|
if out in self.send_queue:
|
|
# Already there, so don't add
|
|
log.debug('({ip}) send_command(out="{data}") Already in queue - skipping'.format(ip=self.ip,
|
|
data=out.strip()))
|
|
elif not queue and len(self.send_queue) == 0:
|
|
# Nothing waiting to send, so just send it
|
|
log.debug('({ip}) send_command(out="{data}") Sending data'.format(ip=self.ip, data=out.strip()))
|
|
return self._send_command(data=out)
|
|
else:
|
|
log.debug('({ip}) send_command(out="{data}") adding to queue'.format(ip=self.ip, data=out.strip()))
|
|
self.send_queue.append(out)
|
|
self.projectorReceivedData.emit()
|
|
log.debug('({ip}) send_command(): send_busy is {data}'.format(ip=self.ip, data=self.send_busy))
|
|
if not self.send_busy:
|
|
log.debug('({ip}) send_command() calling _send_string()'.format(ip=self.ip))
|
|
self._send_command()
|
|
|
|
@pyqtSlot()
|
|
def _send_command(self, data=None):
|
|
"""
|
|
Socket interface to send data. If data=None, then check queue.
|
|
|
|
:param data: Immediate data to send
|
|
"""
|
|
log.debug('({ip}) _send_string()'.format(ip=self.ip))
|
|
log.debug('({ip}) _send_string(): Connection status: {data}'.format(ip=self.ip, data=self.state()))
|
|
if self.state() != self.ConnectedState:
|
|
log.debug('({ip}) _send_string() Not connected - abort'.format(ip=self.ip))
|
|
self.send_queue = []
|
|
self.send_busy = False
|
|
return
|
|
if self.send_busy:
|
|
# Still waiting for response from last command sent
|
|
return
|
|
if data is not None:
|
|
out = data
|
|
log.debug('({ip}) _send_string(data="{data}")'.format(ip=self.ip, data=out.strip()))
|
|
elif len(self.send_queue) != 0:
|
|
out = self.send_queue.pop(0)
|
|
log.debug('({ip}) _send_string(queued data="{data}"%s)'.format(ip=self.ip, data=out.strip()))
|
|
else:
|
|
# No data to send
|
|
log.debug('({ip}) _send_string(): No data to send'.format(ip=self.ip))
|
|
self.send_busy = False
|
|
return
|
|
self.send_busy = True
|
|
log.debug('({ip}) _send_string(): Sending "{data}"'.format(ip=self.ip, data=out.strip()))
|
|
log.debug('({ip}) _send_string(): Queue = {data}'.format(ip=self.ip, data=self.send_queue))
|
|
self.socket_timer.start()
|
|
try:
|
|
self.projectorNetwork.emit(self.ip, S_NETWORK_SENDING)
|
|
sent = self.write(out.encode('ascii'))
|
|
self.waitForBytesWritten(2000) # 2 seconds should be enough
|
|
if sent == -1:
|
|
# Network error?
|
|
self.change_status(E_NETWORK,
|
|
translate('OpenLP.PJLink1', 'Error while sending data to projector'))
|
|
except SocketError as e:
|
|
self.disconnect_from_host(abort=True)
|
|
self.changeStatus(E_NETWORK,
|
|
translate('OpenLP.PJLink1', '{code} : {string}').format(code=e.error(),
|
|
string=e.errorString()))
|
|
|
|
def process_command(self, cmd, data):
|
|
"""
|
|
Verifies any return error code. Calls the appropriate command handler.
|
|
|
|
:param cmd: Command to process
|
|
:param data: Data being processed
|
|
"""
|
|
log.debug('({ip}) Processing command "{data}"'.format(ip=self.ip, data=cmd))
|
|
if data in PJLINK_ERRORS:
|
|
# Oops - projector error
|
|
log.error('({ip}) Projector returned error "{data}"'.format(ip=self.ip, data=data))
|
|
if data.upper() == 'ERRA':
|
|
# Authentication error
|
|
self.disconnect_from_host()
|
|
self.change_status(E_AUTHENTICATION)
|
|
log.debug('({ip}) emitting projectorAuthentication() signal'.format(ip=self.ip))
|
|
self.projectorAuthentication.emit(self.name)
|
|
elif data.upper() == 'ERR1':
|
|
# Undefined command
|
|
self.change_status(E_UNDEFINED, '{error} "{data}"'.format(error=translate('OpenLP.PJLink1',
|
|
'Undefined command:'),
|
|
data=cmd))
|
|
elif data.upper() == 'ERR2':
|
|
# Invalid parameter
|
|
self.change_status(E_PARAMETER)
|
|
elif data.upper() == 'ERR3':
|
|
# Projector busy
|
|
self.change_status(E_UNAVAILABLE)
|
|
elif data.upper() == 'ERR4':
|
|
# Projector/display error
|
|
self.change_status(E_PROJECTOR)
|
|
self.send_busy = False
|
|
self.projectorReceivedData.emit()
|
|
return
|
|
# Command succeeded - no extra information
|
|
elif data.upper() == 'OK':
|
|
log.debug('({ip}) Command returned OK'.format(ip=self.ip))
|
|
# A command returned successfully, recheck data
|
|
self.send_busy = False
|
|
self.projectorReceivedData.emit()
|
|
return
|
|
|
|
if cmd in self.PJLINK1_FUNC:
|
|
self.PJLINK1_FUNC[cmd](data)
|
|
else:
|
|
log.warning('({ip}) Invalid command {data}'.format(ip=self.ip, data=cmd))
|
|
self.send_busy = False
|
|
self.projectorReceivedData.emit()
|
|
|
|
def process_lamp(self, data):
|
|
"""
|
|
Lamp(s) status. See PJLink Specifications for format.
|
|
Data may have more than 1 lamp to process.
|
|
Update self.lamp dictionary with lamp status.
|
|
|
|
:param data: Lamp(s) status.
|
|
"""
|
|
lamps = []
|
|
data_dict = data.split()
|
|
while data_dict:
|
|
try:
|
|
fill = {'Hours': int(data_dict[0]), 'On': False if data_dict[1] == '0' else True}
|
|
except ValueError:
|
|
# In case of invalid entry
|
|
log.warning('({ip}) process_lamp(): Invalid data "{data}"'.format(ip=self.ip, data=data))
|
|
return
|
|
lamps.append(fill)
|
|
data_dict.pop(0) # Remove lamp hours
|
|
data_dict.pop(0) # Remove lamp on/off
|
|
self.lamp = lamps
|
|
return
|
|
|
|
def process_powr(self, data):
|
|
"""
|
|
Power status. See PJLink specification for format.
|
|
Update self.power with status. Update icons if change from previous setting.
|
|
|
|
:param data: Power status
|
|
"""
|
|
if data in PJLINK_POWR_STATUS:
|
|
power = PJLINK_POWR_STATUS[data]
|
|
update_icons = self.power != power
|
|
self.power = power
|
|
self.change_status(PJLINK_POWR_STATUS[data])
|
|
if update_icons:
|
|
self.projectorUpdateIcons.emit()
|
|
# Update the input sources available
|
|
if power == S_ON:
|
|
self.send_command('INST')
|
|
else:
|
|
# Log unknown status response
|
|
log.warning('({ip}) Unknown power response: {data}'.format(ip=self.ip, data=data))
|
|
return
|
|
|
|
def process_avmt(self, data):
|
|
"""
|
|
Process shutter and speaker status. See PJLink specification for format.
|
|
Update self.mute (audio) and self.shutter (video shutter).
|
|
|
|
:param data: Shutter and audio status
|
|
"""
|
|
shutter = self.shutter
|
|
mute = self.mute
|
|
if data == '11':
|
|
shutter = True
|
|
mute = False
|
|
elif data == '21':
|
|
shutter = False
|
|
mute = True
|
|
elif data == '30':
|
|
shutter = False
|
|
mute = False
|
|
elif data == '31':
|
|
shutter = True
|
|
mute = True
|
|
else:
|
|
log.warning('({ip}) Unknown shutter response: {data}'.format(ip=self.ip, data=data))
|
|
update_icons = shutter != self.shutter
|
|
update_icons = update_icons or mute != self.mute
|
|
self.shutter = shutter
|
|
self.mute = mute
|
|
if update_icons:
|
|
self.projectorUpdateIcons.emit()
|
|
return
|
|
|
|
def process_inpt(self, data):
|
|
"""
|
|
Current source input selected. See PJLink specification for format.
|
|
Update self.source
|
|
|
|
:param data: Currently selected source
|
|
"""
|
|
self.source = data
|
|
log.info('({ip}) Setting data source to "{data}"'.format(ip=self.ip, data=self.source))
|
|
return
|
|
|
|
def process_clss(self, data):
|
|
"""
|
|
PJLink class that this projector supports. See PJLink specification for format.
|
|
Updates self.class.
|
|
|
|
:param data: Class that projector supports.
|
|
"""
|
|
# bug 1550891: Projector returns non-standard class response:
|
|
# : Expected: %1CLSS=1
|
|
# : Received: %1CLSS=Class 1
|
|
if len(data) > 1:
|
|
# Split non-standard information from response
|
|
clss = data.split()[-1]
|
|
else:
|
|
clss = data
|
|
self.pjlink_class = clss
|
|
log.debug('({ip}) Setting pjlink_class for this projector to "{data}"'.format(ip=self.ip,
|
|
data=self.pjlink_class))
|
|
return
|
|
|
|
def process_name(self, data):
|
|
"""
|
|
Projector name set in projector.
|
|
Updates self.pjlink_name
|
|
|
|
:param data: Projector name
|
|
"""
|
|
self.pjlink_name = data
|
|
log.debug('({ip}) Setting projector PJLink name to "{data}"'.format(ip=self.ip, data=self.pjlink_name))
|
|
return
|
|
|
|
def process_inf1(self, data):
|
|
"""
|
|
Manufacturer name set in projector.
|
|
Updates self.manufacturer
|
|
|
|
:param data: Projector manufacturer
|
|
"""
|
|
self.manufacturer = data
|
|
log.debug('({ip}) Setting projector manufacturer data to "{data}"'.format(ip=self.ip, data=self.manufacturer))
|
|
return
|
|
|
|
def process_inf2(self, data):
|
|
"""
|
|
Projector Model set in projector.
|
|
Updates self.model.
|
|
|
|
:param data: Model name
|
|
"""
|
|
self.model = data
|
|
log.debug('({ip}) Setting projector model to "{data}"'.format(ip=self.ip, data=self.model))
|
|
return
|
|
|
|
def process_info(self, data):
|
|
"""
|
|
Any extra info set in projector.
|
|
Updates self.other_info.
|
|
|
|
:param data: Projector other info
|
|
"""
|
|
self.other_info = data
|
|
log.debug('({ip}) Setting projector other_info to "{data}"'.format(ip=self.ip, data=self.other_info))
|
|
return
|
|
|
|
def process_inst(self, data):
|
|
"""
|
|
Available source inputs. See PJLink specification for format.
|
|
Updates self.source_available
|
|
|
|
:param data: Sources list
|
|
"""
|
|
sources = []
|
|
check = data.split()
|
|
for source in check:
|
|
sources.append(source)
|
|
sources.sort()
|
|
self.source_available = sources
|
|
self.projectorUpdateIcons.emit()
|
|
log.debug('({ip}) Setting projector sources_available to "{data}"'.format(ip=self.ip,
|
|
data=self.source_available))
|
|
return
|
|
|
|
def process_erst(self, data):
|
|
"""
|
|
Error status. See PJLink Specifications for format.
|
|
Updates self.projector_errors
|
|
|
|
:param data: Error status
|
|
"""
|
|
try:
|
|
datacheck = int(data)
|
|
except ValueError:
|
|
# Bad data - ignore
|
|
return
|
|
if datacheck == 0:
|
|
self.projector_errors = None
|
|
else:
|
|
self.projector_errors = {}
|
|
# Fan
|
|
if data[0] != '0':
|
|
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Fan')] = \
|
|
PJLINK_ERST_STATUS[data[0]]
|
|
# Lamp
|
|
if data[1] != '0':
|
|
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Lamp')] = \
|
|
PJLINK_ERST_STATUS[data[1]]
|
|
# Temp
|
|
if data[2] != '0':
|
|
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Temperature')] = \
|
|
PJLINK_ERST_STATUS[data[2]]
|
|
# Cover
|
|
if data[3] != '0':
|
|
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Cover')] = \
|
|
PJLINK_ERST_STATUS[data[3]]
|
|
# Filter
|
|
if data[4] != '0':
|
|
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Filter')] = \
|
|
PJLINK_ERST_STATUS[data[4]]
|
|
# Other
|
|
if data[5] != '0':
|
|
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Other')] = \
|
|
PJLINK_ERST_STATUS[data[5]]
|
|
return
|
|
|
|
def connect_to_host(self):
|
|
"""
|
|
Initiate connection to projector.
|
|
"""
|
|
if self.state() == self.ConnectedState:
|
|
log.warning('({ip}) connect_to_host(): Already connected - returning'.format(ip=self.ip))
|
|
return
|
|
self.change_status(S_CONNECTING)
|
|
self.connectToHost(self.ip, self.port if type(self.port) is int else int(self.port))
|
|
|
|
@pyqtSlot()
|
|
def disconnect_from_host(self, abort=False):
|
|
"""
|
|
Close socket and cleanup.
|
|
"""
|
|
if abort or self.state() != self.ConnectedState:
|
|
if abort:
|
|
log.warning('({ip}) disconnect_from_host(): Aborting connection'.format(ip=self.ip))
|
|
else:
|
|
log.warning('({ip}) disconnect_from_host(): Not connected - returning'.format(ip=self.ip))
|
|
self.reset_information()
|
|
self.disconnectFromHost()
|
|
try:
|
|
self.readyRead.disconnect(self.get_data)
|
|
except TypeError:
|
|
pass
|
|
if abort:
|
|
self.change_status(E_NOT_CONNECTED)
|
|
else:
|
|
log.debug('({ip}) disconnect_from_host() '
|
|
'Current status {data}'.format(ip=self.ip, data=self._get_status(self.status_connect)[0]))
|
|
if self.status_connect != E_NOT_CONNECTED:
|
|
self.change_status(S_NOT_CONNECTED)
|
|
self.reset_information()
|
|
self.projectorUpdateIcons.emit()
|
|
|
|
def get_available_inputs(self):
|
|
"""
|
|
Send command to retrieve available source inputs.
|
|
"""
|
|
log.debug('({ip}) Sending INST command'.format(ip=self.ip))
|
|
return self.send_command(cmd='INST')
|
|
|
|
def get_error_status(self):
|
|
"""
|
|
Send command to retrieve currently known errors.
|
|
"""
|
|
log.debug('({ip}) Sending ERST command'.format(ip=self.ip))
|
|
return self.send_command(cmd='ERST')
|
|
|
|
def get_input_source(self):
|
|
"""
|
|
Send command to retrieve currently selected source input.
|
|
"""
|
|
log.debug('({ip}) Sending INPT command'.format(ip=self.ip))
|
|
return self.send_command(cmd='INPT')
|
|
|
|
def get_lamp_status(self):
|
|
"""
|
|
Send command to return the lap status.
|
|
"""
|
|
log.debug('({ip}) Sending LAMP command'.format(ip=self.ip))
|
|
return self.send_command(cmd='LAMP')
|
|
|
|
def get_manufacturer(self):
|
|
"""
|
|
Send command to retrieve manufacturer name.
|
|
"""
|
|
log.debug('({ip}) Sending INF1 command'.format(ip=self.ip))
|
|
return self.send_command(cmd='INF1')
|
|
|
|
def get_model(self):
|
|
"""
|
|
Send command to retrieve the model name.
|
|
"""
|
|
log.debug('({ip}) Sending INF2 command'.format(ip=self.ip))
|
|
return self.send_command(cmd='INF2')
|
|
|
|
def get_name(self):
|
|
"""
|
|
Send command to retrieve name as set by end-user (if set).
|
|
"""
|
|
log.debug('({ip}) Sending NAME command'.format(ip=self.ip))
|
|
return self.send_command(cmd='NAME')
|
|
|
|
def get_other_info(self):
|
|
"""
|
|
Send command to retrieve extra info set by manufacturer.
|
|
"""
|
|
log.debug('({ip}) Sending INFO command'.format(ip=self.ip))
|
|
return self.send_command(cmd='INFO')
|
|
|
|
def get_power_status(self):
|
|
"""
|
|
Send command to retrieve power status.
|
|
"""
|
|
log.debug('({ip}) Sending POWR command'.format(ip=self.ip))
|
|
return self.send_command(cmd='POWR')
|
|
|
|
def get_shutter_status(self):
|
|
"""
|
|
Send command to retrieve shutter status.
|
|
"""
|
|
log.debug('({ip}) Sending AVMT command'.format(ip=self.ip))
|
|
return self.send_command(cmd='AVMT')
|
|
|
|
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.
|
|
|
|
:param src: Video source to select in projector
|
|
"""
|
|
log.debug('({ip}) set_input_source(src="{data}")'.format(ip=self.ip, data=src))
|
|
if self.source_available is None:
|
|
return
|
|
elif src not in self.source_available:
|
|
return
|
|
log.debug('({ip}) Setting input source to "{data}"'.format(ip=self.ip, data=src))
|
|
self.send_command(cmd='INPT', opts=src)
|
|
self.poll_loop()
|
|
|
|
def set_power_on(self):
|
|
"""
|
|
Send command to turn power to on.
|
|
"""
|
|
log.debug('({ip}) Setting POWR to 1 (on)'.format(ip=self.ip))
|
|
self.send_command(cmd='POWR', opts='1')
|
|
self.poll_loop()
|
|
|
|
def set_power_off(self):
|
|
"""
|
|
Send command to turn power to standby.
|
|
"""
|
|
log.debug('({ip}) Setting POWR to 0 (standby)'.format(ip=self.ip))
|
|
self.send_command(cmd='POWR', opts='0')
|
|
self.poll_loop()
|
|
|
|
def set_shutter_closed(self):
|
|
"""
|
|
Send command to set shutter to closed position.
|
|
"""
|
|
log.debug('({ip}) Setting AVMT to 11 (shutter closed)'.format(ip=self.ip))
|
|
self.send_command(cmd='AVMT', opts='11')
|
|
self.poll_loop()
|
|
|
|
def set_shutter_open(self):
|
|
"""
|
|
Send command to set shutter to open position.
|
|
"""
|
|
log.debug('({ip}) Setting AVMT to "10" (shutter open)'.format(ip=self.ip))
|
|
self.send_command(cmd='AVMT', opts='10')
|
|
self.poll_loop()
|