Merge branch 'master' of gitlab.com:bhowell/the-screenless-office

workspace
Brendan Howell 6 years ago
commit 73d056561f

@ -1,13 +1,13 @@
import sys import sys
from . import officemgr from . import mgmt
def main(args=None): def main(args=None):
if args is None: if args is None:
args = sys.argv[1:] args = sys.argv[1:]
print("starting Screenless Office...") print("initializing Screenless Office...")
mgr = officemgr.OfficeManager() mgr = mgmt.Management()
print("starting Office Manager...") print("starting Management...")
mgr.run() mgr.run()

@ -1,4 +1,4 @@
import subprocess import vlc
from bureau import Bureau, add_command from bureau import Bureau, add_command
@ -17,7 +17,7 @@ class Audio(Bureau):
Bureau.__init__(self) Bureau.__init__(self)
self.urldb = self.open_db("urldb") 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.") @add_command("p", "Play an album, track or a live stream.")
def play(self, data): def play(self, data):
@ -31,44 +31,58 @@ class Audio(Bureau):
url = self.urldb.get(shortcode) url = self.urldb.get(shortcode)
self.log.debug(" playing url " + url) self.log.debug(" playing url " + url)
subprocess.call(["mocp", "-c"]) self.player.set_mrl(url)
subprocess.call(["mocp", "-a", url]) self.player.play()
subprocess.call(["mocp", "-p"])
@add_command("stop", "Halt audio playback.") @add_command("stop", "Halt audio playback.")
def stop(self): def stop(self):
""" """
Stops all audio currently playing audio output. Stops all audio currently playing audio output.
""" """
subprocess.call(["mocp", "-P"]) self.player.pause()
@add_command("resu", "Resume playback.") @add_command("resu", "Resume playback.")
def resume(self): def resume(self):
""" """
Resume playback of paused audio. Resume playback of paused audio.
""" """
subprocess.call(["mocp", "-U"]) self.player.play()
@add_command("next", "Play the next song.") @add_command("next", "Play the next song.")
def play_next(self): def play_next(self):
""" """
Skip to the next song in the playlist or album. 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.") @add_command("prev", "Play the previous song.")
def play_prev(self): def play_prev(self):
""" """
Skip to the previous song in the playlist or album. 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") @add_command("nowp", "Now Playing")
def now_playing(self): def now_playing(self):
""" """
Prints the currently playing song or stream on the small printer. 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.log.debug("info output:" + out)
self.print_small(out) self.print_small(out)

@ -7,9 +7,9 @@ import os.path
import random import random
import string import string
import subprocess import subprocess
import sys
import tempfile import tempfile
import textwrap import textwrap
#import traceback
import threading import threading
import lmdb import lmdb
@ -60,6 +60,19 @@ def add_api(apistr, name=""):
return decorator 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): class LogPrinter(logging.Handler):
""" """
LogPrinter prints logs on a receipt printer for screenless debugging. LogPrinter prints logs on a receipt printer for screenless debugging.
@ -151,11 +164,6 @@ class Bureau(object):
self.api = {} self.api = {}
modpath = os.path.dirname(__file__) 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__) mypath = inspect.getfile(self.__class__)
self.mdir = os.path.dirname(mypath) self.mdir = os.path.dirname(mypath)
@ -181,7 +189,6 @@ class Bureau(object):
log_format = logging.Formatter('LOG ${levelname} $name: $message', style='$') log_format = logging.Formatter('LOG ${levelname} $name: $message', style='$')
log_printer.setFormatter(log_format) log_printer.setFormatter(log_format)
self.log.addHandler(log_printer) self.log.addHandler(log_printer)
sys.excepthook = self._log_exception
# setup a dir to store files and data # setup a dir to store files and data
self.datadir = os.path.join(basepath, self.prefix) self.datadir = os.path.join(basepath, self.prefix)
@ -200,9 +207,9 @@ class Bureau(object):
self.log.debug("commands: ") self.log.debug("commands: ")
self.log.debug(str(self.commands)) self.log.debug(str(self.commands))
def _log_exception(typ, value, tb): # def _log_exception(typ, value, tb):
self.log.error("CRASH TRACE: {0}".format(str(value)), exc_info=(typ, value, tb)) # self.log.error("CRASH TRACE: {0}".format(str(value)), exc_info=(typ, value, tb))
sys.__excepthook__(typ, value, tb) # sys.__excepthook__(typ, value, tb)
def open_db(self, name): def open_db(self, name):
""" """
@ -346,10 +353,15 @@ class Bureau(object):
""" """
# TODO: make printer id/width configured and easy # TODO: make printer id/width configured and easy
prn = printer.Usb(0x416, 0x5011, in_ep=0x81, out_ep=0x03) prn = printer.Usb(0x416, 0x5011, in_ep=0x81, out_ep=0x03)
if type(img) is PIL.Image.Image:
im = img
else:
im = PIL.Image.open(img) im = PIL.Image.open(img)
# NOTE: might be worth tring to push up brightness # NOTE: might be worth tring to push up brightness
im = PIL.ImageOps.equalize(im) # stretch histogram for nicer dither 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 ?? prn.image(im, impl="bitImageColumn") # not using this impl crashes ??
@add_command("test") @add_command("test")
@ -361,6 +373,16 @@ class Bureau(object):
print(("hi! testing. " + self.name + " bureau seems to work!")) print(("hi! testing. " + self.name + " bureau seems to work!"))
return "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): def run_io(self):
"""process hardware or timed input """process hardware or timed input
@ -375,6 +397,16 @@ class Bureau(object):
pass pass
def run(self): 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 main loop for processing messages
@ -382,7 +414,7 @@ class Bureau(object):
""" """
# start the hardware input handler # start the hardware input handler
io_handler = threading.Thread(target=self.run_io) io_handler = threading.Thread(target=self._run_io)
io_handler.start() io_handler.start()
# register commands and api methods # register commands and api methods
@ -398,10 +430,6 @@ class Bureau(object):
msg = self._recv.recv_string(flags=zmq.NOBLOCK) msg = self._recv.recv_string(flags=zmq.NOBLOCK)
else: else:
continue continue
# msg = self._recv.recv_string(flags=zmq.NOBLOCK)
#except zmq.ZMQError:
# time.sleep(0.05) # don't waste CPU
# continue
try: try:
self.log.debug("got message:" + msg) self.log.debug("got message:" + msg)
dot = msg.find(".") dot = msg.find(".")
@ -411,7 +439,6 @@ class Bureau(object):
self.log.debug("dot at %d", dot) self.log.debug("dot at %d", dot)
# TODO: maybe trim off the trailing "." for convenience # TODO: maybe trim off the trailing "." for convenience
data = msg[dot + 1:] data = msg[dot + 1:]
# data = str(data) # force to be a string
else: else:
data = None data = None
self.log.debug("data: " + str(data)) self.log.debug("data: " + str(data))

@ -1,6 +1,13 @@
import glob
import math
import mimetypes
import os
import subprocess 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): class Humor(Bureau):
@ -24,6 +31,59 @@ class Humor(Bureau):
jux = str(subprocess.check_output("/usr/games/fortune"), encoding="UTF-8") jux = str(subprocess.check_output("/usr/games/fortune"), encoding="UTF-8")
self.print_small(jux) 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(): def main():
ha = Humor() ha = Humor()

@ -49,6 +49,10 @@
width: 50%; width: 50%;
height: 50%; height: 50%;
} }
.endnotebc {
width: 50%;
height: 50%;
}
</style> </style>
<body> <body>
<h1>${title}</h1> <h1>${title}</h1>

@ -175,7 +175,7 @@ class Publications(Bureau):
#TODO: make this barcode inline thing a util method #TODO: make this barcode inline thing a util method
encoded_svg = b64encode(bytes(svg, 'utf-8')).decode() encoded_svg = b64encode(bytes(svg, 'utf-8')).decode()
encoded_data = "data:image/svg+xml;charset=utf-8;base64," + encoded_svg encoded_data = "data:image/svg+xml;charset=utf-8;base64," + encoded_svg
svg = '<img src="%s"/>' % encoded_data svg = '<img class="endnotebc" src="%s"/>' % encoded_data
footnote.append(lxml.html.fromstring(svg)) footnote.append(lxml.html.fromstring(svg))
html.append(footnote) html.append(footnote)

@ -292,6 +292,8 @@ class PublicRelations(Bureau):
prn.codepage = "cp437" prn.codepage = "cp437"
# TODO: add fancier formatting i.e. inverted text for username/handle # 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) toots = self.masto.timeline(limit=count)
out = "" out = ""
for t in toots: for t in toots:
@ -324,8 +326,20 @@ class PublicRelations(Bureau):
tw_shortcode = self.short_tweet_id(str(t["id"])) tw_shortcode = self.short_tweet_id(str(t["id"]))
prn.barcode("PRmad." + tw_shortcode, "CODE128", function_type="B") 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() prn.cut()

@ -22,7 +22,7 @@ class Sales(Bureau):
pass # we've already got a db folder pass # we've already got a db folder
@add_command("p", "Play Media") @add_command("p", "Play Media")
def print_fortune(self, data): def play_media(self, data):
""" """
Shows media on a connected projector or plays audio. Shows media on a connected projector or plays audio.
""" """

@ -67,6 +67,7 @@ class TypingPool(Bureau):
def run_io(self): def run_io(self):
val = "" val = ""
upper = False upper = False
ctrl = False
#TODO: this is crap, needs to be multi-threaded and have one #TODO: this is crap, needs to be multi-threaded and have one
# such loop for each active device # such loop for each active device
self.active_devices[0].grab() self.active_devices[0].grab()
@ -74,7 +75,7 @@ class TypingPool(Bureau):
for ev in self.active_devices[0].read_loop(): for ev in self.active_devices[0].read_loop():
if ev.type == evdev.ecodes.EV_KEY: if ev.type == evdev.ecodes.EV_KEY:
data = evdev.categorize(ev) data = evdev.categorize(ev)
if data.keystate == 1: if data.keystate == 1: # key-down
if data.scancode == 28: if data.scancode == 28:
print("sending barcode:", val) print("sending barcode:", val)
self.send(val[0:2], val[2:]) self.send(val[0:2], val[2:])
@ -82,15 +83,27 @@ class TypingPool(Bureau):
else: else:
try: try:
new_key = KEYS[data.scancode] 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 upper = True
elif new_key == "LCTRL" or new_key == "RCTRL":
ctrl = True
else: else:
if upper: if upper:
new_key = new_key.upper() new_key = new_key.upper()
upper = False
val += new_key val += new_key
except KeyError: except KeyError:
print("Error invalid keycode:", data.scancode) 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(): def main():

Loading…
Cancel
Save