Arquivos de manifesto do MicroPython

Resumo

O MicroPython tem um recurso que permite que o código Python seja “congelado” (frozen) no firmware, como alternativa a carregar o código a partir do sistema de arquivos.

Isso traz os seguintes benefícios:

  • o código é pré-compilado em bytecode, evitando a necessidade de compilar o código-fonte Python no momento do carregamento.

  • o bytecode pode ser executado diretamente da ROM (ou seja, memória flash) em vez de ser copiado para a RAM. Da mesma forma, quaisquer objetos constantes (strings, tuplas, etc.) também são carregados da ROM. Isso pode resultar em muito mais memória disponível para a sua aplicação.

  • em dispositivos que não possuem sistema de arquivos, esta é a única maneira de carregar código Python.

Durante o desenvolvimento, o congelamento geralmente não é recomendado, pois reduzirá significativamente o seu ciclo de desenvolvimento, já que cada atualização exigirá regravar o firmware inteiro. No entanto, ainda pode ser útil congelar seletivamente algumas dependências que mudam raramente (como bibliotecas de terceiros).

A maneira de listar os arquivos Python a serem congelados no firmware é através de um “manifesto”, que é um arquivo Python que será interpretado pelo processo de build. Normalmente você escreveria um arquivo de manifesto como parte de uma definição de placa, mas também pode escrever um arquivo de manifesto autônomo e usá-lo com uma definição de placa existente.

Os arquivos de manifesto podem definir dependências de bibliotecas do micropython-lib, bem como de arquivos Python no sistema de arquivos, e também de outros arquivos de manifesto.

Escrevendo arquivos de manifesto

Um arquivo de manifesto é um arquivo Python que contém uma série de chamadas de função. Veja as funções disponíveis definidas abaixo.

Quaisquer caminhos usados em arquivos de manifesto podem incluir as seguintes variáveis. Todas elas resolvem para caminhos absolutos.

  • $(MPY_DIR) – caminho para o repositório do micropython.

  • $(MPY_LIB_DIR) – caminho para o submódulo micropython-lib. Prefira usar require().

  • $(PORT_DIR) – caminho para a porta atual (por exemplo, ports/stm32)

  • $(BOARD_DIR) – caminho para a placa atual (por exemplo, ports/stm32/boards/OPENMV4)

Arquivos de manifesto personalizados não devem residir no repositório principal do MicroPython. Você deve mantê-los no controle de versão junto com o restante do seu projeto.

Normalmente, um manifesto usado para compilar firmware precisará incluir o manifesto da porta, que pode incluir módulos congelados necessários para o funcionamento da placa. Se você quiser apenas adicionar módulos extras a uma placa existente, então inclua o manifesto da placa (que, por sua vez, incluirá o manifesto da porta).

Compilando com um manifesto personalizado

O seu manifesto pode ser especificado na linha de comando do make com:

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

Isso se aplica a todas as portas, incluindo as baseadas em CMake (por exemplo, rp2), já que o wrapper do Makefile passará isso para o build do CMake.

Adicionando um manifesto a uma definição de placa

Se você tem uma definição de placa personalizada, pode fazer com que ela inclua automaticamente o seu manifesto personalizado. Em portas baseadas em make (a maioria das portas), no seu mpconfigboard.mk defina a variável FROZEN_MANIFEST.

FROZEN_MANIFEST ?= $(BOARD_DIR)/manifest.py

Em portas baseadas em CMake (por exemplo, rp2), use mpconfigboard.cmake em vez disso

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

Funções de alto nível

Estas são as funções que você normalmente usará. Elas adicionam código ao conjunto que é pré-compilado em bytecode e congelado na imagem do firmware:

  • module e package congelam o seu próprio código-fonte local — um único arquivo ou um diretório de pacote inteiro, respectivamente.

  • require congela um pacote publicado (e suas dependências) do micropython-lib, pelo nome.

  • include traz outro manifesto para que seus módulos congelados também sejam adicionados.

  • add_library e metadata são funções de apoio (registrando caminhos de busca extras para require e declarando metadados de pacote).

Um manifesto típico de firmware primeiro faz include do manifesto da porta ou da placa (para que os módulos de que a placa precisa permaneçam congelados) e, em seguida, adiciona suas próprias linhas module/package/require.

Nota: O argumento nomeado opt pode ser definido nas várias funções; ele controla o nível de otimização usado pelo cross-compiler. Veja micropython.opt_level().

add_library(library, library_path, prepend=False)

Registra o caminho para uma library nomeada externa.

Use isto quando quiser que o require resolva pacotes a partir de um diretório diferente do micropython-lib — por exemplo, sua própria coleção de drivers ou um checkout de uma biblioteca de terceiros.

O caminho library_path será pesquisado automaticamente ao usar require. Por padrão, a biblioteca adicionada é colocada no final da lista de bibliotecas a serem pesquisadas. Passe True para prepend, adicionando-a ao início da lista.

Além disso, a biblioteca adicionada pode ser solicitada explicitamente usando require("name", library="library").

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

Congela um pacote inteiro — um diretório de arquivos .py (opcionalmente com subpacotes) — para que possa ser importado como import <package>. Use module em vez disso para um único arquivo autônomo.

Isto equivale a copiar o diretório “package_path” para o dispositivo (exceto como código congelado).

No caso mais simples, para congelar um pacote “foo” no diretório atual:

package("foo")

incluirá recursivamente todos os arquivos .py em foo, e eles serão congelados como foo/**/*.py.

Se o pacote não estiver no mesmo diretório que o arquivo de manifesto, use base_path:

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

Você pode usar as variáveis acima, como $(PORT_DIR) em base_path.

Para restringir a determinados arquivos no pacote, use files (nota: os caminhos devem ser relativos ao pacote): package("foo", files=["bar/baz.py"]).

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

Congela um único arquivo .py autônomo para que possa ser importado pelo seu nome (module("foo.py") faz com que import foo funcione). Use package para um diretório/pacote.

Se o arquivo estiver no diretório atual:

module("foo.py")

Caso contrário, use base_path para localizar o arquivo:

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

Você pode usar as variáveis acima, como $(PORT_DIR) em base_path.

require(name, library=None)

Requer um pacote pelo nome (e suas dependências) do micropython-lib.

É assim que extensões da biblioteca padrão e drivers da comunidade são congelados: o pacote nomeado é obtido do submódulo micropython-lib e congelado junto com tudo de que ele depende. Use module ou package em vez disso para congelar o seu próprio código-fonte em vez de um pacote publicado.

Opcionalmente, especifique library (uma string) para referenciar um pacote de uma biblioteca que tenha sido registrada anteriormente com add_library. Caso contrário, a lista de caminhos de biblioteca será usada.

include(manifest_path)

Inclui outro manifesto. É assim que os manifestos são compostos: um manifesto de firmware personalizado deve fazer include do manifesto da porta (ou da placa) para que os módulos de que a placa precisa permaneçam congelados, e então adicionar suas próprias entradas.

Normalmente, um manifesto usado para compilar firmware precisará incluir o manifesto da porta, que pode incluir módulos congelados necessários para o funcionamento da placa.

O argumento manifest pode ser uma string (nome de arquivo) ou um iterável de strings.

Caminhos relativos são resolvidos em relação ao arquivo de manifesto atual.

Se o caminho apontar para um diretório, então ele inclui implicitamente o arquivo manifest.py dentro desse diretório.

Você pode usar as variáveis acima, como $(PORT_DIR) em manifest_path.

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

Define metadados para este arquivo de manifesto. Isto é útil para manifestos de pacotes do micropython-lib.

Esses campos são consumidos quando um pacote é publicado em / instalado a partir do micropython-lib via mip; eles não são necessários em um manifesto de firmware de placa.

Funções de baixo nível

Estas funções são documentadas para fins de completude, mas, com exceção de freeze_as_str, toda a funcionalidade pode ser acessada através das funções de alto nível.

As funções freeze* diferem apenas em como o código é armazenado:

  • freeze_as_mpy / freeze_mpy armazenam bytecode pré-compilado (.mpy) na flash. O código roda diretamente da flash, usa RAM mínima e importa rapidamente. É isso que module, package e require usam internamente.

  • freeze_as_str em vez disso congela o código-fonte Python, que é compilado em bytecode no momento da importação (usando RAM e exigindo o compilador no dispositivo). Esta é a única capacidade não exposta pelas funções de alto nível, e é por isso que ela é a exceção mencionada acima.

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

A primitiva subjacente sobre a qual as funções de alto nível são construídas; prefira essas. Congela a entrada especificada por path, determinando automaticamente o seu tipo. Um script .py será primeiro compilado em .mpy e então congelado, e um arquivo .mpy será congelado diretamente.

path deve ser um diretório, que é o diretório base a partir do qual a busca por arquivos começa. Ao importar os módulos congelados resultantes, o nome do módulo começará após path, ou seja, path é excluído do nome do módulo.

Se path for relativo, ele é resolvido em relação ao manifest.py atual.

Se script for None, todos os arquivos em path serão congelados.

Se script for um iterável, então freeze() é chamado em todos os itens do iterável (com o mesmo path e opt repassados).

Se script for uma string, então ele especifica o arquivo ou diretório a congelar, e pode incluir diretórios extras antes do arquivo ou do último diretório. O arquivo ou diretório será procurado em path. Se script for um diretório, então todos os arquivos nesse diretório serão congelados.

opt é o nível de otimização a ser passado ao mpy-cross ao compilar .py em .mpy. Esses níveis são descritos em micropython.opt_level().

freeze_as_str(path)

Congela o path fornecido e todos os scripts .py contidos nele como uma string, que será compilada na importação. Use isto apenas quando o código congelado precisar permanecer como código-fonte Python; isso custa RAM no momento da importação em comparação com as variantes .mpy.

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

Congela a entrada compilando primeiro os scripts .py em arquivos .mpy e, em seguida, congelando os arquivos .mpy resultantes. É isso que module e package fazem internamente. Veja freeze() para mais detalhes sobre os argumentos.

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

Congela a entrada, que deve ser arquivos .mpy que são congelados diretamente (sem etapa de compilação). Veja freeze() para mais detalhes sobre os argumentos.

Exemplos

Para congelar um único arquivo do diretório atual que estará disponível como import mydriver, use:

module("mydriver.py")

Para congelar um diretório de arquivos em um subdiretório “mydriver” do diretório atual que estará disponível como import mydriver, use:

package("mydriver")

Para congelar a biblioteca “hmac” do micropython-lib, use:

require("hmac")

Um exemplo mais completo de um arquivo manifest.py personalizado (para uma placa que tem o seu próprio manifesto padrão) é:

# 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")

Então a placa pode ser compilada com

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

Observe que a maioria das placas não tem o seu próprio manifest.py, e sim usam o da porta diretamente, caso em que o seu manifesto deve apenas fazer include("$(PORT_DIR)/boards/manifest.py").