diff --git a/screenless/__init__.py b/screenless/__init__.py index b633544..a50a018 100644 --- a/screenless/__init__.py +++ b/screenless/__init__.py @@ -1,13 +1,13 @@ import sys -from . import officemgr +from . import mgmt def main(args=None): if args is None: args = sys.argv[1:] - print("starting Screenless Office...") - mgr = officemgr.OfficeManager() + print("initializing Screenless Office...") + mgr = mgmt.Management() - print("starting Office Manager...") + print("starting Management...") mgr.run() diff --git a/screenless/bureau/audio/audio.py b/screenless/bureau/audio/audio.py index 6d6574d..7b2cf42 100644 --- a/screenless/bureau/audio/audio.py +++ b/screenless/bureau/audio/audio.py @@ -1,4 +1,4 @@ -import subprocess +import vlc from bureau import Bureau, add_command @@ -17,7 +17,7 @@ class Audio(Bureau): Bureau.__init__(self) self.urldb = self.open_db("urldb") - subprocess.call(["mocp", "-S"]) + self.player = vlc.MediaPlayer() @add_command("p", "Play an album, track or a live stream.") def play(self, data): @@ -31,44 +31,58 @@ class Audio(Bureau): url = self.urldb.get(shortcode) self.log.debug(" playing url " + url) - subprocess.call(["mocp", "-c"]) - subprocess.call(["mocp", "-a", url]) - subprocess.call(["mocp", "-p"]) + self.player.set_mrl(url) + self.player.play() @add_command("stop", "Halt audio playback.") def stop(self): """ Stops all audio currently playing audio output. """ - subprocess.call(["mocp", "-P"]) + self.player.pause() @add_command("resu", "Resume playback.") def resume(self): """ Resume playback of paused audio. """ - subprocess.call(["mocp", "-U"]) + self.player.play() @add_command("next", "Play the next song.") def play_next(self): """ Skip to the next song in the playlist or album. """ - subprocess.call(["mocp", "-f"]) + #subprocess.call(["mocp", "-f"]) + # TODO + pass @add_command("prev", "Play the previous song.") def play_prev(self): """ Skip to the previous song in the playlist or album. """ - subprocess.call(["mocp", "-r"]) + #subprocess.call(["mocp", "-r"]) + # TODO + pass @add_command("nowp", "Now Playing") def now_playing(self): """ Prints the currently playing song or stream on the small printer. """ - out = subprocess.check_output(["mocp", "-i"]).decode("utf-8") + #out = subprocess.check_output(["mocp", "-i"]).decode("utf-8") + # TODO: sort out how to do this with + out = "Now Playing: " + out += self.player.get_media().get_meta(vlc.Meta.Title) + "\n" + nowplaying = self.player.get_media().get_meta(vlc.Meta.NowPlaying) + if nowplaying == "": + out += "by " + self.player.get_media().get_meta(vlc.Meta.Title) + "\n" + out += "from the album '" + self.player.get_media().get_meta(vlc.Meta.Album) \ + + "'\n" + else: + out += nowplaying + "\n" + self.log.debug("info output:" + out) self.print_small(out) diff --git a/screenless/bureau/bureau.py b/screenless/bureau/bureau.py index 7324bb8..b3bb788 100644 --- a/screenless/bureau/bureau.py +++ b/screenless/bureau/bureau.py @@ -7,9 +7,9 @@ import os.path import random import string import subprocess -import sys import tempfile import textwrap +#import traceback import threading import lmdb @@ -60,6 +60,19 @@ def add_api(apistr, name=""): return decorator +#def log_traceback(func): +# """ this is a decorator that catches tracebacks for logging""" +# def wrapper(*args): +# my_bureau = args[0] +# try: +# func(*args) +# except Exception as e: +# my_bureau.log.error("CRASH TRACE: {0}".format(my_bureau.name), +# exc_info=e) +# raise +# return wrapper + + class LogPrinter(logging.Handler): """ LogPrinter prints logs on a receipt printer for screenless debugging. @@ -151,11 +164,6 @@ class Bureau(object): self.api = {} modpath = os.path.dirname(__file__) - #slimerjs = os.path.join(modpath, "..", "lib", "slimerjs", "slimerjs") - #renderer = os.path.join(modpath, "..", "slimerjs", "rasterize.js") - #self.slimerjs = os.path.abspath(slimerjs) - #self.html2pdf = self.slimerjs + " --headless " + \ - # os.path.abspath(renderer) + " " mypath = inspect.getfile(self.__class__) self.mdir = os.path.dirname(mypath) @@ -181,7 +189,6 @@ class Bureau(object): log_format = logging.Formatter('LOG ${levelname} $name: $message', style='$') log_printer.setFormatter(log_format) self.log.addHandler(log_printer) - sys.excepthook = self._log_exception # setup a dir to store files and data self.datadir = os.path.join(basepath, self.prefix) @@ -200,9 +207,9 @@ class Bureau(object): self.log.debug("commands: ") self.log.debug(str(self.commands)) - def _log_exception(typ, value, tb): - self.log.error("CRASH TRACE: {0}".format(str(value)), exc_info=(typ, value, tb)) - sys.__excepthook__(typ, value, tb) +# def _log_exception(typ, value, tb): +# self.log.error("CRASH TRACE: {0}".format(str(value)), exc_info=(typ, value, tb)) +# sys.__excepthook__(typ, value, tb) def open_db(self, name): """ @@ -346,10 +353,15 @@ class Bureau(object): """ # TODO: make printer id/width configured and easy prn = printer.Usb(0x416, 0x5011, in_ep=0x81, out_ep=0x03) - im = PIL.Image.open(img) + + if type(img) is PIL.Image.Image: + im = img + else: + im = PIL.Image.open(img) + # NOTE: might be worth tring to push up brightness im = PIL.ImageOps.equalize(im) # stretch histogram for nicer dither - im.thumbnail((576, 576), PIL.Image.ANTIALIAS) # resize to fit printer + im.thumbnail((576, 1024), PIL.Image.ANTIALIAS) # resize to fit printer prn.image(im, impl="bitImageColumn") # not using this impl crashes ?? @add_command("test") @@ -361,6 +373,16 @@ class Bureau(object): print(("hi! testing. " + self.name + " bureau seems to work!")) return "seems to work." + def _run_io(self): + """ + wrapper for run_io so that we can catch threaded exceptions and log + """ + try: + self.run_io() + except Exception as err: + self.log.exception("%s CRASHED with %s\n", self.name, err) + raise + def run_io(self): """process hardware or timed input @@ -375,6 +397,16 @@ class Bureau(object): pass def run(self): + """ + wrapper running the main loop and logging all exceptions + """ + try: + self._run() + except Exception as err: + self.log.exception("%s CRASHED with %s\n", self.name, err) + raise + + def _run(self): """ main loop for processing messages @@ -382,7 +414,7 @@ class Bureau(object): """ # start the hardware input handler - io_handler = threading.Thread(target=self.run_io) + io_handler = threading.Thread(target=self._run_io) io_handler.start() # register commands and api methods @@ -398,10 +430,6 @@ class Bureau(object): msg = self._recv.recv_string(flags=zmq.NOBLOCK) else: continue - # msg = self._recv.recv_string(flags=zmq.NOBLOCK) - #except zmq.ZMQError: - # time.sleep(0.05) # don't waste CPU - # continue try: self.log.debug("got message:" + msg) dot = msg.find(".") @@ -411,7 +439,6 @@ class Bureau(object): self.log.debug("dot at %d", dot) # TODO: maybe trim off the trailing "." for convenience data = msg[dot + 1:] - # data = str(data) # force to be a string else: data = None self.log.debug("data: " + str(data)) diff --git a/screenless/bureau/jokes/jokes.py b/screenless/bureau/jokes/jokes.py index bedbd97..d614f68 100644 --- a/screenless/bureau/jokes/jokes.py +++ b/screenless/bureau/jokes/jokes.py @@ -1,6 +1,13 @@ +import glob +import math +import mimetypes +import os import subprocess +import urllib -from bureau import Bureau, add_command +from PIL import Image + +from bureau import Bureau, add_command, add_api class Humor(Bureau): @@ -24,6 +31,59 @@ class Humor(Bureau): jux = str(subprocess.check_output("/usr/games/fortune"), encoding="UTF-8") self.print_small(jux) + @add_api("gif", "Moving Picture") + def print_gif(self, data): + """ + Prints out a series of image frames which can be viewed in lively + motion on any standard zoetrope. (Ø200mm) + """ + # download the video file + d_url = data["url"] + filename, headers = urllib.request.urlretrieve(d_url) + print("fetching", d_url, filename) + + # make sure we have a legit filename + ext = mimetypes.guess_extension(headers["Content-Type"]) + if not filename.endswith(ext): + os.rename(filename, filename + ext) + filename = filename + ext + print("renamed to ", filename) + + # if we have something that's a gif or webp (png?) then + # just print 12 frames + if filename.endswith(("gif", "webp")): + print("gif stuff") + img = Image.open(filename) + print(img.n_frames) + grab_frame = 0 + out_frame = 0 + in_len = float(img.n_frames) + # TODO: deal with frame counts lower than 12 + # and maybe a different algo that includes endpoints + for frame in range(img.n_frames): + img.seek(frame) + if grab_frame == frame: + img_rotated = img.rotate(90, expand=True) + self.print_small_image(img_rotated) + out_frame += 1 + grab_frame = math.ceil(out_frame * in_len / 12) + else: + # how many frames do we have? + cli = "ffprobe -i " + filename + \ + " -show_format -v quiet | sed -n 's/duration=//p'" + vid_len = str(subprocess.check_output(cli), encoding="UTF-8") + print("video len: " + vid_len) + # TODO: if vid_len is not a number handle this error + + # lengthen/shorten it to 12 frames + # dump frames to temp files + cli = "ffmpeg -i" + filename + " -vf fps=12/" + vid_len +\ + " thumb%02d.png" + + # print em out! + + self.print_small("") + def main(): ha = Humor() diff --git a/screenless/bureau/publications/article.html b/screenless/bureau/publications/article.html index 414cf34..1ce6d9a 100644 --- a/screenless/bureau/publications/article.html +++ b/screenless/bureau/publications/article.html @@ -49,6 +49,10 @@ width: 50%; height: 50%; } + .endnotebc { + width: 50%; + height: 50%; + }

${title}

diff --git a/screenless/bureau/publications/publications.py b/screenless/bureau/publications/publications.py index 6b7c6de..83b4799 100644 --- a/screenless/bureau/publications/publications.py +++ b/screenless/bureau/publications/publications.py @@ -175,7 +175,7 @@ class Publications(Bureau): #TODO: make this barcode inline thing a util method encoded_svg = b64encode(bytes(svg, 'utf-8')).decode() encoded_data = "data:image/svg+xml;charset=utf-8;base64," + encoded_svg - svg = '' % encoded_data + svg = '' % encoded_data footnote.append(lxml.html.fromstring(svg)) html.append(footnote) diff --git a/screenless/bureau/publicrelations/publicrelations.py b/screenless/bureau/publicrelations/publicrelations.py index 7d3f55d..98b5836 100644 --- a/screenless/bureau/publicrelations/publicrelations.py +++ b/screenless/bureau/publicrelations/publicrelations.py @@ -292,6 +292,8 @@ class PublicRelations(Bureau): prn.codepage = "cp437" # TODO: add fancier formatting i.e. inverted text for username/handle + # TODO: clean this up to use the built in formatting from escpos lib + # and make a print_status function to use for notifications too toots = self.masto.timeline(limit=count) out = "" for t in toots: @@ -324,8 +326,20 @@ class PublicRelations(Bureau): tw_shortcode = self.short_tweet_id(str(t["id"])) prn.barcode("PRmad." + tw_shortcode, "CODE128", function_type="B") - prn.text("\r\n\r\n") + notifications = self.masto.notifications() + if len(notifications) > 0: + prn.set(text_type="B") + prn.text("NOTIFICATIONS:\r\n") + prn.set(text_type="NORMAL") + for note in notifications: + username = t.account.display_name.encode("cp437", "ignore") + \ + b" (" + t.account.acct.encode("cp437", "ignore") + b")" + prn.text(note["type"] + " " + str(note["created_at"]) + " from ") + prn._raw(username) + prn.text(":\r\n" + str(note.keys()) + "\r\n") + + prn.text("\r\n\r\n") prn.cut() diff --git a/screenless/bureau/sales/sales.py b/screenless/bureau/sales/sales.py index dea4538..91ff3da 100644 --- a/screenless/bureau/sales/sales.py +++ b/screenless/bureau/sales/sales.py @@ -22,7 +22,7 @@ class Sales(Bureau): pass # we've already got a db folder @add_command("p", "Play Media") - def print_fortune(self, data): + def play_media(self, data): """ Shows media on a connected projector or plays audio. """ diff --git a/screenless/bureau/typing.py b/screenless/bureau/typing.py index 566d5f2..d6b9c73 100644 --- a/screenless/bureau/typing.py +++ b/screenless/bureau/typing.py @@ -67,6 +67,7 @@ class TypingPool(Bureau): def run_io(self): val = "" upper = False + ctrl = False #TODO: this is crap, needs to be multi-threaded and have one # such loop for each active device self.active_devices[0].grab() @@ -74,7 +75,7 @@ class TypingPool(Bureau): for ev in self.active_devices[0].read_loop(): if ev.type == evdev.ecodes.EV_KEY: data = evdev.categorize(ev) - if data.keystate == 1: + if data.keystate == 1: # key-down if data.scancode == 28: print("sending barcode:", val) self.send(val[0:2], val[2:]) @@ -82,15 +83,27 @@ class TypingPool(Bureau): else: try: new_key = KEYS[data.scancode] - if new_key == "LSHFT" or new_key == "RSHFT": + if ctrl and (new_key == "j"): + self.log.debug("ignoring line-feed") + elif new_key == "LSHFT" or new_key == "RSHFT": upper = True + elif new_key == "LCTRL" or new_key == "RCTRL": + ctrl = True else: if upper: new_key = new_key.upper() - upper = False val += new_key except KeyError: print("Error invalid keycode:", data.scancode) + if data.keystate == 0: # key-up for mod-keys + try: + new_key = KEYS[data.scancode] + if new_key == "LSHFT" or new_key == "RSHFT": + upper = False + if new_key == "LCTRL" or new_key == "RCTRL": + ctrl = False + except KeyError: + print("Error invalid keycode:", data.scancode) def main():