12.9. 양방향 흐름¶
채널은 단방향이 아닙니다. write 를 구현하는 백엔드는 호스트가 캠 쪽으로 바이트를 보낼 수 있게 하고, 캠은 그에 반응합니다. 이것이 모든 실제 대화형 도구의 기반이 되는 패턴입니다. 운영자가 호스트 GUI에서 노브를 돌리면, 호스트는 새 값을 구성 채널에 쓰고, 캠은 다음 캡처 때 그 값을 읽습니다.
12.9.1. 구성 채널¶
스트리밍 캠 측 스크립트에 추가하여, 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)
캡처 루프는 프레임을 압축할 때마다 config.quality 에서 값을 읽습니다:
while True:
img = csi0.snapshot()
latest_jpeg = bytes(
img.compress(quality=config.quality).bytearray()
)
ch.send_event(0x01)
이제 호스트에 노브가 생겼습니다. 50으로 설정하면 다음 프레임이 더 작아지고(그리고 더 보기 흉해지고), 95로 설정하면 다음 프레임이 더 커집니다(그리고 더 선명해집니다). 캠은 재시작 없이 계속 캡처하고, 호스트는 새 스크립트를 보낼 필요가 없습니다.
12.9.2. 호스트로부터의 write 호출¶
호스트 측에서 channel_write() 는 명명된 채널로 바이트를 보냅니다:
cam.channel_write('config', b'50')
호스트 SDK가 바이트를 하나의(또는 프래그먼트화된) CHANNEL_WRITE 패킷으로 인코딩하면, 프로토콜 계층이 이를 캠에 전달하고, 캠의 write(offset=0, data=...) 가 실행되며, 캠 측이 이를 확인 응답합니다. 호출이 반환될 무렵이면 캠은 새 값을 수신하여 받아들인 상태입니다.
이 write는 캠의 관점에서 원자적입니다 – 프로토콜 라이브러리는 그 채널에서 다른 작업이 진행되기 전에 백엔드의 write 가 완료될 때까지 실행되도록 보장합니다. 애플리케이션 코드는 호스트가 스냅샷 도중에 끼어들 걱정 없이 캡처 루프 안에서 config.quality 를 읽을 수 있습니다.
12.9.3. 스텁 size와 쓰기 전용 채널의 read¶
순수한 쓰기 채널이라도 size 와 read 를 정의해야 합니다. 비록 0과 b'' 를 반환하는 스텁일지라도 말입니다. 라이브러리는 메서드의 존재 여부를 사용하여 채널의 기능 플래그를 도출합니다. read 가 없는 백엔드는 CHANNEL_FLAG_READ 가 설정되지 않으며 호스트는 읽기 시도를 거부합니다.
그러나 쓰기 전용 채널에서 read 가 반환하는 바이트는 다른 용도로 유용합니다. 바로 현재 값을 다시 보내주어, 막 연결된 호스트가 기본값에서 시작하는 대신 캠에 “현재 설정이 무엇인가?”라고 물을 수 있게 하는 것입니다. 이것이 동작하려면 양쪽 방향이 직렬화 방식에 합의해야 합니다. 앞선 예제의 원시 바이트 int(bytes(data)) 파싱은 단일 정수 필드에는 동작하지만 설정할 노브가 두 개가 되면 확장되지 않습니다. write 가 JSON을 파싱하도록 전환하고 이에 맞는 JSON 덤프를 반환하는 read 와 짝지으면, 채널은 진정한 왕복 구성 저장소가 됩니다:
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)
이제 호스트는 값을 설정하기 위해 cam.channel_write('config', b'{"quality": 50}') 를 쓰고 현재 상태를 다시 읽기 위해 cam.channel_read('config') 를 씁니다. 캠은 매번 읽기 때마다 새로운 JSON 덤프를 직렬화하므로 호스트는 항상 최신 값을 보게 되며, 또 다른 노브(threshold, exposure, orientation)를 추가하는 것은 양쪽 JSON 딕셔너리에 한 줄을 더하는 일입니다.
12.9.4. 완전한 루프¶
캠 → 호스트 데이터를 위한 프레임 채널, 호스트 → 캠 제어를 위한 구성 채널, 그리고 약간의 접착 코드가 있으면 애플리케이션은 대화형 도구가 됩니다:
호스트는 캠을 열고, 프레임을 끌어오기 시작하며, 창에 표시합니다.
운영자가 슬라이더를 드래그하면, 호스트는 새 값을
config에 씁니다.캠의 캡처 루프는 다음 프레임에서 그 값을 가져옵니다.
새 프레임은 동일한
frame채널을 통해 흐릅니다.
그것이 전체 모델입니다. 두 개의 채널, 각각 두 개의 콜백, 캠의 캡처 루프, 호스트의 읽기-쓰기 루프. 보이는 프레이밍 로직도, 보이는 오류 처리도 없습니다 – 프로토콜 라이브러리가 신뢰성 있는 바이트 이동을 사라지게 만듭니다.
이 지점 이후의 모든 것은 애플리케이션 코드입니다. 히스토그램을 위한 세 번째 채널, 텔레메트리를 위한 네 번째 채널, 센서 트리거를 위한 다섯 번째 채널을 추가하는 것은 동일한 백엔드 클래스와 protocol.register 방식을 반복하는 일입니다. 캠 프로젝트가 이 지점에 이르면 프로토콜은 더 이상 흥미로운 문제가 아니게 되고, 애플리케이션 자체의 로직이 그 자리를 대신합니다.