legoEv3/ev3dev2/display.py
# -----------------------------------------------------------------------------
# Copyright (c) 2015 Ralph Hempel <rhempel@hempeldesigngroup.com>
# Copyright (c) 2015 Anton Vanhoucke <antonvh@gmail.com>
# Copyright (c) 2015 Denis Demidov <dennis.demidov@gmail.com>
# Copyright (c) 2015 Eric Pascual <eric@pobot.org>
#
# 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
if sys.version_info < (3,4):
raise SystemError('Must be using Python 3.4 or higher')
import os
import mmap
import ctypes
import logging
from PIL import Image, ImageDraw
from . import fonts
from . import get_current_platform, library_load_warning_message
from struct import pack
log = logging.getLogger(__name__)
try:
# This is a linux-specific module.
# It is required by the Display class, but failure to import it may be
# safely ignored if one just needs to run API tests on Windows.
import fcntl
except ImportError:
log.warning(library_load_warning_message("fcntl", "Display"))
class FbMem(object):
"""The framebuffer memory object.
Made of:
- the framebuffer file descriptor
- the fix screen info struct
- the var screen info struct
- the mapped memory
"""
# ------------------------------------------------------------------
# The code is adapted from
# https://github.com/LinkCareServices/cairotft/blob/master/cairotft/linuxfb.py
#
# The original code came with the following license:
# ------------------------------------------------------------------
# Copyright (c) 2012 Kurichan
#
# This program is free software. It comes without any warranty, to
# the extent permitted by applicable law. You can redistribute it
# and/or modify it under the terms of the Do What The Fuck You Want
# To Public License, Version 2, as published by Sam Hocevar. See
# http://sam.zoy.org/wtfpl/COPYING for more details.
# ------------------------------------------------------------------
__slots__ = ('fid', 'fix_info', 'var_info', 'mmap')
FBIOGET_VSCREENINFO = 0x4600
FBIOGET_FSCREENINFO = 0x4602
FB_VISUAL_MONO01 = 0
FB_VISUAL_MONO10 = 1
class FixScreenInfo(ctypes.Structure):
"""The fb_fix_screeninfo from fb.h."""
_fields_ = [
('id_name', ctypes.c_char * 16),
('smem_start', ctypes.c_ulong),
('smem_len', ctypes.c_uint32),
('type', ctypes.c_uint32),
('type_aux', ctypes.c_uint32),
('visual', ctypes.c_uint32),
('xpanstep', ctypes.c_uint16),
('ypanstep', ctypes.c_uint16),
('ywrapstep', ctypes.c_uint16),
('line_length', ctypes.c_uint32),
('mmio_start', ctypes.c_ulong),
('mmio_len', ctypes.c_uint32),
('accel', ctypes.c_uint32),
('reserved', ctypes.c_uint16 * 3),
]
class VarScreenInfo(ctypes.Structure):
class FbBitField(ctypes.Structure):
"""The fb_bitfield struct from fb.h."""
_fields_ = [
('offset', ctypes.c_uint32),
('length', ctypes.c_uint32),
('msb_right', ctypes.c_uint32),
]
"""The fb_var_screeninfo struct from fb.h."""
_fields_ = [
('xres', ctypes.c_uint32),
('yres', ctypes.c_uint32),
('xres_virtual', ctypes.c_uint32),
('yres_virtual', ctypes.c_uint32),
('xoffset', ctypes.c_uint32),
('yoffset', ctypes.c_uint32),
('bits_per_pixel', ctypes.c_uint32),
('grayscale', ctypes.c_uint32),
('red', FbBitField),
('green', FbBitField),
('blue', FbBitField),
('transp', FbBitField),
]
def __init__(self, fbdev=None):
"""Create the FbMem framebuffer memory object."""
fid = FbMem._open_fbdev(fbdev)
fix_info = FbMem._get_fix_info(fid)
fbmmap = FbMem._map_fb_memory(fid, fix_info)
self.fid = fid
self.fix_info = fix_info
self.var_info = FbMem._get_var_info(fid)
self.mmap = fbmmap
def __del__(self):
"""Close the FbMem framebuffer memory object."""
self.mmap.close()
FbMem._close_fbdev(self.fid)
@staticmethod
def _open_fbdev(fbdev=None):
"""Return the framebuffer file descriptor.
Try to use the FRAMEBUFFER
environment variable if fbdev is not given. Use '/dev/fb0' by
default.
"""
dev = fbdev or os.getenv('FRAMEBUFFER', '/dev/fb0')
fbfid = os.open(dev, os.O_RDWR)
return fbfid
@staticmethod
def _close_fbdev(fbfid):
"""Close the framebuffer file descriptor."""
os.close(fbfid)
@staticmethod
def _get_fix_info(fbfid):
"""Return the fix screen info from the framebuffer file descriptor."""
fix_info = FbMem.FixScreenInfo()
fcntl.ioctl(fbfid, FbMem.FBIOGET_FSCREENINFO, fix_info)
return fix_info
@staticmethod
def _get_var_info(fbfid):
"""Return the var screen info from the framebuffer file descriptor."""
var_info = FbMem.VarScreenInfo()
fcntl.ioctl(fbfid, FbMem.FBIOGET_VSCREENINFO, var_info)
return var_info
@staticmethod
def _map_fb_memory(fbfid, fix_info):
"""Map the framebuffer memory."""
return mmap.mmap(
fbfid,
fix_info.smem_len,
mmap.MAP_SHARED,
mmap.PROT_READ | mmap.PROT_WRITE,
offset=0
)
class Display(FbMem):
"""
A convenience wrapper for the FbMem class.
Provides drawing functions from the python imaging library (PIL).
"""
GRID_COLUMNS = 22
GRID_COLUMN_PIXELS = 8
GRID_ROWS = 12
GRID_ROW_PIXELS = 10
def __init__(self, desc='Display'):
FbMem.__init__(self)
self.platform = get_current_platform()
if self.var_info.bits_per_pixel == 1:
im_type = "1"
elif self.var_info.bits_per_pixel == 16:
im_type = "RGB"
elif self.platform == "ev3" and self.var_info.bits_per_pixel == 32:
im_type = "L"
else:
raise Exception("Not supported")
self._img = Image.new(
im_type,
(self.fix_info.line_length * 8 // self.var_info.bits_per_pixel, self.yres),
"white")
self._draw = ImageDraw.Draw(self._img)
self.desc = desc
def __str__(self):
return self.desc
@property
def xres(self):
"""
Horizontal screen resolution
"""
return self.var_info.xres
@property
def yres(self):
"""
Vertical screen resolution
"""
return self.var_info.yres
@property
def shape(self):
"""
Dimensions of the screen.
"""
return (self.xres, self.yres)
@property
def draw(self):
"""
Returns a handle to PIL.ImageDraw.Draw class associated with the screen.
Example::
screen.draw.rectangle((10,10,60,20), fill='black')
"""
return self._draw
@property
def image(self):
"""
Returns a handle to PIL.Image class that is backing the screen. This can
be accessed for blitting images to the screen.
Example::
screen.image.paste(picture, (0, 0))
"""
return self._img
def clear(self):
"""
Clears the screen
"""
self._draw.rectangle(((0, 0), self.shape), fill="white")
def _color565(self, r, g, b):
"""Convert red, green, blue components to a 16-bit 565 RGB value. Components
should be values 0 to 255.
"""
return (((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3))
def _img_to_rgb565_bytes(self):
pixels = [self._color565(r, g, b) for (r, g, b) in self._img.getdata()]
return pack('H' * len(pixels), *pixels)
def update(self):
"""
Applies pending changes to the screen.
Nothing will be drawn on the screen until this function is called.
"""
if self.var_info.bits_per_pixel == 1:
b = self._img.tobytes("raw", "1;R")
self.mmap[:len(b)] = b
elif self.var_info.bits_per_pixel == 16:
self.mmap[:] = self._img_to_rgb565_bytes()
elif self.platform == "ev3" and self.var_info.bits_per_pixel == 32:
self.mmap[:] = self._img.convert("RGB").tobytes("raw", "XRGB")
else:
raise Exception("Not supported")
def image_filename(self, filename, clear_screen=True, x1=0, y1=0, x2=None, y2=None):
if clear_screen:
self.clear()
filename_im = Image.open(filename)
if x2 is not None and y2 is not None:
return self._img.paste(filename_im, (x1, y1, x2, y2))
else:
return self._img.paste(filename_im, (x1, y1))
def line(self, clear_screen=True, x1=10, y1=10, x2=50, y2=50, line_color='black', width=1):
"""
Draw a line from (x1, y1) to (x2, y2)
"""
if clear_screen:
self.clear()
return self.draw.line((x1, y1, x2, y2), fill=line_color, width=width)
def circle(self, clear_screen=True, x=50, y=50, radius=40, fill_color='black', outline_color='black'):
"""
Draw a circle of 'radius' centered at (x, y)
"""
if clear_screen:
self.clear()
x1 = x - radius
y1 = y - radius
x2 = x + radius
y2 = y + radius
return self.draw.ellipse((x1, y1, x2, y2), fill=fill_color, outline=outline_color)
def rectangle(self, clear_screen=True, x=10, y=10, width=80, height=40, fill_color='black', outline_color='black'):
"""
Draw a rectangle 'width x height' where the top left corner is at (x, y)
"""
if clear_screen:
self.clear()
return self.draw.rectangle((x, y, width, height), fill=fill_color, outline=outline_color)
def point(self, clear_screen=True, x=10, y=10, point_color='black'):
"""
Draw a single pixel at (x, y)
"""
if clear_screen:
self.clear()
return self.draw.point((x, y), fill=point_color)
def text_pixels(self, text, clear_screen=True, x=0, y=0, text_color='black', font=None):
"""
Display `text` starting at pixel (x, y).
The EV3 display is 178x128 pixels
- (0, 0) would be the top left corner of the display
- (89, 64) would be right in the middle of the display
'text_color' : PIL says it supports "common HTML color names". There
are 140 HTML color names listed here that are supported by all modern
browsers. This is probably a good list to start with.
https://www.w3schools.com/colors/colors_names.asp
'font' : can be any font displayed here
http://ev3dev-lang.readthedocs.io/projects/python-ev3dev/en/ev3dev-stretch/other.html#bitmap-fonts
"""
if clear_screen:
self.clear()
if font is not None:
assert font in fonts.available(), "%s is an invalid font" % font
return self.draw.text((x, y), text, fill=text_color, font=fonts.load(font))
else:
return self.draw.text((x, y), text, fill=text_color)
def text_grid(self, text, clear_screen=True, x=0, y=0, text_color='black', font=None):
"""
Display 'text' starting at grid (x, y)
The EV3 display can be broken down in a grid that is 22 columns wide
and 12 rows tall. Each column is 8 pixels wide and each row is 10
pixels tall.
'text_color' : PIL says it supports "common HTML color names". There
are 140 HTML color names listed here that are supported by all modern
browsers. This is probably a good list to start with.
https://www.w3schools.com/colors/colors_names.asp
'font' : can be any font displayed here
http://ev3dev-lang.readthedocs.io/projects/python-ev3dev/en/ev3dev-stretch/other.html#bitmap-fonts
"""
assert 0 <= x < Display.GRID_COLUMNS,\
"grid columns must be between 0 and %d, %d was requested" %\
((Display.GRID_COLUMNS - 1, x))
assert 0 <= y < Display.GRID_ROWS,\
"grid rows must be between 0 and %d, %d was requested" %\
((Display.GRID_ROWS - 1), y)
return self.text_pixels(text, clear_screen,
x * Display.GRID_COLUMN_PIXELS,
y * Display.GRID_ROW_PIXELS,
text_color, font)
def reset_screen(self):
self.clear()
self.update()