13.3.1.4. Canais personalizados

Um canal é um fluxo de bytes bidirecional com nome entre um script na câmara e o anfitrião. A câmara regista um canal e fornece callbacks que produzem ou consomem dados; o anfitrião lê e escreve nesse canal pelo nome. O mesmo mecanismo que o pacote usa internamente para o canal stream que transporta fotogramas, o canal stdout que transporta a saída do script e o canal stdin que transporta o carregamento do script está exposto aos scripts do utilizador, pelo que quaisquer dados específicos da aplicação de que o anfitrião necessite podem usar a mesma ligação USB sem inventar um segundo protocolo.

Esta é a funcionalidade mais útil do pacote e aquela que a documentação padrão cobre menos bem, pelo que esta página a desenvolve do início ao fim.

13.3.1.4.1. As duas metades

Um canal personalizado precisa de código cooperante em ambos os lados. O script na câmara importa protocol, define uma classe com três métodos (size(), read(), poll()) mais um write() opcional, e chama protocol.register(name=..., backend=...) para publicar o canal com um nome escolhido:

import protocol
import time

class TicksChannel:
    def size(self):
        return 10

    def read(self, offset, size):
        return f'{time.ticks_ms():010d}'

    def poll(self):
        return True

protocol.register(name='ticks', backend=TicksChannel())

O método size() devolve quantos bytes o canal tem atualmente disponíveis. read() é o produtor: dado um offset e size solicitados pelo anfitrião, devolve os bytes (ou uma string que a camada de protocolo codifica). poll() devolve True quando há algo para ler – a camada de protocolo usa isto para sinalizar o canal como pronto em read_status().

O programa no anfitrião usa quatro métodos de openmv.Camera: has_channel() para verificar se o canal existe, channel_size() para perguntar quantos dados estão em espera, channel_read() para retirar bytes, e channel_write() para enviar bytes. read_status() consulta todos os canais de uma vez:

from openmv import Camera

with Camera('/dev/ttyACM0') as cam:
    cam.stop()
    cam.exec(open('ticks_cam.py').read())

    while True:
        status = cam.read_status()

        if status.get('ticks'):
            data = cam.channel_read('ticks')
            print(f"ticks: {data.decode()}")

O ciclo do anfitrião consulta read_status(); quando o canal ticks está pronto, chama channel_read() sem size para retirar o que estiver disponível. O TicksChannel.poll() da câmara devolve True em cada verificação, pelo que o canal está sempre «pronto» e o anfitrião obtém um valor de ticks atualizado a cada consulta.

13.3.1.4.2. Um canal bidirecional

Para um anfitrião que precisa de enviar dados de volta, a classe na câmara adiciona um método write() que aceita os bytes recebidos:

import protocol

class CommandChannel:
    def __init__(self):
        self.last_command = b''
        self.replied = False

    def size(self):
        return len(self.last_command)

    def read(self, offset, size):
        self.replied = True
        return self.last_command

    def write(self, offset, data):
        self.last_command = b'echo: ' + bytes(data)
        self.replied = False

    def poll(self):
        return not self.replied and len(self.last_command) > 0

protocol.register(name='echo', backend=CommandChannel())

O anfitrião escreve para o canal com channel_write() e lê a resposta pelo padrão habitual read_status() / channel_read()

with Camera('/dev/ttyACM0') as cam:
    cam.stop()
    cam.exec(open('echo_cam.py').read())

    cam.channel_write('echo', b'hello')

    while True:
        if cam.read_status().get('echo'):
            print(cam.channel_read('echo').decode())
            break

13.3.1.4.3. O que isto oferece à aplicação

Os canais personalizados são a ferramenta certa sempre que uma aplicação quer usar a ligação USB existente para dados que não sejam fotogramas nem impressões: contadores de telemetria, parâmetros de configuração transmitidos ao vivo a partir de uma interface no anfitrião, comandos de controlo enviados na direção inversa, resultados de uma medição que a câmara calculou e que não se enquadra no formato de «imagem» que o canal de fluxo pressupõe. A camada de protocolo trata do enquadramento, fragmentação, reconhecimento e repetição; o script só precisa de implementar o backend de quatro métodos, e o anfitrião só precisa de conhecer o nome do canal e o formato dos dados.

A opção --channel NAME da CLI é uma forma rápida de verificar um canal personalizado a partir do terminal sem escrever um programa no lado do anfitrião: a CLI consulta o canal com o nome indicado e imprime os primeiros dez bytes de cada atualização.

O limite de tamanho numa única chamada a channel_read() ou channel_write() é o max_payload negociado pelo protocolo – 4096 bytes por predefinição. Os métodos do lado do anfitrião dividem automaticamente escritas maiores no número certo de pacotes, pelo que a aplicação pode passar buffers arbitrariamente grandes; a fragmentação é transparente.