pull/3/merge
Peter Hinch 2016-11-15 12:10:21 +00:00
rodzic bb4c0415d6
commit 1bcf06c531
7 zmienionych plików z 691 dodań i 234 usunięć

216
DRIVERS.md 100644
Wyświetl plik

@ -0,0 +1,216 @@
# DRIVERS.md
This document comprises two sections, the first for users of an existing device
driver and the second for writers of device drivers.
# User Documentation: the Writer class
This class facilitates rendering characters from Python font files to a device,
assuming the device has a driver conforming to the specification below. Typical
use is as follows:
```python
from writer import Writer
from device_driver import Display
import freeserif
import freesans20
display = Display(args_required_by_driver)
wri_serif = Writer(display, freeserif)
wri_sans = Writer(display, freesans20)
Writer.set_clip(True, True)
wri_serif.printstring('Tuesday\n')
wri_sans.printstring('8 Nov 2016\n')
wri_sans.printstring('10.30am')
display.show() # Display the result
```
The file ``driver_test.py`` illustrates the use of font files with an SSD1306
display and a complete example of an SSD1306 driver may be found
[here](https://github.com/peterhinch/micropython-samples/tree/master/SSD1306).
## Class Methods
The ``Writer`` class exposes the following class methods:
1. ``set_textpos`` Args: ``row``, ``col``. This determines where on screen any
subsequent text is to be rendered. The initial value is (0, 0) - the top left
corner. Arguments are in pixels with positive values representing down and
right respectively. They reference the top left hand corner of the first
character to be output.
2. ``set_clip`` Args: boolean ``row_clip``, ``col_clip``. If these are
``True``, characters will be clipped if they extend beyond the boundaries of
the physical display. If ``col_clip`` is ``False`` characters will wrap onto
the next line. If ``row_clip`` is ``False`` the display will, where necessary,
scroll up to ensure the line is rendered.
3. ``mapping`` Arg: an integer. This defines the mapping of bytes in the
buffer onto pixels. The module exposes three constants for use here: ``VERT``
``HORIZ`` and ``WEIRD``, the latter being specific to the official SSD1306
driver. ``VERT`` is for true vertically mapped displays. ``HORIZ``, for
horizontally mapped devices, is currently unsupported. By default the mapping
is for SSD1306 devices using the official driver.
As class methods these settings apply to all font objects. The insertion point
of characters is maintained regardless of the font in use.
## Method
1. ``printstring`` Arg: a text string. Outputs a text string at the current
insertion point. Newline characters are honoured.
# Device Driver Implementation
Display devices comprise two varieties, depending on whether the framebuffer is
located on the controlling system or on the physical display device. In the
former case the ``Writer`` class simplifies the design of the driver. It merely
has to expose certin attributes and methods with ``Writer`` instances taking
care of text rendering. It is strongly recommended that such device drivers use
the oficial ``framebuf`` module, as per the official SSD1306 driver.
Where the buffer is located on the display device the means of controlling the
text insertion point will be device dependent. The driver will need to
implement the functionality of the ``Writer`` class itself.
## Drivers with local buffers
The writer of a device driver need not be concerned with the structure of a
Python font file so long as the driver exposes certain attributes and methods
required by the ``Writer`` class. These are as follows:
Attributes:
1. ``buffer`` The underlying ``bytearray`` instance holding the display
buffer.
2. ``height`` The screen height in pixels.
3. ``width`` The screen width in pixels.
Methods:
1. ``show`` Display the current buffer contents.
2. ``scroll`` Arguments ``x``, ``y`` amount to scroll horizontal and vertical.
3. ``fill`` Argument ``col`` colour 1 == fill 0 == clear.
An example of such a driver, using the official ``framebuf`` module, is the
SSD1306 driver (drivers/display/ssd1306.py in the source tree).
The driver documentation should specify the arguments for font_to_py.py to
ensure users create font files with a layout corresponding to that of the
buffer/device.
## Drivers for remote buffers
### Specifying the font file
Each font file has a ``get_ch()`` function accepting an ASCII character as its
argument. It returns a memoryview instance providing access to a bytearray
corresponding to the individual glyph. The layout of this data is determined by
the command line arguments presented to the ``font_to_py.py`` utility. The
driver documentation should specify these to ensure the layout is optimal to
simplify and speed transfer to the device. Mapping may be horizontal or
vertical, and the bit order of individual bytes may be defined. These are
detailed below.
The Python source file provides a fast means of accessing the byte data
corresponding to an individual character. It is the responsibility of the
driver to copy that data to the physical device. The ``font_to_py.py``
utility has command line arguments specifying the organisation of data returned
by the font's ``get_ch()`` function.
The purpose of the command line arguments specified to the user is to
ensure that the data layout is optimised for the device so that the copy is a
simple bytewise copy or SPI/I2C transfer.
In the case of devices with their own frame buffer the Writer class will need
to be re-written or adapted to match the hardware's method of tracking such
things as the text insertion point. Consider maintaining the class's interface
to simplify the writing of user code capable of being ported between displays.
## Font files
Assume the user has run the utility to produce a file ``myfont.py`` This then
has the following outline definition (in practice the bytes objects are large):
```python
# Code generated by font-to-py.py.
# Font: FreeSerif.ttf
version = '0.1'
def height():
return 21
def max_width():
return 22
def hmap():
return False
def reverse():
return False
def monospaced():
return False
_font =\
b'\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
b'\x00\x00\x00\x00\x08\x00\xfe\xc7\x00\x7e\xc0\x00\x00\x00\x00\x00'\
_index =\
b'\x00\x00\x14\x00\x2e\x00\x4b\x00\x71\x00\x97\x00\xd2\x00\x0a\x01'\
b'\x1b\x01\x35\x01\x4f\x01\x75\x01\x9e\x01\xb2\x01\xcc\x01\xe0\x01'\
# Boilerplate code omitted
def get_ch(ch):
# validate ch, if out of range use '?'
# get offsets into _font and retrieve char width
# Return: address of start of bitmap, height and width
return memoryview(_font[offset + 2, next_offset]), height, width
```
``height`` and ``width`` are specified in bits (pixels).
In the case of monospaced fonts the ``max_width`` function returns the width of
every character. For variable pitch fonts it returns the width of the widest
character. Device drivers can use this to rapidly determine whether a string
will fit the available space. If it will fit on the assumption that all chars
are maximum width, it can be rendered rapidly without doing a character by
character check.
``get_ch()`` returns a memoryview of an individual glyph with its dimensions
and contains all the bytes required to render the character including trailing
space.
## Mapping
A character occupies a space where (0, 0) represents the coordinates of the top
left hand corner of the bitmap. It comprises a set of pixels where increasing x
values represent locations to the right of the origin and increasing y values
represent downward positions. Mapping is the process whereby this two
dimensional array of bits is transformed into a linear sequence of bytes.
Vertical mapping means that the LSB of first byte is pixel (0,0), MSB of first
byte is (0, 7). The second byte (assuming the height is greater than 8 pixels)
is (0, 8) to (0, 15). Once the column is complete the next byte represents
(1, 0) to (1, 7).
Horizontal mapping means that the MSB of byte 0 is pixel (0,0) with LSB at
(7,0), with the second byte covering (8, 0) to (15, 0) if the width is greater
than 8.
Bit reversal provides for the case where the bit order of each byte is reversed
i.e. a byte comprising bits [b7b6b5b4b3b2b1b0] becomes [b0b1b2b3b4b5b6b7].
# Specification and Project Notes
The design aims primarily to minimise RAM usage. Minimising the size of the
bytecode is a secondary aim. Indexed addressing is used to reduce this in
the case of proportional fonts, at a small cost in performance. The size of the
Python source file is a lesser consideration, with readability being prioritised
over size. Hence they are "pretty formatted" with the large bytes objects
split over multiple lines for readability.
The approach has been tested on SSD1306 devices using both the pseudo-horizontal
and true vertical mapping.
The ``font_to_py`` utility has been extensively tested with each of the mapping
options.

97
FONT_TO_PY.md 100644
Wyświetl plik

@ -0,0 +1,97 @@
# font_to_py.py
Convert a font file to Python source code.
# Usage
``font_to_py.py`` is a command line utility written in Python 3. It is run on a
PC. It takes as input a font file with a ``ttf`` or ``otf`` extension and a
required height in pixels and outputs a Python 3 source file. The pixel layout
is determined by command arguments. By default fonts are stored in variable
pitch form. This may be overidden by a command line argument.
Further arguments ensure that the byte contents and layout are correct for the
target display hardware. Their usage should be specified in the documentation
for the device driver.
Example usage to produce a file ``myfont.py`` with height of 23 pixels:
``font_to_py.py FreeSans.ttf 23 myfont.py``
## Arguments
### Mandatory positional arguments:
1. Font file path. Must be a ttf or otf file.
2. Height in pixels.
3. Output file path. Must have a .py extension.
### Optional arguments:
* -f or --fixed If specified, all characters will have the same width. By
default fonts are assumed to be variable pitch.
* -x Specifies horizontal mapping (default is vertical).
* -b Specifies bit reversal in each font byte.
Optional arguments other than the fixed pitch argument will be specified in the
device driver documentation. Bit reversal is required by some display hardware.
## The font file
Assume that the you have employed the utility to create a file ``myfont.py``. In
your code you will issue
```python
import myfont
```
The ``myfont`` module name will then be passed to the device driver to render
strings on demand. The detailed layout of the Python file may be seen [here](./DRIVERS.md).
# Dependencies, links and licence
The code is released under the MIT licence. It requires Python 3.2 or later.
The module relies on [Freetype](https://www.freetype.org/) which is included in most Linux distributions.
It uses the [Freetype Python bindings](http://freetype-py.readthedocs.io/en/latest/index.html)
which will need to be installed.
My solution draws on the excellent example code written by Daniel Bader. This
may be viewed [here](https://dbader.org/blog/monochrome-font-rendering-with-freetype-and-python) and [here](https://gist.github.com/dbader/5488053).
# Appendix: RAM utilisation Test Results
A font file was created, frozen as bytecode and deployed to a version 1.0
Pyboard. The font was saved as variable pitch with a height of 19 pixels. The
following code was then pasted at the REPL:
```python
import gc, micropython
gc.collect()
micropython.mem_info()
import freeserif
gc.collect()
micropython.mem_info()
def foo():
addr, height, width = freeserif.get_ch('a')
foo()
gc.collect()
micropython.mem_info()
print(len(freeserif._font) + len(freeserif._index))
```
The memory used was 5408, 5648, and 5696 bytes. As increments over the initial
state this corresponds to 240 and 288 bytes. The ``print`` statement shows the
RAM which would be consumed by the data arrays: this was 3271 bytes.
The ``foo()`` function emulates the behaviour of a device driver in rendering a
character to a display. The local variables constitute memory which will be
reclaimed on exit from the function. Its additional RAM use was 48 bytes.
## Conclusion
With a font of height 19 pixels RAM saving was an order of magnitude. The
saving will be greater if larger fonts are used

269
README.md
Wyświetl plik

@ -1,21 +1,9 @@
# font_to_py.py
# MicroPython font handling
This is a utility written in Python 3 and intended to be run on a PC. It takes
as input a font file in ttf or otf form and a height and outputs a Python
source file containing the font data. The purpose is to enable font files to be
used on microcontrollers running MicroPython: Python source files may be frozen
as bytecode. In this form they can be accessed at speed while using very little
RAM. The design has the following aims:
This is an attempt to offer a standard method of creating and deploying fonts
to MicroPython display drivers.
* Independence of specific display hardware.
* The path from font file to Python code to be fully open source.
The first is achieved by supplying hardware specific arguments to the utility.
These define horizontal or vertical mapping and the bit order for font data.
The second is achieved by using Freetype and the Freetype Python bindings.
# Rationale
# Introduction
MicroPython platforms generally have limited RAM, but more abundant storage in
the form of flash memory. Font files tend to be relatively large. The
@ -26,16 +14,46 @@ demand is too slow for reasonable performance on most display devices.
This alternative implements a font as a Python source file, with the data being
declared as ``bytes`` objects. Such a file may be frozen as bytecode. On import
very little RAM is used, yet the data may be accessed fast.
very little RAM is used, yet the data may be accessed fast. Note that the use
of frozen bytecode is entirely optional: font files may be imported in the
normal way if RAM usage is not an issue.
It is intended that the resultant file be usable with a variety of display
devices and drivers. These include:
It is intended that the resultant file be usable with two varieties of display
devices and drivers. These comprise:
1. A driver for the official ``framebuffer`` class.
2. Drivers using ``bytearray`` instances as frame buffers.
3. Drivers for displays where the frame buffer is implemented in the display
1. Drivers using ``bytearray`` instances as frame buffers, including the
official ``framebuffer`` class.
2. Drivers for displays where the frame buffer is implemented in the display
device hardware.
# The proposed solution
This consists of three components:
1. font_to_py.py This is a utility intended to be run on a PC and converts a
font file to Python source. See below.
2. The Writer class (writer.py) This facilitates writing text to a device
given a suitably designed device driver. See [here](./DRIVERS.md).
3. A device driver specification. This includes an example for rendering text
to an SSD1306 device with arbitrary fonts. See [here](./DRIVERS.md).
# font_to_py.py
This is a command line utility written in Python 3 to be run on a PC. It takes
as input a font file in ttf or otf form and a height and outputs a Python
source file containing the font data. Fixed and variable pitch rendering are
supported. The design has the following aims:
* Independence of specific display hardware.
* The path from font file to Python code to be fully open source.
The first is achieved by supplying hardware specific arguments to the utility.
These define horizontal or vertical mapping and the bit order for font data.
The second is achieved by using Freetype and the Freetype Python bindings. Its
use is documented [here](./FONT_TO_PY.md). This also details measurements of
RAM usage when importing fonts stored as frozen bytecode.
# Limitations
Only the ASCII character set from ``chr(32)`` to ``chr(126)`` is supported.
@ -43,207 +61,10 @@ Kerning is not supported. Fonts are one bit per pixel. This does not rule out
colour displays: the device driver can add colour information at the rendering
stage. It does assume that all pixels of a character are rendered identically.
# Usage
Converting font files programmatically works best for larger fonts. For small
fonts, like the 8*8 default used by the SSD1306 driver, it is best to use
binary font files: these are hand designed for rendering at a specific size.
``font_to_py.py`` is a command line utility written in Python 3. It is run on a
PC. It takes as input a font file with a ``ttf`` or ``otf`` extension and a
required height in pixels and outputs a Python 3 source file. The pixel layout
is determined by command arguments. By default fonts are stored in variable
pitch form. This may be overidden by a command line argument.
# Licence
Further arguments ensure that the byte contents and layout are correct for the
target display hardware. Their usage should be specified in the documentation
for the device driver.
Example usage to produce a file ``myfont.py`` with height of 23 pixels:
``font_to_py.py FreeSans.ttf 23 myfont.py``
## Arguments
### Mandatory positional arguments:
1. Font file path. Must be a ttf or otf file.
2. Height in pixels.
3. Output file path. Must have a .py extension.
### Optional arguments:
* -f or --fixed If specified, all characters will have the same width. By
default fonts are assumed to be variable pitch.
* -x Specifies horizontal mapping (default is vertical).
* -b Specifies bit reversal in each font byte.
Optional arguments other than the fixed pitch argument will be specified in the
device driver documentation. Bit reversal is required by some display hardware.
## The font file
Assume that the you have employed the utility to create a file ``myfont.py``. In
your code you will issue
```python
import myfont
```
The ``myfont`` module name will then be passed to the device driver to render
strings on demand.
# Test Results: RAM utilisation
A font file was created, frozen as bytecode and deployed to a version 1.0
Pyboard. The font was saved as variable pitch with a height of 19 pixels. The
following code was then pasted at the REPL:
```python
import gc, micropython
gc.collect()
micropython.mem_info()
from uctypes import bytearray_at
import freeserif
gc.collect()
micropython.mem_info()
def foo():
addr, height, width = freeserif.get_ch('a')
arr = bytearray_at(addr, 3*2)
foo()
gc.collect()
micropython.mem_info()
print(len(freeserif._font) + len(freeserif._index))
```
The memory used was 4416, 4704, and 4736 bytes. As increments over the initial
state this corresponds to 288 and 320 bytes. The ``print`` statement shows the
RAM which would be consumed by the data arrays: this was 3271 bytes.
The ``foo()`` function emulates the behaviour of a device driver in rendering a
character to a display. The local variables constitute memory which will be
reclaimed on exit from the function. Its additional RAM use was 32 bytes.
## Conclusion
With a font of height 19 pixels RAM saving was an order of magnitude. The
saving will be greater if larger fonts are used
# Dependencies, links and licence
The code is released under the MIT licence. It requires Python 3.2 or later.
The module relies on [Freetype](https://www.freetype.org/) which is included in most Linux distributions.
It uses the [Freetype Python bindings](http://freetype-py.readthedocs.io/en/latest/index.html)
which will need to be installed.
My solution draws on the excellent example code written by Daniel Bader. This
may be viewed [here](https://dbader.org/blog/monochrome-font-rendering-with-freetype-and-python) and [here](https://gist.github.com/dbader/5488053).
# Implementation
This section of the README is intended for writers of device drivers.
## Overview
The Python source file produced by ``font_to_py.py`` provides a fast means of
accessing the byte data corresponding to an individual character. It is the
responsibility of the driver to copy that data to the framebuffer or physical
device. The purpose of the command line arguments specified to the user is to
ensure that the data layout is optimised for the device so that the copy is a
simple bytewise copy.
The user program imports a Python font file. When the user program needs to
display a string it passes the module name to the device driver. The module
exposes appropriate font metrics (defined in pixels) and a ``get_ch()``
function. The latter provides fast access to the bytes corresponding to an
individual character together with character specific metrics.
Fixed width characters include blank bits after the character bits to pad out
the width. Variable pitch characters include a small, character specific,
number of blank "advance" bits to provide correct spacing between characters.
## Font files
Assume the user has run the utility to produce a file ``myfont.py`` This then
has the following outline definition (in practice the bytes objects are large):
```python
# Code generated by font-to-py.py.
# Font: FreeSerif.ttf
version = '0.1'
def height():
return 21
def max_width():
return 22
def hmap():
return False
def reverse():
return False
def monospaced():
return False
_font =\
b'\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
b'\x00\x00\x00\x00\x08\x00\xfe\xc7\x00\x7e\xc0\x00\x00\x00\x00\x00'\
_index =\
b'\x00\x00\x14\x00\x2e\x00\x4b\x00\x71\x00\x97\x00\xd2\x00\x0a\x01'\
b'\x1b\x01\x35\x01\x4f\x01\x75\x01\x9e\x01\xb2\x01\xcc\x01\xe0\x01'\
# Boilerplate code omitted
def get_ch(ch):
# validate ch, if out of range use '?'
# get offsets into _font and retrieve char width
# Return: address of start of bitmap, height and width
return memoryview(_font[offset + 2, next_offset]), height, width
```
``height`` and ``width`` are specified in bits (pixels).
In the case of monospaced fonts the ``max_width`` function returns the width of
every character. For variable pitch fonts it returns the width of the widest
character. Device drivers can use this to rapidly determine whether a string
will fit the available space. If it will fit on the assumption that all chars
are maximum width, it can be rendered rapidly without doing a character by
character check.
``get_ch()`` returns a memoryview of an individual glyph with its dimensions.
## Mapping
A character occupies a space where (0, 0) represents the coordinates of the top
left hand corner of the bitmap. It comprises a set of pixels where increasing x
values represent locations to the right of the origin and increasing y values
represent downward positions. Mapping is the process whereby this two
dimensional array of bits is transformed into a linear sequence of bytes.
Vertical mapping means that the LSB of first byte is pixel (0,0), MSB of first
byte is (0, 7). The second byte (assuming the height is greater than 8 pixels)
is (0, 8) to (0, 15). Once the column is complete the next byte represents
(1, 0) to (1, 7).
Horizontal mapping means that the MSB of byte 0 is pixel (0,0) with LSB at
(7,0), with the second byte covering (8, 0) to (15, 0) if the width is greater
than 8.
Bit reversal provides for the case where the bit order of each byte is reversed
i.e. a byte comprising bits [b7b6b5b4b3b2b1b0] becomes [b0b1b2b3b4b5b6b7].
# Specification Notes
The design aims primarily to minimise RAM usage. Minimising the size of the
bytecode is a secondary aim. Indexed addressing will be used to reduce this in
the case of proportional fonts, at a small cost in performance. The size of the
Python source file is a lesser consideration, with readability being prioritised
over size. Hence they will be "pretty printed" with the large bytes objects
split over multiple lines for readability.
This general approach has been tested on a Pyboard connected to LCD hardware
having an onboard frame buffer. The visual performance is good.
All code is released under the MIT licence.

84
driver_test.py 100644
Wyświetl plik

@ -0,0 +1,84 @@
# driver_test.py Simple test for rendering text to an ssd1306 display in
# arbitrary fonts
# V0.1 Peter Hinch Nov 2016
# The MIT License (MIT)
#
# Copyright (c) 2016 Peter Hinch
#
# 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.
# This demo uses a modified version of the official SSD1306 driver to fix an
# issue with the scroll method. Once the official version is fixed, it should
# be substituted for ssd1306_drv
import machine
import utime
from writer import Writer
from ssd1306_drv import SSD1306_I2C, SSD1306_SPI # Until official module is fixed
import freeserif
import freesans20
import inconsolata16
# Display parameters
WIDTH = const(128)
SPI = False
# Note that HEIGHT is set below
if SPI:
# Pyb SSD
# 3v3 Vin
# Gnd Gnd
# X1 DC
# X2 CS
# X3 Rst
# X6 CLK
# X8 DATA
HEIGHT = 32
pdc = machine.Pin('X1', machine.Pin.OUT_PP)
pcs = machine.Pin('X2', machine.Pin.OUT_PP)
prst = machine.Pin('X3', machine.Pin.OUT_PP)
spi = machine.SPI(1)
display = SSD1306_SPI(WIDTH, HEIGHT, spi, pdc, prst, pcs)
else: # I2C
# Pyb SSD
# 3v3 Vin
# Gnd Gnd
# Y9 CLK
# Y10 DATA
HEIGHT = 64
pscl = machine.Pin('Y9', machine.Pin.OUT_PP)
psda = machine.Pin('Y10', machine.Pin.OUT_PP)
i2c = machine.I2C(scl=pscl, sda=psda)
display = SSD1306_I2C(WIDTH, HEIGHT, i2c)
serif = Writer(display, freeserif)
sans = Writer(display, freesans20)
Writer.set_clip(True, True) # Disable auto scrolling and wrapping.
serif.printstring('Tuesday\n')
sans.printstring('8 Nov 2016\n')
sans.printstring('10.30am')
display.show()
def scroll_test(x, y):
t = utime.ticks_us()
display.scroll(x, y) # 125ms
print(utime.ticks_diff(utime.ticks_us(), t))
display.show()

Wyświetl plik

@ -325,25 +325,19 @@ version = '0.1'
"""
STR02 = """
try:
from uctypes import addressof
except ImportError:
pass
def _chr_addr(ordch):
offset = 2 * (ordch - 32)
return int.from_bytes(_index[offset:offset + 2], 'little')
def get_ch(ch, test=False):
def get_ch(ch):
ordch = ord(ch)
ordch = ordch if ordch >= 32 and ordch <= 126 else ord('?')
offset = _chr_addr(ordch)
width = int.from_bytes(_font[offset:offset + 2], 'little')
if test:
next_offs = _chr_addr(ordch +1)
return _font[offset + 2:next_offs], {}, width
return addressof(_font) + offset + 2, {}, width
next_offs = _chr_addr(ordch +1)
return memoryview(_font[offset + 2:next_offs]), {}, width
"""
def write_func(stream, name, arg):

122
ssd1306_drv.py 100644
Wyświetl plik

@ -0,0 +1,122 @@
# ssd1306_drv.py An implementation of a Display class for SSD1306 based displays
# V0.1 Peter Hinch Nov 2016
# https://learn.adafruit.com/monochrome-oled-breakouts/wiring-128x32-spi-oled-display
# https://www.proto-pic.co.uk/monochrome-128x32-oled-graphic-display.html
# Version supports vertical and "horizontal" modes.
# The MIT License (MIT)
#
# Copyright (c) 2016 Peter Hinch
#
# 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.
# SSD1306 classes to fix the official one whose scroll method is broken
# This also demonstrates a way to do scrolling for devices with true vertical
# mapping (the official driver sets the device into its "horizontal" mode).
# This is a hybrid mode where bytes are aligned vertically but arranged
# horizontally
import ssd1306
class SSD1306(object):
def __init__(self, device):
self.width = device.width # In pixels
self.height = device.height
self.bpc = (self.height + 7) >> 3
self.device = device
self.vmap = False # Currently the official driver uses "horizontal"
@property
def buffer(self): # property emulates underlying device
return self.device.buffer
def show(self):
self.device.show()
# Return map-independent offset into buffer
def _offset(self, x, y):
if self.vmap:
return y + x * self.bpc
else:
return y * self.width + x
def scroll(self, x, y):
dy = abs(y)
if dy:
self._scrolly(dy, y > 0)
dx = abs(x)
if dx:
self._scrollx(dx, x > 0)
def _scrolly(self, ystep, down):
buf = self.device.buffer
bpc = self.bpc
ystep_bytes = (ystep >> 3) + 1
ystep_bits = ystep & 7
if down:
for x in range(self.width):
for ydest in range(bpc - 1, -1, -1):
ysource = ydest - ystep_bytes
data = 0
if ysource + 1 >= 0:
data = buf[self._offset(x, ysource + 1)] << ystep_bits
if ysource >= 0:
data |= buf[self._offset(x, ysource)] >> 8 - ystep_bits
buf[self._offset(x, ydest)] = data
else:
for x in range(self.width):
for ydest in range(bpc):
ysource = ydest + ystep_bytes
data = 0
if ysource < bpc:
data = buf[self._offset(x, ysource)] << (8-ystep_bits)
if ysource - 1 < bpc:
data |= buf[self._offset(x, ysource - 1)] >> ystep_bits
buf[self._offset(x, ydest)] = data
def _scrollx(self, xstep, right): # scroll x
buf = self.device.buffer
bpc = self.bpc
for y in range(bpc):
if right: # Scroll right
for xdest in range(self.width - 1, -1, -1):
data = 0
xsource = xdest - xstep
if xsource >= 0:
data = buf[self._offset(xsource, y)]
buf[self._offset(xdest, y)] = data
else:
for xdest in range(0, self.width):
data = 0
xsource = xdest + xstep
if xsource < self.width:
data = buf[self._offset(xsource, y)]
buf[self._offset(xdest, y)] = data
class SSD1306_I2C(SSD1306):
def __init__(self, width, height, i2c, addr=0x3c, external_vcc=False):
device = ssd1306.SSD1306_I2C(width, height, i2c, addr, external_vcc)
super().__init__(device)
class SSD1306_SPI(SSD1306):
def __init__(self, width, height, spi, dc, res, cs, external_vcc=False):
device = ssd1306.SSD1306_SPI(width, height, spi, dc, res, cs, external_vcc)
super().__init__(device)

123
writer.py 100644
Wyświetl plik

@ -0,0 +1,123 @@
# writer.py Implements the Writer class.
# V0.1 Peter Hinch Nov 2016
# The MIT License (MIT)
#
# Copyright (c) 2016 Peter Hinch
#
# 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.
# A Writer supports rendering text to a Display instance in a given font.
# Currently supports vertical mapping and the SSD1306 "pseudo-horizontal" map
# only
# Multiple Writer instances may be created, each rendering a font to the
# same Display object.
VERT = 0
HORIZ = 1
WEIRD = 2
class Writer(object):
text_row = 0 # attributes common to all Writer instances
text_col = 0
row_clip = False # Clip or scroll when screen full
col_clip = False # Clip or new line when row is full
bmap = WEIRD # SSD1306 pseudo-horizontal
@classmethod
def set_textpos(cls, row, col):
cls.text_row = row
cls.text_col = col
@classmethod
def set_clip(cls, row_clip, col_clip):
cls.row_clip = row_clip
cls.col_clip = col_clip
@classmethod
def mapping(cls, bmap):
if bmap in (VERT, HORIZ):
cls.bmap = bmap
else:
raise ValueError('Unsupported mapping')
def __init__(self, device, font):
super().__init__()
self.device = device
self.font = font
if font.hmap():
raise OSError('Font must be vertically mapped')
self.screenwidth = device.width # In pixels
self.screenheight = device.height
div, mod = divmod(device.height, 8)
self.bytes_per_col = div + 1 if mod else div
def _newline(self):
height = self.font.height()
Writer.text_row += height
Writer.text_col = 0
margin = self.screenheight - (Writer.text_row + height)
if margin < 0:
if not Writer.row_clip:
self.device.scroll(0, margin)
Writer.text_row += margin
def printstring(self, string):
for char in string:
self._printchar(char)
def _printchar(self, char):
bmap = Writer.bmap # Buffer mapping
if char == '\n':
self._newline()
return
fbuff = self.device.buffer
glyph, char_height, char_width = self.font.get_ch(char)
if Writer.text_row+char_height > self.screenheight and Writer.row_clip:
return
if Writer.text_col + char_width > self.screenwidth:
if Writer.col_clip:
return
else:
self._newline()
div, mod = divmod(char_height, 8)
gbytes = div + 1 if mod else div # No. of bytes per column of glyph
start_row, align = divmod(Writer.text_row, 8)
for scol in range(0, char_width): # Source column
dcol = scol + Writer.text_col # Destination column
dest_row_byte = start_row
for gbyte in range(gbytes): # Each glyph byte in column
if bmap == VERT:
dest = dcol * self.bytes_per_col + dest_row_byte
elif bmap == WEIRD:
dest = dcol + dest_row_byte * self.screenwidth
source = scol * gbytes + gbyte
data = fbuff[dest] & (0xff >> (8 - align))
fbuff[dest] = data | glyph[source] << align
if align and (dest_row_byte + 1) < self.bytes_per_col:
if bmap == VERT:
dest += 1
elif bmap == WEIRD:
dest += self.screenwidth
data = fbuff[dest] & (0xff << align)
fbuff[dest] = data | glyph[source] >> (8 - align)
dest_row_byte += 1
if dest_row_byte >= self.bytes_per_col:
break
Writer.text_col += char_width