教程:VENTUNO Q/手势拍照亭

来自WindwizPi wiki

简介

本应用说明探讨了交互式 Photo Booth 的实现:一个独立的信息亭,使用 USB 摄像头捕获访客的照片,使用边缘 AI 模型实时跟踪手势以选择主题图形框架,并在按下按钮时自动将合成的照片提交到打印服务器。整个 AI 推理和 Web 应用程序主机在 Arduino® VENTUNO™ Q 上本地运行,而不依赖任何云基础设施作为面向用户的路径。


hero-banner.png


Photo Booth 应用程序汇集了 VENTUNO Q 生态系统的核心边缘人工智能和系统集成构建模块:

  • 设备上手势识别:人工智能视觉模型处理相机帧以实时检测关键手势(张开手掌、胜利、拇指向上、拇指向下、向上指、握拳和ILoveYou,在界面中显示为摇滚)。该手势可以选择将哪个主题框架合成到照片上,按住一个主题框架就可以进行拍摄。
  • 物理触发按钮(HID 抽象):围绕 Arduino® Nano ESP32 构建的分立按钮,配置为 USB HID 键盘。此按钮是展位的唯一物理控制:它启动会话、启动捕获倒计时并确认打印。
  • 动态帧合成:网络信息亭前端可在检测到的手势变化时立即交换图形帧叠加和视觉反馈,然后将所选帧合成到捕获的照片上,然后发送到打印。
  • 异步打印管道:后台工作线程,对 CUPS 支持的打印服务器执行飞行前运行状况检查,并管理多次尝试的作业提交,而不会阻塞相机或界面。
  • 3D 打印支架安装:定制 3D 打印支架,可将 VENTUNO Q 干净地固定到移动信息亭支架的下层架上。
  • 全屏信息亭界面:由 VENTUNO Q 提供的 Web UI,并在信息亭模式下在显示屏或触摸显示器上全屏呈现。

通过阅读本应用说明,您将了解高级 Arduino Bricks(“gesture_recognition”和“web_ui”)如何组成单个“arduino-app-cli”应用程序、Nano ESP32 如何充当硬件解耦的 HID 执行器,以及如何构建适用于公共展览、贸易展览和零售安装的稳健、容错的打印桥。

目标

本应用笔记的主要目标如下:

  • 在 VENTUNO Q 上构建功能齐全的 Photo Booth 应用程序,该应用程序可处理实时相机输入、检测手势以选择主题框架,并通过按下物理按钮输出打印照片。
  • 展示如何使用 Python® 编排入口点将 Bricks“arduino:gesture_recognition”和“arduino:web_ui”组合成单个“arduino-app-cli”应用程序。
  • 演示如何将单独的 Nano ESP32 用作 USB HID 键盘按钮,以便可以通过分立的物理执行器触发展位,同时保持主机应用程序与执行器硬件完全解耦。
  • 使用 3D 打印的 VENTUNO Q 支架详细说明物理组装和安装。
  • 实现具有飞行前状态验证、瞬态错误重试和非阻塞事件处理功能的异步容错打印子系统。
  • 提供分步设置指南,用于将应用程序部署为 VENTUNO Q 上的自动化、无人值守的信息亭系统。

硬件与软件要求

硬件要求

hardware.png


  • Arduino® VENTUNO™ Q:运行手势识别视觉模型、Web UI 和打印工作线程的计算主机。
  • Arduino® Nano ESP32:用于物理触发按钮的 USB HID 键盘控制器。
  • 瞬时按钮(22 毫米面板安装式)和一个用于容纳按钮和 Nano ESP32 的小外壳。
  • 3D 打印的 VENTUNO Q 支架:用于将电路板固定到滚动亭支架或较低架子上的定制支架(3D 打印 VENTUNO Q 支架 中提供的 STL 文件)。
  • USB 摄像头:具有自动对焦和宽视野的高清或全高清 USB 摄像头。
  • 显示器/监视器:连接到 VENTUNO Q 板载 HDMI® 输出端口的高亮度显示器或触摸面板,以纵向安装和配置。信息亭前端布局为纵向屏幕,打印的照片为 2:3 纵向,因此横向显示使界面在屏幕中间留下一条窄带。
  • 打印机:任何 CUPS 可以驱动、通过 USB 或网络连接的打印机。 4×6 照片打印机可提供最佳效果,但该软件会根据所连接的任何内容协商纸张尺寸,并且与办公室打印机进行测试时同样可以正常工作。
  • 适用于 VENTUNO Q 的 60 W 桶形插孔电源。
  • 标准布线:用于显示器的 HDMI 电缆、用于 Nano ESP32 的 USB-C® 至 USB-A 电缆、用于相机和打印机的 USB 电缆、用于按钮的跳线。

软件要求

应用架构概述

Photo Booth 应用程序使用模块化 Bricks 架构。应用程序清单在“app.yaml”中定义:

name: photo-booth-demo
icon: 📸
description: Strike a pose with hand gestures — capture, composite, and print a photo. Requires a USB webcam.
bricks:
  - arduino:gesture_recognition
  - arduino:web_ui

每个块都封装了一个专用服务,该服务向 Python 应用程序公开一个干净的 API:

  • arduino:gesture_recognition 连接到相机管道,通过手部地标模型处理视频帧,并针对识别的手势发出事件回调。 Brick 在自己的容器中运行,并将推理卸载到板公开的 Qualcomm® Hexagon™ DSP,因此主要的 Python 进程可以自由进行编排。
  • arduino:web_ui 在端口 7000 上提供静态资源(HTML/CSS/JavaScript),为实时双向消息传递提供 Socket.IO 连接,并托管自定义 API 处理程序。

当“arduino-app-cli”启动应用程序时,它会启动容器环境,其中 Python 脚本协调相机捕获流、手势事件、前端套接字消息和后台打印提交。


architecture.png


该图中的两个细节值得一提,因为这两个细节很容易反过来假设:

  • Python 应用程序拥有摄像头。 它打开“/dev/video*”,手势识别客户端(一个在同一进程内运行的库)从中读取帧并通过 WebSocket 将它们中继到手势容器,并在第二个帧上接收检测结果。
  • 实时预览不会通过应用程序。 手势容器在其自己的端口上提供带注释的视频,前端直接将“<img>”指向它。该页面来自端口“7000”上的 Web UI Brick;该视频来自端口“5002”上的手势 Brick。它们是单独的连接,这就是为什么 <img> 被标记为 crossorigin="anonymous",没有它,浏览器以后无法将该框架绘制到画布上以合成照片。

理解关键的架构概念

设备上手势识别

传统的自助服务终端应用程序仅依赖于触摸屏或云视觉 API。 Photo Booth 使用在 VENTUNO Q 上本地运行的设备上手势识别:

  • 手部地标跟踪:底层模型定位手部并返回每只手的一组 3D 关键点,从中对手指配置和姿势进行分类。每个检测回调都会收到地标列表、边界框、置信度分数以及哪只手产生的分数。
  • 左手和右手跟踪:回调可以绑定到左手、右手或两者,因此单个手势可能意味着不同的含义,具体取决于哪只手执行该手势。
  • 本地加速:推理在 Brick 的容器内针对板的 DSP 运行,而不是在 CPU 上运行,这有助于保持请求的 30 FPS 相机模式和 Web UI 同时响应。实际帧速率仍然取决于相机和协商的视频模式。
  • 隐私第一:视频帧永远不会离开板,使得安装可以直接部署在公共场所,否则在公共场所捕获和传输访客图像将需要额外的保护措施。
  • 富有表现力的选择:用户通过摆出姿势来选择照片的主题,而不是从屏幕菜单中进行选择,这使得界面没有触摸目标,并使选择成为照片本身的一部分。

七个识别的姿势和每个人选择的主题框架如下所示。这里的名字就是界面中显示的名字;传递给on_gesture()的标签略有不同,并在主应用程序中列出:


gesture-ui-flow.png


交互模型:手势选择、按钮触发

两个输入之间的分工驱动了整个前端设计。

注意:

这是最常假设的部分:手势永远不会拍照,按钮永远不会选择框架。

  • 手势选择主题。 每个识别的姿势都会映射到一个主题框架,并且更改姿势会交换屏幕上的叠加层,仅此而已。每次更改后 2 秒锁定会吸收识别器短暂振荡或报告“无”。
  • 该按钮驱动每个状态转换。 HID 击键或单击同一屏幕按钮可启动会话、启动捕获倒计时、取消捕获并确认打印。
  • 手势也是捕获的前提条件。 在倒计时结束时,前端会捕获一帧仅当仍保持姿势时。如果访客放下手,则不会拍摄照片,并且信息亭会显示“无手势”屏幕。

因此,访问者的顺序是:按 开始,摆出一个姿势来选取帧,按 开始 5 秒倒计时,保持该姿势直到闪光,然后再按一次进行打印。

每个等待状态都受计时器限制,因此放弃的会话始终会在无需人员干预的情况下恢复:

计时器 期间 到期时的行为
捕捉倒计时 5秒 拍摄照片,或显示“无手势”屏幕
结果自动丢弃 10秒 丢弃未打印的照片并返回空闲状态
打印确认书 长达 90 秒 后端回复时显示成功或失败; 90秒后,报告结果未知
重启倒计时 5秒 返回主屏幕
手势变换锁 2秒 允许再次更改叠加层
丢失手势去抖动 5秒 恢复到空闲提示

由于物理按钮只是简单的 Enter 键,因此在开发过程中每一步都可以通过键盘实现,并且前端还公开了一个应用随机帧的“模拟手势”控件,因此整个流程可以在没有相机或闪存 Nano ESP32 的情况下进行。

HID 作为硬件抽象

将按钮直接连接到主计算主机上的 GPIO 会将应用程序代码耦合到特定的引脚映射和驱动程序配置。

通过配置为 USB HID 键盘的 Nano ESP32 路由触发按钮可以消除这种耦合。主机 VENTUNO Q 看到标准 USB 键盘发送通用按键事件(“KEY_RETURN”)。这提供了主要好处:

  • 硬件独立性:主机应用程序不需要特殊的 GPIO 驱动程序或权限修改。
  • 可交换执行器:按钮可以更换为脚踏板、接近传感器或硬币接收器,而无需更改一行主机 Python 代码。
  • 通用前端绑定:Web UI 为 Enter 绑定单个“keydown”处理程序,并将其路由到当前屏幕上的任何操作:启动、拍照或打印。因此,一个按键驱动整个流程,并且同一按钮在每种状态下都能正确运行,而固件对此一无所知。

解耦自助服务终端打印

公共场所的打印机缺纸、插头被拔掉并且驱动程序停止运行。每次在主线程上同步调用打印 API 都会冻结相机和界面。

展位通过在守护进程工作线程中进行所有打印来避免这种情况,在飞行前状态检查后面进行快速失败,而不是等待 CUPS 超时。 打印服务器 涵盖了管道和实现它的服务。

物理硬件和执行器组件

完成的安装将纵向显示器、相机、VENTUNO Q、物理触发器和打印机组合到一个独立的信息亭中:


completed-photo-booth-kiosk.png


3D 打印 VENTUNO Q 支架

小型 3D 打印支架使 VENTUNO Q 保持直立并牢固地安装在信息亭支架的下层架或框架上。该参考设计为 HDMI、USB-A 和 USB-C 端口提供了坚固的安装点和宽敞的电缆间隙。


3d-printed-holder.png


直接从资源包下载STL文件:[assets/ventuno-q-holder.stl ventuno-q-holder.stl]。

推荐的打印设置:

范围 建议值
层高 0.2 毫米(0.1 毫米用于更高质量,0.25–0.30 毫米用于快速打印)
壁厚 切片器默认值
填充 15–20%
支持 是(推荐树/有机)
材料 解放军

物理触发按钮(Nano ESP32 HID)

物理触发器使用 22 毫米面板安装按钮,连接到紧凑外壳中的 Nano ESP32:

  • 将按钮的一个端子连接到 Nano ESP32 上的 GND。
  • 将按钮的另一个端子连接到 Nano ESP32 上的 D2 引脚。

scan-button-wiring.png


固件为D2配置了内部上拉电阻;按下按钮会将线路拉低并触发击键。不需要外部电阻器或去抖组件,因为去抖是在固件中处理的。

Nano ESP32 固件草图

固件使用内部上拉电阻配置“D2”,在软件中消除状态转换的抖动,并在按下按钮时发出“KEY_RETURN”击键。请注意“setup()”中的初始化顺序:在使用“USB.begin()”启动 USB 堆栈之前,使用“Keyboard.begin()”注册 HID 键盘,这使得主板枚举为键盘。

#include "USB.h"
#include "USBHIDKeyboard.h"

USBHIDKeyboard Keyboard;

const int buttonPin = D2;
const unsigned long debounceDelay = 50;

int buttonState = HIGH;
int lastButtonState = HIGH;
unsigned long lastDebounceTime = 0;

void setup() {
  // Button wired to GND, using the internal pull-up
  pinMode(buttonPin, INPUT_PULLUP);

  // Register the HID keyboard first, then start the USB stack
  Keyboard.begin();
  USB.begin();
}

void loop() {
  int reading = digitalRead(buttonPin);

  // Debounce: reset the timer on any change
  if (reading != lastButtonState) {
    lastDebounceTime = millis();
  }

  // Accept the new value once the line has been stable long enough
  if ((millis() - lastDebounceTime) > debounceDelay) {
    if (reading != buttonState) {
      buttonState = reading;

      // LOW means pressed (because of INPUT_PULLUP)
      if (buttonState == LOW) {
        Keyboard.press(KEY_RETURN);
        delay(10);
        Keyboard.release(KEY_RETURN);
      }
    }
  }

  lastButtonState = reading;
}

“KEY_RETURN”的选择是经过深思熟虑的。 Photo Booth 前端将 Enter 键与其主要操作绑定在一起,从启动画面开始,从实时预览中拍摄照片,因此相同的固件无需任何主机端配置即可工作。

闪烁工作流程

  1. 在 Arduino IDE 2.0+ 中打开草图。
  2. 从板列表中选择 Arduino Nano ESP32。
  3. 通过 USB-C 将 Nano ESP32 连接到您的开发机器,然后单击 上传。
  4. 刷新后,在开发机器上验证该板是否枚举为 HID 键盘。
  5. 将 Nano ESP32 插入 VENTUNO Q USB 端口之一。它需要零主机配置。

系统布线和互连

cabling.png


周边 板卡/端口 笔记
信息亭展示 VENTUNO Q 板载 HDMI 全屏显示输出
打印机 VENTUNO Q USB-A (1) 或网络 由主机上的 CUPS 通过打印服务器驱动
USB 摄像头 VENTUNO Q USB-A (2) 高清网络摄像头
纳米ESP32按钮 VENTUNO Q USB-C,通过 USB-C 适配器 枚举为 USB HID 键盘
电源 VENTUNO Q 桶形千斤顶 60 瓦电源

Photo Booth应用程序代码

应用程序入口点由 python/ 中的两个主要 Python 模块组成:

  • python/main.py:主应用程序设置、相机配置、手势事件注册和 UI 事件绑定。
  • python/printer.py:线程安全的打印客户端模块处理打印机状态检查、上传重试和作业提交。

主应用程序(`python/main.py`)

入口点故意很小:它打开相机,将手势名称绑定到消息,并将打印请求发送到工作线程。其他一切都位于 Bricks 或前端中。

有两个块值得仔细阅读,因为它们是您要编辑的块。

这里打开相机,直接递给手势砖:

camera = Camera(resolution=(1280, 720), fps=30, codec="MJPG", adjustments=cropped_to_aspect_ratio((2, 3)))

pd = GestureRecognition(camera)

“GestureRecognition”拥有相机生命周期,并在“App.run()”激活 Brick 时启动提供的相机,因此应用程序不得单独调用“camera.start()”。

然后将每个识别的姿势绑定到 Socket.IO 消息。后端对手势不做任何其他事情:它转发标签并让前端决定它的含义,这就是整个捕获和打印序列存在于浏览器中的原因:

pd.on_gesture("None", lambda meta: ui.send_message('gesture_detected', {'gesture': 'None'}))
pd.on_gesture("Closed_Fist", lambda meta: ui.send_message('gesture_detected', {'gesture': 'Closed Fist'}))
pd.on_gesture("Open_Palm", lambda meta: ui.send_message('gesture_detected', {'gesture': 'Open Palm'}))
pd.on_gesture("Pointing_Up", lambda meta: ui.send_message('gesture_detected', {'gesture': 'Pointing Up'}))
pd.on_gesture("Thumb_Down", lambda meta: ui.send_message('gesture_detected', {'gesture': 'Thumb Down'}))
pd.on_gesture("Thumb_Up", lambda meta: ui.send_message('gesture_detected', {'gesture': 'Thumb Up'}))
pd.on_gesture("Victory", lambda meta: ui.send_message('gesture_detected', {'gesture': 'Victory'}))
pd.on_gesture("ILoveYou", lambda meta: ui.send_message('gesture_detected', {'gesture': 'Rock'}))

请注意最后一行:模型的“ILoveYou”标签被重新映射到界面使用的更友好的“Rock”,而“None”伪手势可以让前端在没有识别的姿势时清除覆盖。要添加或重命名主题框架,此列表和前端的“手势”数组是两个需要更改的地方。

打印以相同的方式绑定,并立即传递,因此主循环永远不会阻塞:

def on_print_photo(client_id, data: dict):
    request_id = data.get("request_id", "")
    image_bytes = base64.b64decode(data["image"], validate=True)
    if not _print_slot.acquire(blocking=False):
        _notify_print("print_failed", request_id, error="Printer is busy")
        return
    threading.Thread(
        target=_print_worker,
        args=(image_bytes, request_id),
        daemon=True,
    ).start()


ui.on_message('print_photo', on_print_photo)

前端将照片及其叠加层合成在画布上,并将结果作为质量为 0.9 的 Base64 编码的 JPEG 发送。这样就可以避免第二次往返和服务器端重新编码;后端仅转发经过验证的字节。每个请求都带有一个唯一的 ID,工作人员会报告带有 CUPS 作业 ID 的“print_success”或“print_failed”。前端仅接受与活动请求匹配的回复,因此先前访问者的延迟响应无法更改当前屏幕。

相机参数是一个请求,而不是保证。如果相机无法提供确切的模式,驱动程序会选择它支持的最接近的模式并记录它所选择的模式:

WARNING V4LCamera: Camera codec set to YUYV instead of requested MJPG
WARNING V4LCamera: Camera resolution set to 1280x960 instead of requested 1280x720
WARNING V4LCamera: Camera FPS set to 9 instead of requested 30

该应用程序仍然可以工作(2:3 裁剪适用于任何到达的内容),但低帧速率下的未压缩格式使手势叠加明显滞后。

警告:

首次启动后检查应用程序日志。将任何这些警告视为更换摄像头而不是更改代码的原因,例如,Logitech C920 接受请求的模式,根本不发出任何警告。

完整的模块位于“python/main.py”下的捆绑包中。

打印客户端子系统(`python/printer.py`)

打印客户端是一个瘦 HTTP 包装器,其中根本没有特定于打印机的代码,有关驱动程序和纸张的所有内容都位于主机上的打印服务器中。它的全部工作是在打印机未准备好时快速失败,并在网络速度缓慢时保持耐心。

重要的模式是飞行前检查,这可以阻止展位将照片上传到离线的打印机:

def print_photo(image: bytes, request_id: str | None = None) -> dict:
    # 1) Cheap pre-flight: bail out early if the printer is known to be
    #    offline/disabled. Saves the user a 30s+ CUPS timeout.
    status = printer_status()
    if not status.get("available", False):
        raise PrintServerUnavailable(
            f"Printer unavailable: {status.get('status', 'unknown')}"
        )

    # 2) Upload the composed JPEG, then 3) submit the print job.
    upload = _safe_json(_post_with_retry(
        f"{PRINT_SERVER_URL}/api/upload", ..., _UPLOAD_RETRIES
    ), "upload")
    result = _safe_json(_post_with_retry(
        f"{PRINT_SERVER_URL}/api/print",
        lambda: {"json": {"filename": upload["filename"], "request_id": request_id}},
        _PRINT_RETRIES,
    ), "print")
    return result

超时和重试策略在模块顶部设置一次,并且是唯一值得调整的值:

PRINT_SERVER_URL = os.environ.get("PRINT_SERVER_URL", "http://172.17.0.1:5003")
_UPLOAD_TIMEOUT = (5.0, 30.0)   # (connect, read) seconds — upload is fast
_PRINT_TIMEOUT = (5.0, 60.0)    # read can be longer; CUPS spool varies per driver
_STATUS_TIMEOUT = (3.0, 5.0)
_UPLOAD_RETRIES = 2             # one original attempt + up to 2 upload retries
_PRINT_RETRIES = 2              # safe because /api/print is request-ID idempotent
_RETRY_BACKOFF_S = 0.75

这些价值观产生三种行为:

  • 仅重试连接错误和超时:上传和打印提交有单独的重试计数;故意不重试状态检查,因为它会立即失败。为每次上传尝试构建一个新的字节流。
  • 打印重试无法创建重复作业。 重复请求 ID 会从服务器返回其正在进行或已完成的结果,而无需提交另一个 CUPS 作业。
  • 格式良好的success: false是最终的。 服务器已应答;重试只会重复同样的拒绝。
  • 每个响应都会进行防御性解析。 返回 HTML 而不是 JSON 的代理或崩溃的服务器将被报告为干净的错误,而不是“JSONDecodeError”。

完整的模块位于“python/printer.py”下的捆绑包中。

打印服务器

Photo Booth 应用程序在容器内运行,因此无法直接访问 USB 打印机。打印被委托给一个小型 HTTP 服务,该服务与 CUPS 和打印机本身一起在 VENTUNO Q 主机上运行。该服务作为“print-server/”包含在捆绑包中。

这种拆分使应用程序保持可移植性。该摊位对打印机一无所知:它发布了 JPEG 并要求打印它。特定于供应商的所有内容都保留在主机端,可以在不触及 Brick 组成的情况下进行更改。


print-flow.png


合同

该应用程序恰好依赖于三个 JSON 端点:

方法及路径 要求 回复
获取/api/printer_status - {“可用”:真/假,“打印机”:“<队列>”,“状态”:“<人类可读状态>”}
POST /api/上传 带有“文件”部分的“multipart/form-data”(“photo.jpg”、“image/jpeg”) {"success": true, "filename": "<服务器端名称>", "size": <字节>}
POST /api/print {"filename": "<上传返回的名称>", "request_id": "<唯一 ID>"} {"success": true, "job_id": "<cups id>", "request_id": "<same ID>"}

上传和打印失败会返回“{"success": false, "error": "<reason>"}`,应用程序会在信息亭屏幕上显示该信息。连接错误和超时被视为暂时的并重试; HTTP 5xx 响应和格式正确的“success: false”响应是最终的并立即报告。

由于应用程序在容器内运行,因此默认的“http://172.17.0.1:5003”指向 Docker 主机网关,即 VENTUNO Q 主机。设置“PRINT_SERVER_URL”以定位网络上其他位置的打印服务器。

附带的服务还在“http://<board-ip>:5003”处提供测试页,您可以在其中拖入照片并打印它。 在运行展位之前,用它来确认打印机是否正常工作;它将打印机问题与应用程序问题隔离开来。

使用任何打印机

该服务不包含特定于供应商的代码。任何打印机 CUPS 都可以驱动 USB 或网络、专用照片打印机或办公激光打印机,无需修改即可通过三种机制工作:

  • 与供应商无关的检测和准备情况。 队列从“lpstat -v”读取,优先选择具有直接设备 URI(“usb://”、“ipp://”、...)的队列,而不是“implicitclass”代理,然后是系统默认值,然后是其他配置的队列。仅当“lpstat -p”表示队列已启用并且“lpstat -a”表示它接受新作业时,才会报告队列可用。
  • 纸张协商而不是假设。 请求的纸张根据“lpoptions -p <printer> -l”进行验证,并映射到打印机实际报告的最接近的尺寸,同时保留准确的 CUPS 令牌。同一张纸在不同供应商中的命名不同,“4x6”、“明信片”、“L”、“A6”,因此别名和无边框变体也匹配。在提交之前,照片会被裁剪为商定的论文的长宽比; A4 和 Letter 等文档尺寸会跳过裁剪。
  • 热插拔恢复。 如果所选队列消失,则在下一次状态检查时重新运行检测,并针对新检测到的打印机重试一次失败的打印。因此更换 USB 打印机不需要重新启动服务。通过“PRINTER_NAME”固定的打印机永远不会自动替换。

安装打印服务器

CUPS 通常会在插入 USB 打印机时自动配置它。

sudo apt install -y cups python3-venv
sudo usermod -aG lpadmin arduino

cp -r print-server /home/arduino/
sudo cp /home/arduino/print-server/print-server.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now print-server

警告:

python3-venv 是必需的,并且 不 出现在库存图像上。如果没有它,“python3 -m venv”会构建目录树,但永远不会生成“pip”,并且服务无法以“status=203/EXEC”启动。已存在虚拟环境的主板不会显示此信息。

给定该软件包后,该单元将创建自己的虚拟环境并在首次启动时安装所需的要求,因此无需手动准备任何其他内容。确认打印机可见并且服务同意:

lpstat -p
lpstat -a
curl localhost:5003/api/printer_status

健康的响应会命名检测到的队列:

{"available":true,"printer":"Brother_DCP_L3550CDW_series","status":"printer Brother_DCP_L3550CDW_series is idle. enabled since ...; Brother_DCP_L3550CDW_series accepting requests since ..."}

配置

每个设置都是可选的;默认值适合单打印机信息亭。将它们设置为单元的“[Service]”部分中的“Environment=KEY=value”行。

多变的 默认 意义
打印机名称 自动检测 固定特定的 CUPS 队列。保持未设置状态以自动检测并跟踪打印机更改。
纸张大小 4x6 请求的纸张,根据打印机支持的内容协商。
“无边界” 离开 当打印机提供无边框纸张尺寸时,首选纸张尺寸的无边框版本。
端口 5003 监听端口。必须与应用程序中的“PRINT_SERVER_URL”匹配。
KEEP_UPLOADS 200 磁盘上要保留多少张最近的照片;较旧的会自动修剪。
上传目录 打印服务器/上传 保留上传照片的目录。

该服务未设置的选项、介质类型、打印质量、颜色模式均取自 CUPS 默认值,并且可以使用“lpoptions”在系统范围内进行更改:

sudo lpoptions -p <printer> -o MediaType=Auto
sudo lpoptions -p <printer> -o cupsPrintQuality=High

警告:

该服务绑定到所有接口并且没有身份验证,这适用于受信任网络上的信息亭,但不适用于面向互联网的主机。上传的照片未加密存储在“uploads/”中;如果展位拍摄了公众的图像,请根据这些图像应保留的时间设置“KEEP_UPLOADS”。

运行应用程序

在 VENTUNO Q 上组装硬件并安装 Arduino App Lab 后,可以从 Arduino App Lab 界面或板上的外壳部署 Photo Booth。

在 Arduino App Lab 中导入应用程序

最简单的路线根本不需要 shell。在 Arduino 应用实验室中:

  1. 选择左侧边栏中的 我的应用程序 选项卡。
  2. 在右上角,单击“创建新应用程序+”,然后单击“导入应用程序”。
  3. 将“photo-booth-app.zip”拖到对话框中,或使用“从计算机导入”将其选中。

applab-import-app.png


Arduino App Lab 仅接受“.zip”,这就是为什么捆绑包将应用程序打包为“photo-booth-app.zip”而不是松散文件夹的原因。它将应用程序复制到用户应用程序目录中,并将其与内置示例一起列出,从中可以启动、停止和监视它。它的日志可以直接在界面中获取,这通常是确认相机和手势容器已出现的最快方法。

注意:

仅导入“photo-booth-app.zip”,而不是整个包。 print-server/ 文件夹不是 Arduino App Lab 应用程序;它在主机上运行并单独安装,如打印服务器中所述。

从命令行开始

可以从 shell 管理相同的应用程序,这也是 kiosk 自动启动单元稍后使用的。

  1. 将“photo-booth-app.zip”解压到 VENTUNO Q 上的用户应用程序目录中,以便该应用程序位于“~/ArduinoApps/photo-booth”。 CLI 使用的标识符来自此文件夹名称,而不是来自“app.yaml”中的“name:”字段:名为“photo-booth”的文件夹被寻址为“user:photo-booth”。
  2. 从板上的任何 shell 启动应用程序:
   arduino-app-cli app start user:photo-booth

注意:

第一次运行会拉动手势识别容器,可能需要几分钟。随后的运行将在几秒钟内开始。

  1. 在浏览器中打开“http://<board-ip>:7000”。信息亭页面应该出现,准备好使用物理按钮或屏幕上的开始按钮触发。

要跟踪应用程序正在执行的操作,例如确认手势容器已出现并且已检测到相机,请跟踪应用程序日志:

arduino-app-cli app logs user:photo-booth --follow

要停止应用程序(例如在部署另一个也绑定端口 7000 的应用程序之前):

arduino-app-cli app stop user:photo-booth

Arduino App Lab GUI 提供相同的操作,该 GUI 列出了应用程序及其当前状态,并直接公开日志视图。

Kiosk 设置和自动启动配置

要将 Photo Booth 部署为直接启动到全屏模式的专用、无人值守安装,请首先通过 Arduino App Lab 安装应用程序,如上所述,然后最后运行设置脚本:

sudo bash setup-kiosk.sh

使用匹配的拆卸脚本随时撤消它:

sudo bash remove-kiosk.sh

两者都是幂等的,因此运行两次都是无害的。

安装脚本会清除任何现有的守护程序级默认应用程序,因此它无法在启动时与信息亭服务竞争。

首先将显示设置为纵向

展位界面专为纵向屏幕设计,因此在配置展位之前请旋转显示屏。在板上,打开 设置 > 显示,将 方向 设置为 纵向,然后应用。许多商业面板还可以从自己的屏幕菜单进行旋转,这在可用时值得优先选择,因为旋转可以在桌面会话发生任何变化时继续存在。

警告:

在横向屏幕上,应用程序仍然运行,但界面仅限于显示屏中间的狭窄纵向条带,两侧都有宽阔的空白边距。信息亭似乎无法全屏显示,而实际上浏览器是全屏的,并且布局对于面板来说根本就是错误的形状。

脚本配置什么

  1. Chromium,如果不存在“chromium”二进制文件,则可以快速安装。它不是董事会图像的一部分。
  2. 为“arduino”用户自动登录和 Wayland 桌面会话,因此无需任何人输入密码即可存在兼容的桌面会话。如果使用 WaylandEnable=false 显式强制 GDM 为 X11,则脚本会注释该设置并记录它,以便 remove-kiosk.sh 可以在以后恢复它。否则,即使使用“--kiosk”,Chromium 也可以保留其标题栏并使 GNOME 顶部栏可见。
  3. 一个 ​​systemd 单元,在端口 8800 上等待 arduino-app-cli 守护进程套接字,然后启动应用程序。它将“已经运行”视为成功,因此在其他程序首先启动应用程序的情况下启动不会导致设备失败。
  4. 信息亭启动器和 GNOME 自动启动条目。启动器在启动 Chromium 之前会轮询端口“7000”,因此用户端自动启动和系统端单元可以自由竞争,无论哪个先完成,都会等待另一个。
  5. 抑制对话否则会引发无人值守会话(见下文)。
  6. 睡眠、空闲消隐和屏幕锁定被禁用,分两层:‘systemd’睡眠目标被屏蔽和系统范围的‘dconf’默认关闭 GNOME 的空闲行为。任何一个单独都会留下一个间隙:即使 GNOME 被告知不要空闲,“systemd”也可以挂起主板,并且即使在从不挂起的主板上,GNOME 也会使屏幕空白。

警告:

apt install chromium 在此映像上不起作用,因为该软件包在 Ubuntu 存档中没有候选软件包,而 chromium-browser 只是一个过渡软件包,可在同一快照中提取。

该脚本会查找已安装的应用程序本身,而不是假设名称,因为通过 Arduino App Lab 导入会生成带时间戳的标识符,例如“user:photo-booth-20260728-230522”。如果安装了多个,请命名要启动的那个:

APP_ID=user:photo-booth-20260728-230522 sudo -E bash setup-kiosk.sh

两个值得了解的陷阱

自动登录解决了一个问题并创建了另外两个问题,这两个问题都在信息亭顶部放置了一个模式对话框,没有人可以将其关闭。该脚本可以处理这两种情况,但如果板子是手动配置的,那么它们就值得理解。

  • 登录密钥环使用用户的密码进行加密,并且使用自动登录时无需输入密码,因此 pam_gnome_keyring 无法解锁它,并且 GNOME 在每次启动时都会询问。该脚本将密钥环移到一边,以便 PAM 使用空密码重新创建它,对其进行备份而不是删除。 kiosk 浏览器不需要它:Chromium 使用 --password-store=basic 运行。
  • update-notifier 在登录时运行“pkexec”帮助程序,引发自己的 polkit 密码提示。该脚本为信息亭用户禁用它。

关于自动登录的进一步细节:库存映像附带了“/etc/gdm3/custom.conf”,其中存在相关行,但注释掉了,这很容易被误认为是工作配置。该脚本写入活动行并仅匹配未注释的条目,因此第二次运行不会添加重复项。

验证和撤销

重新启动以确认整个链已自动启动:

sudo reboot

remove-kiosk.sh 反转所有 kiosk 拥有的状态:它恢复任何预先存在的单元、启动器、自动启动条目、自动登录配置、更新通知程序覆盖、密钥环、睡眠掩码和 dconf 配置文件状态。如果这些文件之一在安装后发生更改,则拆卸会保留较新的用户管理内容。它只保留已安装和运行的应用程序以及打印服务器。仅当选择加入以下且仅当安装程序记录安装 Chromium 时,才会删除 Chromium:

STOP_APP=1 sudo -E bash remove-kiosk.sh          # also stop the application
REMOVE_CHROMIUM=1 sudo -E bash remove-kiosk.sh   # also remove the Chromium snap

照相亭演示

当应用程序运行、打印服务器启动并插入按钮后,展位就可以接待参观者了。它可以显示的每个屏幕以及在它们之间移动的内容如下:


ux-flow.png


从访问者的角度来看,这是相同的顺序:

  1. 按下按钮开始。 初始屏幕让位于全屏实时预览。
  2. 摆出姿势。 举起七个已识别手势之一,预览周围会出现匹配的主题框架。改变姿势,框架就会随之改变。没有姿势,没有框架。
  3. 再次按下按钮。 标签更改为 PRESS TO STOP,并开始五秒倒计时。再次按下可将其取消。
  4. 保持姿势直到闪光。 在零时,拍摄照片时应用了主题边框,并且按钮重新标记为“按打印”。
  5. 再按一次即可打印。 照片及其覆盖图被合成并发送到打印服务器,然后出现打印屏幕。
  6. 收集照片。 信息亭自行返回启动屏幕,为下一位访客做好准备。

注意:

如果访客在闪光灯前松开手,则不会拍摄照片,展位上会显示“无手势”屏幕。在倒计时期间一直保持姿势是值得给人们的一个指示。

每个等待状态都受计时器限制,因此放弃的会话始终会自行恢复:未打印的照片将在 10 秒后被丢弃。请求打印后,屏幕等待明确的成功或失败确认,然后返回空闲状态;如果 90 秒内没有收到确认,则在重置之前报告打印状态未知。流逝的时间永远不会被呈现为成功的打印。

故障排除和维护

常见问题和补救措施

  • 摆姿势时不会出现主题框:

确保拍摄对象的手上有足够的正面照明。极暗的环境条件或背光会降低关键点检测的置信度。请记住,该框架仅在保持已识别姿势时出现。

  • 倒计时进行但未拍照:

这是预期的行为,而不是错误:前端仅捕获在倒计时结束时仍保持手势的情况,否则显示“无手势”屏幕。指导参观者保持姿势直到闪光。

  • 物理触发按钮不执行任何操作:

确保 Nano ESP32 已使用 HID 固件刷新并连接到 VENTUNO Q 上的 USB 端口。验证按钮是否连接在“D2”和“GND”之间,并且草图在“USB.begin()”之前调用“Keyboard.begin()”,因为颠倒顺序会阻止电路板枚举为键盘。因为相同的 Enter 按键驱动启动、捕获和打印,所以在一种状态下失败的按钮在所有状态下都会失败;使用 USB 键盘进行测试可将固件故障与前端故障隔离开来。

  • 相机流无法加载:

使用“v4l2-ctl --list-devices”或“lsusb”验证 USB 摄像头连接。如有必要,请重新插入相机。如果相机不支持 1280x720 的 MJPG,请相应调整 main.py 中的 Camera(...) 参数。

  • 打印服务器错误(PrintServerUnavailable):

分别检查服务和打印机。 systemctl status print-server 显示服务是否启动; curl localhost:5003/api/printer_status 显示是否可以看到打印机。如果服务正在运行但报告 "available": false,则问题出在 CUPS 端,因此请使用 lpstat -p 确认队列状态,并使用 lpstat -a 确认它是否接受作业。打开“http://<board-ip>:5003”并打印测试照片可将打印机故障与应用程序故障隔离开来。

  • 打印出来的照片被裁剪或尺寸错误:

纸张尺寸是根据打印机报告的尺寸进行协商的,因此意外结果通常意味着未提供所请求的尺寸。 lpoptions -p <printer> -l 列出了支持的 PageSize 值;将服务单元中的“PAPER_SIZE”设置为其中之一。失败的“lp”提交会在错误消息中报告其尝试的页面大小。

  • 打印服务器无法启动,并显示status=203/EXEC:

缺少“python3-venv”,因此该服务的虚拟环境没有“pip”。使用sudo apt install -y python3-venv安装它,使用sudo rm -rf /home/arduino/print-server/.venv删除半构建的环境,然后sudo systemctl restart print-server。

  • 退出全屏信息亭模式进行维护:

按“Ctrl+Alt+F3”(或“F3”–“F6”中的任意一个)打开 TTY 终端,然后按“Ctrl+Alt+F1”返回信息亭会话。该板还始终接受 SSH,与运行信息亭的会话无关。要使主板永久退出 kiosk 模式,请运行“sudo bash remove-kiosk.sh”。

  • 启用自动登录后,信息亭顶部会出现一个对话框:

这是登录密钥环或“更新通知程序”,要求输入无人输入的密码。 setup-kiosk.sh 处理两者;如果板是手动配置的,请重新运行脚本。

  • Chromium 标题栏或 GNOME 顶部栏仍然可见:

桌面可能在 X11 下运行,因为 GDM 具有活动的“WaylandEnable=false”设置。运行 grep -n 'WaylandEnable' /etc/gdm3/custom.conf 进行检查,然后重新运行 sudo bash setup-kiosk.sh 并重新启动。设置脚本启用默认 Wayland 会话,同时保留要恢复的“remove-kiosk.sh”的先前设置。

下载

[https://github.com/91volt/ventuno-q-photo-booth/releases/download/v1.0.0/photo-booth-bundle.zip downloads-banner.png ]

部署此构建所需的一切:

  • 完整包 (photo-booth-bundle.zip):版本v1.0.0,在一个存档中包含以下所有内容,即准备导入的应用程序、打印服务器、自助服务终端脚本和可 3D 打印支架。
  • 源存储库 (91volt/ventuno-q-photo-booth):GPL-3.0 许可证下的可浏览应用程序、打印服务器、信息亭和硬件源。
  • 校验和 (SHA256SUMS):两个版本存档的 SHA-256 哈希值。

单个文件(也包含在上面的捆绑包中):

  • 应用程序 (photo-booth-app.zip):Bricks 应用程序,准备按原样导入到 Arduino 应用实验室。包含 app.yaml、python/main.py、python/printer.py、前端资源和 Nano ESP32 HID 固件草图。
  • 打印服务器(print-server/,在捆绑包内):将照片传递给 CUPS 的主机端服务,具有自己的自述文件、systemd 单元和测试页。
  • 3D 模型 ([assets/ventuno-q-holder.stl ventuno-q-holder.stl]):用于将 VENTUNO Q 安装在自助服务终端架上的 3D 打印支架。
  • Kiosk 设置脚本 ([assets/setup-kiosk.sh setup-kiosk.sh]):如果需要,安装 Chromium,启用自动登录,连接 systemd 单元、启动器和 GNOME 自动启动条目,使无人值守会话将显示的对话框静音,并禁用睡眠和消隐。使用“sudo”运行一次。
  • Kiosk 拆卸脚本 ([assets/remove-kiosk.sh remove-kiosk.sh]):反转安装脚本所做的一切,将应用程序、打印服务器和 Chromium 保留在原位,除非另有说明。
  • Kiosk 启动器 ([assets/launch-booth-kiosk.sh launch-booth-kiosk.sh]):在端口 7000 上等待后端,然后以 kiosk 模式启动 Chromium。已由 setup-kiosk.sh 嵌入;独立提供用于手动安装。

结论

在本应用说明中,我们在 VENTUNO Q 上构建了一个边缘 AI Photo Booth,它使用设备上的手势识别来挑选主题框架,该框架由“arduino:gesture_recognition”和“arduino:web_ui”积木组成,并与充当 USB HID 触发器的 Nano ESP32 和 3D 打印支架安装座配对。

该构建演示了实时计算机视觉模型如何完全在设备上运行,从而实现富有表现力的免提用户交互,同时将访客图像保留在板上。分离式 HID 按钮和打印架构使安装能够在繁忙的事件中幸存下来:发生故障的打印机或换出的触发器永远不会阻塞摄像机流或接口。

后续步骤

  • 放弃 Nano ESP32 并使用板载 MCU:VENTUNO Q 有自己的微控制器,因此按钮可以直接连接到数字引脚,而不是连接到第二块板。从 Arduino 应用实验室的 Arduino 选项卡上传草图,并通过桥读取引脚,触发器就成为同一应用程序的一部分,展位上少了一块板和一根 USB 电缆。
  • QR 码数字下载:扩展 Web UI 以生成屏幕 QR 码,将用户链接到本地​​ Web 画廊,在那里他们可以下载照片的数字副本。
  • 自定义手势操作:使用“on_gesture()”的“hand”参数绑定其他手势或特定手上的相同手势,以触发多镜头序列或替代颜色滤镜。
  • 车队管理和使用遥测:将匿名事件指标(拍摄和打印的照片数量)发送到 Arduino 云,以便跨多个展位安装进行远程监控。

原文:https://docs.arduino.cc/tutorials/ventuno-q/photo-booth/ 本页为 Arduino 官方教程的中文翻译,供内部查阅。命令与代码保持原文。