17 de maio de 2021

Do bit Ao Byte

Embarcados, Linux e programação

RPi Pico com RF HC12 e MicroPython

RPi Pico com RF
Um boot manager por RF para Rpi Pico? Claro que sim! Confira!

Gosto demais desse HC12 da Fulltronic. Com a baixa frequência o alcance é maior; em contrapartida, mais lento. Porém, o que ele tem de especial está em outro aspecto: O barramento UART. Com isso, não precisamos de driver para o dispositivo e fica fácil colocar a RPi Pico com RF pra se comunicar com qualquer outro dispositivo.

A propósito, já conhece a RPi Pico? Se não, está um bocado atrasado.

RPi Pico com RF HC12

Vimos em detalhes como configurar a UART no Raspberry Pi Pico. É fácil, rápido e indolor, usando o firmware MicroPython. Assim, basta fazer a comunicação serial e escrever, como exemplificado nesse outro artigo. Mas vamos fazer algo um pouco diferente, primeiro pelo fato de que agora um dos lados é Python.

Pra ficar divertido, vamos definir um projeto que vem bem a calhar.

Boot manager – selecionado o programa

No artigo anterior vimos como fazer um boot manager, para selecionar qual programa deve iniciar junto com a RPi Pico. O arquivo de parametrização precisa ser editado na Thonny IDE. Vamos fazer uma função adicional ao código anterior para criar o arquivo de parametrização através do HC12. Mas sério; você precisará ler o artigo “Boot Manager na RPi Pico“, porque devemos seguir o padrão para que tudo funcione de forma transparente para seus próprios programas. A função de main.py (como descrito no artigo “Boot Manager na RPi Pico”) é exclusivamente executar o programa e função especificados no arquivo params.ini. Com esse artigo, além de estarmos adicionando o HC12 ao RPi Pico, ainda finalizamos a série sobre boot manager.

Definição de interface do boot manager

Vamos definir o formato da mensagem:

[menu] - para mostrar os programas na RPi Pico
* A seleção será feita conforme o número do item
* A função deverá ser a run(). O boot manager validará
* Parâmetros de função não serão implementados agora

A mensagem deverá ficar sempre entre [colchetes], para definir início e fim de mensagem, de forma a não ler desordenadamente.

Dá pra fazer upload de novos programas, remoção dos já existentes, enfim, gerenciamento de arquivos. Mas vamos por partes, se der vontade escrevo depois um gerenciador de arquivos pro RPi Pico.

Configurar a UART da RPi Pico

Escrevi um artigo dedicado à UART na RPi Pico, inclusive coloquei nele uma série de links para outros recursos. Usar a RPi Pico com RF e HC12 é praticamente a mesma coisa.

Vamos configurar a UART 1, que por padrão usa o GP4 como TX e GP5 como RX. Vamos também manter a velocidade em 9600. Após todas as configurações, teremos um programa rodando no segundo núcleo da RP2040, cuja tarefa é servir à comunicação serial pelo HC12.

O HC12 trabalha de 3v2 à 5v5 e tem um alcance teórico de até 1km em espaço aberto, livre de todas as interferências e pensamentos negativos. Sua velocidade serial vai de 9600 à 115200, sendo 9200 o padrão. Já escrevi também uma comunicação entre dois módulos HC12, um conectado a uma MCU e o outro conectado ao computador pessoal. Ao todo, 3 programas são fundamentais para o funcionamento do boot seletivo.

main.py

No script main.py teremos uma instância da classe bm, que é o gerenciador de boot, do qual discorro mais adiante. O método chamado dessa classe é o start(), que é iniciado em uma thread no segundo núcleo da RP2040. Em seguida temos o script menu.py, do qual também detalho o conteúdo mais adiante. Uma vez que estiver em execução, o REPL não estará mais acessível, portanto é necessário limitar o número de execuções enquanto estiver implementando. E isso serve para qualquer implementação: Não prenda a saída. Mas se tiver que prender ou se acontecer, mais adiante também mostro uma solução para interromper programas “imortais”.

O arquivo main.py fica desse jeito:

import _thread
import bm
import menu
from time import sleep_ms as delay

bootManager = bm.bm()

#bootManager.start()
_thread.start_new_thread(bootManager.start,())

for _ in range(600):
    menu.uart_read()
    delay(100)

#bootManager.test()

Repare que tem a chamada de um método comentado. É uma boa prática criar um método ou função “inocente” para testar implementações antes de uma execução mais elaborada. Isso garante, por exemplo, o funcionamento de uma instância.

bm.py : Boot Manager

Devido ao relacionamento mais elaborado, achei melhor escrever uma classe para facilitar, invés de um monte de funções soltas. Você não tem que se preocupar com o conteúdo, esse script apenas deve existir na RPi Pico com o nome bm.py.

Seu conteúdo é bastante limpo, se já tem experiência com Python, não vai ter dificuldade em interpretá-lo.

#RPi Pico com RF
from os import listdir as ls
from sys import exit
from machine import reset

class bm:
    global run
    run    = ''
    params = ''
    go     = ''
    script = ''
    
    def step_one(self):
        #verifica se existe o arquivo params.ini
        if not 'params.ini' in ls():
            #se nao existe, nao tem nada a iniciar
            print("Nenhum programa a inicializar")
        else:
            self.step_two()
            
    def step_two(self):
        #se nao entrou na condicao acima, podemos tentar ler o arquivo
        file   = open("params.ini")
        self.params = file.readline()
        file.close()

        #supondo que o 'minimo' esperado sao 10 bytes:
        if not len(self.params) > 9:
            #nao passou, sai aqui
            print("Tamanho curto: ", len(self.params))
        else:
            self.step_three()
            
    def step_three(self):
        #se passou pela condicao acima, tentamos gerar o dicionario
        try:
            self.params = eval(self.params)
        except:
            pass

        if not type(self.params) is dict:
            #se nao for dicionario, algo de errado no conteudo
            print("erro no formato? confira abaixo:")
            print(self.params)
        else:
            self.step_four()

    def step_four(self):
        #se passou pela condicao acima, podemos verificar a chave do programa
            if not 'exec' in self.params.keys():
                print("Chave faltando: exec")
            else:
                self.step_five()

    def step_five(self):
        #Se passou pelas chaves:
        #1 - fazer o import do script referenciado pela chave exec. ver se existe o arquivo
        self.script = self.params['exec'] + ".py"
        if not self.script in ls():
            #nao tem o script referenciado
            print("Arquivo nao encontrado: ",self.script)
        else:
            self.step_six()

    def step_six(self):
        #se encontrou, import
        self.program  = __import__(self.params['exec'])

        #agora procura a funcao/metodo run
        if not 'run' in dir(self.program):
            print("Funcao run() nao encontrada")
        else:
            self.step_seven()

    def step_seven(self):
        self.program.run()
        
    def test(self):
        print("Hello world")
        
    def start(self):
        self.step_one()


Se programa em Python, vai perceber que a lógica é atípica. Tirando o método test(), o resto é uma sequência de passos de 1 à 7, iniciado pelo método start(). Mas fiz dessa maneira por uma razão; o programa simplesmente finaliza no passo em que a condição não for atendida, deixando de executar o passo seguinte. Não tem break, return, exit etc. Se todas as condições forem atendidas, o passo 7 é executado. E vamos ver um pouco sobre o passo 6 assim que apresentar um programa de exemplo.

menu.py

Esse script rodará no loop dentro da main.py, aguardando por um dado advindo do HC12. A seleção do programa a iniciar com a RPi Pico deve ser feita através desse menu. Uma vez selecionado o programa, o arquivo params.ini é criado e toda a vez que a placa for iniciada o programa bm.py lerá seu conteúdo e executará o programa definido. Para trocar o programa, basta fazer a comunicação pelo HC12 novamente e selecionar seu programa no menu.

from machine import UART
from machine import reset
from time import sleep_ms as delay
from os import listdir as ls
#TODO:
#[show] para mostrar execucao atual
#[stop] para parar a execucao atual

DEBUG = True

tty1 = UART(1,9600)

''' Pega somente a primeira entrada, que deve ser [menu].
    Outras entradas iniciais devem ser colocadas aqui '''
def uart_read():
    global msg
    msg = ''
    while tty1.any():
        msg = msg + tty1.read(1).decode("utf-8")
        if DEBUG:
            print(msg)
        if not msg.startswith('['):
            msg = ''
        
    if msg.startswith('[') and msg.endswith(']'):
        msg_menu()

''' Se a primeira entrada foi [menu], mostra os programas
    que estao na pico e passa para a proxima funcao   '''
def msg_menu():
    if 'menu' in msg:
        tty1.write("Programs:")
        i = 0
        tty1.write("\r\n") #comeca enviando CR LF
        delay(100)
        for item in ls():
            line = str(i) + " " + item + "\r\n" 
            tty1.write(line)
            delay(100) #tem que ter um delay senao vira bagunca pelo HC12
            if DEBUG:
                print(line)
            i = i + 1
        tty1.write("opt> ")
        delay(200)  
        while tty1.any():
            clear = tty1.read(1)
        msg_method()

''' Comeca pegando o indice do programa escolhido. Depois
faz uma instancia temporaria para poder pegar as funcoes '''
def msg_method():
    go     = False
    method = []
    msg    = ''
    
    while not go:
        delay(50)
        
        while tty1.any():
            msg = msg + tty1.read(1).decode("utf-8")
            print(type(msg))
            print("--- ",end=' ')
            print(msg, end=' ')
            print(" ---")
            
            if not msg.startswith('['):
                msg = ''
        if msg.startswith('[') and msg.endswith(']'):
            #pegar o nome se o valor for numero:
            itens  = ls()
            if msg[1].isdigit() and len(itens) > int(msg[1]):                          
                target = itens[int(msg[1])]
                #faz o import temporario pra pegar os metodos
                tmp    = __import__(target.split(".")[0])
                method = dir(tmp)
                go     = True
  
                #programa coletado. Falta garantir a funcao/metodo run 
    
                if not 'run' in  method:
                    print("Metodo run() nao encontrado")
                    tty1.write("Metodo run() nao encontrado")
                else:
                    #temos programa e metodo. agora escrevemos e executamos
                    params = "{'exec':'"  +  target.split(".")[0] + "'}"
                    file = open("params.ini","w")
                    file.write(str(params))
                    file.close()
                    tty1.write("reiniciando")
                    delay(700)
                    reset()

Os programas que desejar manter na RPi Pico devem ser colocados nela através da IDE Thonny. Algumas implementações estão pendentes. Estou bastante satisfeito com os resultados, por isso pretendo implementar o restante das funcionalidades.

Vale lembrar que estamos usando a UART1, cujos pinos padrão são TX=4 e RX=5. Lembre-se que o RX da RPi Pico vai ao TX do HC12 e o RX do HC12 vai ao TX da RPi Pico.

Programa de exemplo

Para fazer esse teste usei a leitura do sensor de temperatura interno da RP2040. Criei um script chamado temp.py com o seguinte conteúdo:

from machine import ADC
from time import sleep_ms as delay

def temperature():
    adc_temp          = ADC(4)
    readings          = 0
    conversion_factor = 0
    
    for _ in range(4):
        readings += adc_temp.read_u16()
    
    conversion_factor = 3.3 / (65535)
    voltage = (readings/4) * conversion_factor
    
    temperature_now = 21 - (voltage - 0.706) / 0.001721
    
    print("Temperatura: ",temperature_now)
    
#funcao para boot manager - tem que ser run
def run():
    for i in range(10):
        temperature()
        delay(800)
    
if __name__ == '__main__':
    run()

E aqui temos um assunto importante a tratar.

No artigo anterior fiz a implementação com seleção do método/função a chamar, também gravado no params.ini. Experimente o artigo anterior, funciona a contento. A questão é que dessa vez tive uma limitação técnica por uma mudança de comportamento do eval, quando a referência do método estava vindo através da classe, mas nem vou entrar nos detalhes para não estender o texto. Enfim, a solução simples e racional é: Em todo o programa que for rodar pelo boot manager, crie uma função chamada run(), sem parâmetros. O passo 6 do boot manager é validar a existência do método/função, tudo o que você precisa fazer é criar a função run() no seu programa.

Vantagens em utilizar o boot manager

Primeiro, podemos manter vários programas dentro de uma RPi Pico para cumprir diferentes funções e, se precisar cumprir um papel diferente, basta trocar o programa de boot. Outra vantagem é que assim podemos testar diferentes versões de um mesmo programa, bastando nomear cada versão e caso algo dê errado, basta fazer rollback. Acredito que isso já seria o suficiente para adotar o padrão, mas tem algumas vantagens extras. Dependendo do que for fazer, pode ser que o programa não seja interrompido na IDE Thonny e, nesse caso complica um pouco. Assim, basta ter seu programa em um script, por exemplo, temperatura.py, então testá-lo pelo prompt REPL:

Se entrasse em um loop ininterrompível, bastaria reiniciar a pico.

Como recuperar o boot da RPi Pico?

Se acontecer de entrar em um loop infinito que não possa ser interrompido, não adianta colocar outro firmware padrão, porque ele não sobrescreverá a flash e o programa main.py continuará sendo executado. Nesse caso, uma solução é utilizar o firmware alternativo RenameMainDotPy. Basta gravá-lo, aguardar a primeira ou segunda série de blink e então colocar o firmware MicroPython padrão novamente. Seu programa main.py será renomeado para main-1.py, bastando abri-lo na IDE Thonny, corrigir o problema e renomeá-lo. Para renomear no prompt REPL, faça simplesmente isso (mas não esqueça de corrigir o problema primeiro):

from os import rename
rename('main-1.py','main.py')

Depois faça um soft reboot com Ctrl+D e o programa main.py será executado novamente. Ou, clique na seta run.

Como formatar o sistema de arquivos?

Não sei se atualmente ainda existe essa fragilidade, mas há alguns anos acontecia de corromper o sistema de arquivos no MicroPython. Além dessa possibilidade, pode ser que você deseje simplesmente remover todos os arquivos e, invés de fazer um a um, uma formatação pode ser um meio mais prático, dependendo do volume de arquivos. Para isso, pelo prompt do REPL digite:

from os import mkfs
mkfs('/flash')

Estou ousando dar a dica que utilizava no MicroPython no ESP8266, não sei se é o mesmo porque com os.getcwd() o retorno é ‘/’ e com os.listdir() vemos todos os arquivos nesse nível de diretório. Enfim, é questão de testar, mas como tenho uma série de programas dentro dele, não será agora que farei esse teste.

Onde comprar a RPi Pico?

Fico até triste em informar, mas no momento em que eu estava finalizando esse artigo restavam algumas ínfimas unidades. Corre lá na Robocore e pegue sua RPi Pico antes que acabe!

Vídeo da RPi Pico com RF

Sem sombra de dúvidas esse artigo merece vídeo: “RPi Pico com RF”. Passei alguns dias concebendo a ideia e pra implementar do jeitinho que imaginei não foi trivial. Agora que a ideia existe, dá pra fazer um monte de coisas mais, mas o importante foi a concepção. Só que o vídeo não deve sair no mesmo dia do artigo porque agora começa o trabalho de filmagem e edição. Que tal dar uma motivada? Se não é inscrito, inscreva-se no canal para ajudar a crescer, hum? Sempre procuro fazer o melhor para quem lê e quem assiste, cada inscrição é um sorriso!