Archivos de manifiesto de MicroPython

Resumen

MicroPython tiene una característica que permite «congelar» código Python dentro del firmware, como alternativa a cargar el código desde el sistema de archivos.

Esto tiene los siguientes beneficios:

  • el código se precompila a bytecode, evitando la necesidad de compilar el código fuente Python en el momento de la carga.

  • el bytecode puede ejecutarse directamente desde la ROM (es decir, la memoria flash) en lugar de copiarse en la RAM. De manera similar, cualquier objeto constante (cadenas, tuplas, etc.) también se carga desde la ROM. Esto puede dejar disponible significativamente más memoria para tu aplicación.

  • en dispositivos que no tienen un sistema de archivos, esta es la única forma de cargar código Python.

Durante el desarrollo, congelar generalmente no es recomendable, ya que ralentizará considerablemente tu ciclo de desarrollo, pues cada actualización requerirá volver a programar todo el firmware. Sin embargo, todavía puede ser útil congelar de forma selectiva algunas dependencias que rara vez cambian (como bibliotecas de terceros).

La forma de listar los archivos Python que se van a congelar en el firmware es mediante un «manifiesto», que es un archivo Python que será interpretado por el proceso de compilación. Normalmente escribirías un archivo de manifiesto como parte de una definición de placa, pero también puedes escribir un archivo de manifiesto independiente y usarlo con una definición de placa existente.

Los archivos de manifiesto pueden definir dependencias de bibliotecas de micropython-lib, así como de archivos Python en el sistema de archivos, y también de otros archivos de manifiesto.

Escribir archivos de manifiesto

Un archivo de manifiesto es un archivo Python que contiene una serie de llamadas a funciones. Consulta las funciones disponibles definidas a continuación.

Cualquier ruta utilizada en los archivos de manifiesto puede incluir las siguientes variables. Todas estas se resuelven a rutas absolutas.

  • $(MPY_DIR) – ruta al repositorio de micropython.

  • $(MPY_LIB_DIR) – ruta al submódulo micropython-lib. Es preferible usar require().

  • $(PORT_DIR) – ruta al puerto actual (p. ej. ports/stm32)

  • $(BOARD_DIR) – ruta a la placa actual (p. ej. ports/stm32/boards/OPENMV4)

Los archivos de manifiesto personalizados no deben residir en el repositorio principal de MicroPython. Debes mantenerlos bajo control de versiones junto con el resto de tu proyecto.

Normalmente, un manifiesto utilizado para compilar firmware necesitará incluir el manifiesto del puerto, que puede incluir módulos congelados que son necesarios para que la placa funcione. Si solo quieres añadir módulos adicionales a una placa existente, entonces incluye el manifiesto de la placa (que a su vez incluirá el manifiesto del puerto).

Compilar con un manifiesto personalizado

Tu manifiesto puede especificarse en la línea de comandos de make con:

$ make BOARD=MYBOARD FROZEN_MANIFEST=/path/to/my/project/manifest.py

Esto aplica a todos los puertos, incluidos los basados en CMake (p. ej. rp2), ya que el envoltorio del Makefile lo pasará a la compilación de CMake.

Añadir un manifiesto a una definición de placa

Si tienes una definición de placa personalizada, puedes hacer que incluya tu manifiesto personalizado automáticamente. En los puertos basados en make (la mayoría de los puertos), establece la variable FROZEN_MANIFEST en tu mpconfigboard.mk.

FROZEN_MANIFEST ?= $(BOARD_DIR)/manifest.py

En los puertos basados en CMake (p. ej. rp2), usa en su lugar mpconfigboard.cmake

set(MICROPY_FROZEN_MANIFEST ${MICROPY_BOARD_DIR}/manifest.py)

Funciones de alto nivel

Estas son las funciones que normalmente usarás. Añaden código al conjunto que se precompila a bytecode y se congela en la imagen del firmware:

  • module y package congelan tu propio código fuente local: un único archivo o un directorio de paquete completo, respectivamente.

  • require congela un paquete publicado (y sus dependencias) de micropython-lib, por nombre.

  • include incorpora otro manifiesto para que sus módulos congelados también se añadan.

  • add_library y metadata son funciones de apoyo (registran rutas de búsqueda adicionales para require y declaran metadatos del paquete).

Un manifiesto de firmware típico primero hace include del manifiesto del puerto o de la placa (para que los módulos que la placa necesita permanezcan congelados) y luego añade sus propias líneas de module/package/require.

Nota: El argumento de palabra clave opt puede establecerse en las distintas funciones; esto controla el nivel de optimización utilizado por el compilador cruzado. Consulta micropython.opt_level().

add_library(library, library_path, prepend=False)

Registra la ruta a una biblioteca externa con nombre.

Usa esto cuando quieras que require resuelva paquetes desde un directorio distinto de micropython-lib, por ejemplo tu propia colección de controladores, o una copia de trabajo de una biblioteca de terceros.

La ruta library_path se buscará automáticamente al usar require. De forma predeterminada, la biblioteca añadida se agrega al final de la lista de bibliotecas en las que buscar. Pasa True para anteponerla y añadirla al inicio de la lista.

Además, la biblioteca añadida puede solicitarse explícitamente usando require("name", library="library").

package(package_path, files=None, base_path='.', opt=None)

Congela un paquete completo: un directorio de archivos .py (opcionalmente con subpaquetes), de modo que pueda importarse como import <package>. Usa module en su lugar para un único archivo independiente.

Esto equivale a copiar el directorio «package_path» al dispositivo (salvo que como código congelado).

En el caso más sencillo, para congelar un paquete «foo» en el directorio actual:

package("foo")

incluirá de forma recursiva todos los archivos .py en foo, y se congelarán como foo/**/*.py.

Si el paquete no está en el mismo directorio que el archivo de manifiesto, usa base_path:

package("foo", base_path="path/to/libraries")

Puedes usar las variables anteriores, como $(PORT_DIR) en base_path.

Para restringir a ciertos archivos del paquete usa files (nota: las rutas deben ser relativas al paquete): package("foo", files=["bar/baz.py"]).

module(module_path, base_path='.', opt=None)

Congela un único archivo .py independiente para que pueda importarse por su nombre (module("foo.py") hace que funcione import foo). Usa package para un directorio/paquete.

Si el archivo está en el directorio actual:

module("foo.py")

De lo contrario, usa base_path para localizar el archivo:

module("foo.py", base_path="src/drivers")

Puedes usar las variables anteriores, como $(PORT_DIR) en base_path.

require(name, library=None)

Requiere un paquete por nombre (y sus dependencias) de micropython-lib.

Así es como se congelan las extensiones de la biblioteca estándar y los controladores de la comunidad: el paquete nombrado se obtiene del submódulo micropython-lib y se congela junto con todo aquello de lo que depende. Usa module o package en su lugar para congelar tu propio código fuente en vez de un paquete publicado.

Opcionalmente, especifica library (una cadena) para referenciar un paquete de una biblioteca que se haya registrado previamente con add_library. De lo contrario, se usará la lista de rutas de bibliotecas.

include(manifest_path)

Incluye otro manifiesto. Así es como se componen los manifiestos: un manifiesto de firmware personalizado debe hacer include del manifiesto del puerto (o de la placa) para que los módulos que la placa necesita permanezcan congelados, y luego añadir sus propias entradas.

Normalmente, un manifiesto utilizado para compilar firmware necesitará incluir el manifiesto del puerto, que puede incluir módulos congelados que son necesarios para que la placa funcione.

El argumento manifest puede ser una cadena (nombre de archivo) o un iterable de cadenas.

Las rutas relativas se resuelven con respecto al archivo de manifiesto actual.

Si la ruta apunta a un directorio, entonces incluye implícitamente el archivo manifest.py que hay dentro de ese directorio.

Puedes usar las variables anteriores, como $(PORT_DIR) en manifest_path.

metadata(description=None, version=None, license=None, author=None)

Define metadatos para este archivo de manifiesto. Esto es útil para los manifiestos de paquetes de micropython-lib.

Estos campos se utilizan cuando un paquete se publica en / instala desde micropython-lib mediante mip; no son necesarios en un manifiesto de firmware de placa.

Funciones de bajo nivel

Estas funciones se documentan para que la referencia esté completa, pero con la excepción de freeze_as_str toda la funcionalidad puede accederse mediante las funciones de alto nivel.

Las funciones freeze* difieren únicamente en cómo se almacena el código:

  • freeze_as_mpy / freeze_mpy almacenan bytecode precompilado (.mpy) en la memoria flash. El código se ejecuta directamente desde la flash, usa una cantidad mínima de RAM y se importa rápidamente. Esto es lo que usan internamente module, package y require.

  • freeze_as_str en cambio congela el código fuente Python, que se compila a bytecode en el momento de la importación (usando RAM y requiriendo el compilador del dispositivo). Esta es la única capacidad no expuesta por las funciones de alto nivel, motivo por el cual es la excepción señalada anteriormente.

freeze(path, script=None, opt=0)

La primitiva subyacente sobre la que se construyen las funciones de alto nivel; es preferible usar aquellas. Congela la entrada especificada por path, determinando automáticamente su tipo. Un script .py se compilará primero a .mpy y luego se congelará, y un archivo .mpy se congelará directamente.

path debe ser un directorio, que es el directorio base desde el que empezar a buscar archivos. Al importar los módulos congelados resultantes, el nombre del módulo empezará después de path, es decir, path se excluye del nombre del módulo.

Si path es relativa, se resuelve con respecto al manifest.py actual.

Si script es None, se congelarán todos los archivos de path.

Si script es un iterable, entonces se llama a freeze() sobre todos los elementos del iterable (pasando el mismo path y opt).

Si script es una cadena, entonces especifica el archivo o directorio que se va a congelar, y puede incluir directorios adicionales antes del archivo o del último directorio. El archivo o directorio se buscará en path. Si script es un directorio, entonces se congelarán todos los archivos de ese directorio.

opt es el nivel de optimización que se pasa a mpy-cross al compilar .py a .mpy. Estos niveles se describen en micropython.opt_level().

freeze_as_str(path)

Congela el path dado y todos los scripts .py que contiene como cadena, que se compilará al importarse. Usa esto solo cuando el código congelado deba seguir siendo código fuente Python; conlleva un coste de RAM en el momento de la importación en comparación con las variantes .mpy.

freeze_as_mpy(path, script=None, opt=0)

Congela la entrada compilando primero los scripts .py a archivos .mpy y luego congelando los archivos .mpy resultantes. Esto es lo que hacen module y package internamente. Consulta freeze() para más detalles sobre los argumentos.

freeze_mpy(path, script=None, opt=0)

Congela la entrada, que debe ser archivos .mpy que se congelan directamente (sin paso de compilación). Consulta freeze() para más detalles sobre los argumentos.

Ejemplos

Para congelar un único archivo del directorio actual que estará disponible como import mydriver, usa:

module("mydriver.py")

Para congelar un directorio de archivos en un subdirectorio «mydriver» del directorio actual que estará disponible como import mydriver, usa:

package("mydriver")

Para congelar la biblioteca «hmac» de micropython-lib, usa:

require("hmac")

Un ejemplo más completo de un archivo manifest.py personalizado (para una placa que tiene su propio manifiesto predeterminado) es:

# Include the board's default manifest.
include("$(BOARD_DIR)/manifest.py")
# Add a custom driver
module("mydriver.py")
# Add aiorepl from micropython-lib
require("aiorepl")

Entonces la placa puede compilarse con

$ cd ports/stm32
$ make BOARD=MYBOARD FROZEN_MANIFEST=~/src/myproject/manifest.py

Ten en cuenta que la mayoría de las placas no tienen su propio manifest.py, sino que usan el del puerto directamente, en cuyo caso tu manifiesto simplemente debería hacer include("$(PORT_DIR)/boards/manifest.py").