14.2.2.1. Incorporar scripts no firmware

Um módulo congelado é um ficheiro .py compilado para bytecode e ligado à imagem de firmware em tempo de compilação. O runtime importa um módulo congelado diretamente a partir da flash, sem nunca consultar o sistema de ficheiros em disco. Para um produto expedido, este é o lugar certo para o código da aplicação: nada que o utilizador final possa apagar, nada que um .py desatualizado no cartão SD possa substituir, e a câmara executa o mesmo código em cada arranque independentemente do que estiver (ou não) nas suas unidades.

Esta página descreve a sequência de arranque que a câmara segue, e depois como o manifest.py e a diretiva freeze incorporam uma aplicação na compilação.

14.2.2.1.1. A sequência de arranque

O que é executado, e quando, numa câmara que sai do estado de reset:

  • O bootloader. O arranque entra numa curta janela DFU que o IDE utiliza para enviar atualizações de firmware. A janela fecha após alguns segundos e o bootloader passa o controlo ao MicroPython. Um script em execução pode reentrar nesta janela a pedido chamando machine.bootloader().

  • Inicialização do sistema de ficheiros congelado. Antes de qualquer código de aplicação ser executado, o runtime inicializa os sistemas de ficheiros. A flash interna é montada em /flash (e formatada vazia se não houver nada). Se um cartão SD estiver presente e um ficheiro marcador chamado SKIPSD não existir na flash interna, o cartão SD é montado em /sdcard. A ROMFS, quando incluída na compilação, é montada automaticamente em /rom. O diretório de trabalho é definido para o diretório de arranque (/sdcard se o cartão foi montado, /flash caso contrário), e sys.path é preenchido com /flash, /flash/lib, /sdcard, /sdcard/lib, /rom e /rom/lib. A configuração residente em flash é tratada por um módulo congelado chamado _boot.py – infraestrutura de port e placa, não um gancho de aplicação. As aplicações não personalizam o _boot.py; a compilação faz isso. Colocar um ficheiro SKIPSD na flash a partir do IDE é a forma suportada de fazer a câmara arrancar a partir da flash interna em vez do cartão SD.

  • Configuração pré-REPL. O boot.py é executado em cada reset suave – arranque a frio, Ctrl-D a partir do REPL, o script em execução ao terminar, e recuperação por watchdog – antes de o REPL ficar acessível. A sua função é preparar o ambiente em que o resto do sistema funciona: o tipo de configuração que o REPL, a aplicação e qualquer ferramenta de recuperação precisam para funcionar. Não é onde a própria aplicação reside. O main.py é o ponto de entrada da aplicação.

  • Ciclo principal. O main.py é o ciclo principal da aplicação. É executado uma vez no arranque a frio, imediatamente após o boot.py. Não é executado novamente em resets suaves subsequentes – a câmara passa para o REPL em vez disso. Essa assimetria é importante para o desenvolvimento (um Ctrl-D passa para o REPL sem reexecutar o ciclo, para que o programador possa inspecionar o estado) mas não para produção: uma câmara em campo vê arranques, watchdog e resets forçados, que são todos resets de hardware que reentram no caminho de arranque a frio e executam novamente o main.py.

14.2.2.1.2. Incorporar no firmware

O conjunto de módulos congelados de uma placa é declarado em boards/<TARGET>/manifest.py na árvore de firmware. O manifesto é um pequeno ficheiro Python que chama um conjunto de diretivas:

  • freeze("$(OMV_LIB_DIR)/", "foo.py") – incorpora um único foo.py na compilação.

  • package("mylib", base_path="...") – incorpora um pacote Python com múltiplos ficheiros, preservando a sua estrutura de diretórios sob o caminho base indicado.

  • include("...") – inclui outro ficheiro de manifesto. Os manifestos de placa utilizam isto para partilhar conjuntos de módulos comuns.

  • require("logging") – inclui um módulo micropython-lib nomeado pelo seu nome.

Um manifesto de aplicação mínimo adiciona uma linha freeze por script de nível superior e uma linha package por pacote de que a aplicação depende.

14.2.2.1.2.1. Onde reside o código-fonte

O código-fonte da aplicação reside em scripts/libraries/ na árvore de firmware, junto com os módulos que a compilação já congela. A variável de manifesto $(OMV_LIB_DIR) expande para esse caminho, pelo que as entradas do manifesto ficam curtas. Editar o manifesto já é uma operação dentro da árvore, por isso manter o código-fonte na árvore evita ter de gerir um repositório de projeto separado na resolução de caminhos.

Uma estrutura típica para uma aplicação que expede um único main.py mais um pacote de suporte:

scripts/libraries/
    main.py
    my_lib/
        __init__.py
        helpers.py

E no boards/<TARGET>/manifest.py da placa, uma linha freeze para o script e uma linha package para o pacote:

freeze("$(OMV_LIB_DIR)/", "main.py")
package("my_lib", base_path="$(OMV_LIB_DIR)/my_lib")

Scripts de ficheiro único – main.py aqui, mas a mesma regra aplica-se ao boot.py ou a qualquer auxiliar independente – utilizam freeze. Pacotes com múltiplos ficheiros utilizam package. Adicionar outro script é mais uma linha freeze; adicionar outro pacote é mais uma linha package.

14.2.2.1.2.2. Compilar e gravar

Depois de o manifesto estar em vigor, compile o firmware exatamente como o capítulo firmware descreve:

make -j$(nproc) -C lib/micropython/mpy-cross   # once, builds the cross-compiler
make -j$(nproc) TARGET=<TARGET>                # builds the firmware

O resultado fica em build/<TARGET>/bin/

build/<TARGET>/bin/
    firmware.bin     # flash through the IDE
    romfs0.img       # flash through the IDE in a separate step

Gravar o .bin e o .img através do IDE resulta numa câmara cuja aplicação faz parte da compilação.

A sequência de arranque descrita acima é o que torna a incorporação eficaz: o runtime resolve boot.py e main.py para as cópias congeladas antes de verificar o sistema de ficheiros, pelo que uma câmara expedida executa o código da compilação mesmo que o cartão SD contenha um boot.py desatualizado do desenvolvimento.

14.2.2.1.2.3. Ordem de pesquisa

A semântica de substituição é diferente para o caminho de execução boot.py / main.py e para instruções import comuns. Saber qual é qual é importante tanto para produção como para desenvolvimento:

  • Para boot.py e main.py: o runtime procura primeiro uma cópia congelada, depois o sistema de ficheiros. Um boot.py congelado não pode ser substituído colocando um no cartão SD – quem tiver a câmara não pode alterar o ponto de entrada sem regravar o firmware.

  • Para import foo: o runtime pesquisa primeiro sys.path – que cobre /flash, /sdcard, /rom e os seus subdiretórios lib – depois os módulos congelados. Um foo.py com o mesmo nome em flash ou SD substitui um foo congelado. Esta é a facilidade de desenvolvimento: coloque um módulo corrigido no cartão, faça um reset suave, veja a alteração sem regravar o firmware.

Um produto expedido que queira suprimir o comportamento de substituição do sistema de ficheiros sobre congelados para importações pode limpar sys.path no início do boot.py

import sys

sys.path.clear()

Com sys.path vazio, todas as importações são resolvidas apenas a partir dos módulos congelados; nada em flash, SD ou ROMFS pode obscurecê-los.

14.2.2.1.2.4. O problema dos recursos

O congelamento é excelente para código. Não é excelente para grandes recursos binários: ficheiros de modelos de machine learning, tabelas de etiquetas, configuração JSON, modelos de imagem. Incorporá-los como literais Python incha o código-fonte, recompila lentamente e desperdiça o contentor de bytecode em dados que o interpretador vai apenas ler em bruto de qualquer forma. A página Criar uma imagem ROMFS descreve o sistema de ficheiros flash de leitura apenas que preenche esta lacuna.