12.6. Canais nomeados

O ID de canal no cabeçalho de cada pacote permite que até 32 fluxos independentes partilhem o mesmo transporte físico. A camada de canais transforma esses IDs numéricos em endpoints nomeados, visíveis pela aplicação, que o código do host pode referenciar por string.

One transport wire on the left fanning out into four labelled channels on the cam side -- stdin, stdout, stream, and a user-registered status channel -- each showing as an independent box.

12.6.1. Os quatro canais integrados

A câmara regista quatro canais no arranque, antes de qualquer código de aplicação ser executado:

  • stdin – bytes de script que o host envia para a câmara executar. O IDE usa este canal para enviar o script em edição; exec() no SDK do host é a chamada equivalente a partir de um programa Python.

  • stdout – bytes provenientes de chamadas print() na câmara e tracebacks de exceções não tratadas. A consola série do IDE lê este canal.

  • stream – o canal de pré-visualização em direto. O IDE obtém fotogramas JPEG a partir dele; qualquer script do host pode fazer o mesmo com read_frame().

  • profile – eventos do profiler, presentes apenas quando a câmara foi compilada com profiling ativo. A maioria das versões de lançamento omite-o.

O código de aplicação raramente precisa de interagir com os canais integrados; o trabalho interessante acontece nos canais que a própria aplicação regista.

12.6.2. Registar um canal

Um script do lado da câmara regista um novo canal chamando protocol.register() com um nome e um objeto Python de backend

import json
import protocol
import time

trigger_count = 0

class StatusChannel:
    def size(self):
        # Refresh the snapshot on every host query.
        self._buf = json.dumps({
            'uptime_s': time.ticks_ms() // 1000,
            'triggers': trigger_count,
        }).encode()
        return len(self._buf)

    def read(self, offset, size):
        return self._buf[offset:offset + size]

protocol.register(name='status', backend=StatusChannel())

Os métodos do objeto de backend determinam o que o canal pode fazer. Um backend com apenas size e read é um canal de dados só de leitura; adicionar write torna-o bidirecional; adicionar poll permite ao host perguntar se há dados novos antes de pagar o custo de uma leitura. Fazer a amostragem dentro de size é o padrão mais simples quando o payload cabe num único fragmento – o buffer é gerado a pedido, nunca em cache, nunca com condições de corrida. Payloads maiores – fotogramas de imagem, traços de sensor – precisam de um padrão de latch que mantenha o buffer até o host concluir a leitura de múltiplos fragmentos, abordado com o canal de fotogramas.

Uma pequena quantidade de bookkeeping acontece automaticamente:

  • A biblioteca atribui o próximo ID de canal livre (entre 0 e 31).

  • Os sinalizadores de capacidade são derivados dos métodos presentes: CHANNEL_FLAG_READ se read estiver definido, CHANNEL_FLAG_WRITE se write estiver definido, CHANNEL_FLAG_LOCK se lock / unlock estiverem definidos.

  • Um pacote de evento CHANNEL_REGISTERED é enviado a qualquer host ligado para que a sua lista de canais seja atualizada.

O valor de retorno é um handle protocol.ProtocolChannel que a aplicação pode guardar. O método send_event() do handle é o gancho do lado da câmara para informar o host de que «algo aconteceu neste canal sem alterar os dados legíveis» – um gatilho disparou, um botão foi pressionado, um marco de contagem de amostras foi atingido.

12.6.3. Ler canais a partir do host

O SDK do host é distribuído como o pacote openmv no PyPI (pip install openmv), assente em pyserial para o transporte. A sua classe openmv.camera.Camera expõe os canais nomeados da câmara através de métodos de alto nível:

from openmv.camera import Camera

with Camera('/dev/ttyACM0', baudrate=921600) as cam:
    cam.update_channels()
    if cam.has_channel('status'):
        size = cam.channel_size('status')
        data = cam.channel_read('status', size)

Aviso

O pacote openmv requer CPython 3.12 ou mais recente. Interpretadores mais antigos não têm as funcionalidades de que o SDK depende; instale uma versão 3.12+ antes de pip install openmv.

Alguns aspetos a notar sobre a configuração:

  • A string da porta série – /dev/ttyACM0 aqui – é do estilo COM3 no Windows, /dev/cu.usbmodemXXXX no macOS e /dev/ttyACM* no Linux. O número real depende da porta em que a câmara foi enumerada.

  • A taxa de baud é o valor mágico do protocolo 921600, que a pilha USB-CDC da câmara reconhece como «este cliente fala o protocolo, não o REPL.» Qualquer outra taxa recorre a uma linha série simples.

  • O gestor de contexto with Camera(...) as cam: abre o transporte, executa PROTO_SYNC, troca capacidades e, na saída, fecha a porta de forma limpa. A chamada explícita a update_channels() após a entrada atualiza a lista de canais local com quaisquer canais que a aplicação tenha registado após o arranque.

channel_size() e channel_read() são os métodos de trabalho; channel_write() transfere um buffer para a câmara se o backend tiver um método write; has_channel() é a forma segura de verificar se um nome está registado antes de o usar. O nome do canal é resolvido uma vez para o ID de canal atribuído pela câmara durante o register e usado em todos os pacotes a partir daí.

Cada par channel_size() / channel_read() custa duas idas e voltas: um pacote para pedir o tamanho, outro para pedir os bytes. Por USB-CDC ambos terminam em cerca de um milissegundo no total; por UART a mesma troca demora mais, em proporção com a taxa de baud da linha série. O código de aplicação que lê em ciclo apertado deve chamar channel_size() apenas quando o tamanho pode realmente mudar – para dados de tamanho fixo, o tamanho da primeira chamada pode ser colocado em cache.

12.6.4. Independência entre canais

Três aspetos merecem atenção sobre a forma como os canais interagem:

  • Controlo de fluxo independente. Cada canal tem o seu próprio estado de leitura pendente, os seus próprios dados e os seus próprios callbacks size / read / write. Uma leitura demorada no canal stream não bloqueia as leituras no canal config da aplicação.

  • Sequencial por canal. Dentro de um único canal, os pacotes são entregues por ordem. A camada de fiabilidade garante isso mesmo quando há retransmissões.

  • Transporte partilhado, orçamento de retransmissão partilhado. Todos os canais partilham o mesmo elo físico, pelo que uma torrente de tráfego num canal abranda os outros ao monopolizar o fio. O mecanismo CHANNEL_LOCK permite a um canal reservar o fio para uma leitura atómica de múltiplos pacotes; o backend adere implementando os callbacks lock / unlock.

Um canal é a área de superfície mínima sobre a qual um programa do host e um programa da câmara concordam em cooperar. O nome, a direcionalidade (leitura ou escrita ou ambas), os métodos de callback do lado da câmara e as chamadas de método correspondentes do lado do host constituem o contrato completo.