Module eink.generate

Expand source code
from .client_code_generator import ClientCodeGenerator
from .client_config import ClientConfig
from .rotation import Rotation
from .server_code_generator import ServerCodeGenerator
from .status_images import StatusImages
from .transport import Transport
from .web_transport import WebTransport

__all__ = [
    'ClientCodeGenerator', 'ClientConfig', 'Rotation', 'ServerCodeGenerator',
    'StatusImages', 'Transport', 'WebTransport']

Sub-modules

eink.generate.client_code_generator
eink.generate.client_config
eink.generate.rotation
eink.generate.server_code_generator
eink.generate.status_images
eink.generate.transport
eink.generate.web_transport

Classes

class ClientCodeGenerator

Generates client-side source code for the Inkplate device.

Expand source code
class ClientCodeGenerator:
    """Generates client-side source code for the Inkplate device."""

    # The cached return value of _str_literal_list()
    _str_literal_list_cache = None

    @staticmethod
    def gen(config, dir_):
        """Generate client-side source code files for the Inkplate device.

        Arguments:
            config (ClientConfig): The configuration for the program.
            dir_ (str): The directory in which to store the resulting
                source code files. This directory must already exist.
        """
        ClientCodeGenerator._validate(config, dir_)
        ClientCodeGenerator._copy_static_files(dir_)
        ClientCodeGenerator._gen_dynamic_files(config, dir_)

    @staticmethod
    def _validate(config, dir_):
        """Raise if we detect an error in the specified arguments to ``gen``.
        """
        if not os.path.isdir(dir_):
            raise OSError('No such directory {:s}'.format(dir_))
        if not config._transports:
            raise ValueError('No Transports provided')
        if not config._wi_fi_networks:
            raise ValueError('No Wi-Fi networks provided')

        status_images = config._status_images
        images = status_images._images
        if status_images._initial_image_name not in images:
            raise ValueError(
                'StatusImages is missing the initial image {:s}'.format(
                    status_images._initial_image_name))
        if status_images._low_battery_image_name not in images:
            raise ValueError(
                'StatusImages is missing the low battery image {:s}'.format(
                    status_images._low_battery_image_name))

        for name, image in images.items():
            if (image.width != status_images._width or
                    image.height != status_images._height):
                raise ValueError(
                    'The size of the image {:s} does not match the size '
                    'passed to the StatusImages constructor'.format(name))
            if EinkGraphics._has_alpha(image):
                raise ValueError(
                    'Alpha channels are not supported. The image {:s} has an '
                    'alpha channel.'.format(image))

    @staticmethod
    def _copy_static_files(dir_):
        """Copy the static files to the specified directory.

        Copy the source code files for the client that are not generated
        programatically to the specified directory.
        """
        client_dir = Project.client_code_dir()
        for subfile in os.listdir(client_dir):
            if subfile == 'client.ino':
                output_subfile = '{:s}.ino'.format(
                    os.path.basename(os.path.abspath(dir_)))
                shutil.copy(
                    os.path.join(client_dir, subfile),
                    os.path.join(dir_, output_subfile))
            elif subfile.endswith(('.cpp', '.h', '.ino')):
                shutil.copy(
                    os.path.join(client_dir, subfile),
                    os.path.join(dir_, subfile))

    @staticmethod
    def _write_bytes_literal(file, bytes_, multiline):
        """Write C code for a literal byte array to the specified file.

        Arguments:
            file (file): The file object.
            bytes_ (bytes): The contents of the byte array.
            multiline (bool): Whether to format the results by writing
                line breaks as appropriate.
        """
        file.write('{')
        if multiline:
            file.write('\n    ')
            column = 4
        else:
            column = 1

        first = True
        for byte in bytes_:
            if not first:
                if not multiline or column < 73:
                    file.write(', ')
                    column += 2
                else:
                    file.write(',\n    ')
                    column = 4
            first = False

            file.write('0x{:02x}'.format(byte))
            column += 4

        if multiline:
            file.write('\n')
        file.write('}')

    @staticmethod
    def _str_literal_list():
        r"""Return a list of literal C string fragments for each character.

        The return value has 256 elements. The (i + 1)th element of the
        return value is for ``chr(i)``. For example, the element for the
        newline character may be ``'\\n'``, while the element for the
        dash character may be ``'-'``. The only escape sequences we use
        are non-numeric escape sequences and three-digit octal escape
        sequences such as ``'\123'``.
        """
        if ClientCodeGenerator._str_literal_list_cache is None:
            literal_list = []
            for i in range(0x20):
                literal_list.append('\\{:03o}'.format(i))
            for i in range(0x20, 0x7f):
                literal_list.append(chr(i))
            for i in range(0x7f, 0x100):
                literal_list.append('\\{:03o}'.format(i))

            escapes = [
                ('"', '\\"'), ('\\', '\\\\'), ('\a', '\\a'), ('\a', '\\a'),
                ('\b', '\\b'), ('\f', '\\f'), ('\n', '\\n'), ('\r', '\\r'),
                ('\t', '\\t'), ('\v', '\\v'),
            ]
            for char, escape in escapes:
                literal_list[ord(char)] = escape
            ClientCodeGenerator._str_literal_list_cache = literal_list
        return ClientCodeGenerator._str_literal_list_cache

    @staticmethod
    def _write_str_literal(file, bytes_):
        """Write C code for a literal string to the specified file.

        The string is null-terminated.

        Arguments:
            file (file): The file object.
            bytes_ (bytes): The C string.
        """
        literal_list = ClientCodeGenerator._str_literal_list()
        file.write('"')
        prev_literal = None
        for byte in bytes_:
            literal = literal_list[byte]
            if literal == '?' and prev_literal == '?':
                # Avoid the possibility of trigraphs
                literal = '\\?'
            file.write(literal)
            prev_literal = literal
        file.write('"')

    @staticmethod
    def _write_str_array(file, strs):
        """Write C code for a literal string array to the specified file.

        The strings are null-terminated.

        Arguments:
            file (file): The file object.
            strs (list<bytes>): The C strings. An element of ``None`` is
                permitted. We encode such elements as ``NULL``.
        """
        if not strs:
            file.write('{}')
            return

        file.write('{\n')
        first = True
        for str_ in strs:
            if not first:
                file.write(',\n')
            first = False
            file.write('    ')
            if str_ is not None:
                ClientCodeGenerator._write_str_literal(file, str_)
            else:
                file.write('NULL')
        file.write('\n}')

    @staticmethod
    def _write_int_array(file, values):
        """Write C code for a literal ``int`` array to the specified file.

        Arguments:
            file (file): The file object.
            values (list<int>): The integers.
        """
        file.write('{')
        first = True
        for value in values:
            if not first:
                file.write(', ')
            first = False
            file.write('{:d}'.format(value))
        file.write('}')

    @staticmethod
    def _write_generated_message(file):
        """Write a C comment indicating a file was generated programatically.

        Arguments:
            file (file): The file object to write the comment to.
        """
        file.write(
            '// Auto-generated by the Python class '
            'eink.generate.ClientCodeGenerator\n\n')

    @staticmethod
    def _write_secrets_cpp(file, config):
        """Write the contents of the secrets.cpp file.

        This contains all of the information that we would like to keep
        private. We should refrain from committing the file to a
        repository and from opening the file in a text editor.

        Arguments:
            file (file): The file object to write to.
            config (ClientConfig): The configuration for the program.
        """
        passwords = []
        for _, password in config._wi_fi_networks:
            if password is not None:
                passwords.append(password.encode())
            else:
                passwords.append(None)

        ClientCodeGenerator._write_generated_message(file)
        if None in passwords:
            file.write('#include <stddef.h>\n\n')
        file.write('#include "secrets_constants.h"\n\n\n')
        file.write('const char* WI_FI_PASSWORDS[] = ')
        ClientCodeGenerator._write_str_array(file, passwords)
        file.write(';\n')

    @staticmethod
    def _write_status_image_data_h(file, status_images):
        """Write the contents of the status_image_data.h file.

        This declares the constants that are provided in
        status_image_data.cpp.

        Arguments:
            file (file): The file object to write to.
            status_images (StatusImages): The status images for the
                program.
        """
        ClientCodeGenerator._write_generated_message(file)
        file.write(
            '#ifndef __STATUS_IMAGE_DATA_H__\n'
            '#define __STATUS_IMAGE_DATA_H__\n\n'
            '// The contents of each of the status image files, in the same '
            'order as\n'
            '// STATUS_IMAGE_DATA\n')
        for index in range(len(status_images._images)):
            file.write(
                'extern const char STATUS_IMAGE_DATA{:d}[];\n'.format(index))

        file.write(
            '\n// The number of bytes in each of the status image files, in '
            'the same order as\n'
            '// STATUS_IMAGE_DATA\n')
        for index in range(len(status_images._images)):
            file.write(
                'extern const int STATUS_IMAGE_DATA_LENGTH{:d};\n'.format(
                    index))
        file.write('\n#endif\n')

    @staticmethod
    def _render_status_image(image, quality, palette):
        """Render the specified status image.

        Arguments:
            image (image): The image.
            quality (int): The quality, as in the ``quality`` argument
                to ``StatusImages.set_image``.
            palette (Palette): The color palette to use.

        Returns:
            bytes: The contents of the image file for the image.
        """
        image = EinkGraphics.round(image, palette)
        png = ImageData.render_png(image, palette, True)
        if quality < 100:
            jpeg = ImageData.render_jpeg(image, quality)
            if len(jpeg) < len(png):
                return jpeg
        return png

    @staticmethod
    def _write_status_image_data_cpp(file, status_images, palette):
        """Write the contents of the status_image_data.cpp file.

        This contains the contents and sizes of the image files for the
        status images. We keep these in a separate file in order to
        improve the readability of generated.cpp.

        Arguments:
            file (file): The file object to write to.
            status_images (StatusImages): The status images for the
                program.
            palette (Palette): The color palette to use.
        """
        ClientCodeGenerator._write_generated_message(file)
        file.write('#include "status_image_data.h"\n\n')

        images = []
        for name, image in status_images._images.items():
            quality = status_images._quality[name]
            images.append((ServerIO.image_id(name), image, quality))

        sorted_images = sorted(images, key=lambda image: image[0])
        for index, (_, image, quality) in enumerate(sorted_images):
            image_data = ClientCodeGenerator._render_status_image(
                image, quality, palette)
            file.write('\n')
            file.write(
                'const int STATUS_IMAGE_DATA_LENGTH{:d} = {:d};\n'.format(
                    index, len(image_data)))
            file.write('const char STATUS_IMAGE_DATA{:d}[] = '.format(index))
            ClientCodeGenerator._write_bytes_literal(file, image_data, True)
            file.write(';\n')

    @staticmethod
    def _write_generated_h(file, config):
        """Write the contents of the generated.h file.

        This ``#includes`` the header files that declare all of the
        generated constants, and it defines all of the constants that
        are defined using ``#define``.

        Arguments:
            file (file): The file object to write to.
            config (ClientConfig): The configuration for the program.
        """
        ClientCodeGenerator._write_generated_message(file)
        file.write(
            '#ifndef __GENERATED_H__\n'
            '#define __GENERATED_H__\n\n'
            '#include "generated_constants.h"\n'
            '#include "secrets_constants.h"\n\n\n')
        file.write(
            '// The maximum number of elements in ClientState.requestTimesDs\n'
            '#define MAX_REQUEST_TIMES {:d}\n\n'.format(
                Server._MAX_REQUEST_TIMES))
        file.write(
            '// The number of bytes in an image ID, as in the return value of '
            'the Python\n'
            '// method ServerIO.image_id\n'
            '#define STATUS_IMAGE_ID_LENGTH {:d}\n\n'.format(
                ServerIO.STATUS_IMAGE_ID_LENGTH))
        file.write(
            '// The number of elements in the return value of '
            'requestTransports()\n'
            '#define TRANSPORT_COUNT {:d}\n\n'.format(len(config._transports)))
        file.write(
            '// The color palette to use\n'
            '#define PALETTE_{:s}\n\n'.format(config._palette._name))
        file.write('#endif\n')

    @staticmethod
    def _write_status_images(file, status_images):
        """Write the portion of the generated.cpp file for status images.

        Arguments:
            file (file): The file object to write to.
            status_images (StatusImages): The status images for the
                program.
        """
        image_id_to_name = {}
        for name in status_images._images.keys():
            image_id_to_name[ServerIO.image_id(name)] = name
        sorted_image_ids = sorted(list(image_id_to_name.keys()))
        image_count = len(sorted_image_ids)
        file.write(
            'const int STATUS_IMAGE_COUNT = {:d};\n'.format(image_count))

        file.write('const int STATUS_IMAGE_DATA_LENGTHS[] = {\n')
        for index in range(image_count):
            if index > 0:
                file.write(',\n')
            file.write('    STATUS_IMAGE_DATA_LENGTH{:d}'.format(index))
        file.write('\n};\n')

        file.write('const char* STATUS_IMAGE_DATA[] = {\n')
        for index in range(image_count):
            if index > 0:
                file.write(',\n')
            file.write('    STATUS_IMAGE_DATA{:d}'.format(index))
        file.write('\n};\n\n')

        for index, image_id in enumerate(sorted_image_ids):
            file.write('const char STATUS_IMAGE_ID{:d}[] = '.format(index))
            ClientCodeGenerator._write_bytes_literal(file, image_id, True)
            file.write(';\n')
        file.write('const char* STATUS_IMAGE_IDS[] = {\n')
        for index in range(image_count):
            if index > 0:
                file.write(',\n')
            file.write('    STATUS_IMAGE_ID{:d}'.format(index))
        file.write('\n};\n')

        # Compute STATUS_IMAGES_BY_TYPE
        image_name_to_index = {}
        for index, image_id in enumerate(sorted_image_ids):
            name = image_id_to_name[image_id]
            image_name_to_index[name] = index
        status_images_by_type = [
            status_images._initial_image_name,
            status_images._low_battery_image_name]
        status_image_indices = []
        for name in status_images_by_type:
            status_image_indices.append(image_name_to_index[name])

        file.write('const int STATUS_IMAGES_BY_TYPE[] = ')
        ClientCodeGenerator._write_int_array(file, status_image_indices)
        file.write(';\n')

    @staticmethod
    def _write_transports(file, transports):
        """Write the portion of the generated.cpp file for transports.

        Arguments:
            file (file): The file object to write to.
            transports (list<Transport>): The transports for the
                program, in the order the client should try to connect
                to them.
        """
        transport_urls = list([
            transport._url.encode() for transport in transports])
        file.write('const char* TRANSPORT_URLS[] = ')
        ClientCodeGenerator._write_str_array(file, transport_urls)
        file.write(';\n')

    @staticmethod
    def _write_generated_cpp(file, config):
        """Write the contents of the generated.cpp file.

        This provides all of the generated constants that don't have
        some special reason to be provided elsewhere.

        Arguments:
            file (file): The file object to write to.
            config (ClientConfig): The configuration for the program.
        """
        ClientCodeGenerator._write_generated_message(file)
        file.write(
            '#include "generated.h"\n'
            '#include "status_image_data.h"\n\n\n')
        file.write(
            'const int ROTATION = {:d};\n'.format(config._rotation.value))

        file.write('const char HEADER[] = ')
        ClientCodeGenerator._write_bytes_literal(file, ServerIO.HEADER, True)
        file.write(';\n')
        file.write(
            'const int HEADER_LENGTH = {:d};\n'.format(len(ServerIO.HEADER)))

        file.write('const char PROTOCOL_VERSION[] = ')
        ClientCodeGenerator._write_str_literal(file, ServerIO.PROTOCOL_VERSION)
        file.write(';\n')
        file.write(
            'const int PROTOCOL_VERSION_LENGTH = {:d};\n\n'.format(
                len(ServerIO.PROTOCOL_VERSION)))

        ClientCodeGenerator._write_status_images(file, config._status_images)
        file.write('\n')
        ClientCodeGenerator._write_transports(file, config._transports)
        file.write('\n')

        file.write(
            'const int WI_FI_NETWORK_COUNT = {:d};\n'.format(
                len(config._wi_fi_networks)))

        ssids = list([
            network[0].encode() for network in config._wi_fi_networks])
        file.write('const char* WI_FI_SSIDS[] = ')
        ClientCodeGenerator._write_str_array(file, ssids)
        file.write(';\n')

        networks_with_indices = []
        for index, (ssid, _) in enumerate(config._wi_fi_networks):
            networks_with_indices.append((ssid.encode(), index))
        sorted_networks = sorted(networks_with_indices)
        network_indices = list([network[1] for network in sorted_networks])
        file.write('const int WI_FI_NETWORK_INDICES[] = ')
        ClientCodeGenerator._write_int_array(file, network_indices)
        file.write(';\n')

    @staticmethod
    def _gen_dynamic_files(config, dir_):
        """Write the contents of all of the programatically generated files.

        Arguments:
            config (ClientConfig): The configuration for the program.
            dir_ (str): The directory in which to store the files.
        """
        # Contains the secret values
        with open(os.path.join(dir_, 'secrets.cpp'), 'w') as file:
            ClientCodeGenerator._write_secrets_cpp(file, config)

        # Declares constants for status_image_data.cpp
        with open(os.path.join(dir_, 'status_image_data.h'), 'w') as file:
            ClientCodeGenerator._write_status_image_data_h(
                file, config._status_images)

        # Contains the image files for the status images
        with open(os.path.join(dir_, 'status_image_data.cpp'), 'w') as file:
            ClientCodeGenerator._write_status_image_data_cpp(
                file, config._status_images, config._palette)

        # #includes the header files that declare the generated constants, and
        # defines all of the constants that use #define
        with open(os.path.join(dir_, 'generated.h'), 'w') as file:
            ClientCodeGenerator._write_generated_h(file, config)

        # Contains the rest of the generated constants
        with open(os.path.join(dir_, 'generated.cpp'), 'w') as file:
            ClientCodeGenerator._write_generated_cpp(file, config)

Static methods

def gen(config, dir_)

Generate client-side source code files for the Inkplate device.

Arguments

config : ClientConfig
The configuration for the program.
dir_ : str
The directory in which to store the resulting source code files. This directory must already exist.
Expand source code
@staticmethod
def gen(config, dir_):
    """Generate client-side source code files for the Inkplate device.

    Arguments:
        config (ClientConfig): The configuration for the program.
        dir_ (str): The directory in which to store the resulting
            source code files. This directory must already exist.
    """
    ClientCodeGenerator._validate(config, dir_)
    ClientCodeGenerator._copy_static_files(dir_)
    ClientCodeGenerator._gen_dynamic_files(config, dir_)
class ClientConfig (transport, status_images)

The configuration information for generating the client's source code.

Initialize a new ClientConfig.

Arguments

transport (Transport|list): The server or servers
the client should try to connect to. Each time the
client tries to fetch updated content, it tries each of
the transports in order until it succeeds.
eink.generate.status_images : StatusImages
The status images. See the comments for StatusImages.
Expand source code
class ClientConfig:
    """The configuration information for generating the client's source code.
    """

    # Private attributes:
    #
    # Palette _palette - The color palette to use.
    # Rotation _rotation - The rotation to use when drawing to the Inkplate
    #     device.
    # StatusImages _status_images - The status images. See the comments for
    #     StatusImages.
    # list<Transport> _transports - The servers the client should try to
    #     connect to, in order. Each time the client tries to fetch updated
    #     content, it tries each of the transports in order until it succeeds.
    # list<tuple<str, str>> _wi_fi_networks - The Wi-Fi networks the client may
    #     connect to, in descending order of preference. Each network is
    #     represented as a pair of the SSID and the password, if any.

    def __init__(self, transport, status_images):
        """Initialize a new ``ClientConfig``.

        Arguments:
            transport (Transport|list<Transport>): The server or servers
                the client should try to connect to. Each time the
                client tries to fetch updated content, it tries each of
                the transports in order until it succeeds.
            status_images (StatusImages): The status images. See the
                comments for ``StatusImages``.
        """
        if isinstance(transport, Transport):
            self._transports = [transport]
        else:
            self._transports = transport
        self._status_images = status_images
        self._wi_fi_networks = []
        self._palette = Palette.THREE_BIT_GRAYSCALE
        self._rotation = Rotation.LANDSCAPE

    def add_wi_fi_network(self, ssid, password):
        """Add the specified Wi-Fi network to the list of networks.

        Add the specified Wi-Fi network to the list of networks the
        client may connect to. The SSID must be visible. Networks from
        earlier calls to ``add_wi_fi_network`` are connected to in
        preference to networks from later calls.

        Arguments:
            ssid (str): The network's SSID.
            password (str): The network's password, if any.
        """
        self._wi_fi_networks.append((ssid, password))

    def set_palette(self, palette):
        """Set the color palette to use.

        The default is ``Palette.THREE_BIT_GRAYSCALE``. This must be a
        palette that the e-ink device supports.

        Arguments:
            palette (Palette): The palette.
        """
        self._palette = palette

    def set_rotation(self, rotation):
        """Set the rotation to use when drawing to the Inkplate device.

        The default is ``Rotation.LANDSCAPE``.

        Arguments:
            rotation (Rotation): The rotation.
        """
        self._rotation = rotation

Methods

def add_wi_fi_network(self, ssid, password)

Add the specified Wi-Fi network to the list of networks.

Add the specified Wi-Fi network to the list of networks the client may connect to. The SSID must be visible. Networks from earlier calls to add_wi_fi_network are connected to in preference to networks from later calls.

Arguments

ssid : str
The network's SSID.
password : str
The network's password, if any.
Expand source code
def add_wi_fi_network(self, ssid, password):
    """Add the specified Wi-Fi network to the list of networks.

    Add the specified Wi-Fi network to the list of networks the
    client may connect to. The SSID must be visible. Networks from
    earlier calls to ``add_wi_fi_network`` are connected to in
    preference to networks from later calls.

    Arguments:
        ssid (str): The network's SSID.
        password (str): The network's password, if any.
    """
    self._wi_fi_networks.append((ssid, password))
def set_palette(self, palette)

Set the color palette to use.

The default is Palette.THREE_BIT_GRAYSCALE. This must be a palette that the e-ink device supports.

Arguments

palette : Palette
The palette.
Expand source code
def set_palette(self, palette):
    """Set the color palette to use.

    The default is ``Palette.THREE_BIT_GRAYSCALE``. This must be a
    palette that the e-ink device supports.

    Arguments:
        palette (Palette): The palette.
    """
    self._palette = palette
def set_rotation(self, rotation)

Set the rotation to use when drawing to the Inkplate device.

The default is Rotation.LANDSCAPE.

Arguments

eink.generate.rotation : Rotation
The rotation.
Expand source code
def set_rotation(self, rotation):
    """Set the rotation to use when drawing to the Inkplate device.

    The default is ``Rotation.LANDSCAPE``.

    Arguments:
        rotation (Rotation): The rotation.
    """
    self._rotation = rotation
class Rotation (value, names=None, *, module=None, qualname=None, type=None, start=1)

A rotation to use when drawing to the Inkplate device.

Expand source code
class Rotation(Enum):
    """A rotation to use when drawing to the Inkplate device."""

    # Landscape rotation
    LANDSCAPE = 4

    # Portrait left (bottom of device on left)
    PORTRAIT_LEFT = 3

    # Portrait right (bottom of device on right)
    PORTRAIT_RIGHT = 1

    # Upside down landscape rotation
    LANDSCAPE_UPSIDE_DOWN = 2

Ancestors

  • enum.Enum

Class variables

var LANDSCAPE
var LANDSCAPE_UPSIDE_DOWN
var PORTRAIT_LEFT
var PORTRAIT_RIGHT
class ServerCodeGenerator

Generates skeleton code for an e-ink server.

Expand source code
class ServerCodeGenerator:
    """Generates skeleton code for an e-ink server."""

    @staticmethod
    def gen_skeleton():
        """Generate skeleton code for an e-ink server.

        We request information about the server using standard input and
        output.
        """
        ServerCodeGenerator._gen_skeleton(
            *ServerCodeGenerator._input_skeleton_params())

    @staticmethod
    def _input(prompt, default, validation_func, hidden=False):
        """Prompt the user for a string input.

        The basic procedure is something like this:

        * Display the prompt.
        * If nothing is entered, return the default value if there is
          one. If not, print an error message and repeat.
        * Otherwise, validate the value using ``validation_func``.
        * If valid, return the value entered.
        * If invalid, print an error message and repeat.

        However, the ``_input`` method is responsible for the details.

        Arguments:
            prompt (str): Text to present to the user indicating what
                information to enter.
            default (str): The value to return if the user enters the
                empty string. If this is ``None``, then there is no
                default value, and we require the user to enter a
                non-empty string.
            validation_func (callable): The function for validating the
                result, if any. If it raises a ``ValueError`` when we
                pass in the value the user entered, then that value is
                not permitted. Such errors should have messages that are
                suitable to display to the user. We do not check
                ``validation_func`` if the user enters the empty string.
            hidden (bool): Whether to hide the user's input as he enters
                it. This is useful for passwords.

        Returns:
            str: The value the user entered, or the default value.
        """
        if default is not None:
            default_str = default
        else:
            default_str = ''
        if prompt.endswith('\n'):
            prompt_with_default = '{:s}[{:s}] '.format(prompt, default_str)
        else:
            prompt_with_default = '{:s} [{:s}] '.format(prompt, default_str)

        while True:
            if hidden:
                value = getpass.getpass(prompt_with_default)
            else:
                value = input(prompt_with_default)

            if value == '':
                if default is None:
                    print()
                    print('Please enter a value')
                    print()
                    continue
                return default
            elif validation_func is None:
                return value

            try:
                validation_func(value)
            except ValueError as error:
                print()
                print(error)
                print()
            else:
                return value

    @staticmethod
    def _input_multiple_choice(prompt, options, default):
        """Prompt the user to select from a list of options.

        The basic procedure is similar to that of ``_input``.

        Arguments:
            prompt (str): Text to present to the user indicating what
                information to enter.
            options (list<tuple<str, object>>): An array of the possible
                options. Each option is represented as a pair of a
                human-readable string identifying the option and the
                value for the option.
            default (str): The index in ``options`` of the option to
                select if the user enters the empty string. If this is
                ``None``, then there is no default option, and we
                require the user to enter a non-empty string.

        Returns:
            object: The option the user selected, as in the second
                element of the ``options`` pairs.
        """
        prompt_components = [prompt, ':\n']
        for index, option in enumerate(options):
            prompt_components.append(str(index + 1))
            prompt_components.append(') ')
            prompt_components.append(option[0])
            prompt_components.append('\n')
        if default is not None:
            default_value = str(default + 1)
        else:
            default_value = None
        value = ServerCodeGenerator._input(
            ''.join(prompt_components), default_value,
            functools.partial(
                ServerCodeGenerator._validate_1_to_n, len(options)))
        return options[int(value) - 1][1]

    @staticmethod
    def _normalize_filename(filename):
        """Equivalent implementation is contractually guaranteed."""
        return os.path.abspath(os.path.expanduser(filename))

    @staticmethod
    def _validate_server_dir(dir_):
        """Validate the server directory.

        Raise a ``ValueError`` if the specified user entry is not a
        valid server directory.

        Arguments:
            dir_ (str): The user entry.
        """
        try:
            absolute_dir = ServerCodeGenerator._normalize_filename(dir_)
        except OSError:
            raise ValueError(
                'Error normalizing the filename {:s}'.format(dir_))

        parent = os.path.dirname(absolute_dir)
        if not os.path.isdir(parent):
            raise ValueError(
                'The parent folder {:s} does not exist'.format(parent))

    @staticmethod
    def _validate_client_dir(server_dir, dir_):
        """Validate the client directory.

        Raise a ``ValueError`` if the specified user entry is not a
        valid client directory.

        Arguments:
            server_dir (str): The server directory.
            dir_ (str): The user entry.
        """
        try:
            absolute_dir = ServerCodeGenerator._normalize_filename(dir_)
        except OSError:
            raise ValueError(
                'Error normalizing the filename {:s}'.format(dir_))

        try:
            if (absolute_dir ==
                    ServerCodeGenerator._normalize_filename(server_dir)):
                raise ValueError(
                    'The client code directory may not be the same as the '
                    'server code directory')
        except OSError:
            pass

        parent = os.path.dirname(absolute_dir)
        try:
            if parent == ServerCodeGenerator._normalize_filename(server_dir):
                return
        except OSError:
            pass

        if not os.path.isdir(parent):
            raise ValueError(
                'The parent folder {:s} does not exist'.format(parent))

    @staticmethod
    def _validate_url(url):
        """Validate the server URL.

        Raise a ``ValueError`` if the specified user entry is not a
        valid server URL.

        Arguments:
            url (str): The user entry.
        """
        scheme = urlparse(url).scheme
        if scheme == 'https':
            raise ValueError(
                'The URL must begin with http://. HTTPS is currently not '
                'supported.')
        elif scheme != 'http':
            raise ValueError('The URL must begin with http://')

    @staticmethod
    def _validate_non_empty(value):
        """Validate an input that may not be empty.

        Raise a ``ValueError`` if the specified user entry is the empty
        string.

        Arguments:
            value (str): The user entry.
        """
        if not value:
            raise ValueError('Please enter a value')

    @staticmethod
    def _is_positive_int(value):
        """Return whether the specified user entry is a positive integer.

        Arguments:
            value (str): The user entry.

        Returns:
            bool: The result.
        """
        return re.search(r'^[1-9]\d*$', value) is not None

    @staticmethod
    def _validate_positive_int(value):
        """Validate a positive integer.

        Raise a ``ValueError`` if the specified user entry is not a
        positive integer.

        Arguments:
            value (str): The user entry.
        """
        if not ServerCodeGenerator._is_positive_int(value):
            raise ValueError('Please enter a positive integer')

    @staticmethod
    def _validate_1_to_n(n, value):
        """Validate an integer from 1 to ``n``.

        Raise a ``ValueError`` if the specified user entry is not an
        integer from 1 to ``n``.

        Arguments:
            n (int): The maximum value.
            value (str): The user entry.
        """
        if not ServerCodeGenerator._is_positive_int(value) or int(value) > n:
            raise ValueError('Please enter 1 - {:d}'.format(n))

    @staticmethod
    def _local_ip_address():
        """Return the device's local IP address, e.g. ``'192.168.1.70'``."""
        # Based on https://stackoverflow.com/a/28950776/10935386
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        try:
            # Doesn't even have to be reachable
            s.connect(('10.255.255.255', 1))
            return s.getsockname()[0]
        except InterruptedError:
            return '127.0.0.1'
        finally:
            s.close()

    @staticmethod
    def _ssid():
        """Return the SSID for the Wi-Fi network we are connected to, if any.

        Return ``None`` if we were unable to determine the SSID.
        """
        if os.name == 'nt' or sys.platform == 'darwin':
            if os.name == 'nt':
                command = ['Netsh', 'WLAN', 'show', 'interfaces']
            else:
                command = [
                    '/System/Library/PrivateFrameworks/Apple80211.framework/'
                    'Resources/airport',
                    '-I']

            try:
                output = subprocess.check_output(command).decode()
            except (
                    OSError, subprocess.CalledProcessError,
                    UnicodeDecodeError):
                return None
            for line in output.split('\n'):
                stripped_line = line.strip()
                if stripped_line.startswith('SSID'):
                    index = stripped_line.index(':')
                    return stripped_line[index + 2:]
            return None
        else:
            try:
                output = subprocess.check_output([
                    '/sbin/iwgetid', '-r']).decode()
            except (
                    OSError, subprocess.CalledProcessError,
                    UnicodeDecodeError):
                return None
            ssid = output.rstrip('\n')
            if ssid:
                return ssid
            else:
                return None

    @staticmethod
    def _input_skeleton_params():
        """Prompt the user for all of the parameters to ``_gen_skeleton``.

        The return value is a tuple with all of those parameters, in
        order.
        """
        local_ip_address = ServerCodeGenerator._local_ip_address()
        connected_ssid = ServerCodeGenerator._ssid()
        if os.name == 'nt':
            default_server_dir = os.path.join(
                os.path.expanduser('~'), 'Documents', 'eink_server')
        else:
            default_server_dir = os.path.join('~', 'eink_server')

        server_dir = ServerCodeGenerator._input(
            'Directory for the server code', default_server_dir,
            ServerCodeGenerator._validate_server_dir)
        client_dir = ServerCodeGenerator._input(
            'Directory for the client code',
            os.path.join(server_dir, 'client'),
            functools.partial(
                ServerCodeGenerator._validate_client_dir, server_dir))
        url = ServerCodeGenerator._input(
            'Server URL',
            'http://{:s}:5000/eink_server'.format(local_ip_address),
            ServerCodeGenerator._validate_url)
        ssid = ServerCodeGenerator._input(
            'Wi-Fi SSID for device to connect to', connected_ssid,
            ServerCodeGenerator._validate_non_empty)
        wi_fi_password = ServerCodeGenerator._input(
            'Password for Wi-Fi (leave blank if none)', '', None, True)

        device = ServerCodeGenerator._input_multiple_choice(
            'Device',
            [
                ('Inkplate 2', Device(212, 104, 'BLACK_WHITE_AND_RED')),
                (
                    'Inkplate 4 TEMPERA',
                    Device(600, 600, 'THREE_BIT_GRAYSCALE')),
                ('Inkplate 5', Device(960, 540, 'THREE_BIT_GRAYSCALE')),
                ('Inkplate 6', Device(800, 600, 'THREE_BIT_GRAYSCALE')),
                ('Inkplate 6COLOR', Device(600, 448, 'SEVEN_COLOR')),
                ('Inkplate 6MOTION', Device(1024, 758, 'FOUR_BIT_GRAYSCALE')),
                ('Inkplate 6PLUS', Device(1024, 758, 'THREE_BIT_GRAYSCALE')),
                ('Inkplate 10', Device(1200, 825, 'THREE_BIT_GRAYSCALE')),
                ('Other/enter parameters manually', None)],
            None)
        rotation = ServerCodeGenerator._input_multiple_choice(
            'Device rotation',
            [
                (
                    'Portrait right (bottom of device on right)',
                    Rotation.PORTRAIT_RIGHT),
                ('Landscape upside down', Rotation.LANDSCAPE_UPSIDE_DOWN),
                (
                    'Portrait left (bottom of device on left)',
                    Rotation.PORTRAIT_LEFT),
                ('Landscape', Rotation.LANDSCAPE)],
            3)

        if device is not None:
            if (rotation == Rotation.LANDSCAPE or
                    rotation == Rotation.LANDSCAPE_UPSIDE_DOWN):
                width = device.width
                height = device.height
            else:
                width = device.height
                height = device.width
            palette_name = device.palette_name
        else:
            if (rotation == Rotation.LANDSCAPE or
                    rotation == Rotation.LANDSCAPE_UPSIDE_DOWN):
                width_prompt = 'Device width in pixels'
                height_prompt = 'Device height in pixels'
            else:
                width_prompt = 'Device width in pixels, after rotation'
                height_prompt = 'Device height in pixels, after rotation'
            width = int(
                ServerCodeGenerator._input(
                    width_prompt, None,
                    ServerCodeGenerator._validate_positive_int))
            height = int(
                ServerCodeGenerator._input(
                    height_prompt, None,
                    ServerCodeGenerator._validate_positive_int))
            palette_name = ServerCodeGenerator._input_multiple_choice(
                'Color palette',
                [
                    ('3-bit grayscale', 'THREE_BIT_GRAYSCALE'),
                    ('4-bit grayscale', 'FOUR_BIT_GRAYSCALE'),
                    ('Black, white, and red', 'BLACK_WHITE_AND_RED'),
                    ('7-color', 'SEVEN_COLOR')],
                0)
        return (
            server_dir, client_dir, url, ssid, wi_fi_password, rotation, width,
            height, palette_name)

    @staticmethod
    def _eval_template(template_filename, output_filename, params):
        """Evaluate a source code template.

        This reads the template at
        assets/server_skeleton/[template_filename], performs string
        substitutions using ``string.Template.substitute(params)``, and
        stores the result in ``output_filename``.

        Arguments:
            template_filename (str): The filename of the template file,
                excluding the directory.
            output_filename (str): The filename of the resulting file.
            params (dict<str, str>): The parameters to the template.
        """
        input_filename = os.path.join(
            Project.server_skeleton_dir(), template_filename)
        with open(input_filename, 'r') as file:
            template = file.read()

        contents = string.Template(template).substitute(params)
        with open(output_filename, 'w') as file:
            file.write(contents)

    @staticmethod
    def _write_gen_client_code(
            server_dir, client_dir, url, ssid, wi_fi_password, rotation,
            palette_name):
        """Write the contents of the gen_client_code.py file.

        This contains code for generating the client's source code.

        Arguments:
            server_dir (str): The directory in which to store the
                resulting file.
            client_dir (str): The directory in which the client code is
                stored.
            url (str): The server's URL.
            ssid (str): The SSID of the Wi-Fi network that the client
                should connect to.
            wi_fi_password (str): The password of the Wi-Fi network that
                the client should connect to. If this is ``''``, then
                there is no password.
            rotation (Rotation): The rotation to use when drawing to the
                e-ink device.
            palette_name (str): The name of the ``Palette`` constant for
                the palette to use. The palette is given by
                ``getattr(Palette, palette_name)``.
        """
        import_os = ''
        import_separator = ''
        if not wi_fi_password:
            wi_fi_password_code = 'None'
            import_json = ''
            read_secrets = ''
        else:
            wi_fi_password_code = "_read_secrets()['wiFiPassword']"
            import_json = '\nimport json'
            import_os = '\nimport os'
            import_separator = '\n'
            read_secrets = (
                '\ndef _read_secrets():\n'
                '    """Return the JSON value stored in '
                'assets/secrets.json."""\n'
                '    project_dir = '
                'os.path.dirname(os.path.abspath(__file__))\n'
                '    secrets_filename = '
                "os.path.join(project_dir, 'assets', 'secrets.json')\n"
                "    with open(secrets_filename, 'r') as file:\n"
                '        return json.load(file)\n\n')

        if rotation == Rotation.LANDSCAPE:
            set_rotation = ''
            import_rotation = ''
        else:
            set_rotation = '\n    config.set_rotation(Rotation.{:s})'.format(
                rotation.name)
            import_rotation = 'from eink.generate import Rotation\n'

        if palette_name == 'THREE_BIT_GRAYSCALE':
            set_palette = ''
        else:
            set_palette = '\n    config.set_palette(MyServer.PALETTE)'

        if os.path.dirname(client_dir) != server_dir:
            set_client_dir = '    dir_ = {:s}'.format(repr(client_dir))
        else:
            set_client_dir = (
                '    project_dir = '
                'os.path.dirname(os.path.abspath(__file__))\n'
                '    dir_ = os.path.join(project_dir, {:s})'.format(
                    repr(os.path.basename(client_dir))))
            import_os = '\nimport os'
            import_separator = '\n'

        ServerCodeGenerator._eval_template(
            'gen_client_code.py.tpl',
            os.path.join(server_dir, 'gen_client_code.py'), {
                'import_json': import_json,
                'import_os': import_os,
                'import_rotation': import_rotation,
                'import_separator': import_separator,
                'read_secrets': read_secrets,
                'set_client_dir': set_client_dir,
                'set_palette': set_palette,
                'set_rotation': set_rotation,
                'ssid': repr(ssid),
                'url': repr(url),
                'wi_fi_password': wi_fi_password_code,
            })

    @staticmethod
    def _gen_client_code(
            client_dir, url, ssid, wi_fi_password, rotation, width, height,
            palette_name):
        """Generate the client code for the skeleton.

        Arguments:
            client_dir (str): The directory in which to store the client
                code.
            url (str): The server's URL.
            ssid (str): The SSID of the Wi-Fi network that the client
                should connect to.
            wi_fi_password (str): The password of the Wi-Fi network that
                the client should connect to. If this is ``''``, then
                there is no password.
            rotation (Rotation): The rotation to use when drawing to the
                e-ink device.
            width (int): The width of the e-ink device in pixels, after
                applying the rotation suggested by ``rotation``.
            height (int): The height of the e-ink device in pixels,
                after applying the rotation suggested by ``rotation``.
            palette_name (str): The name of the ``Palette`` constant for
                the palette to use. The palette is given by
                ``getattr(Palette, palette_name)``.
        """
        status_images = StatusImages.create_default(width, height)
        config = ClientConfig(WebTransport(url), status_images)
        if wi_fi_password:
            config.add_wi_fi_network(ssid, wi_fi_password)
        else:
            config.add_wi_fi_network(ssid, None)
        config.set_rotation(rotation)
        config.set_palette(getattr(Palette, palette_name))
        ClientCodeGenerator.gen(config, client_dir)

    @staticmethod
    def _gen_skeleton(
            server_dir, client_dir, url, ssid, wi_fi_password, rotation, width,
            height, palette_name):
        """Generate a skeleton server.

        Arguments:
            server_dir (str): The directory in which to store the
                server.
            client_dir (str): The directory in which to store the client
                code.
            url (str): The server's URL.
            ssid (str): The SSID of the Wi-Fi network that the client
                should connect to.
            wi_fi_password (str): The password of the Wi-Fi network that
                the client should connect to. If this is ``''``, then
                there is no password.
            rotation (Rotation): The rotation to use when drawing to the
                e-ink device.
            width (int): The width of the e-ink device in pixels, after
                applying the rotation suggested by ``rotation``.
            height (int): The height of the e-ink device in pixels,
                after applying the rotation suggested by ``rotation``.
            palette_name (str): The name of the ``Palette`` constant for
                the palette to use. The palette is given by
                ``getattr(Palette, palette_name)``.
        """
        server_dir = ServerCodeGenerator._normalize_filename(server_dir)
        assets_dir = os.path.join(server_dir, 'assets')
        client_dir = ServerCodeGenerator._normalize_filename(client_dir)
        if not os.path.exists(server_dir):
            os.mkdir(server_dir)
        if not os.path.exists(assets_dir):
            os.mkdir(assets_dir)
        if not os.path.exists(client_dir):
            os.mkdir(client_dir)

        if width >= 440 and height >= 370:
            template_filename = 'my_server.py.tpl'
        else:
            template_filename = 'my_server_low_res.py.tpl'

        if palette_name == 'THREE_BIT_GRAYSCALE':
            import_palette = ''
            palette_constant = ''
            palette_method = ''
            palette_arg = ''
        else:
            import_palette = 'from eink.image import Palette\n'
            palette_constant = (
                '\n    # The palette to use\n'
                '    PALETTE = Palette.{:s}\n'.format(palette_name))
            palette_method = (
                '\n    def palette(self):\n'
                '        return MyServer.PALETTE\n')
            palette_arg = ', MyServer.PALETTE'

        if getattr(Palette, palette_name)._is_grayscale:
            image_line_break = ''
            image_mode = "'L'"
            background_color = '255'
            header_text_color = '0'
            body_text_color = '0'
        else:
            image_line_break = '\n            '
            image_mode = "'RGB'"
            background_color = '(255, 255, 255)'
            body_text_color = '(0, 0, 0)'
            if palette_name == 'SEVEN_COLOR':
                header_text_color = '(67, 138, 28)'
            elif palette_name == 'BLACK_WHITE_AND_RED':
                header_text_color = '(255, 0, 0)'
            else:
                header_text_color = '(0, 0, 0)'

        ServerCodeGenerator._eval_template(
            template_filename, os.path.join(server_dir, 'my_server.py'), {
                'background_color': background_color,
                'body_text_color': body_text_color,
                'header_text_color': header_text_color,
                'height': str(height),
                'image_line_break': image_line_break,
                'image_mode': image_mode,
                'import_palette': import_palette,
                'palette_arg': palette_arg,
                'palette_constant': palette_constant,
                'palette_method': palette_method,
                'width': str(width),
            })

        parsed_url = urlparse(url)
        if parsed_url.path:
            path = parsed_url.path
        else:
            path = '/'
        ServerCodeGenerator._eval_template(
            'app.py.tpl', os.path.join(server_dir, 'app.py'),
            {'path': repr(path)})

        shutil.copy(
            os.path.join(Project.server_skeleton_dir(), 'requirements.txt'),
            os.path.join(server_dir, 'requirements.txt'))
        shutil.copy(
            os.path.join(Project.server_skeleton_dir(), 'GentiumPlus-R.ttf'),
            os.path.join(assets_dir, 'GentiumPlus-R.ttf'))
        shutil.copy(
            os.path.join(Project.server_skeleton_dir(), 'OFL.txt'),
            os.path.join(assets_dir, 'OFL.txt'))

        if wi_fi_password:
            secrets = {'wiFiPassword': wi_fi_password}
            with open(os.path.join(assets_dir, 'secrets.json'), 'w') as file:
                file.write(json.dumps(secrets, indent=4))
                file.write('\n')

        ServerCodeGenerator._write_gen_client_code(
            server_dir, client_dir, url, ssid, wi_fi_password, rotation,
            palette_name)
        ServerCodeGenerator._gen_client_code(
            client_dir, url, ssid, wi_fi_password, rotation, width, height,
            palette_name)

Static methods

def gen_skeleton()

Generate skeleton code for an e-ink server.

We request information about the server using standard input and output.

Expand source code
@staticmethod
def gen_skeleton():
    """Generate skeleton code for an e-ink server.

    We request information about the server using standard input and
    output.
    """
    ServerCodeGenerator._gen_skeleton(
        *ServerCodeGenerator._input_skeleton_params())
class StatusImages (width, height)

Describes the program's "status images."

A "status image" is a special full-screen image that is not produced by Server.render(). It is hardcoded as part of the software on the client device, so that the client can render it without contacting the server. There are three types of status images: the initial image, the low battery image, and the screensaver. Each status image is identified by a string name.

Initialize a new StatusImages object.

Arguments

width : int
The width of the Inkplate display, after rotation (as in ClientConfig.set_rotation).
height : int
The height of the Inkplate display, after rotation (as in ClientConfig.set_rotation).
Expand source code
class StatusImages:
    """Describes the program's "status images."

    A "status image" is a special full-screen image that is not produced
    by ``Server.render()``. It is hardcoded as part of the software on
    the client device, so that the client can render it without
    contacting the server. There are three types of status images: the
    initial image, the low battery image, and the screensaver. Each
    status image is identified by a string name.
    """

    # Private attributes:
    #
    # int _height - The height of the Inkplate display, after rotation (as in
    #     ClientConfig.set_rotation).
    # dict<str, Image> _images - A map from the names of the status images to
    #     the images.
    # str _initial_image_name - The name of the status image to display when
    #     the device is turned on.
    # str _low_battery_image_name - The name of the status image to display if
    #     the device is low on battery. When this happens, we stop trying to
    #     connect to the server.
    # dict<str, int> _quality - A map from the names of the status images to
    #     their qualities, as in the "quality" argument to set_image.
    # int _width - The width of the Inkplate display, after rotation (as in
    #     ClientConfig.set_rotation).

    def __init__(self, width, height):
        """Initialize a new ``StatusImages`` object.

        Arguments:
            width (int): The width of the Inkplate display, after
                rotation (as in ``ClientConfig.set_rotation``).
            height (int): The height of the Inkplate display, after
                rotation (as in ``ClientConfig.set_rotation``).
        """
        self._width = width
        self._height = height
        self._images = {}
        self._quality = {}
        self._initial_image_name = 'connecting'
        self._low_battery_image_name = 'low_battery'

    def set_image(self, name, image, quality=100):
        """Set (or add) the status image with the specified name.

        We automatically reduce the image to the appropriate color
        palette using ``EinkGraphics.round``.

        Arguments:
            name (str): The name of the status image.
            image (Image): The status image. This must be the same size
                as the display.
            quality (int): The compression quality to use to store the
                image. This is a number from 0 to 100, as in the JPEG
                file format. The higher the number, the more faithfully
                we will be able to reproduce the image, but the more
                memory it will require. 100 indicates perfect quality
                (or lossless).

                Normally, a quality of 100 is recommended. But if the
                status images are too numerous and large, it's possible
                that they won't fit in the client's program memory. In
                that case, a lower level of quality is required.
        """
        if image.width != self._width or image.height != self._height:
            raise ValueError(
                'A status image must be the same size as the display')
        self._images[name] = image
        self._quality[name] = quality

    def set_initial_image_name(self, name):
        """Set the name of the initial status image.

        Set the name of the status image to display when the device is
        turned on. The default is ``'connecting'``.
        """
        self._initial_image_name = name

    def set_low_battery_image_name(self, name):
        """Set the name of the low battery image.

        Set the name of the status image to display if the device is low
        on battery. When this happens, we stop trying to connect to the
        server. The default low battery image name is ``'low_battery'``.
        """
        self._low_battery_image_name = name

    def default_initial_image(self):
        """Return the "default" initial status image.

        Return the "default" status image to display when the display is
        turned on. This is a standard image that the ``eink-server``
        library provides as a default.

        Returns:
            Image: The image.
        """
        if self._width >= 320 and self._height >= 220:
            return self._default_image('connecting.png')
        else:
            return self._default_image('connecting_low_res.png')

    def default_low_battery_image(self):
        """Return the "default" low battery image.

        Return the "default" status image to display if the device is
        low on battery. This is a standard image that the
        ``eink-server`` library provides as a default.

        Returns:
            Image: The image.
        """
        if self._width >= 208 and self._height >= 130:
            return self._default_image('low_battery.png')
        else:
            return self._default_image('low_battery_low_res.png')

    @staticmethod
    def create_default(width, height):
        """Return an instance of ``StatusImages`` with "default" images.

        This uses standard images that the ``eink-server`` library
        provides as defaults. It uses the default image names.

        Arguments:
            width (int): The width of the Inkplate display, after
                rotation (as in ``ClientConfig.set_rotation``).
            height (int): The height of the Inkplate display, after
                rotation (as in ``ClientConfig.set_rotation``).
        """
        images = StatusImages(width, height)
        images.set_image('connecting', images.default_initial_image())
        images.set_image('low_battery', images.default_low_battery_image())
        return images

    def _default_image(self, filename):
        """Return a default status image from the specified image file.

        Arguments:
            filename (str): The filename of the image file, excluding
                the directory (``Project.images_dir()``).

        Returns:
            Image: The image.
        """
        image = Image.open(os.path.join(Project.images_dir(), filename))
        output = Image.new('L', (self._width, self._height), 255)
        output.paste(
            image, (
                (self._width - image.width) // 2,
                (self._height - image.height) // 2))
        return output

Static methods

def create_default(width, height)

Return an instance of StatusImages with "default" images.

This uses standard images that the eink-server library provides as defaults. It uses the default image names.

Arguments

width : int
The width of the Inkplate display, after rotation (as in ClientConfig.set_rotation).
height : int
The height of the Inkplate display, after rotation (as in ClientConfig.set_rotation).
Expand source code
@staticmethod
def create_default(width, height):
    """Return an instance of ``StatusImages`` with "default" images.

    This uses standard images that the ``eink-server`` library
    provides as defaults. It uses the default image names.

    Arguments:
        width (int): The width of the Inkplate display, after
            rotation (as in ``ClientConfig.set_rotation``).
        height (int): The height of the Inkplate display, after
            rotation (as in ``ClientConfig.set_rotation``).
    """
    images = StatusImages(width, height)
    images.set_image('connecting', images.default_initial_image())
    images.set_image('low_battery', images.default_low_battery_image())
    return images

Methods

def default_initial_image(self)

Return the "default" initial status image.

Return the "default" status image to display when the display is turned on. This is a standard image that the eink-server library provides as a default.

Returns

Image
The image.
Expand source code
def default_initial_image(self):
    """Return the "default" initial status image.

    Return the "default" status image to display when the display is
    turned on. This is a standard image that the ``eink-server``
    library provides as a default.

    Returns:
        Image: The image.
    """
    if self._width >= 320 and self._height >= 220:
        return self._default_image('connecting.png')
    else:
        return self._default_image('connecting_low_res.png')
def default_low_battery_image(self)

Return the "default" low battery image.

Return the "default" status image to display if the device is low on battery. This is a standard image that the eink-server library provides as a default.

Returns

Image
The image.
Expand source code
def default_low_battery_image(self):
    """Return the "default" low battery image.

    Return the "default" status image to display if the device is
    low on battery. This is a standard image that the
    ``eink-server`` library provides as a default.

    Returns:
        Image: The image.
    """
    if self._width >= 208 and self._height >= 130:
        return self._default_image('low_battery.png')
    else:
        return self._default_image('low_battery_low_res.png')
def set_image(self, name, image, quality=100)

Set (or add) the status image with the specified name.

We automatically reduce the image to the appropriate color palette using EinkGraphics.round.

Arguments

name : str
The name of the status image.
image : Image
The status image. This must be the same size as the display.
quality : int

The compression quality to use to store the image. This is a number from 0 to 100, as in the JPEG file format. The higher the number, the more faithfully we will be able to reproduce the image, but the more memory it will require. 100 indicates perfect quality (or lossless).

Normally, a quality of 100 is recommended. But if the status images are too numerous and large, it's possible that they won't fit in the client's program memory. In that case, a lower level of quality is required.

Expand source code
def set_image(self, name, image, quality=100):
    """Set (or add) the status image with the specified name.

    We automatically reduce the image to the appropriate color
    palette using ``EinkGraphics.round``.

    Arguments:
        name (str): The name of the status image.
        image (Image): The status image. This must be the same size
            as the display.
        quality (int): The compression quality to use to store the
            image. This is a number from 0 to 100, as in the JPEG
            file format. The higher the number, the more faithfully
            we will be able to reproduce the image, but the more
            memory it will require. 100 indicates perfect quality
            (or lossless).

            Normally, a quality of 100 is recommended. But if the
            status images are too numerous and large, it's possible
            that they won't fit in the client's program memory. In
            that case, a lower level of quality is required.
    """
    if image.width != self._width or image.height != self._height:
        raise ValueError(
            'A status image must be the same size as the display')
    self._images[name] = image
    self._quality[name] = quality
def set_initial_image_name(self, name)

Set the name of the initial status image.

Set the name of the status image to display when the device is turned on. The default is 'connecting'.

Expand source code
def set_initial_image_name(self, name):
    """Set the name of the initial status image.

    Set the name of the status image to display when the device is
    turned on. The default is ``'connecting'``.
    """
    self._initial_image_name = name
def set_low_battery_image_name(self, name)

Set the name of the low battery image.

Set the name of the status image to display if the device is low on battery. When this happens, we stop trying to connect to the server. The default low battery image name is 'low_battery'.

Expand source code
def set_low_battery_image_name(self, name):
    """Set the name of the low battery image.

    Set the name of the status image to display if the device is low
    on battery. When this happens, we stop trying to connect to the
    server. The default low battery image name is ``'low_battery'``.
    """
    self._low_battery_image_name = name
class Transport

A transport mechanism for the client to communicate with a server.

This is an abstract base class. For now, the only subclass is WebTransport, but we could imagine adding BluetoothTransport or SerialTransport in the future.

Expand source code
class Transport:
    """A transport mechanism for the client to communicate with a server.

    This is an abstract base class. For now, the only subclass is
    ``WebTransport``, but we could imagine adding ``BluetoothTransport``
    or ``SerialTransport`` in the future.
    """
    pass

Subclasses

class WebTransport (url)

A Transport for connecting to a web server.

Each request is submitted as a POST request to a given URL, with the request payload as the POST body. The response is returned in the response payload.

Initialize a new WebTransport.

Arguments

url : str
The server URL.
Expand source code
class WebTransport(Transport):
    """A ``Transport`` for connecting to a web server.

    Each request is submitted as a POST request to a given URL, with the
    request payload as the POST body. The response is returned in the
    response payload.
    """

    # Private attributes:
    #
    # str _url - The server URL.

    def __init__(self, url):
        """Initialize a new ``WebTransport``.

        Arguments:
            url (str): The server URL.
        """
        self._url = url

Ancestors