Source code for oled.emulator

# -*- coding: utf-8 -*-
# Copyright (c) 2016 Richard Hull and contributors
# See LICENSE.rst for details.

import os
import sys
import atexit
import logging
from PIL import Image
from oled.device import device
from oled.serial import noop

logger = logging.getLogger(__name__)


[docs]class emulator(device): """ Base class for emulated OLED driver classes """ def __init__(self, width, height, rotate, mode, transform, scale): super(emulator, self).__init__(serial_interface=noop()) try: import pygame except: raise RuntimeError("Emulator requires pygame to be installed") self._pygame = pygame self.capabilities(width, height, rotate, mode) self.scale = 1 if transform == "none" else scale self._transform = getattr(transformer(pygame, width, height, scale), "none" if scale == 1 else transform)
[docs] def cleanup(self): pass
[docs] def to_surface(self, image): """ Converts a :py:mod:`PIL.Image` into a :class:`pygame.Surface`, transforming it according to the ``transform`` and ``scale`` constructor arguments. """ im = image.convert("RGB") mode = im.mode size = im.size data = im.tobytes() del im surface = self._pygame.image.fromstring(data, size, mode) return self._transform(surface)
[docs]class capture(emulator): """ Pseudo-device that acts like an OLED display, except that it writes the image to a numbered PNG file when the :func:`display` method is called. While the capability of an OLED device is monochrome, there is no limitation here, and hence supports 24-bit color depth. """ def __init__(self, width=128, height=64, rotate=0, mode="RGB", transform="scale2x", scale=2, file_template="oled_{0:06}.png", **kwargs): super(capture, self).__init__(width, height, rotate, mode, transform, scale) self._count = 0 self._file_template = file_template
[docs] def display(self, image): """ Takes a :py:mod:`PIL.Image` and dumps it to a numbered PNG file. """ assert(image.size == self.size) self._count += 1 filename = self._file_template.format(self._count) image = self.preprocess(image) surface = self.to_surface(image) logger.debug("Writing: {0}".format(filename)) self._pygame.image.save(surface, filename)
[docs]class gifanim(emulator): """ Pseudo-device that acts like an OLED display, except that it collects the images when the :func:`display` method is called, and on exit, assembles them into an animated GIF image. While the capability of an OLED device is monochrome, there is no limitation here, and hence supports 24-bit color depth, albeit with an indexed color palette. """ def __init__(self, width=128, height=64, rotate=0, mode="RGB", transform="scale2x", scale=2, filename="oled_anim.gif", duration=0.01, loop=0, max_frames=None, **kwargs): super(gifanim, self).__init__(width, height, rotate, mode, transform, scale) self._images = [] self._count = 0 self._max_frames = max_frames self._filename = filename self._loop = loop self._duration = duration atexit.register(self.write_animation)
[docs] def display(self, image): """ Takes an image, scales it according to the nominated transform, and stores it for later building into an animated GIF. """ assert(image.size == self.size) image = self.preprocess(image) surface = self.to_surface(image) rawbytes = self._pygame.image.tostring(surface, "RGB", False) im = Image.frombytes("RGB", (self._w * self.scale, self._h * self.scale), rawbytes) self._images.append(im) self._count += 1 logger.debug("Recording frame: {0}".format(self._count)) if self._max_frames and self._count >= self._max_frames: sys.exit(0)
[docs] def write_animation(self): logger.debug("Please wait... building animated GIF") with open(self._filename, "w+b") as fp: self._images[0].save(fp, save_all=True, loop=self._loop, duration=int(self._duration * 1000), append_images=self._images[1:], format="GIF") logger.debug("Wrote {0} frames to file: {1} ({2} bytes)".format( len(self._images), self._filename, os.stat(self._filename).st_size))
[docs]class pygame(emulator): """ Pseudo-device that acts like an OLED display, except that it renders to an displayed window. The frame rate is limited to 60FPS (much faster than a Raspberry Pi can acheive, but this can be overridden as necessary). While the capability of an OLED device is monochrome, there is no limitation here, and hence supports 24-bit color depth. :mod:`pygame` is used to render the emulated display window, and it's event loop is checked to see if the ESC key was pressed or the window was dismissed: if so :func:`sys.exit()` is called. """ def __init__(self, width=128, height=64, rotate=0, mode="RGB", transform="scale2x", scale=2, frame_rate=60, **kwargs): super(pygame, self).__init__(width, height, rotate, mode, transform, scale) self._pygame.init() self._pygame.font.init() self._pygame.display.set_caption("OLED Emulator") self._clock = self._pygame.time.Clock() self._fps = frame_rate self._screen = self._pygame.display.set_mode((width * self.scale, height * self.scale)) self._screen.fill((0, 0, 0)) self._pygame.display.flip() def _abort(self): keystate = self._pygame.key.get_pressed() return keystate[self._pygame.K_ESCAPE] or self._pygame.event.peek(self._pygame.QUIT)
[docs] def display(self, image): """ Takes a :py:mod:`PIL.Image` and renders it to a pygame display surface. """ assert(image.size == self.size) image = self.preprocess(image) self._clock.tick(self._fps) self._pygame.event.pump() if self._abort(): self._pygame.quit() sys.exit() surface = self.to_surface(image) self._screen.blit(surface, (0, 0)) self._pygame.display.flip()
[docs]class dummy(emulator): """ Pseudo-device that acts like an OLED display, except that it does nothing other than retain a copy of the displayed image. It is mostly useful for testing. While the capability of an OLED device is monochrome, there is no limitation here, and hence supports 24-bit color depth. """ def __init__(self, width=128, height=64, rotate=0, mode="RGB", transform="scale2x", scale=2, **kwargs): super(dummy, self).__init__(width, height, rotate, mode, transform, scale) self.image = None
[docs] def display(self, image): """ Takes a :py:mod:`PIL.Image` and makes a copy of it for later use/inspection. """ assert(image.size == self.size) self.image = self.preprocess(image).copy()
[docs]class transformer(object): """ Helper class used to dispatch transformation operations. """ def __init__(self, pygame, width, height, scale): self._pygame = pygame self._output_size = (width * scale, height * scale) self.scale = scale
[docs] def none(self, surface): """ No-op transform - used when ``scale`` = 1 """ return surface
[docs] def scale2x(self, surface): """ Scales using the AdvanceMAME Scale2X algorithm which does a 'jaggie-less' scale of bitmap graphics. """ assert(self.scale == 2) return self._pygame.transform.scale2x(surface)
[docs] def smoothscale(self, surface): """ Smooth scaling using MMX or SSE extensions if available """ return self._pygame.transform.smoothscale(surface, self._output_size)
[docs] def identity(self, surface): """ Fast scale operation that does not sample the results """ return self._pygame.transform.scale(surface, self._output_size)