← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~alisonken1/openlp/test-projector into lp:openlp

 

Ken Roberts has proposed merging lp:~alisonken1/openlp/test-projector into lp:openlp.

Requested reviews:
  OpenLP Core (openlp-core)

For more details, see:
https://code.launchpad.net/~alisonken1/openlp/test-projector/+merge/270902

Added test server code that emulates a PJLink projector for live testing of projector controller without an available network projector.

NOTE: Python 2 code at this time.
-- 
Your team OpenLP Core is requested to review the proposed merge of lp:~alisonken1/openlp/test-projector into lp:openlp.
=== added directory 'scripts/pyProjector'
=== added file 'scripts/pyProjector/LICENSE'
--- scripts/pyProjector/LICENSE	1970-01-01 00:00:00 +0000
+++ scripts/pyProjector/LICENSE	2015-09-13 16:44:10 +0000
@@ -0,0 +1,339 @@
+GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    {description}
+    Copyright (C) {year}  {fullname}
+
+    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 2 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, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  {signature of Ty Coon}, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
\ No newline at end of file

=== added file 'scripts/pyProjector/README.md'
--- scripts/pyProjector/README.md	1970-01-01 00:00:00 +0000
+++ scripts/pyProjector/README.md	2015-09-13 16:44:10 +0000
@@ -0,0 +1,41 @@
+Test Servers - Remote control for projectors
+
+- Currently requires Python v2.5+ - will not work in Python 3
+- Initialy, only network-connected PJLink enabled projectors can be controlled.
+
+Roadmap:
+
+  - Connection handler
+  - PJLink module
+  - Other projectors as needed
+  - Possibly add other connection methods (i.e., serial port)
+
+Modularization:
+    Projector emulators are modularized to make it easer to test different
+    types of projector interfaces.
+
+    Projector personalities are modules in the servers/ directory
+
+Projector personalities are threaded, so ensure your new server modules are
+thread safe.
+
+projector/server-net.py [options]
+    Network connection handler. Once a new server module is added, update the
+    argparse section with the proper code to load your new module.
+    Default module will be the PJLink protocol server.
+
+    Example:
+
+    log.debug("Setting projector type")
+    if args.type.upper() == 'PJLINK':
+        log.info("Loading PJLink projector settings")
+        from servers.PJLink import *
+        projector = Projector()
+        projector.start()
+
+    elif args.type.upper() == 'YOURNEWSERVER':
+        log.info("Loading YourNewServer projector settings")
+        from servers.YourNewServer import *
+        # Don't forget to include any (opts), if needed, here
+        projector = Projector(opts)
+        projector.start()

=== added directory 'scripts/pyProjector/projector'
=== added file 'scripts/pyProjector/projector/example-server.py'
--- scripts/pyProjector/projector/example-server.py	1970-01-01 00:00:00 +0000
+++ scripts/pyProjector/projector/example-server.py	2015-09-13 16:44:10 +0000
@@ -0,0 +1,93 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
+
+ """example-server.py: Example skeleton module for creating a new server"""
+
+
+__name__ = "example-server"
+__version__ = "0.0.1"
+_v = __version__.split(".")
+__version_hex__ = int(_v[0]) << 24 | \
+                  int(_v[1]) << 16 | \
+                  int(_v[2]) << 8
+__all__ = ['BUFFER', 'HEADER', 'PORT', 'SUFFIX', 'TIMEOUT_MAX', 'Projector']
+
+import logging as log
+import threading
+import time
+import random
+from time import sleep
+from hashlib import md5
+from socket import timeout
+log.info("example-server %s module loading" % __version__)
+
+log.info("example-server: Setting required variables")
+PORT = 4352
+BUFFER = 140
+TIMEOUT_MAX = 30.0
+
+log.info("example: Setting local constants")
+CR = chr(0x0D) # \r
+LF = chr(0x0A) # \n
+SUFFIX = CR # Suffix for your emulated server - default PJLink
+PREFIX = '%' # Prefix character(s) for emulated server - default PJLink
+CLASS = '1'  # Command class if needed - default PJLink
+HEADER = PREFIX + CLASS # Modify for your server
+
+log.info("example-server: Creating Projector() class")
+class Projector(threading.Thread):
+    def __init__(self,
+                 group=None,
+                 target=None,
+                 name='example-server',
+                 user=None,
+                 password = None
+                 args=[],
+                 kwargs={}):
+        super(Projector, self).__init__(group, target, name, args, kwargs)
+        self.name = name
+        # Fill in self.info with proper keys for information based on
+        # the emulated projector being coded for
+        self.info = { }
+        self.running = False
+        self.user = user
+        self.passwd = password
+        self.hashes = {} # Dictionary to hold multiple login hashes
+        self.hashstring = None
+
+    def stop(self):
+        """stop() Clears running flag so thread can safely stop"""
+        log.debug('example-server: stop() called')
+        self.running = False
+        return
+
+    def run(self):
+        """run() Runs while thread is alive"""
+        log.debug('example-server: run() called')
+        self.running = True
+        while self.running:
+            # Add code to handle runtime settings
+            sleep(10)
+        return
+
+    def client_connect(self, client, addr):
+        """client_connect() Handle connection initialization with client"""
+        log.debug("example-server: client_connect(addr='%s')" % repr(addr))
+        # This check should be for user and/or password options
+        if self.passwd is None:
+            """Add code for non-authenticated connection request"""
+            pass
+        else:
+            """Add code for authenticated connection request"""
+            pass
+        return
+
+    def client_disconnect(self, addr):
+        """client_disconnect() Cleans up from client disconnect"""
+        log.debug("example-server: client_disconnect(addr=%s)" % repr(addr))
+        _ip, _port = addr
+        return
+
+    def handle_request(self, opts):
+        log.debug("example-server: handle_request(opts='%s')" % opts)
+        return

=== added file 'scripts/pyProjector/projector/server-net.py'
--- scripts/pyProjector/projector/server-net.py	1970-01-01 00:00:00 +0000
+++ scripts/pyProjector/projector/server-net.py	2015-09-13 16:44:10 +0000
@@ -0,0 +1,203 @@
+#!/usr/bin/env python
+"""server-net.py
+
+Network server to handle network connections and pass commands
+to projector emulators
+"""
+
+__version__ = '0.0.2'
+_v = __version__.split(".")
+__version_hex__ = int(_v[0]) << 24 | \
+                  int(_v[1]) << 16 | \
+                  int(_v[2]) << 8
+
+import thread
+import threading
+import time
+import logging
+import argparse
+import os
+import sys
+from time import sleep
+from socket import *
+
+if os.geteuid() < 1000:
+    print "Best to run this as a normal user"
+    sys.exit()
+
+# Setup logging to console
+FORMAT = '%(asctime)-15s [%(name)s]: %(message)s'
+logging.basicConfig(format=FORMAT, datefmt='%Y-%m-%d %H:%M:%S')
+log = logging.getLogger()
+log.setLevel(logging.WARN)
+
+log.info("Initializing net server")
+
+# Get arguments
+use_text = """server-net.py [options]
+
+Network server to emulate a projector for testing programs
+that control projectors.
+
+Currently only PJLink is supported.
+"""
+parser = argparse.ArgumentParser(prog="server-net.py",
+                                 usage=use_text)
+parser.add_argument('-d', action='count',
+    help="Increase debugging for each -d - max 2")
+parser.add_argument('--debug', action='store',
+    help='Set debugging level: DEBUG INFO WARN ERROR CRITICAL')
+parser.add_argument('-f', '--fail', action='store', default=None,
+    help='Set random fail check in minutes')
+parser.add_argument('--host', action='store',
+    help='Set interface to listen to - default all')
+parser.add_argument('--lamps', action="store", default=1,
+    help='Specify number of lamps installed')
+parser.add_argument('-p', '--password', action='store',
+    help='Set password for projector authorization. Set to "TESTME" to use test algorithm')
+parser.add_argument('--port', action='store',
+    help='Set a different port to use other than default')
+parser.add_argument('-u', '--user', action='store',
+    help="Set username for projector authorization")
+parser.add_argument('-t', '--type',
+                    default='PJLINK',
+                    help='type of projector to emulate - default="PJLINK"')
+parser.add_argument('-v', '--version',
+                    action='version',
+                    version='%(prog)s ' + str( __version__))
+args = parser.parse_args()
+
+# Time to process
+if args.debug is not None:
+    if args.debug.upper() not in "DEBUG INFO WARN ERROR CRITICAL":
+        print "--debug: invalid level: %s" % args.debug.upper()
+        print "Valid levels are DEBUG INFO WARN ERROR CRITICAL"
+    else:
+        log.setLevel(logging._levelNames[args.debug.upper()])
+elif args.d is not None:
+    d = log.getEffectiveLevel() - (args.d * 10)
+    if d < logging.DEBUG: d = logging.DEBUG
+    log.setLevel(logging._levelNames[d])
+
+print "Log level set to %s" % logging.getLevelName(log.getEffectiveLevel())
+
+log.debug('Parsing arguments')
+log.debug("args: %s" % args)
+
+log.debug("Setting projector type")
+# Only pass the arguments that your type can use. For example, if your
+# projector does not need a username, then don't use the 'user' option.
+if args.type.upper() == 'PJLINK':
+    log.info("Loading PJLink projector settings")
+    from servers.PJLink import *
+    projector = Projector(user=args.user,
+                          password=args.password,
+                          failcheck=args.fail,
+                          lamps=args.lamps)
+
+if args.host is None:
+    HOST = '' # Listen on all interfaces
+    log.info("Listening on all interfaces")
+else:
+    try:
+        HOST = gethostbyname(args.host)
+        log.info("Listening on interface %s" % HOST)
+    except:
+        HOST = '127.0.0.1'
+        log.warn("Unkown host: %s" % args.host)
+        log.warn("Setting to localhost (127.0.0.1)")
+
+if args.port is not None:
+    PORT = int(args.port)
+    if PORT < 1024 and os.geteuid() >= 1000:
+        print "Cannot use ports below 1024 as non-root user"
+        sys.exit(1)
+
+log.info("Setting constants")
+CR = chr(0x0D) # \r
+LF = chr(0x0A) # \n
+# SUFFIX needs to be defined in emulator - normally CR but can be CR+LF
+log.info("Setting main variables")
+ADDR = (HOST, PORT)
+serversock = None       # Holds port binding
+main_running = False    # Keep track of main service running
+
+log.info("Creating server functions")
+def send(s, d):
+    """send(s,d): Sends data d to socket s"""
+    log.debug("%s : Sending '%s'" % (repr(addr), d))
+    c.send("%s%s" % (d, SUFFIX))
+
+def handler(clientsock, addr):
+    """handler(clientsock, addr)
+
+    Handler for new connection. Sends socket/address to Projector() class
+    for connection setup.
+    """
+    log.info("New socket created for %s" % repr(addr))
+    log.debug("handler: Setting %s timeout to %s" % (repr(addr), TIMEOUT_MAX))
+    clientsock.settimeout(TIMEOUT_MAX)
+    log.info("handler: Initializing projector connection")
+    v, data = projector.client_connect(clientsock, addr)
+    log.debug("handler(): Connection validated: %s - Initial data: '%s'" % \
+        (v, data))
+    if not v:
+        log.error("Authentication failure - closing connection to %s" % \
+                repr(addr))
+        projector.client_disconnect(addr)
+        clientsock.close()
+        return
+    if data is None:
+        log.debug("handler(): Authenticated request with no command")
+    else:
+        _d = projector.handle_request(data)
+        log.debug("handler(): Athenticated connection - sending %s" % _d)
+        clientsock.send("%s%s" % (_d, SUFFIX))
+    handler_running = True
+    while handler_running:
+        try:
+            data = clientsock.recv(BUFFER).strip()
+            if not data:
+                break
+            elif '\xff\xf4\xff\xfd\x06' in data:
+                # ^C pressed from telnet
+                log.info("%s: Received ^C - closing connection" % repr(addr))
+                break
+            else:
+                log.debug("handler(): Calling projector.handle_request(%s)" % data)
+                _d = projector.handle_request(data)
+                log.debug("handler(): Sending '%s'" % _d)
+                clientsock.send("%s%s" % (_d, SUFFIX))
+
+        except timeout, e:
+            log.info("Timeout - closing connection to %s" % repr(addr))
+            break
+
+        except error, e:
+            log.info("Socket error - closing connection to %s" % repr(addr))
+            break
+    projector.client_disconnect(addr)
+    log.debug("handler() closing connection to %s" % repr(addr))
+    clientsock.close()
+    handler_running = False
+
+if __name__ == "__main__":
+    log.info("Starting main connection service address: %s" % repr(ADDR))
+    serversock = socket(AF_INET, SOCK_STREAM)
+    serversock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
+    serversock.bind(ADDR)
+    log.debug("Socket bound to address %s" % repr(serversock.getsockname()))
+    serversock.listen(5)
+    main_running = True
+    projector.start()
+    while main_running:
+        try:
+            log.info("MAIN: Waiting for connection - listening on port %s" % \
+                      PORT)
+            clientsock, addr = serversock.accept()
+            thread.start_new_thread(handler, (clientsock, addr))
+        except KeyboardInterrupt:
+            log.warn("Keyboard interrupt - stopping")
+            projector.stop()
+            handler_running = False
+            main_running = False

=== added directory 'scripts/pyProjector/projector/servers'
=== added file 'scripts/pyProjector/projector/servers/CHANGELOG'
--- scripts/pyProjector/projector/servers/CHANGELOG	1970-01-01 00:00:00 +0000
+++ scripts/pyProjector/projector/servers/CHANGELOG	2015-09-13 16:44:10 +0000
@@ -0,0 +1,1 @@
+ 

=== added file 'scripts/pyProjector/projector/servers/Eiki.py'
--- scripts/pyProjector/projector/servers/Eiki.py	1970-01-01 00:00:00 +0000
+++ scripts/pyProjector/projector/servers/Eiki.py	2015-09-13 16:44:10 +0000
@@ -0,0 +1,107 @@
+ """example-server.py: Example skeleton module for creating a new server"""
+
+"""
+Eiki XL200 Projector Video Inputs:
+    INF1='EIKI'
+    INF2='XL200'
+    INST='11 12 31 32 21 22 13 23 24 25'
+        Param   PJLink      Projector
+        11      RGB 1       Input 1 - RGB (PC analog)
+        12      RGB 2       Input 1 - RGB (Scart)
+        13      RGB 3       Input 2 - RGB
+        21      VIDEO 1     Input 2 - Video
+        22      VIDEO 2     Input 2 - Y, Pb/Cb, Pr/Cr
+        23      VIDEO 3     Inout 3 - Video
+        24      VIDEO 4     Input 3 - Y, Pb/Cb, Pr/Cr
+        25      VIDEO 5     Input 3 - S-video
+        31      DIGITAL 1   Input 1 - rgb (PC digital)
+        32      DIGITAL 2   Input 1 - RGB (AV HDCP)
+        51      NETWORK 1   Input 4 - Network
+"""
+
+__version__ = "0.0.1"
+_v = __version__.split(".")
+__version_hex__ = int(_v[0]) << 24 | \
+                  int(_v[1]) << 16 | \
+                  int(_v[2]) << 8
+__all__ = ['BUFFER', 'PORT', 'SUFFIX', 'TIMEOUT_MAX', 'Projector']
+
+import logging as log
+import threading
+import time
+import random
+from time import sleep
+from hashlib import md5
+from socket import timeout
+log.info("example-server %s module loading" % __version__)
+
+log.info("example-server: Setting required variables")
+PORT = 4352
+BUFFER = 140
+TIMEOUT_MAX = 30.0
+
+log.info("example: Setting local constants")
+CR = chr(0x0D) # \r
+LF = chr(0x0A) # \n
+SUFFIX = CR+LF
+PREFIX = '%' # Prefix character(s) for emulated server
+CLASS = '1'  # Command class if needed
+HEADER = PREFIX + CLASS # Modify for your server
+
+log.info("example-server: Creating Projector() class")
+class Projector(threading.Thread):
+    def __init__(self,
+                 group=None,
+                 target=None,
+                 name='example-server',
+                 user=None,
+                 password = None
+                 args=[],
+                 kwargs={}):
+        super(Projector, self).__init__(group, target, name, args, kwargs)
+        self.name = name
+        # Fill in self.info with proper keys for information based on
+        # the emulated projector being coded for
+        self.info = { }
+        self.running = False
+        self.user = user
+        self.passwd = password
+        self.hashes = {} # Dictionary to hold multiple login hashes
+        self.hashstring = None
+
+    def stop(self):
+        """stop() Clears running flag so thread can safely stop"""
+        log.debug('example-server: stop() called')
+        self.running = False
+        return
+
+    def run(self):
+        """run() Runs while thread is alive"""
+        log.debug('example-server: run() called')
+        self.running = True
+        while self.running:
+            # Add code to handle runtime settings
+            sleep(2)
+        return
+
+    def client_connect(self, client, addr):
+        """client_connect() Handle connection initialization with client"""
+        log.debug("example-server: client_connect(addr='%s')" % repr(addr))
+        # This check should be for user and/or password options
+        if self.passwd is None:
+            """Add code for non-authenticated connection request"""
+            pass
+        else:
+            """Add code for authenticated connection request"""
+            pass
+        return
+
+    def client_disconnect(self, addr):
+        """client_disconnect() Cleans up from client disconnect"""
+        log.debug("example-server: client_disconnect(addr=%s)" % repr(addr))
+        _ip, _port = addr
+        return
+
+    def handle_request(self, opts):
+        log.debug("example-server: handle_request(opts='%s')" % opts)
+        return

=== added file 'scripts/pyProjector/projector/servers/PJLink.py'
--- scripts/pyProjector/projector/servers/PJLink.py	1970-01-01 00:00:00 +0000
+++ scripts/pyProjector/projector/servers/PJLink.py	2015-09-13 16:44:10 +0000
@@ -0,0 +1,756 @@
+"""PJLink.py: Module to emulate a PJLink server"""
+
+__version__ = "0.1.0"
+_v = __version__.split(".")
+__version_hex__ = int(_v[0]) << 24 | \
+                  int(_v[1]) << 16 | \
+                  int(_v[2]) << 8
+
+__all__ = ['BUFFER', 'HEADER', 'PORT', 'SUFFIX', 'TIMEOUT_MAX', 'Projector']
+
+import logging
+import threading
+import time
+import random
+from time import sleep
+from hashlib import md5
+from socket import timeout
+
+from projectorbase import *
+
+log = logging.getLogger(__name__)
+
+log.info("Module loading version %s" % __version__)
+
+log.info("PJLink: Setting local constants")
+PJLINK_PREFIX = '%'
+PJLINK_CLASS = '1'
+
+PJLINK_INFO = { 'NAME': "PJLinkProjector", # Can be changed"
+                'INF1': "OpenLP",
+                'INF2': "PythonProjector",
+                'INFO': "Model PS01 Serial P274001"
+            }
+
+LAMP_DICT = { "STATUS": 0,
+              "NUMBER": 0,
+              "LAMP_ON": False,
+              "FAN": 0,
+              "FAN_ON": False,
+              "TEMP": 0,
+              "FILTER": 0, # random.randint(0, 6000), # Minutes
+              "COVER": 0,
+              "TIME": 0, #random.randint(0, 300000), # Minutes
+              "CLOCK": None,
+              "WARN": False,
+            }
+
+log.info("Setting required variables")
+BUFFER = 140
+HEADER = PJLINK_PREFIX + PJLINK_CLASS
+PORT = 4352
+SUFFIX = CR
+SUFFIX = CR+LF # Telnet needs LF otherwise change back to CR only
+TIMEOUT_MAX = 30.0
+
+log.info("Creating PJLinkStatus() class")
+class PJLinkStatus(Status):
+    """PJLinkStatus(Status)
+
+    Extends basic error code mappings with PJLink code mappings
+    """
+    # Map some error codes to PJLink physical parts.
+    names = { E_NETWORK: "Network",
+              E_FAN: "Fan",
+              E_LAMP: "Lamp",
+              E_TEMP: "Temperature",
+              E_COVER: "Cover",
+              E_FILTER: "Filter",
+              E_PROJECTOR: "Projector"
+             }
+
+    # PJLink power status mapping
+    power = { S_STANDBY: '0',
+              S_ON: '1',
+              S_COOLDOWN: '2',
+              S_WARMUP: '3',
+              '0': S_STANDBY,
+              '1': S_ON,
+              '2': S_COOLDOWN,
+              '3': S_WARMUP
+             }
+    # PJLink fan/lamp/temp/cover/filter/other mapping
+    erst = { E_OK: '0',
+             E_WARN: '1',
+             E_ERROR: '2',
+             '0': E_OK,
+             '1': E_WARN,
+             '2': E_ERROR
+            }
+
+    errors = { E_OK: 'OK',
+               E_UNDEFINED: 'ERR1',
+               E_PARAMETER: 'ERR2',
+               E_UNAVAILABLE: 'ERR3',
+               E_PROJECTOR: 'ERR4',
+               E_AUTHENTICATION: 'ERRA',
+               'OK': E_OK,
+               'ERR1': E_UNDEFINED,
+               'ERR2': E_PARAMETER,
+               'ERR3': E_UNAVAILABLE,
+               'ERR4': E_PROJECTOR,
+               'ERRA': E_AUTHENTICATION
+              }
+
+    # Define inputs - This lists all valid PJLink inputs
+    inputs = { '11': "RGB 1",
+               '12': "RGB 2",
+               '13': "RGB 3",
+               '21': "VIDEO 1",
+               '22': "VIDEO 2",
+               '23': "VIDEO 3",
+               '31': "DIGITAL 1",
+               '32': "DIGITAL 2",
+               '33': "DIGITAL 3",
+               '41': "STORAGE 1",
+               '42': "STORAGE 2",
+               '51': "NETWORK 1",
+               '52': "NETWORK 2"
+             }
+
+log.debug("Setting non-standard help")
+_CMDHELP = """
+
+All command require a prefix.
+    PREFIX = %s
+
+Example command to request power status:
+    %sPOWR ?<return>
+
+PJLink options are:
+    POWR [? 0 1]
+    INPT [? %s]
+    AVMT [? 10 11 20 21 30 31]
+    LAMP ?
+    INST ?
+    NAME ?
+    INF1 ?
+    INF2 ?
+    INFO ?
+    CLSS ?
+
+    Non-error returns will be:
+        OK - Command executed
+        <data> - Some string based on the command given
+
+    Error returns will be:
+        ERR1 - Invalid command
+        ERR2 - Invalid option
+        ERR3 - Unavailable time
+        ERR4 - Projector/Display error
+        ERRA - Authentication error
+
+""" % (PJLINK_PREFIX+PJLINK_CLASS, PJLINK_PREFIX+PJLINK_CLASS, PJLinkStatus.inputs.keys())
+
+log.info("Creating Projector() class")
+class Projector(ProjectorBase):
+    def __init__(self,
+                 group=None, # Thread required
+                 target=None, # Thread required
+                 name='PJLink',
+                 user=None,
+                 password=None,
+                 failcheck=None,
+                 lamps=None,
+                 args=[], # Thread required
+                 kwargs={}): # Thread required
+        log.info("Projector.__init__(user='%s' password='%s')" % \
+            (user, password if password is None else 'password'))
+        super(Projector, self).__init__(group, target, name, args, kwargs)
+        self.client_ip = None
+        self.name = name
+        self.running = False
+        self.user = user
+        if password == 'TESTME':
+            log.debug('Projector.__init__(): Setting self.passwd to JBMIAProjectorLink')
+            self.passwd = 'JBMIAProjectorLink'
+        else:
+            log.debug('Projector.__init__(): Using password option')
+            self.passwd = password
+        self.hashes = {} # Dictionary to hold multiple login hashes
+        self.shutter = False # True=shutter close=False shutter open
+        self.audio = False # True=audio muted False=audio normal
+        self.source = '11' # Default to first analog input
+        self.PROJ_ERR = None
+        self.PROJ_PART = [] # May have multiple failures
+        self.PROJ_ERRSTAT = { 1: 0,  # Fan
+                              2: 0,  # Lamp
+                              3: 0,  # Temperature
+                              4: 0,  # Cover open
+                              5: 0,  # Filter
+                              6: 0   # Other
+                            }
+        # Time between random failure check (minutes)
+        if failcheck is None or failcheck.isdigit():
+            self.failcheck = failcheck
+        else:
+            self.failcheck = None
+            log.warn("Projector.__init__(): Invalid failcheck option - disabling failcheck")
+        self.failclock = None   # Clock for next random failures
+        self.timer = None
+        """ self.powerstat
+                S_STANDBY: 0
+                S_ON: 1
+                S_COOLDOWN: 2
+                S_WARMUP: 3
+        """
+        self.powerstat = S_STANDBY # Initial condition is standby
+        """ self.lamp
+
+        Dictionary of lamps in projector.
+        Each lamp is another dictionary of status
+            STATUS: See PJLinkStatus.[erst | power]
+            FAN:    Fan state S_OFF, S_ON, E_ERR
+            TIME:   Time in minutes lamp has been in the ON state.
+                    Initial lamp time is a random int from 0 (new)
+                    to 300000 minutes (approx. 5000 hours)
+            CLOCK:  time.time() when lamp was initially turned ON.
+                    This includes warmup/cooldown times.
+                    None when lamp is off.
+            FILTER: Time since last filter cleaning
+            COVER:  Whether the lamp access cover is open
+            WARN:   Warning indicator for lamp time or filter time
+        """
+        # First define number of lamps, then fill out in next step
+
+        self.lamp = {}
+        if lamps is None:
+            _lamps = 1
+        elif (type(lamps) is type(str)) and not lamps.isdigit():
+            _lamps = 1
+            log.warn("Projector.__init__(): Invalid option for lamps - defaulting to 1")
+        else:
+            _lamps = int(lamps)
+        for i in range(1,_lamps+1): # Taken from initialization
+            self.lamp[i] = None
+        for i in self.lamp:
+            self.lamp[i] = LAMP_DICT.copy()
+            self.lamp[i]['FILTER'] = random.randint(0,6000)
+            self.lamp[i]['TIME'] = random.randint(0,300000)
+            self.lamp[i]['LAMP_ON'] = False
+            if self.lamp[i]['TIME'] >= 240000 or self.lamp[i]['FILTER'] > 6000:
+                # Lamp has > 4000 hours runtime
+                # Filter has > 100 hours since last cleaning
+                self.lamp[i]['WARN'] = True
+            log.debug("Projector.__init__(): Lamp %s TIME=%s" % (i, self.lamp[i]['TIME']))
+            log.debug("Projector.__init__(): Lamp %s FILTER=%s" % (i, self.lamp[i]['FILTER']))
+            log.debug("Projector.__init__(): Lamp %s WARN=%s" % (i, self.lamp[i]['WARN']))
+
+    ######################################################################
+    #                        Required functions                          #
+    ######################################################################
+    def client_disconnect(self, addr):
+        """client_disconnect(addr)
+
+        Removes connection cached hash key.
+        """
+        log.info("Projector.client_disconnect(addr=%s)" % repr(addr))
+        _ip, _port = addr
+        _key = '%s:%s' % (_ip, _port)
+        if self.hashes.has_key(_key):
+            self.hashes.pop(_key)
+        return
+
+    def client_connect(self, client, addr):
+        """client_connect(client, addr)
+
+        Handle connection initialization with client and
+        adds new client to self.hashes.
+        """
+        log.info("Projector.client_connect(addr='%s')" % repr(addr))
+        self.client_ip, self.client_port = addr
+        if self.passwd is None:
+            log.debug("Projector.client_connect(%s) Unauthenticated connection" % self.client_ip)
+            client.send("PJLINK 0" + SUFFIX)
+            return (True, 'OK')
+
+        else:
+            # For testing purposes:
+            # _rand = '498e4a67'
+            # Password is 'JBMIAProjectorLink'
+            # Return hash should be '5d8409bc1c3fa39749434aa3a5c38682'
+            if self.passwd == 'JBMIAProjectorLink':
+                log.debug("Projector(%s): self.passwd == 'JBMIAProjectorLink'" % self.client_ip)
+                log.debug("Projector(%s): setting _rand='498e4a67'" % self.client_ip)
+                _rand = '498e4a67'
+            else:
+                _rand = str(hex(random.randint(268435456, 4294967295L)))[2:-1]
+                log.debug("Projector(%s): Setting _rand to '%s'" % (self.client_ip, _rand))
+            _hashed = md5()
+            _hashed.update(_rand)
+            _hashed.update(self.passwd)
+            _digest = str(_hashed.hexdigest())[:32]
+            _ip, _port = addr
+            _key = "%s:%s" % (_ip, _port)
+            self.hashes[_key] = _digest
+            try:
+                log.debug('Projector(%s).client_connect(): Sending "PJLINK 1 randomkey"' % self.client_ip)
+                client.send("PJLINK 1 %s%s" % (_rand, SUFFIX))
+                data = client.recv(BUFFER).strip()
+                log.debug("Projector.server_connect(%s) Received %s" % (self.client_ip, data))
+            except timeout, e:
+                log.debug("Projector.server_connect(%s): Timeout" % self.client_ip)
+                return (False, None)
+
+            if len(data) > 32 and data[32:34] == HEADER:
+                log.debug("Projector.server_connect(%s): Authentication with command" % self.client_ip)
+                _a = data[:32]
+                _d = data[32:].strip()
+            else:
+                log.debug("Projector.server_connect(%s): Authentication only" % self.client_ip)
+                _a = data[:32]
+                _d = None
+
+            log.debug("Projector.server_connect(%s): %s Received" % (self.client_ip, _a))
+            log.debug("Projector.server_connect(%s): %s Expected" % \
+                    (self.client_ip, self.hashes[_key]))
+            if _a == self.hashes[_key]:
+                log.debug('Projector.server_connect(%s): Validated login data="%s"' % (self.client_ip, _d))
+                return (True, _d)
+            else:
+                log.debug("Projector.server_connect(%s): Invalid login" % self.client_ip)
+                client.send('PJLINK=%s%s' % (PJLinkStatus.errors[E_AUTHENTICATION],SUFFIX))
+                return (False, None)
+
+        return (False, None) # Catchall failed
+
+    def stop(self):
+        """stop()
+
+        Clears running flag so thread can safely stop
+        """
+        log.info('Projector.stop() called')
+        self.running = False
+        return
+
+    def run(self):
+        """run()
+
+        Called when thread is started, then checks status of projector
+        for any changes.
+        """
+        log.info('Projector.run() called')
+        self.running = True
+        self._power_change(S_STANDBY)
+        while self.running:
+            sleep(2.0) # 2 second intervals between checks
+            if self.timer is None: continue
+            if time.time() < self.timer: continue
+            # Time to do something
+            # The only changes should be warmup -> on/cooldown -> standby
+            log.debug("Projector.run() Time to change something")
+            if self.powerstat == S_WARMUP:
+                self._power_change(S_ON)
+            elif self.powerstat == S_COOLDOWN:
+                self._power_change(S_STANDBY)
+
+            if self.failclock is not None and self.failclock < time.time():
+                _random_fail()
+
+            continue # Not required, but explicit > implicit
+        return
+
+    def handle_request(self, data):
+        """handle_request(data)
+
+        Called from main thread to handle projector requests.
+        """
+        log.info("Projector(%s).handle_request(data='%s')" % (self.client_ip, data))
+        if data is None:
+            return
+        if data.upper() == '--HELPME--':
+            return _CMDHELP
+        if len(data) < 2:
+            log.debug("Projector(%s).handle_request(): Packet length < 2" % self.client_ip)
+            return ''
+        elif data[0] != PJLINK_PREFIX:
+            log.debug("Projector(%s).handle_request(): data[0] != %s" % (self.client_ip, PJLINK_PREFIX))
+            return ''
+        elif data[1] != PJLINK_CLASS:
+            log.debug("Projector(%s).handle_request(): Invalid class %s != %s" % (self.client_ip, data[1], PJLINK_CLASS))
+            return ''
+
+        _r = None
+        if len(data) < 8:
+            log.debug("Projector(%s).handle_request(): Packet length < 8" % self.client_ip)
+            log.debug("Projector(%s).handle_request(): Packet length: %s" % (self.client_ip, len(data)))
+            # Minimum packet length not met
+            return "%s=%s" % (data, PJLinkStatus.errors[E_UNDEFINED])
+        elif data[6] != ' ':
+            log.debug("Projector(%s).handle_request(): char 7 not a space: %s" % (self.client_ip, data[6]))
+            return "%s=%s" % (data, PJLinkStatus.errors[E_UNDEFINED])
+        else:
+            _hdr = data[:2]
+            _cmd, _param = data[2:].split(' ')
+
+        if _r is not None:
+            pass # Already have a reply
+        else:
+            # Commands with mutliple options
+            if _cmd.upper() == 'POWR':
+                _d = self.power(_param)
+                if _d == S_OK:
+                    _r = PJLinkStatus.errors[S_OK]
+                else:
+                    _r = _d
+            elif _cmd.upper() == 'INPT':
+                _d = self.input_select(_param)
+                if _d == S_OK:
+                    _r = PJLinkStatus.errors[S_OK]
+                else:
+                    _r = _d
+            elif _cmd.upper() == 'AVMT':
+                _d = self.avmute(_param)
+                if _d == S_OK:
+                    _r = PJLinkStatus.errors[S_OK]
+                else:
+                    _r = _d
+
+            # The rest require ? as parameter and nothing else
+            elif _param != '?':
+                _r = PJLinkStatus[E_PARAMETER]
+            elif _cmd.upper() == 'ERST':
+                _r = self.error_status(_param)
+            elif _cmd.upper() == 'LAMP':
+                _r = self.lamp_status(_param)
+            elif _cmd.upper() == 'INST':
+                _r = self.input_list(_param)
+            elif _cmd.upper() == 'NAME':
+                _r = PJLINK_INFO['NAME']
+            elif _cmd.upper() == 'INF1':
+                _r = PJLINK_INFO['INF1']
+            elif _cmd.upper() == 'INF2':
+                _r = PJLINK_INFO['INF2']
+            elif _cmd.upper() == 'INFO':
+                _r = PJLINK_INFO['INFO']
+            elif _cmd.upper() == 'CLSS':
+                _r = PJLINK_CLASS
+            else:
+                # Unknown command
+                _r = PJLinkStatus.errors[E_UNDEFINED]
+        _d = "%s%s=%s" % (_hdr, _cmd, _r)
+        log.debug("Projector(%s).handle_request() returning '%s')" % (self.client_ip, _d))
+        return _d
+
+    ######################################################################
+    #                        Local functions                             #
+    ######################################################################
+    def _random_fail(self):
+        """_random_fail()
+
+        Random check if a part failed and sets self.PROJ_ERRSTAT[]
+        list to which part(s) are failed.
+        """
+        log.info("Projector(%s)._random_fail() called", self.client_ip)
+        if int(random.random()) == 1:
+            _list = [ 'POWER', 'LENS' ]
+            for i in self.lamp:
+                _list.append('LAMP%s' % self.lamp[i]['NUMBER'])
+                _list.append('FAN%s' % self.lamp[i]['NUMBER'])
+                _list.append('COVER%s' % self.lamp[i]['NUMBER'])
+                _list.append('FILTER%s' % self.lamp[i]['NUMBER'])
+                if self.lamp[i]['TIME'] > 30000:
+                    # Lamp exceeds rated time - extra chance of failure
+                    _list.append('LAMP%s' % self.lamp[i]['NUMBER'])
+            failed = random.choice(_list)
+            self.PROJ_PART = failed
+            if 'FAN' in failed:
+                if failed not in self.PROJ_PART:
+                    self.PROJ_ERRSTAT[1] = 2
+            elif 'LAMP' in failed:
+                if failed not in self.PROJ_PART:
+                    self.PROJ_ERRSTAT[2] = 2
+            elif 'TEMP' in failed:
+                # Probably fan failed or filter clogged
+                if failed not in self.PROJ_PART:
+                    self.PROJ_ERRSTAT[3] = 2
+            elif 'COVER' in failed:
+                # Cover open
+                if failed not in self.PROJ_PART:
+                    self.PROJ_ERRSTAT[4] = 2
+            elif 'FILTER' in failed:
+                # Filter clogged
+                if failed not in self.PROJ_PART:
+                    self.PROJ_ERRSTAT[5] = 2
+            else:
+                self.PROJ_ERRSTAT[6] = 2
+
+    def _lamp_clock_update(self, lamp, stop=False):
+        """
+        Update the clock time for the lamp
+        """
+        if lamp['CLOCK'] is not None:
+            _t = int(time.time() - lamp['CLOCK'])
+            lamp['TIME'] = lamp['TIME'] + (_t / 60)
+        if stop:
+            lamp['CLOCK'] = None
+
+    def _lamp_change(self, lamp, opt, failed='LAMP'):
+        """_lamp_change(lamp, opt)
+
+        Handle lamp changes. Must be called with a dict of a single lamp.
+        """
+        log.info("Projector(%s)._lamp_change(opt='%s')" % (self.client_ip, Status.keys[opt]))
+
+        if not type(lamp) == type({}):
+            log.warn('Projector(%s)._lamp_change() lamp not a dict' % self.client_ip)
+            log.warn('Projector(%s)._lamp_change(lamp): %s' % (self.client_ip, lamp))
+            return E_PROJECTOR
+        elif opt is None:
+            # Update clock below
+            pass
+        elif opt != S_OFF and \
+                 opt not in PJLinkStatus.power and \
+                 opt not in PJLinkStatus.erst:
+            log.warn('Projector(%s)._lamp_change() invalid option %s' % (self.client_ip, opt))
+            return E_PARAMETER
+
+        _r = S_OK
+        if opt is None:
+            self._lamp_clock_update(lamp)
+        elif opt == E_ERROR:
+            _r = PJLinkStatus.errors[E_PROJECTOR]
+            if failed == 'LAMP':
+                self._lamp_clock_update(lamp, True)
+                lamp['STATUS'] = PJLinkStatus.erst[E_ERROR]
+                PROJ_ERRSTAT[0] = PJLinkStatus.erst[E_ERROR]
+            elif failed == 'FAN':
+                lamp['FAN'] = PJLinkStatus.erst[E_ERROR]
+                lamp['FAN_ON'] = False
+                lamp['TEMP'] = PJLinkStatus.erst[E_WARN]
+            elif failed == 'COVER':
+                lamp['COVER'] = PJLinkStatus.erst[E_ERROR]
+                lamp['TEMP'] = PJLinkStatus.erst[E_WARN]
+        elif opt == S_ON:
+            lamp['FAN_ON'] = True
+            lamp['LAMP_ON'] = True
+        elif opt == S_COOLDOWN:
+            self._lamp_clock_update(lamp, True)
+            lamp['STATUS'] = S_OFF
+            lamp['LAMP_ON'] = False
+        elif opt == S_WARMUP:
+            lamp['STATUS'] = S_ON
+            lamp['FAN_ON'] = True
+            lamp['CLOCK'] = time.time()
+            lamp['LAMP_ON'] = True
+        return _r
+
+    def _power_change(self, opt):
+        """_power_change(opt)
+
+        Handle power setting changes.
+        """
+        log.info("Projector(%s)._power_change(opt='%s')" % (self.client_ip, Status.keys[opt]))
+        log.info("Projector(%s)._power_change() self.timer= %s" % (self.client_ip, self.timer))
+        if self.PROJ_ERR is not None:
+            return self.PROJ_ERR
+        elif self.timer is not None and time.time() < self.timer:
+            return PJLinkStatus.errors[E_UNAVAILABLE]
+        elif not opt in PJLinkStatus.power:
+            return PJLinkStatus.errors[E_PARAMETER]
+
+        # Error checks passed - time to change something
+        lampset = None
+        _r = S_OK
+        self.powerstat = opt
+        if opt == S_WARMUP:
+            self.timer = time.time() + 20.0
+            lampset = S_ON
+        elif opt == S_STANDBY:
+            self.timer = None
+            lampset = S_OFF
+        elif self.powerstat == S_COOLDOWN:
+            self.timer = time.time() + 20.0
+            lampset = S_COOLDOWN
+        elif opt == S_ON:
+            self.timer = None
+            # No lamp change
+
+        _dd = S_OK
+        if lampset is not None:
+            for i in self.lamp:
+                _d = self._lamp_change(lamp=self.lamp[i], opt=lampset)
+                if _d != S_OK:
+                    _dd = _d
+                log.debug("PJLink(%s)._power_change(lamp=%s) received lamp status %s" % (self.client_ip, i, _d))
+
+        if _dd != S_OK or  _r != S_OK:
+            _r = PJLinkStatus.errors[E_PROJECTOR]
+
+        log.debug("Projector(%s)._power_change() returning %s" % (self.client_ip, PJLinkStatus.keys[_r]))
+        return _r
+
+    def power(self, opt):
+        """power(opt)
+
+        Handle POWR command requests.
+        """
+        log.info("Projector(%s).power(opt='%s')" % (self.client_ip, opt))
+        log.debug("Projector(%s).powerstat = %s" % \
+            (self.client_ip, PJLinkStatus.keys[self.powerstat]))
+        if opt not in '?01':
+            log.debug('Projector(%s).power(): opt %s not in "?01"' % (self.client_ip, opt))
+            return PJLinkStatus.errors[E_PARAMETER]
+        elif self.PROJ_ERR is not None:
+            log.debug('Projector(%s).power() returning self.PROJ_ERR' % self.client_ip)
+            return self.PROJ_ERR
+
+        log.debug('Projector(%s).power(): Checking opt %s' % (self.client_ip, opt))
+        if opt == '?':
+            if self.powerstat in PJLinkStatus.power:
+                return PJLinkStatus.power[self.powerstat]
+            else:
+                return PJLinkStatus.errors[E_PROJECTOR]
+
+        log.debug('Projector(%s).power(): Checking for unavailable time' % self.client_ip)
+        if self.powerstat == S_WARMUP or self.powerstat ==  S_COOLDOWN:
+            return PJLinkStatus.errors[E_UNAVAILABLE]
+
+        log.debug('Projector(%s).power(): Checking for set on/off' % self.client_ip)
+        _r = S_OK
+        if opt == '1':
+            if self.powerstat == S_ON:
+                return S_OK
+            else:
+                _d = self._power_change(S_WARMUP)
+        elif opt == '0':
+            if self.powerstat == S_STANDBY:
+                return S_OK
+            else:
+                _d = self._power_change(S_COOLDOWN)
+
+        if _d != S_OK:
+            return _d
+        else:
+            return _r
+
+    def error_status(self, opt):
+        """error_status(opt)
+
+        Returns the current status of:
+            fan[s],
+            lamp[s],
+            temperature,
+            cover open,
+            filter[s],
+            other
+        """
+        log.info("Projector(%s).error_status(opt=%s)" % (self.client_ip, opt))
+        if opt != "?":
+            return PJLinkStatus.errors[E_PARAMETER]
+        elif self.PROJ_ERR is not None:
+            return self.PROJ_ERR
+
+        return '%s%s%s%s%s%s' % ( self.PROJ_ERRSTAT[1],
+                                  self.PROJ_ERRSTAT[2],
+                                  self.PROJ_ERRSTAT[3],
+                                  self.PROJ_ERRSTAT[4],
+                                  self.PROJ_ERRSTAT[5],
+                                  self.PROJ_ERRSTAT[6]
+                                 )
+
+    def avmute(self, opt):
+        """avmute(opt)
+
+        Handles AVMT requests.
+        """
+        log.info("Projector(%s).avmute(opt=%s)" % (self.client_ip, opt))
+        if self.PROJ_ERR is not None: return self.PROJ_ERR
+        if opt == '?':
+            if self.powerstat == 2:
+                return PJLinkStatus.errors[3]
+            elif not self.audio and not self.shutter:
+                return '30'
+            elif self.audio and self.shutter:
+                return '31'
+            elif self.shutter:
+                # video mute (shutter closed)
+                return '11'
+            else:
+                # Audio muted only
+                return '21'
+        elif opt == '11':
+            self.shutter = True
+        elif opt == '10':
+            self.shutter = False
+        elif opt == '21':
+            self.audio = True
+        elif opt == '20':
+            self.audio = False
+        elif opt == '31':
+            self.shutter = True
+            self.audio = True
+        elif opt == '30':
+            self.shutter = False
+            self.audio = False
+        else:
+            return PJLinkStatus.errors[E_PARAMETER]
+        return PJLinkStatus.errors[S_OK]
+
+    def lamp_status(self, opt):
+        """lamp_status(opt)
+
+        Returns the lamp hours and current on/off status of
+        available lamps.
+        """
+        log.info("Projector(%s).lamp_status(): called" % self.client_ip)
+        if self.PROJ_ERR is not None:
+            return self.PROJ_ERR
+        if opt != '?': return PJLinkStatus.errors[E_PARAMETER]
+        _r = ''
+        for i in self.lamp:
+            _r = "%s %s %s" % ( _r, self.lamp[i]['TIME'], \
+                ("1" if self.lamp[i]['LAMP_ON'] else "0"))
+        return _r.strip()
+
+    def input_list(self, opt):
+        """"input_list(opt)
+
+        Handles INST request.
+        Returns a string of the available source inputs.
+        """
+        log.info("Projector(%s).input_list(opt='%s')" % (self.client_ip, opt))
+        if self.PROJ_ERR is not None:
+            return self.PROJ_ERR
+        if opt != '?':
+            log.debug("Projector(%s).input_list(opt='%s')" % (self.client_ip, opt))
+            return PJLinkStatus.errors[E_PARAMETER]
+        if self.powerstat == S_STANDBY:
+            log.debug("Projector(%s).input_list() self.powerstat=%s" % (self.client_ip, PJLinkStatus.codes[self.powerstat]))
+            return PJLinkStatus.errors[E_UNAVAILABLE]
+        _r = []
+        for key in PJLinkStatus.inputs.keys():
+            _r.append(key)
+        log.debug('Projector(%s).input_list(): %s' % (self.client_ip, _r))
+        _r.sort()
+        return ' '.join(_r).strip()
+
+    def input_select(self, opt):
+        """input_select(opt)
+
+        Handles INPT requests.
+        """
+        log.info("Projector(%s).input_select(opt='%s')" % (self.client_ip, opt))
+        if self.PROJ_ERR is not None:
+            return self.PROJ_ERR
+        if opt == '?':
+            return self.source
+        elif self.powerstat != S_ON and self.powerstat != S_WARMUP:
+            log.debug("Projector(%s).input_select(opt=%s) Power state %s != %s" % \
+                (self.client_ip, opt, PJLinkStatus.codes[self.powerstat], PJLinkStatus.codes[S_ON]))
+            return PJLinkStatus.errors[E_UNAVAILABLE]
+        elif opt not in PJLinkStatus.inputs:
+            log.debug("Projector(%s).input_select(opt=%s) Invalid input" % (self.client_ip, opt))
+            return PJLinkStatus.errors[E_PARAMETER]
+        self.source = opt
+        return PJLinkStatus.errors[S_OK]

=== added file 'scripts/pyProjector/projector/servers/README.md'
--- scripts/pyProjector/projector/servers/README.md	1970-01-01 00:00:00 +0000
+++ scripts/pyProjector/projector/servers/README.md	2015-09-13 16:44:10 +0000
@@ -0,0 +1,73 @@
+This is the test server suite. New server personalities can be added here.
+
+Create a new server module in the test/servers directory.
+
+        - create/edit Your-New-Server.py
+
+Add the following minimum to your new Server.py file:
+    * Make sure __all__ is properly set with the following
+        values:
+    __all__ = [ 'Projector', 'BUFFER', 'PORT', 'TIMEOUT_MAX' ]
+
+    * Make sure you have the following variables available:
+    BUFFER = <size of input/output buffer>
+    PORT = <tcp port to use>
+    TIMEOUT_MAX = <float of maximum idle time for connection>
+
+    * Make sure you have the following base class:
+    class Projector(telnetlib.Telnet)
+
+In the Projector class, add the following minimum:
+
+    class Projector(telnetlib.Telnet)
+        def __init__(<base args>, user=None, password=None)
+        def client_connect(self, client, addr)
+        def client_disconnect(self, addr)
+        def stop(self)
+        def run(self)
+        def handle_request(self, data)
+
+client_connect(self, client, addr):
+
+    Used for initial authentication.
+    client: socket of new connection
+    addr: tuple of [ipaddr, port]
+
+    < Code for new connection. If needed, both authenticated
+      and non-authenticated connection requests>
+
+    Returns a tuple of
+    (Bool, String), where:
+        Bool:
+            True - Authorized connection
+            False - Failed authorization
+
+        String:
+            Either a string with initial command or None if no
+            initial command requested.
+
+client_disconnect(self, addr):
+    Cleanups when client disconnects
+    addr: tuple of [ipaddr, port]
+
+stop():
+    Cleanups when stop is requested and sets a notification that
+    a thread stop has been requested.
+
+run():
+    Called when thread starts. Make this a loop with a check
+    so cleanups can be done when system is shutdown. Typically
+    a while loop is used with a self.running variable check.
+    You can name your variable whatever you want, it's just best
+    to use a True/False variable so looping can halt when it's set
+    to False. Allows for cleanups when system is stopped.
+
+handle_request(data):
+    Called whenever data is received
+    data = command received from client
+
+Once those minimums are added, fill in the rest of the code to make
+your new projector emulator work.
+
+Be sure to add the proper import option in server-net.py file so your
+new module will be available.

=== added file 'scripts/pyProjector/projector/servers/__init__.py'
--- scripts/pyProjector/projector/servers/__init__.py	1970-01-01 00:00:00 +0000
+++ scripts/pyProjector/projector/servers/__init__.py	2015-09-13 16:44:10 +0000
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4

=== added file 'scripts/pyProjector/projector/servers/projectorbase.py'
--- scripts/pyProjector/projector/servers/projectorbase.py	1970-01-01 00:00:00 +0000
+++ scripts/pyProjector/projector/servers/projectorbase.py	2015-09-13 16:44:10 +0000
@@ -0,0 +1,362 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
+
+"""projectorbase.py: Base module for projector emulators"""
+
+__version__ = "0.0.1"
+_v = __version__.split(".")
+__version_hex__ = int(_v[0]) << 24 | \
+                  int(_v[1]) << 16 | \
+                  int(_v[2]) << 8
+
+# Define exportables for *
+__all__ = ['CR', 'LF', 'BUFFER', 'HEADER', 'PORT', 'SUFFIX', 'TIMEOUT_MAX',
+           'S_OK', 'E_OK', 'E_GENERAL', 'E_NOTCONNECTED', 'E_NETWORK', 'E_FAN',
+           'E_LAMP', 'E_TEMP', 'E_COVER', 'E_FILTER', 'E_AUTHENTICATION',
+           'E_UNDEFINED', 'E_PARAMETER', 'E_UNAVAILABLE', 'E_PROJECTOR',
+           'E_INVALID_DATA', 'E_WARN', 'E_ERROR', 'S_STANDBY', 'S_ON',
+           'S_WARMUP', 'S_COOLDOWN', 'S_NOT_CONNECTED', 'S_CONNECTING',
+           'S_STATUS', 'S_OFF', 'S_INITIALIZE', 'S_INFO', 'Status',
+           'ProjectorError', 'ProjectorNotConnectedError',
+           'ProjectorLampError', 'ProjectorFailedError',
+           'ProjectorNetworkError', 'ProjectorBase']
+
+import os
+import errno
+import logging
+import socket
+import sys
+import threading
+import time
+
+# Log should already be setup before getting here
+log = logging.getLogger(__name__)
+
+# Common codes
+log.debug('projectorbase: Setting constants')
+CR = chr(0x0D) # *nix \r
+LF = chr(0x0A) # *nix \n
+BUFFER = 140 # Default PJLink + 4
+HEADER = '%1' # Default PJLink class 1 header
+PORT = 4352 # Default PJLink
+SUFFIX = CR # Default PJLink
+TIMEOUT_MAX = 30.0 # Default PJLink
+
+# Enumeration of status and error codes used
+S_OK = 0
+E_OK = 0
+# Error codes. Start at 200 so we don't duplicate system error codes.
+E_GENERAL = 200 # Unknown error
+E_NOTCONNECTED = 201
+E_NETWORK = 202
+E_FAN = 203
+E_LAMP = 204
+E_TEMP = 205
+E_COVER = 206
+E_FILTER = 207
+E_AUTHENTICATION = 208 # PJLink ERRA
+E_UNDEFINED = 209      # PJLink ERR1
+E_PARAMETER = 210      # PJLink ERR2
+E_UNAVAILABLE = 211    # PJLink ERR3
+E_PROJECTOR = 212      # PJLink ERR4
+E_INVALID_DATA = 213
+E_WARN = 214
+E_ERROR = 215
+
+# Status codes. Start with 300 so we don't duplicate system error codes.
+S_STANDBY = 300        # PJLink power 0
+S_ON = 301             # PJLink power 1
+S_WARMUP = 302         # PJlink power 2
+S_COOLDOWN = 303       # PJLink power 3
+S_NOT_CONNECTED = 304
+S_CONNECTING = 305
+S_STATUS = 306
+S_OFF = 307
+S_INITIALIZE = 308
+S_INFO = 309
+
+# Enumeration classes
+class Status(object):
+
+    # Start at 200 so we don't cover system error codes
+    keys = { S_OK: 'S_OK',
+             E_GENERAL: 'E_GENERAL',
+             E_NOTCONNECTED: 'E_NOTCONNECTED',
+             E_NETWORK: 'E_NETWORK',
+             E_FAN: 'E_FAN',
+             E_LAMP: 'E_LAMP',
+             E_TEMP: 'E_TEMP',
+             E_COVER: 'E_COVER',
+             E_FILTER: 'E_FILTER',
+             E_AUTHENTICATION: 'E_AUTHENTICATION',
+             E_UNDEFINED: 'E_UNDEFINED',
+             E_PARAMETER: 'E_PARAMETER',
+             E_UNAVAILABLE: 'E_UNAVAILABLE',
+             E_PROJECTOR: 'E_PROJECTOR',
+             E_INVALID_DATA: 'E_INVALID_DATA',
+             E_WARN: 'E_WARN',
+             E_ERROR: 'E_ERROR',
+             S_STANDBY: 'S_STANDBY',
+             S_ON: 'S_ON',
+             S_WARMUP: 'S_WARMUP',
+             S_COOLDOWN: 'S_COOLDOWN',
+             S_NOT_CONNECTED: 'S_NOT_CONNECTED',
+             S_CONNECTING: 'S_CONNECTING',
+             S_STATUS: 'S_STATUS',
+             S_OFF: 'S_OFF',
+             S_INITIALIZE: 'S_INITIALIZE',
+             S_INFO: 'S_INFO'
+             }
+
+    codes = { S_OK: 'OK',
+              E_OK: 'OK',
+              E_GENERAL: "General projector error",
+              E_NOTCONNECTED: "Not connected error",
+              E_NETWORK: "Network error",
+              E_LAMP: "Lamp error",
+              E_FAN: "Fan error",
+              E_TEMP: "High temperature detected",
+              E_COVER: "Cover open detected",
+              E_FILTER: "Check filter",
+              E_AUTHENTICATION: "Authentication Error",
+              E_UNDEFINED: "Undefined Command",
+              E_PARAMETER: "Invalid Parameter",
+              E_UNAVAILABLE: "Projector Busy",
+              E_PROJECTOR: "Projector/Display Error",
+              E_INVALID_DATA: "Invald packet received",
+              E_WARN: "Warning condition detected",
+              E_ERROR: "Error condition detected",
+              S_NOT_CONNECTED: "Not connected",
+              S_CONNECTING: "Connecting",
+              S_STATUS: "Getting status",
+              S_OFF: "Off",
+              S_INITIALIZE: "Initialize in progress",
+              S_STANDBY: "Power standby",
+              S_WARMUP: "Warmup in progress",
+              S_ON: "Power on",
+              S_COOLDOWN: "Cooldown in progress",
+              S_INFO: "Projector Information availble"
+              }
+
+# Error classes
+class ProjectorError(Exception):
+    """
+    Base projector error class
+    """
+    def __init__(self, errno=None, message=None):
+        self.errno = errno if errno is not None else E_GENERAL
+        self.message = message if message is not None else 'General projector error'
+    def __repr__(self):
+        return ((self.errno, self.message))
+    def __str__(self):
+        return self.message
+
+class ProjectorNotConnectedError(ProjectorError):
+    """
+    Projector not connected
+    """
+    def __init__(self, errno=E_NOTCONNECTED,
+                       message=u'Projector Not Connected'):
+        super(ProjectorNotConnectedError, self).__init__(errno, message)
+
+class ProjectorLampError(ProjectorError):
+    """
+    Lamp failure
+    """
+    def __init__(self, errno=E_LAMP,
+                       message=u'Lamp Failure'):
+        super(ProjectorLampError, self).__init__(errno, message)
+
+class ProjectorFailedError(ProjectorError):
+    """
+    General projector failure
+    """
+    def __init__(self, errno=E_GENERAL,
+                       message=u'Unknown projector failure'):
+        super(ProjectorFailedError, self).__init__(errno, message)
+
+class ProjectorNetworkError(ProjectorError):
+    """
+    Network failure
+    """
+    def __init__(self, errno=E_NETWORK,
+                       message=u'Projector network error'):
+        super(ProjectorNetworkError, self).__init__(errno, message)
+
+class ProjectorFanError(ProjectorError):
+    """
+    Fan failure
+    """
+    def __init__(self, errno=E_FAN,
+                       message=u'Projector fan failure'):
+        super(ProjectorFanError, self).__init__(errno, message)
+
+log.info('Creating base projector class')
+class ProjectorBase(threading.Thread):
+    def __init__(self,
+                group=None, # Thread required - defined as None at this time
+                target=None, # Thread required
+                name='ProjectorBase',
+                user=None,
+                password=None,
+                host=None,
+                port=PORT,
+                args=[],
+                kwargs={}
+                ):
+        super(ProjectorBase, self).__init__(group, target, name, args, kwargs)
+        self.class_ = '1' # Current PJLink class defined
+        self.connected = False
+        self.connecting = False
+        self.host = host
+        self.name = name
+        self.password = password
+        self.port = port
+        self.prefix = '%' # PJlink command prefix
+        self.running = False # Keep track of thread running status
+        self.status = 0
+        self.status_error = None
+        self.status_message = ''
+        self.status_time = time.time()
+        self.suffix = CR # Default PJLink suffix
+        self.user = user
+        self.header = self.prefix + self.class_
+
+    # Required methods to override
+    def run(self):
+        """ProjectorBase.run()
+
+        Method used to initialize and run thread.
+        """
+        self.__run = False
+        assert self.__run, "You must override this method."
+
+    def client_connect(self):
+        """ProjectorBase.sock_connect()
+
+        Method used to connect to projector.
+        You must define steps for authenticated and unauthenticated connections.
+        """
+        self.__connect = False
+        assert self.__connect, "You must override this method."
+
+    # Methods that should not need to be overridden
+
+    def _set_status(self, code=None, message=None):
+        if self.status_error is not None:
+            log.debug('%s(%s): _set_status(self.status_error)' % (self.name, self.host))
+            _code = self.status_error.errno
+            _msg = self.status_error.message
+        elif code == self.status:
+            # No status change
+            return
+        else:
+            log.debug("%s(%s): _set_status(code=%s, message='%s')" % \
+                      (self.name, self.host, code, message))
+            _code = code
+            if code in Status.codes:
+                _msg = Status.codes[code]
+            else:
+                _msg = message
+
+        self.status = _code
+        self.status_message = _msg
+
+        """
+        If a callback function is registered, then send status update.
+        """
+        if self.callback is None:
+            return
+
+        if self.callback_code == self.status and time.time() < self.status_time:
+            return
+
+        self.callback((self.status, self.status_message))
+        self.callback_code = self.status
+        self.status_time = time.time() # Last time callback was called
+        return
+
+    def send(self, data, timeout=5.0):
+        """ProjectorBase.send()
+
+        Send a packet of data. Assumes telnet protocol.
+        """
+        log.debug('%s(%s).send() called' % (self.name, self.host))
+        if self.status_error is not None:
+            log.debug("%s(%s): Error status code %s" % (self.host, self.port,
+                       self.status_error.errno))
+            return
+
+        if self.socket is None and not self.connecting:
+            log.debug("%s(%s).send(): Reconnecting" % (self.name, self.host))
+            self.sock_connect()
+
+        try:
+            log.debug("%s(%s).send(data='%s')" % (self.name, self.host, data))
+            self.socket.write("%s%s%s" % (self.header, data, self.SUFFIX))
+        except socket.error, e:
+            try:
+                _errno = e.errno
+            except AttributeError:
+                _errno = None
+
+            log.debug('%s(%s).send(): Socket error (%s) %s' % \
+                      (self.name, self.host, e.errno, e.message))
+            self.status_error = ProjectorNetworkError(errno=_errno,
+                                                      message=e.message)
+
+    def recv(self, search, timeout=5.0):
+        """ProjectorBase.recv()
+
+        Receive data. Assumes telnet protocol.
+        """
+        log.debug('%s(%s).recv(): Fetching data' % (self.name, self.host))
+        if self.status_error is not None:
+            return None
+
+        if self.socket is None:
+            log.debug('ProjectorBase.recv(%s): No socket open' % self.host)
+            self.status_error = ProjectorNotConnectedError(
+                errno=E_NOTCONNECTED,
+                message=u'Socket not open')
+            self._set_status()
+
+        try:
+            for i in range(5):
+                # Clean out empty lines
+                data = self.socket.read_until(search, timeout)
+                if len(data) < 3:
+                    continue
+                else:
+                    break
+            if data is None:
+                self.socket.close()
+                self.socket=None
+                self.status_error = ProjectorNetworkError(errno=E_NOTCONNECTED,
+                    message=u'%s(%s): Socket closed by other side' % (self.name, self.host))
+        except socket.timeout, e:
+            log.debug("%s(%s)_recv(): Timeout error: %s" % (self.name, self.host, e.message))
+            self.socket.close()
+            self.socket = None
+            self.status_error = ProjectorNetworkError(errno=e.errno,
+                message=u"%s(%s): Timeout error: %s" % (self.name, self.host, e.message))
+        except socket.error, e:
+            log.debug("%s(%s)_recv(): Socket error: %s" % (self.name, self.host, e.message))
+            self.socket.close()
+            self.socket = None
+            self.status_error = ProjectorNetworkError(errno=e.errno,
+                message=u"%s(%s): Socket error: %s" % \
+                        (self.name, self.host, e.message))
+        except EOFError, e:
+            log.debug("%s(%s)_recv(): Socket closed by other side: %s" % (self.name, self.host, e.message))
+            self.socket.close()
+            self.socket=None
+            self.status_error = ProjectorNetworkError(errno=E_NOTCONNECTED,
+                message=u'%s(%s): Socket closed by other side' % (self.name, self.host))
+
+        if self.status_error is not None:
+            self._set_status()
+            return None
+        else:
+            log.debug("%s(%s).recv() returning '%s'" % (self.name, self.host, data.strip()))
+            return data.strip()

=== added file 'scripts/pyProjector/projector/test_server.old.py'
--- scripts/pyProjector/projector/test_server.old.py	1970-01-01 00:00:00 +0000
+++ scripts/pyProjector/projector/test_server.old.py	2015-09-13 16:44:10 +0000
@@ -0,0 +1,392 @@
+#!/usr/bin/env python
+"""test_server.py
+
+Test server for checking PJLink controllers.
+Warning: Not multi-connection safe - use only one instance at a time
+"""
+
+import thread
+import threading
+import random
+import time
+import sys
+from time import sleep
+from socket import *
+from hashlib import md5
+
+# Static defines
+BUFFER = 136 # Maximum packet size including \n
+HOST = '127.0.0.1'
+SERVICE = { 'PORT': 4352,
+            'NAME': 'pjlink'
+          }
+PJLINK_PREFIX = "%"
+PJLINK = { "CLASS": '1',
+           "COMMANDS": { '1': ['POWR',
+                               'INPT',
+                               'AVMT',
+                               'ERST',
+                               'LAMP',
+                               'INST',
+                               'NAME',
+                               'INF1',
+                               'INF2',
+                               'INFO',
+                               'CLSS'
+                              ]
+                       }
+         }
+
+PJLINK_ERR = { 'OK': "Success",
+               'ERR1': "Undefined command",
+               'ERR2': "Out of parameter",
+               'ERR3': "Unavailable time",
+               'ERR4': "Projector/Display error",
+               0: 'OK',
+               1: 'ERR1',
+               2: 'ERR2',
+               3: 'ERR3',
+               4: 'ERR4',
+               10: "Empty packet",
+               11: "Invalid prefix",
+               12: "Invalid class",
+               13: "Invalid packet"
+               }
+
+TIMEOUT_MAX = 30.0 # Number of idle seconds before forced close
+CR = chr(0x0D) # \r
+LF = chr(0x0A) # \n
+CRLF = CR + LF
+
+# Authentication purposes
+PASSWD = None
+
+# Dictionary of commands mapped to functions
+PJLINK_CMD = {}
+
+projector = None # used for class Projector
+class Projector(threading.Thread):
+    """Threaded projector class
+
+    Acts as a single projector that will update itself outside of the socket
+    thread.
+    """
+    def __init__(self, group=None,
+                       target=None,
+                       name="ProjectorThread",
+                       args=[], kwargs={}):
+        super(Projector, self).__init__(group, target, name, args, kwargs)
+        self.name = name
+        self.info = { 'NAME': "DefaultProjector", # Can be changed"
+                      'INF1': "OpenLP",
+                      'INF2': "PythonProjector",
+                      'INFO': "Model PS01 Serial P274001"
+                    }
+        self.model = 'PS01' # Python single lamp model 1
+        self.other = "Serial number P274001"
+        self.lockedPower = threading.Lock() # Power changes
+        self.lockedVideo = threading.Lock() # Video changes
+        self.lockedLamp = threading.Lock()  # Lamp changes
+        self.PROJECTOR_ERROR = None # ERR3 or ERR4
+        # See self.powerstat for status,fan values
+        self.lamp = { 0: {"status": '0',
+                          "time": random.randint(0, 30000), # time in minutes
+                          "clock": 0, # Clock time when lamp was turned on
+                          "fan": '0'
+                          }
+                    }
+        self.time = 0
+        for i in self.lamp:
+            # Random failure of lamp on startup
+            self.lamp[i]['status'] = 4 if int(random.random()) == 1 \
+                                        else 0
+            if self.lamp[i]['status'] > 3: self.PROJECTOR_ERROR = PJLINK_ERR[4]
+            self.time = self.lamp[i]['time'] \
+                if self.time < self.lamp[i]['time'] \
+                else self.time
+        self.time = self.time + 60 # 1 hour longer than longest lamp time
+        # self.powerstat
+        #   0 = off
+        #   1 = startup
+        #   2 = standby
+        #   3 = warmup - check self.lamp
+        #   4 = on
+        #   5 = cooldown check self.lamp
+        #   6 = warning
+        #   7 = error
+        self.powerstat = 0
+        self.powerup = 0 # Timer startup/lamp on/lamp off
+        self.audio_mute = False # False=aduio out True=audio mute
+        self.video_mute = False # False=open True=closed
+        self.video = '11'
+        self.video_select = { 1: 4, # 1=vga, 2=rgb1, 3=rgb2, 4=dvi-a
+                              2: 3, # 1=composite, 2=svideo, 3=SCART
+                              3: 3, # 1=dvi-i, 2=dvi-d, 3=hdmi
+                              4: 0, # 1=usb input, 2=internal storage input
+                              5: 0,  # 1=network input
+                              '11': "Computer RGB",
+                              '12': "Analog RGB",
+                              '13': "SCART1",
+                              '14': "DVI-A",
+                              '21': "Composite Video",
+                              '22': "S-Video",
+                              '23': "SCART2",
+                              '31': "DVI-I",
+                              '32': "DVI-D",
+                              '33': "HDMI"
+                            }
+        self.temp = 0 # 0=ok, 1=warning 2=error
+        self._filter = 0 # 0=ok, 2=warning, 3=error
+
+    def stop(self):
+        self._running = False
+
+    def run(self):
+        """Start projector and maintain projector status"""
+        self._running = True
+        print "%s Projector class run called" % self.name
+        self.PROJECTOR_ERROR = PJLINK_ERR[3]
+        print "%s Projector status 1: startup" % self.name
+        self._powerchange('1')
+        while self._running:
+            sleep(2) # 2 second sleep interval between updates
+            # Check for powerup status
+            if self.powerup is not None and self.powerup < time.time():
+                # Time to change status
+                print "Projector.run(): Changing powerup state"
+                if self.powerstat == '5':
+                    self._powerchange(2)
+                else:
+                    self._powerchange(str(int(self.powerstat) + 1))
+
+    def handle_request(self, cmd, opt):
+        print "Projector.handle_request(cmd='%s' opt='%s'" % (cmd, opt)
+        if self.PROJECTOR_ERROR is not None:
+            return self.PROJECTOR_ERROR
+        if not cmd in PJLINK["COMMANDS"]['1']:
+            return PJLINK_ERR[1]
+
+        elif cmd == "POWR":
+            return self.power(opt)
+
+    def _lampchange(self, lamp, opt):
+        """Projector._lampchange(): Change lamp and fan state"""
+        # Called by self._powerchange
+        # See 234567 of self.powerstat
+        print "Projector._lampchange(opt='%s')" % opt
+        if lamp['status'] == '7' or lamp['fan'] > '7':
+            # Error condition
+            return PJLINK_ERR[4]
+
+        with self.lockedLamp:
+            if opt == '2':
+                # Lamp off
+                if lamp['clock'] is not None:
+                    _c = int(time.time() - lamp['clock']) / 60
+                    lamp['time'] = lamp['time'] + _c
+                    lamp['clock'] = None
+                    lamp['fan'] = 2
+
+            elif opt == '3':
+                # Lamp warmup to on
+                lamp['clock'] = time.time()
+                lamp['fan'] = 4
+                
+            lamp['status'] = opt
+
+        return PJLINK_ERR[0]
+
+    def _powerchange(self, opt):
+        """Projector._poewrchange(): Change power state"""
+        print "Projector._powerchange(opt='%s')" % opt
+        # Called by 
+        if not opt in '012345': return 
+        with self.lockedPower:
+            if opt == '1':
+                self.PROJECTOR_ERROR = PJLINK_ERR[3] \
+                    if self.PROJECTOR_ERROR != PJLINK_ERR[4] \
+                    else self.PROJECTOR_ERROR
+            if opt in '135':
+                self.powerup = time.time() + 30.0 \
+                    if opt == 1 \
+                    else time.time() + 60
+            elif opt in '024':
+                self.powerup = None
+                self.PROJECTOR_ERROR = None \
+                    if self.PROJECTOR_ERROR != PJLINK_ERR[3] \
+                        or self.PROJECTOR_ERROR != PJLINK_ERR[4] \
+                    else self.PROJECTOR_ERROR
+            self.powerstat = opt
+
+        r = PJLINK_ERR[0]
+        for i in self.lamp:
+            j = self._lampchange(self.lamp[i], opt)
+            r = j if r != PJLINK_ERR[4] else r
+        return r
+
+    def power(self, opt=None):
+        """Set/get power status - Called from self.handle_request()"""
+        print "Projector.power(opt='%s')" % opt
+        # Initial error checks
+        try:
+            if opt not in '01?':
+               return PJLINK_ERR[2]
+        except TypeError:
+            return PJLINK_ERR[2]
+        # Status query
+        if self.PROJECTOR_ERROR is not None:
+            return self.PROJECTOR_ERROR
+        elif opt == '?':
+            print 'Projector.power() self.powerstat = %s' % self.powerstat
+            if self.powerstat == 2: return '0'
+            elif self.powerstat == 4: return '1'
+            elif self.powerstat == 5: return '2'
+            elif self.powerstat == 3: return '3'
+            elif self.powerstat >= 6: return PJLINK_ERR[4]
+
+        # Time to change power
+        if self.powerstat == 1 and self.powerup is not None: return PJLINK_ERR[3]
+        elif self.powerstat in '35' and self.powerup is not None: return PJLINK_ERR[0]
+
+        if opt == '1':
+            if self.powerstat == 4:
+                # Already on
+                return PJLINK_ERR[0]
+            elif self.powerstat == 5: 
+                # Power up during cooldown
+                return PJLINK_ERR[3]
+            elif self.powerstat == 2:
+                # Set to warmup
+                return self._powerchange('3')
+        elif opt == '0':
+            if self.powerstat == 2: 
+                # Aldready in standby
+                return PJLINK_ERR[0]
+            elif self.powerstat == 3:
+                # Power off during warmup
+                return PJLINK_ERR[3]
+            elif self.powerstat == 4:
+                # Set to cooldown
+                return self._powerchange('5')
+
+def logme(data):
+    """Print socket logging info to console"""
+    print repr(addr) + " " + data
+
+def getRandom():
+    """Return an 8-char random string"""
+    return str(hex(random.randint(268435456, 4294967295L)))[2:-1]
+
+def send(data):
+    """Send data to client socket"""
+    clientsock.send(data + CRLF)
+
+def handler(clientsock, addr, seed=None, hashed=None):
+    """Socket handler"""
+    print "New thread addr='%s' seed='%s' hashed='%s" % \
+        (repr(addr), seed, hashed)
+    clientsock.settimeout(TIMEOUT_MAX)
+    handler_running = True
+    while handler_running:
+        RESPONSE = None
+        ERR = 0 # > 10 non-standard error for internal use
+        try:
+            d = clientsock.recv(BUFFER + 1) # Allow for braindamaged \r
+            data = d.strip((CR))
+            if not data: break
+            if '\xff\xf4\xff\xfd\x06' in data:
+                # ^C pressed - close connection
+                print repr(addr) + " Received ^C"
+                break
+            logme("Received: '%s' " % repr(data))
+        except timeout, e:
+            break
+        # OK - got data, time to verify if proper format
+        # If invalid prefix - ignore
+        if len(data) <= 1:
+            ERR = 10
+        elif len(data) <= 8 or len(data) > BUFFER:
+            # Invalid packet
+            logme("Invalid packet length: %s data: '%s' " % \
+                (len(data), repr(data)))
+        elif data[0] != PJLINK_PREFIX:
+            # Invalid start character - ignore
+            logme("Invalid prefix: %s" % data[0])
+            ERR = 11 # Invalid prefix
+        elif not data[1] in PJLINK['CLASS']:
+            # invalid class = send ERR1
+            logme("Invalid class: %s " % data[1])
+            ERR = 12 # Invalid class
+        elif data[6] != ' ':
+            ERR = 1
+        else:
+            # Basic packet checks OK - process rest of command
+            pass
+
+        # End of checks - time to send back
+        if ERR == 0:
+            # packet checks OK
+            pass
+        elif ERR == 10 and RESPONSE is None:
+            # Invalid packet - ignore
+            logme("Blank packet received")
+            send('')
+            continue
+        elif ERR > 10:
+            send("%s=%s" % (data[:6].strip(), PJLINK_ERR[1]))
+            continue
+        elif ERR >= 1 and ERR in PJLINK_ERR:
+            send("%s=%s" % (data[:6].strip(), PJLINK_ERR[ERR]))
+            continue
+        else:
+            logme("Unknown error %s" % ERR)
+            continue
+
+        cmd = data[2:6].upper().strip()
+        cls = data[1]
+        response = projector.handle_request(cmd, data[7:].strip())
+        data = data[:2].strip() + cmd + "=" +response + CRLF
+        logme("Sending %s" % data)
+        send(data)
+    # Done with the socket - time to cleanup
+    clientsock.close()
+    print "Connection closed addr=" + repr(addr)
+    if not handler_running:
+        print "handler terminated"
+
+if __name__ == '__main__':
+    # Start the projector
+    if projector is None:
+        projector = Projector()
+        projector.start()
+    # Start the socket service
+    ADDR = (HOST, SERVICE['PORT'])
+    serversock = socket(AF_INET, SOCK_STREAM)
+    serversock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
+    serversock.bind(ADDR)
+    serversock.listen(1)
+    main_running = True
+    while main_running:
+        try:
+            print "Waiting for connection ... listening on port" + \
+                str(SERVICE["PORT"])
+            clientsock, addr = serversock.accept()
+            print "Connected from:" + repr(addr)
+            if PASSWD is None:
+                mainthread = thread.start_new_thread(handler, (clientsock, addr))
+            else:
+                rand = getRandom()
+                hashed = md5()
+                hashed.update(rand)
+                hashed.update(PASSWD)
+                thread.start_new_thread(handler,
+                                        (clientsock,
+                                            addr,
+                                            rand,
+                                            hashed.hexdigest())
+                                        )
+        except KeyboardInterrupt:
+            print "Keyboard interrupt - stopping"
+            projector.stop()
+            handler_running = False
+            main_running = False


Follow ups