12.6. แชนเนลที่มีชื่อ

channel ID ในส่วนหัวของแต่ละแพ็กเก็ตช่วยให้ stream อิสระสูงสุด 32 เส้นแชร์ transport จริงเดียวกัน ชั้นแชนเนลแปลง ID ตัวเลขเหล่านั้นเป็น endpoint ที่มีชื่อ ซึ่งแอปพลิเคชันสามารถมองเห็นได้ และโค้ดฝั่ง host สามารถอ้างอิงได้ด้วย 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. แชนเนล built-in สี่ตัว

cam ลงทะเบียนแชนเนลสี่ตัวเมื่อบูต ก่อนที่โค้ดแอปพลิเคชันใดจะทำงาน:

  • stdin -- ไบต์ของสคริปต์ที่ host ส่งมายัง cam เพื่อประมวลผล IDE ใช้แชนเนลนี้เพื่อส่งสคริปต์ที่กำลังแก้ไข; exec() บน host SDK คือการเรียกที่เทียบเท่าจากโปรแกรม Python

  • stdout -- ไบต์จากการเรียก print() บน cam และ traceback ของ exception ที่ไม่ถูกจัดการ คอนโซลซีเรียลของ IDE อ่านแชนเนลนี้

  • stream -- แชนเนลแสดงตัวอย่างสด IDE ดึงเฟรม JPEG จากมัน; สคริปต์ใดๆ บน host ก็สามารถทำเช่นเดียวกันด้วย read_frame()

  • profile -- เหตุการณ์ profiler มีเฉพาะเมื่อ cam ถูก build พร้อม profiling เปิดใช้งาน build สำหรับ release ส่วนใหญ่จะไม่มี

โค้ดแอปพลิเคชันแทบไม่จำเป็นต้องแตะ built-in ใดๆ งานที่น่าสนใจเกิดขึ้นบนแชนเนลที่แอปพลิเคชันลงทะเบียนเอง

12.6.2. การลงทะเบียนแชนเนล

สคริปต์ฝั่ง cam ลงทะเบียนแชนเนลใหม่โดยเรียก protocol.register() พร้อมชื่อและออบเจกต์ backend ของ Python:

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())

เมธอดของออบเจกต์ backend กำหนดว่าแชนเนลทำอะไรได้ backend ที่มีแค่ size และ read คือ แชนเนลข้อมูลแบบอ่านอย่างเดียว; เพิ่ม write แล้วจะกลายเป็นสองทิศทาง; เพิ่ม poll แล้ว host สามารถถามว่ามีข้อมูลใหม่พร้อมหรือยังก่อนจ่ายค่าการอ่าน การสุ่มตัวอย่างข้อมูลภายใน size คือรูปแบบที่ง่ายที่สุดเมื่อ payload เล็กพอที่จะใส่ในส่วนเดียว -- บัฟเฟอร์ถูกสร้างตามความต้องการ ไม่เคยถูก cache และไม่มี race condition payload ขนาดใหญ่ -- เฟรมภาพ, trace ของ sensor -- ต้องการรูปแบบการล็อกที่ถือบัฟเฟอร์ไว้จนกว่า host จะอ่านหลายส่วนเสร็จ ซึ่งอธิบายพร้อมกับแชนเนลเฟรม

การจัดการบัญชีเล็กน้อยเกิดขึ้นโดยอัตโนมัติ:

  • ไลบรารีจะกำหนด channel ID ถัดไปที่ว่าง (ระหว่าง 0 ถึง 31)

  • flag ความสามารถจะได้มาจากเมธอดที่มีอยู่: CHANNEL_FLAG_READ ถ้า read ถูกกำหนด, CHANNEL_FLAG_WRITE ถ้า write ถูกกำหนด, CHANNEL_FLAG_LOCK ถ้า lock / unlock ถูกกำหนด

  • แพ็กเก็ตเหตุการณ์ CHANNEL_REGISTERED จะถูกส่งไปยัง host ที่เชื่อมต่ออยู่ เพื่ออัปเดตรายการแชนเนล

ค่าที่คืนมาคือ handle protocol.ProtocolChannel ที่แอปพลิเคชันสามารถเก็บไว้ เมธอด send_event() ของ handle คือ hook ฝั่ง cam สำหรับบอก host ว่า "มีบางอย่างเกิดขึ้นบนแชนเนลนี้โดยไม่เปลี่ยนข้อมูลที่อ่านได้" -- trigger ทำงาน ปุ่มถูกกด หรือถึงจุดสำคัญของจำนวนตัวอย่าง

12.6.3. การอ่านแชนเนลจาก host

Host SDK มาพร้อมกับแพ็กเกจ openmv บน PyPI (pip install openmv) ซึ่งสร้างบน pyserial สำหรับ transport คลาส openmv.camera.Camera ของมันเปิดเผยแชนเนลที่มีชื่อของ cam ผ่านเมธอดระดับสูง:

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)

Warning

แพ็กเกจ openmv ต้องการ CPython 3.12 หรือใหม่กว่า interpreter เวอร์ชันเก่ากว่าขาดลักษณะเด่นที่ SDK ต้องการ ติดตั้ง build 3.12+ ก่อน pip install openmv

สิ่งที่ควรสังเกตในการตั้งค่ามีดังนี้:

  • สตริง serial-port -- /dev/ttyACM0 ที่นี่ -- จะเป็นรูปแบบ COM3 บน Windows, /dev/cu.usbmodemXXXX บน macOS และ /dev/ttyACM* บน Linux ตัวเลขจริงขึ้นอยู่กับ port ที่ cam ลงทะเบียนไว้

  • อัตราบอดคือค่าพิเศษ 921600 ของโปรโตคอล ซึ่ง USB-CDC stack ของ cam จะรู้จักว่า "client นี้พูดโปรโตคอล ไม่ใช่ REPL" อัตราอื่นจะถอยกลับไปเป็นสายซีเรียลธรรมดา

  • context manager with Camera(...) as cam: จะเปิด transport ทำ PROTO_SYNC แลกเปลี่ยน capability และเมื่อออกจะปิด port อย่างสะอาด การเรียก update_channels() อย่างชัดเจนหลังเข้าสู่ context จะรีเฟรชรายการแชนเนล local ด้วยแชนเนลที่แอปพลิเคชันลงทะเบียนหลังบูต

channel_size() และ channel_read() คือเมธอดหลัก; channel_write() ส่งบัฟเฟอร์ไปกลับกับ cam ถ้า backend มีเมธอด write; has_channel() คือวิธีปลอดภัยในการตรวจสอบว่าชื่อถูกลงทะเบียนก่อนใช้งาน ชื่อแชนเนลจะถูกค้นหาครั้งเดียวเพื่อแปลงเป็น channel ID ที่ cam กำหนดตอน register และใช้ในทุกแพ็กเก็ตนับจากนั้น

แต่ละคู่ channel_size() / channel_read() ใช้สอง round-trip: หนึ่งแพ็กเก็ตเพื่อถามขนาด หนึ่งแพ็กเก็ตเพื่อถามไบต์ ผ่าน USB-CDC ทั้งสองเสร็จในเวลาประมาณหนึ่งมิลลิวินาทีรวมกัน ผ่าน UART การแลกเปลี่ยนเดียวกันใช้เวลานานกว่าตามสัดส่วนของอัตราบอดของสายซีเรียล โค้ดแอปพลิเคชันที่อ่านใน loop แบบต่อเนื่องควรเรียก channel_size() เฉพาะเมื่อขนาดสามารถเปลี่ยนแปลงได้จริงๆ -- สำหรับข้อมูลขนาดคงที่ ขนาดจากการเรียกครั้งแรกสามารถ cache ไว้ได้

12.6.4. ความเป็นอิสระระหว่างแชนเนล

มีสามสิ่งที่ควรทราบเกี่ยวกับการที่แชนเนลโต้ตอบกัน:

  • การควบคุมการไหลแบบอิสระ. แต่ละแชนเนลมีสถานะการอ่านที่รอดำเนินการของตัวเอง ข้อมูลของตัวเอง และคอลแบ็ก size / read / write ของตัวเอง การอ่านที่ใช้เวลานานบนแชนเนล stream ไม่ block การอ่านบนแชนเนล config ของแอปพลิเคชัน

  • ตามลำดับต่อแชนเนล. ภายในแชนเนลเดียว แพ็กเก็ตจะถูกส่งตามลำดับ ชั้น reliability รับประกันสิ่งนี้แม้ในกรณีที่มีการส่งใหม่

  • Transport ร่วม, งบประมาณการส่งใหม่ร่วม. ทุกแชนเนลแชร์ link จริงเดียว ดังนั้นปริมาณงานที่ผ่านได้สูงบนแชนเนลหนึ่งจะทำให้แชนเนลอื่นช้าลงเพราะครอบครองสาย กลไก CHANNEL_LOCK ให้แชนเนลหนึ่งจองสายสำหรับการอ่าน atomic หลายแพ็กเก็ต โดย backend เลือกใช้โดยการ implement คอลแบ็ก lock / unlock

แชนเนลคือพื้นที่ผิวขั้นต่ำที่โปรแกรม host และโปรแกรม cam ตกลงที่จะร่วมมือกัน ชื่อ ทิศทาง (อ่านหรือเขียนหรือทั้งสอง) เมธอดคอลแบ็กฝั่ง cam และการเรียกเมธอดที่ตรงกันฝั่ง host คือสัญญาทั้งหมด