from functools import wraps
from copy import deepcopy
import importlib
import os
from PIL import ImageFont
from zpui_lib.ui.canvas import get_default_font as gdf, fonts_dir
from zpui_lib.helpers import setup_logger
logger = setup_logger(__name__)
current_font = None
current_font_size = None
def get_default_font(width=None, height=None):
global current_font, current_font_size
if current_font:
return current_font, current_font_size
if width <= 128:
font = gdf()
font_size = (8, 6)
elif width <=240:
font_size = (16, 8)
path = os.path.join(fonts_dir, "Fixedsys62.ttf")
font = ImageFont.truetype(path, font_size[0])
else:
font_size = (24, 12)
path = os.path.join(fonts_dir, "Fixedsys62.ttf")
font = ImageFont.truetype(path, font_size[0])
current_font = font; current_font_size = font_size
return font, font_size
def lines_to_image(d, args, font, cheight, cwidth, color, cpos, cposition):
# unified interface teehee
#print("lti", repr(args), font, cheight, cwidth, color, cpos, cposition)
if cposition:
dims = (cpos[0] - 1 + 2,
cpos[1] - 1,
cpos[0] + cwidth + 2,
cpos[1] + cheight + 1)
d.rectangle(dims, outline=color)
for i, line in enumerate(args):
y = (i * cheight - 1) if i != 0 else 0
d.text((2, y), line, fill=color, font=font)
# These base classes document functions that
# different output devices are expected to have.
class OutputDevice(object):
"""Common class for all OutputDevices, no matter if they're graphical or character-based."""
current_proxy = None
def attach_new_proxy(self, proxy):
self.detach_current_proxy()
self.attach_proxy(proxy)
def detach_current_proxy(self):
self.current_proxy = None
def attach_proxy(self, proxy):
self.current_proxy = proxy
proxy.on_attach()
def init_proxy(self, proxy):
base_classes = self.__base_classes__
base_classes_items = sum([list(cls.__dict__.items()) for cls in base_classes], [])
#print(base_classes_items)
public_attributes = [ (k, v) for (k, v) in base_classes_items if not k.startswith("_") ]
hidden_attributes = ["current_proxy", "current_image"]
hidden_methods = ["init_proxy", "proxify_method", "detach_current_proxy", "attach_proxy", "get_proxied_method", "proxify_method"]
attribute_names = [ k for (k, v) in public_attributes if not callable(v) and k not in hidden_attributes]
method_names = [ k for (k, v) in public_attributes if callable(v) and k not in hidden_methods]
direct_methods = ["display_data_onto_image"]
for attribute_name in attribute_names:
#print("attribute: {}".format(attribute_name))
# Setting attributes of the proxy object to the same values
# as the current output device has them
value = getattr(self, attribute_name)
# Making sure that changing the proxy object's attributes
# won't change the attributes of the original object
copied_value = deepcopy(value)
setattr(proxy, attribute_name, copied_value)
#print("Setting to value {}".format(copied_value))
for method_name in method_names:
#print("method: {}".format(method_name))
# Setting functions to proxy the current object's
self.proxify_method(proxy, method_name)
for method_name in direct_methods:
# Methods that are proxied directly
setattr(proxy, method_name, getattr(self, method_name))
# Proxies have no use for hidden methods
# Unless
#for attribute_name in hidden_attributes+hidden_methods:
# if hasattr(proxy, attribute_name):
# delattr(proxy, attribute_name)
def get_proxied_method(o, proxy, method_name, sideeffect):
method = getattr(o, method_name)
@wraps(method)
def wrapper(*args, **kwargs):
#print("Method: "+method_name)
sideeffect(*args, **kwargs)
#print("Current: "+o.current_proxy.context_alias)
#print("Called from: "+proxy.context_alias)
if o.current_proxy.context_alias == proxy.context_alias:
#print("Calling the method directly!")
#print(args)
method(*args, **kwargs)
else:
pass #print("Not calling the method directly!")
#print("")
return wrapper
def proxify_method(self, proxy, method_name):
# If a proxy defines a side effect to happen when the function is called,
# we call it with same arguments and kwargs as the function received
# (and we make a small empty lambda if there's no side effect)
sideeffect = getattr(proxy, "_"+method_name, lambda *a, **k: None)
proxied_method = self.get_proxied_method(proxy, method_name, sideeffect)
setattr(proxy, method_name, proxied_method)
class CharacterOutputDevice(OutputDevice):
"""Common class for all character-based OutputDevices."""
rows = None # number of columns
cols = None # number of rows
type = ["char"] # supported output type list
def cursor(self):
"""
Enables the cursor.
"""
raise NotImplementedError
def noCursor(self):
"""
Disables the cursor.
"""
raise NotImplementedError
def setCursor(self, row, column):
"""
Sets the cursor's position.
"""
raise NotImplementedError
def display_data(self, *data):
"""
A function that is called to show text on the display.
Each positional argument is one line of text.
"""
raise NotImplementedError
class GraphicalOutputDevice(OutputDevice):
"""Common class for all graphical OutputDevices."""
height = None # height of display in pixels
width = None # width of display in pixels
type = ["b&w"] # supported output type list
device_mode = "1" # a "mode" parameter for PIL
char_height = 8 # height of the default font
char_width = 8 # width of the default font
current_image = None #an attribute for storing the currently displayed image
def display_data_onto_image(self, *args, **kwargs):
"""
A function that draws text on a PIL.Image, emulating
character displays (to be used with display_data).
"""
raise NotImplementedError
def display_image(self, image):
"""
A function that shows a PIL.Image on the display.
It also saves it in the current_image attribute.
"""
raise NotImplementedError
def clear(self):
"""
Clears the display, so that there's nothing shown on it.
"""
raise NotImplementedError
[docs]class OutputProxy(CharacterOutputDevice, GraphicalOutputDevice):
current_image = None
__cursor_enabled = False
__cursor_position = (0, 0)
def __init__(self, context_alias):
self.context_alias = context_alias
def _display_image(self, image, **kwargs):
"""
**kwargs here is because the backlight driver (or other overlays)
can be passed other arguments
"""
self.current_image = image
def _clear(self):
self.current_image = None
def _display_data(self, *data):
cursor_position = self.__cursor_position if self.__cursor_enabled else None
self.current_image = self.display_data_onto_image(*data, cursor_position=cursor_position)
def _cursor(self):
self.__cursor_enabled = True
def _noCursor(self):
self.__cursor_enabled = False
def _setCursor(self, *position):
self.__cursor_position = position
def get_current_image(self):
return self.current_image
def on_attach(self):
if self.current_image:
self.display_image(self.current_image)
def init(driver_configs):
# type: (list) -> None
""" This function is called by main.py to read the output configuration, pick the corresponding drivers and initialize a Screen object. Returns the screen object created. """
if isinstance(driver_configs, str):
# just a driver name provided, good, we can do that
driver_configs = [{"driver":driver_configs}]
# allow providing a dict instead of a list if there's only one driver
if not isinstance(driver_configs, list):
driver_configs = [driver_configs]
# Currently only the first screen in the config is initialized
try:
driver_config = driver_configs[0]
driver_name = driver_config["driver"]
driver_module = importlib.import_module("output.drivers." + driver_name)
args = driver_config["args"] if "args" in driver_config else []
if "kwargs" not in driver_config:
# a shortening letting us avoid building yaml or json staircases with magic words
kwargs = driver_config # taking the root level dict
kwargs.pop("driver") # and removing the driver name from it
# that's our kwargs now
else:
kwargs = driver_config["kwargs"]
except:
logger.exception(driver_configs)
raise
return driver_module.Screen(*args, **kwargs)
if __name__ == "__main__":
o = type("OD", (GraphicalOutputDevice, CharacterOutputDevice), {})()
op = type("OP", (o.__class__, ), {})()
o.init_proxy(op)