Module eink.generate.client_code_generator

Expand source code
import os
import shutil

from ..image import EinkGraphics
from ..image.image_data import ImageData
from ..project.project import Project
from ..server import Server
from ..server.server_io import ServerIO


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)

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_)