#!/usr/bin/python

'''
printermsg.py -- Display messages on networked HP printers

Run "printermsg.py --help" to see usage information.  This script
has only been tested under Python 2.5 (but will likely work on 2.4).

Please don't abuse this.  Eric Hayes might get annoyed.
'''

# Copyright (c) 2008 Thomas W. Most
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.


import sys
import time
import random
import socket
import textwrap
import optparse


# Just guessing at what's allowed here
SAFE_CHARS = set(
		"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
		"abcdefghijklmnopqrstuvwxyz"
		"1234567890!?-'\\/_,.: "
)

# Takes a single % argument
COMMAND = ('\x1B%%-12345X@PJL RDYMSG DISPLAY = '
			'"%s"\r\n\x1B%%-12345X\r\n')
DEFAULT_PORT = 9100


def send_message(sock, message, wrap=0, take_last=0):
	'''
	Send the message to the socket, optionally wrapping
	to `wrap` characters (0 means no wrap).  Use 
	`take_last` to display only the last n wrapped lines,
	or specify ``0`` to have the text overflow if it is
	too long.
	'''
	if wrap:
		lines = textwrap.wrap(message, wrap)
		if take_last:
			lines = lines[-take_last:]
		message = ''.join(l.lstrip().ljust(wrap) for l in lines)
	try:
		sock.send(COMMAND % message)
	except socket.error, err:
		sys.stderr.write("Error sending message on %s: %s" % (sock, err))


def scroll_message(sockets, message, cols):
	'''
	Scroll the `message` across the displays of the
	printer at each socket in `sockets`.  `cols`
	is the length of a single line on each printer's
	display.
	'''
	message = message.ljust(cols) + (" " * cols)
	
	try:
		while True:
			m = message[:cols]
			for socket in sockets:
				send_message(socket, m)
			message = message[1:] + message[0]
			print message
			time.sleep(.2)
	except KeyboardInterrupt:
		for s in sockets: send_message(s, "")
		print "Animation terminated."


def type_message(sockets, message, rows, cols):
	'''
	Types out the `message` on each of `sockets`.
	'''
	chars = list(message)
	current = []
	while chars:
		current.append(chars.pop(0))
		m = ''.join(current)
		for s in sockets:
			send_message(s, m, cols, rows)
		print m
		time.sleep(.2)


def main(arguments=sys.argv[1:]):
	usage = "usage: %prog [options] printer-host[:port] ..."
	parser = optparse.OptionParser(usage=usage, version="%prog 1.0")
	parser.add_option("-m", "--message", dest="messages",
		metavar="MESSAGE", action="append", 
		help="A message to display.  If more than one is specified, "
			"one will be chosen randomly from among those provided.")
	parser.add_option("-d", "--display",
		default="wrap",
		help="How to display: static, wrap, typewriter or scroll. "
			"(Specifying keyboard or scroll will result in a long-"
			"running process.) [default: %default]")
	parser.add_option("-p", "--postfix",
		default="",
		help="Text to append to all printer-host hostnames")
	parser.add_option("-c", "--cols", 
		default="16",
		help="The character length of the printer's display.")
	parser.add_option("-r", "--rows",
		default="2",
		help="The number of character rows on the printer's display.")
	options, args = parser.parse_args(arguments)
	
	messages = []
	for m in (options.messages or ()):
		messages.append(''.join(c for c in m if c in SAFE_CHARS))
	
	if not messages:
		messages.append("TOUCH ME")
	
	try:
		cols = int(options.cols)
		if cols <= 0:
			raise ValueError
	except ValueError:
		parser.error("%s is not valid for --cols")
	
	try:
		rows = int(options.rows)
		if rows <= 0:
			raise ValueError
	except ValueError:
		parser.error("%s is not valid for --rows")
	
	
	if len(args) < 1:
		parser.error("you must specify at least one printer-host")
	
	sockets = []
	for host in args:
		if ":" in host:
			host, port = host.rsplit(':', 1)
			try:
				port = int(port)
			except ValueError:
				parser.error("%s is an invalid port number" % port)
		else:
			port = DEFAULT_PORT
		if options.postfix:
			host = host + options.postfix
		
		try:
			s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
			s.connect((host, port))
		except socket.error:
			sys.stderr.write("Unable to connect to %s:%s\n" % (
							host, port))
		else:
			sockets.append(s)
	
	if options.display in ("static", "wrap"):
		for s in sockets:
			send_message(s, random.choice(messages),
				cols if options.display == "static" else 0)
	elif options.display == "typewriter":
		type_message(sockets, random.choice(messages), rows, cols)
	elif options.display == "scroll":
		scroll_message(sockets, random.choice(messages), cols)
	else:
		parser.error("%s is not a valid display value" % 
				options.display)


if __name__ == '__main__':
	main()