12.9. Fluxo bidirecional

Os canais não são unidirecionais. Um backend que implementa write permite ao host enviar bytes para a câmara, e a câmara reage. Esse é o padrão por detrás de todas as ferramentas interativas reais: o operador roda um botão na GUI do host, o host escreve o novo valor num canal de configuração, a câmara lê-o na próxima captura.

12.9.1. Um canal de configuração

Adicionando ao script do lado da câmara em transmissão contínua, exponha um segundo canal para a qualidade JPEG:

class ConfigChannel:
    def __init__(self):
        self.quality = 85

    def size(self):
        return 0

    def read(self, offset, size):
        # Not used for "host writes to cam" -- but the library
        # still needs the method present.
        return b''

    def write(self, offset, data):
        # data is a bytearray view into the protocol buffer.
        # Copy out the contents before doing anything with it.
        new_q = int(bytes(data))
        if 1 <= new_q <= 100:
            self.quality = new_q
        return len(data)

config = ConfigChannel()
protocol.register(name='config', backend=config)

O ciclo de captura lê de config.quality sempre que comprime um fotograma:

while True:
    img = csi0.snapshot()
    latest_jpeg = bytes(
        img.compress(quality=config.quality).bytearray()
    )
    ch.send_event(0x01)

O host tem agora um botão. Defina-o para 50 e o próximo fotograma é menor (e mais feio); defina-o para 95 e o próximo fotograma é maior (e mais nítido). A câmara continua a capturar sem reiniciar; o host não precisa de enviar um novo script.

12.9.2. A chamada de escrita do host

No lado do host, channel_write() envia bytes para um canal com nome:

cam.channel_write('config', b'50')

O SDK do host codifica os bytes como um único pacote (ou fragmentado) CHANNEL_WRITE, a camada de protocolo entrega-o à câmara, o write(offset=0, data=...) da câmara é executado, e o lado da câmara reconhece. Quando a chamada retorna, a câmara recebeu e aceitou o novo valor.

A escrita é atómica do ponto de vista da câmara – a biblioteca de protocolo garante que o write do backend é executado até à conclusão antes de qualquer outra operação nesse canal prosseguir. O código de aplicação pode ler config.quality de dentro do ciclo de captura sem se preocupar com o host a sobrescrever durante uma captura de imagem.

12.9.3. Tamanho do stub e leitura num canal apenas de escrita

Um canal de escrita pura ainda necessita de size e read definidos, mesmo que sejam stubs a devolver 0 e b''. A biblioteca usa a presença de métodos para derivar os flags de capacidade do canal; um backend sem read não terá CHANNEL_FLAG_READ definido e o host recusará uma tentativa de leitura.

Os bytes devolvidos por read num canal apenas de escrita são úteis para um propósito diferente, no entanto: fazer eco do valor atual para que um host que acabou de se ligar possa perguntar à câmara «qual é a configuração atual?» em vez de começar a partir de um padrão. Para que isso funcione, ambas as direções têm de concordar numa serialização. A análise de bytes brutos int(bytes(data)) no exemplo anterior funciona para um único campo inteiro, mas não escalará quando existir um segundo botão a definir. Mudar write para analisar JSON e emparelhar com um read que devolve o JSON correspondente transforma o canal numa verdadeira memória de configuração de ida e volta:

import json

class ConfigChannel:
    def __init__(self):
        self.quality = 85
        self._buf = b''
    def size(self):
        self._buf = json.dumps({'quality': self.quality}).encode()
        return len(self._buf)
    def read(self, offset, size):
        return self._buf[offset:offset + size]
    def write(self, offset, data):
        new = json.loads(bytes(data))
        if 'quality' in new:
            self.quality = int(new['quality'])
        return len(data)

O host escreve agora cam.channel_write('config', b'{"quality": 50}') para definir um valor e cam.channel_read('config') para ler o estado atual de volta. A câmara serializa um dump JSON fresco em cada leitura, pelo que o host vê sempre os valores mais recentes, e adicionar outro botão (threshold, exposure, orientation) é uma linha no dicionário JSON de cada lado.

12.9.4. Um ciclo completo

Com um canal de fotograma para dados câmara → host, um canal de configuração para controlo host → câmara, e uma pequena quantidade de código de ligação, a aplicação é uma ferramenta interativa:

  • O host abre a câmara, começa a extrair fotogramas e exibe-os numa janela.

  • Quando o operador arrasta um deslizador, o host escreve o novo valor em config.

  • O ciclo de captura da câmara recolhe o valor no próximo fotograma.

  • Os novos fotogramas fluem pelo mesmo canal frame.

Esse é o modelo completo. Dois canais, dois callbacks cada, um ciclo de captura na câmara, um ciclo de leitura e escrita no host. Sem lógica de enquadramento visível, sem tratamento de erros visível – a biblioteca de protocolo faz o movimento fiável de bytes desaparecer.

Tudo a partir deste ponto é código de aplicação. Adicionar um terceiro canal para um histograma, um quarto para telemetria, ou um quinto para gatilhos de sensor é a mesma receita de classe backend e protocol.register, repetida. Quando um projeto de câmara chega a este ponto, o protocolo deixa de ser o problema interessante; a lógica própria da aplicação passa a sê-lo.