13.3.1.4. Canais personalizados

Um canal é um fluxo de bytes nomeado e bidirecional entre um script do lado da cam e o host. A cam registra um canal e fornece callbacks que produzem ou consomem dados; o host lê e escreve nesse canal pelo nome. O mesmo mecanismo que o pacote usa internamente para o canal stream que transporta os quadros, o canal stdout que transporta a saída do script e o canal stdin que transporta o upload do script é exposto aos scripts do usuário, de modo que quaisquer dados específicos da aplicação que o host precise possam trafegar pela mesma conexão USB sem inventar um segundo protocolo.

Este é o recurso mais útil do pacote e o que a documentação padrão cobre pior, então esta página trabalha-o de ponta a ponta.

13.3.1.4.1. As duas metades

Um canal personalizado precisa de código cooperante em ambos os lados. O script do lado da cam 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 sob 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() retorna quantos bytes o canal tem disponíveis no momento. read() é o produtor: dados um offset e um size solicitados pelo host, retorna os bytes (ou uma string que a camada de protocolo codifica). poll() retorna True quando há algo para ler – a camada de protocolo usa isso para sinalizar o canal como pronto em read_status().

O programa do lado do host usa quatro métodos de openmv.Camera: has_channel() para verificar se o canal existe, channel_size() para perguntar quantos dados estão aguardando, channel_read() para extrair bytes e channel_write() para inserir 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 loop do host consulta read_status(); quando o canal ticks está pronto, ele chama channel_read() sem size para extrair o que estiver disponível. O método TicksChannel.poll() da cam retorna True em toda verificação, então o canal está sempre “pronto” e o host obtém um valor de tick atualizado a cada polling.

13.3.1.4.2. Um canal bidirecional

Para um host que precisa enviar dados de volta, a classe do lado da cam 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 host escreve no canal com channel_write() e lê a resposta de volta pelo padrão habitual de 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 isso traz para a aplicação

Canais personalizados são a ferramenta certa sempre que uma aplicação quer aproveitar a conexão USB existente para dados que não são quadros nem impressões: contadores de telemetria, parâmetros de configuração transmitidos ao vivo a partir de uma interface no host, comandos de controle enviados no sentido oposto, resultados de uma medição que a cam computou e que não se encaixam no enquadramento de “imagem” que o canal de stream pressupõe. A camada de protocolo cuida do enquadramento, da fragmentação, do reconhecimento e da retentativa; o script só precisa implementar o backend de quatro métodos, e o host só precisa conhecer o nome do canal e o formato dos dados.

A flag --channel NAME da CLI é uma maneira rápida de verificar um canal personalizado a partir do terminal sem escrever um programa do lado do host: a CLI consulta o canal nomeado e imprime os primeiros dez bytes de cada atualização.

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