13.1.10. 独立终端窗口

Tools → Open Terminal 会打开独立的终端窗口——每个窗口都是一个微型的 OpenMV IDE 会话,自带帧缓冲区查看器、直方图和交互式终端,通过你选择的传输方式连接。主窗口的连接不受影响,因此独立终端可以让你在第一台摄像头保持连接的同时观察第二台摄像头,或调试位于网络另一端的摄像头。

一个独立终端窗口:左侧是带工具栏的交互式终端,右侧是帧缓冲区和直方图

一个通过串口连接的独立终端窗口:左侧是带运行 / 停止 / 复位工具栏的交互式终端,右侧是帧缓冲区和直方图——当摄像头在带内流式传输帧时它们会亮起。

New Terminal 会询问以下三种传输方式之一:

  • Serial port——任意波特率(默认 115,200)的任意串口。这涵盖了第二台摄像头的 USB 端口、通过 USB 转 UART 桥接器接入的摄像头、蓝牙串行链路(它会显示为普通串口),或任何需要终端的非 OpenMV 串行设备。在 USB 端口上波特率并不限制速度——数据始终以 USB 链路的速度传输——但在摄像头的 USB 端口上请避免使用 921,600 和 12,000,000,它们会把摄像头从 REPL 切换到 IDE 的调试协议。

  • TCP——以选定的主机和端口连接到服务器,或在选定的端口上作为服务器侦听。

  • UDP——同样的一对角色,但通过 UDP。

IDE 会记住最近十个配置,并在 Open Terminal 子菜单中列出它们以便一键重新打开;Clear Menu 会清除它们。

与主窗口仅用于输出的窗格不同,独立终端是完全交互式的:它是一个 REPL。在提示符处输入,Python 就会在所连接的摄像头上逐行执行,历史记录和 Tab 补全由 MicroPython 自身提供。工具栏还增加了常用控制序列的一键等效操作——运行当前编辑器脚本、停止正在运行的脚本以及软复位——以及与主终端窗格相同的清除、保存和换行控件。

终端上方的帧缓冲区也是实时的。当所连接的摄像头在带内流式传输压缩帧时——即嵌入在与其打印输出相同的流中——终端会解码并显示它们,而 Record 和 Zoom 按钮的工作方式与主窗口中完全一致。这种组合正是 IDE 的网络调试方案:一台通过 Wi-Fi 暴露其 REPL 的摄像头,无需任何 USB 线缆即可获得完整的编辑-运行-预览循环。

13.1.10.1. 带内流式传输帧

终端所理解的编码很简单:一个 0xFE 字节开启一帧,第二个 0xFE 关闭它,而它们之间的每个有效载荷字节都将其最高位置位,并携带压缩图像的六个比特——三个图像字节变成四个有效载荷字节。纯文本永远不会用到这些字节值,因此帧和 print() 输出可以共享同一个流而不发生冲突:终端既显示文本,又显示帧。

下面的脚本进行捕获,将每一帧转换为 JPEG,并以这种形式打印出来。位打包通过 ulab 完成(它没有移位运算符,因此移位被写成乘法和除法),并且速度足够快,能够跟上摄像头的节奏:

import csi
import sys
import time
from ulab import numpy as np

csi0 = csi.CSI()
csi0.reset()
csi0.pixformat(csi.RGB565)
csi0.framesize(csi.QVGA)

clock = time.clock()


def encode_for_ide(data):
    n = len(data)
    n3 = n - (n % 3)
    m = (n3 // 3) * 4
    out = bytearray(((n * 8) + 5) // 6 + 2)
    out[0] = 0xFE
    out[-1] = 0xFE
    if n3:
        src = np.frombuffer(data, dtype=np.uint8)
        dst = np.frombuffer(out, dtype=np.uint8)
        b0 = src[0:n3:3]
        b1 = src[1:n3:3]
        b2 = src[2:n3:3]
        dst[1:m + 1:4] = (b0 & 0x3F) | 0x80
        dst[2:m + 2:4] = (b0 // 64) | ((b1 & 0x0F) * 4) | 0x80
        dst[3:m + 3:4] = (b1 // 16) | ((b2 & 0x03) * 16) | 0x80
        dst[4:m + 4:4] = (b2 // 4) | 0x80
    if n % 3 == 2:
        x = data[n - 2] | (data[n - 1] << 8)
        out[m + 1] = 0x80 | (x & 0x3F)
        out[m + 2] = 0x80 | ((x >> 6) & 0x3F)
        out[m + 3] = 0x80 | ((x >> 12) & 0x3F)
    elif n % 3 == 1:
        out[m + 1] = 0x80 | (data[n - 1] & 0x3F)
        out[m + 2] = 0x80 | (data[n - 1] >> 6)
    return out


while True:
    clock.tick()
    img = csi0.snapshot().to_jpeg(quality=80)
    sys.stdout.write(encode_for_ide(img.bytearray()))
    print(clock.fps())

这在独立终端中可行,是因为 REPL 打印会等待:摄像头会阻塞,直到终端取走数据,因此帧的每个字节都会到达,而 JPEG 通过 USB 全速或高速链路传输得很快。同样的脚本在 TCP 终端上无需更改即可工作,这正是上文提到的网络调试路径。唯一不适用的地方是 IDE 的主调试连接,其输出通道在溢出时会自我复位而不是等待——在那里帧缓冲区查看器已经原生显示摄像头的帧,所以不会有任何遗漏。