EmbeddedSkills logo

mpous/ai-skills · arduino-unoq-applab/SKILL.md

arduino-unoq-applab

Comprehensive guide for adapting applications for the Arduino UNO Q, integrating them into the Arduino App Lab ecosystem as apps and bricks, deploying Edge Impulse ML models, and building Flask-based web UIs. Use this skill when users ask about Arduino UNO Q development, App Lab apps/bricks, ML model deployment, or converting existing projects into App Lab-compatible applications.

MCU Cortex-A (AArch64)RTOS Linux embeddedRTOS ZephyrAGENT Claude CodeAGENT Codex

name: arduino-unoq-applab description: Comprehensive guide for adapting applications for the Arduino UNO Q, integrating them into the Arduino App Lab ecosystem as apps and bricks, deploying Edge Impulse ML models, and building Flask-based web UIs. Use this skill when users ask about Arduino UNO Q development, App Lab apps/bricks, ML model deployment, or converting existing projects into App Lab-compatible applications. argument-hint: [topic or task] allowed-tools: Read, Grep, Glob, WebFetch, Write, Edit, Bash

Arduino UNO Q & Arduino App Lab Development Guide

Table of Contents

  1. Arduino UNO Q Hardware
  2. Arduino App Lab Overview
  3. App Structure & Configuration
  4. Bricks System
  5. App Lab CLI Reference
  6. Bridge: MCU-MPU Communication
  7. Deploying Edge Impulse ML Models
  8. Building Flask Web Apps for App Lab
  9. Converting Existing Projects to App Lab Apps
  10. Camera & Video Handling on UNO Q
  11. Storage & Dependency Management
  12. Reference Example: Object Detection with Flask
  13. Quick Reference Commands

Arduino UNO Q Hardware

Overview

The Arduino UNO Q is a hybrid board combining a powerful MPU and MCU in the classic UNO form factor. It is the first product from the Qualcomm-Arduino partnership.

Specifications

Main Processor (MPU) - Qualcomm Dragonwing QRB2210:

  • Quad-core Arm Cortex-A53 at 2.0 GHz
  • Adreno 702 GPU (supports ML inference and 3D graphics)
  • Dual 13MP Image Signal Processors (ISPs) - supports 2 cameras simultaneously (or single 25MP at 30fps)
  • Runs Debian-based Linux
  • AI inference on CPU and GPU

Microcontroller (MCU) - STM32U585:

  • Arm Cortex-M33 up to 160 MHz
  • 2 MB flash
  • 786 KB SRAM
  • Integrated floating-point unit
  • Runs Arduino Code on Zephyr RTOS

SKU Variants:

VariantRAMeMMC StorageSKU
Standard2 GB16 GBABX00162
Extended4 GB32 GBABX00173

Connectivity:

  • Wi-Fi 5 Dual-band 2.4/5 GHz (integrated antenna)
  • Bluetooth 5.1 (onboard antenna)
  • WCBN3536A wireless module
  • USB-C: power (5V 3A / 7-24 VDC barrel), DisplayPort/HDMI, USB hub

I/O:

  • 47 digital I/O pins, 6x 14-bit analog inputs, 2x 12-bit DACs, 6x PWM
  • 3x USART, 2x UART, 4x I2C, 3x SPI, CAN-FD
  • Qwiic connector for Modulino nodes
  • Compatible with classic UNO shields
  • MIPI-CSI camera and MIPI-DSI display connectors
  • Dual RGB LEDs, 8x13 LED matrix

Dimensions: 53.34 mm x 68.58 mm

Usage Modes

  1. Desktop (Tethered): USB-connected to PC running Arduino App Lab
  2. Network: Over local Wi-Fi (requires initial USB setup)
  3. Standalone (SBC): Connect monitor + keyboard + mouse via USB-C hub, runs as single-board computer (recommended with 4GB variant)

Key Filesystem Paths on the Board

/home/arduino/                          # User home directory
/home/arduino/ArduinoApps/              # All App Lab applications
/home/arduino/ArduinoApps/<app-name>/   # Individual app directory
/home/arduino/.arduino-bricks/          # Installed bricks
/home/arduino/.arduino-bricks/ei-models/ # Edge Impulse model storage

Arduino App Lab Overview

Arduino App Lab is the unified IDE preloaded on the UNO Q. It combines Arduino Sketches, Python scripts, and containerized AI models in one interface.

Key Features

  • Unified development for Linux (Python) and MCU (Arduino C++)
  • Pre-loaded AI model bricks: object detection, anomaly detection, image classification, sound recognition, keyword spotting
  • Automatic library dependency installation
  • Web-based IDE accessible via http://<device-ip>:7000 or USB
  • Docker-based brick containers for AI workloads
  • Bridge for MCU-MPU inter-process communication

Platform Compatibility (for App Lab desktop client)

  • Windows 10+ (64-bit)
  • macOS 11+ (64-bit)
  • Ubuntu 22.04+
  • Debian Trixie (64-bit)

Connection Methods

  • USB: Direct connection, first-time setup
  • Wi-Fi: After initial USB setup, access via http://<boardname>.local:7000
  • SSH: ssh arduino@<boardname>.local or ssh arduino@<device-ip>
  • ADB: adb shell for low-level access

App Structure & Configuration

App Directory Layout

/home/arduino/ArduinoApps/<app-name>/
+-- app.yaml          # App metadata, brick dependencies, ports (read-only in IDE)
+-- main.py           # Primary Python entry point (runs on Linux MPU)
+-- sketch/
|   +-- sketch.ino    # MCU code (runs on STM32, optional)
+-- README.md         # Documentation (optional)
+-- models/           # ML model files (optional, for custom apps)
+-- assets/           # Images, videos, static files (optional)
+-- python/           # Additional Python modules (optional)

Apps can be:

  • Python only: main.py runs on the Linux MPU
  • Sketch only: sketch.ino runs on the MCU
  • Both: Python + Sketch communicating via Bridge

Sketch Configuration (sketch/sketch.yaml)

When including an Arduino sketch, you must create a sketch.yaml file to specify the board profile and libraries with version numbers:

# sketch/sketch.yaml
profiles:
  default:
    platforms:
      - platform: arduino:zephyr
    libraries:
      - Arduino_RouterBridge (0.2.2)
      - ArduinoGraphics (1.1.4)
      - dependency: Arduino_RPClite (0.2.0)
      - dependency: ArxContainer (0.7.0)
      - dependency: ArxTypeTraits (0.3.2)
      - dependency: DebugLog (0.8.4)
      - dependency: MsgPack (0.4.2)
default_profile: default

Key points:

  • Library names MUST include version in parentheses: LibraryName (x.y.z)
  • Use dependency: prefix for transitive dependencies
  • No fqbn needed - platform: arduino:zephyr is sufficient
  • Arduino_LED_Matrix is bundled with the Zephyr core (no need to list)
  • Without version numbers, you'll get "Library not found" errors

app.yaml Configuration

# app.yaml: Main configuration file for an Arduino App Lab application

# User-visible name of the application
name: My Application

# Brief description
description: "What this app does"

# Icon (emoji or short string)
icon: "icon-emoji-here"

# Network ports the application exposes
# IMPORTANT: Use port 5001 for Flask web apps (port 7000 is reserved for App Lab IDE)
ports: [5001]

# Bricks used by this application (empty for standalone Flask apps)
bricks: []

# Example with bricks and Edge Impulse model:
# bricks:
# - arduino:video_object_detection: {}
# - arduino:web_ui: {}

App Execution Model

  • Only ONE app can run per board at a time
  • App.run() must appear at the end of main.py when using bricks
  • Code after App.run() will NOT execute
  • Sketch compilation occurs at launch (can take up to a minute)
  • For standalone Flask apps (without bricks), just run python3 main.py

Bricks System

What Are Bricks?

Bricks are reusable, modular software components that launch as separate processes alongside apps. They can be Python modules or Docker containers providing specialized functionality.

Key Characteristics

  • Launch in parallel with the app
  • Can be AI models (Docker containers), web servers, API connectors
  • Multiple bricks can run simultaneously
  • Each brick has its own API documentation in the IDE
  • Imported as Python modules in main.py

IMPORTANT: Always prefer using a brick when one exists for the task. Bricks are the standard App Lab pattern and provide tested, containerized functionality. Only fall back to direct SDK usage (e.g., edge_impulse_linux.image.ImageImpulseRunner) when no suitable brick is available. If no brick exists for a common capability, consider whether it makes sense to build a new brick — this packages the functionality as a reusable component that other developers can benefit from in the App Lab ecosystem.

Available Brick Categories

CategoryDescription
AI - AudioSound classification, keyword spotting
AI - Computer VisionObject detection, image classification, anomaly detection
AI - Sensor dataMotion detection, sensor analysis
APIREST API handlers, external data sources
IoTIoT connectivity and protocols
StorageData persistence
Web User InterfaceWeb UI hosting via WebSockets at port 7000

Pre-loaded AI Bricks

BrickImportUse Case
video_object_detectionfrom arduino.app_bricks.video_objectdetection import VideoObjectDetectionDetect objects using camera (YoloX Nano default)
ei_keyword_spottingfrom arduino.app_bricks.keyword_spotting import KeywordSpottingVoice command recognition
ei_image_classificationfrom arduino.app_bricks.image_classification import ImageClassificationClassify images
ei_anomaly_detectionfrom arduino.app_bricks.anomaly_detection import AnomalyDetectionVisual anomaly detection
ei_sound_classificationfrom arduino.app_bricks.sound_classification import SoundClassificationAudio classification
motion_detectionfrom arduino.app_bricks.motion_detection import MotionDetectionSensor-based motion detection
web_uifrom arduino.app_bricks.web_ui import WebUIWeb UI hosting (port 7000)

Using Bricks in Code

# main.py
from arduino.app_bricks.web_ui import WebUI
from arduino.app_bricks.video_objectdetection import VideoObjectDetection

# Initialize bricks
web = WebUI()
detector = VideoObjectDetection(confidence=0.7)

# Register detection callback
def handle_detections(detections):
    # detections is a dict: {"label": confidence, ...}
    # e.g. {"person": 0.87, "bicycle": 0.66}
    best_label = max(detections, key=detections.get)
    print(f"Detected: {best_label} ({detections[best_label]:.0%})")

detector.on_detect_all(handle_detections)

# MUST be last line - activates all bricks
App.run()

VideoObjectDetection Brick API

MethodDescription
VideoObjectDetection(confidence=0.3, debounce_sec=2.0)Constructor. confidence sets the detection threshold, debounce_sec sets minimum seconds between repeated detections of the same object
on_detect(label, callback)Register a callback for a specific object class. Callback takes no arguments
on_detect_all(callback)Register a callback for all detections. Callback receives a dict mapping labels to confidence scores
override_threshold(value)Change the confidence threshold at runtime
start() / stop()Control the detection lifecycle

The brick manages the USB camera and runs inference in a Docker container. When using a custom Edge Impulse model, set EI_OBJ_DETECTION_MODEL in app.yaml bricks variables.

Edge Impulse Model Variables

When using AI bricks with custom Edge Impulse models, set these in app.yaml:

VariableUse Case
EI_OBJ_DETECTION_MODELObject detection
EI_CLASSIFICATION_MODELGeneral classification
EI_IMAGE_CLASSIFICATION_MODELImage classification
EI_KEYWORD_SPOTTING_MODELKeyword/voice spotting
EI_AUDIO_CLASSIFICATION_MODELAudio classification
EI_MOTION_DETECTION_MODELMotion/sensor detection
EI_V_ANOMALY_DETECTION_MODELVisual anomaly detection

web_ui Brick Details

  • Serves HTML/CSS/JS from an assets folder
  • Communicates with Python via WebSockets
  • Available at http://localhost:7000
  • Use this brick when you want a simple UI tightly integrated with App Lab

App Lab CLI Reference

The arduino-app-cli is pre-installed on the Arduino UNO Q for managing apps without the desktop IDE.

App Management

# Create a new app
arduino-app-cli app new "my-app"

# Start an app (full path)
arduino-app-cli app start "/home/arduino/ArduinoApps/my-app"

# Start an app (shortcut for user apps)
arduino-app-cli app start user:my-app

# Start an example app
arduino-app-cli app start examples:blink

# Stop a running app
arduino-app-cli app stop "/home/arduino/ArduinoApps/my-app"

# List all apps and their status
arduino-app-cli app list

# View app logs
arduino-app-cli app logs /home/arduino/ArduinoApps/my-app --all

Brick Management

# List installed bricks
arduino-app-cli brick list

# View brick details
arduino-app-cli brick details arduino:<brick-name>

System Commands

# Check for system updates
arduino-app-cli system update

# Rename the board
arduino-app-cli system set-name "my-board"

# Enable/disable network access
arduino-app-cli system network enable
arduino-app-cli system network disable

# Clean up unused Docker containers/images (frees storage)
arduino-app-cli system cleanup

Bridge: MCU-MPU Communication

The Bridge enables bidirectional communication between the MCU (sketch.ino) and MPU (main.py) using MessagePack RPC over UART (Serial1 at 115200 baud).

Architecture

  • Router: Hosted on the MPU as the Arduino-Router service
  • Topology: Star network with the Router as the central hub
  • Protocol: MessagePack RPC with frame format [type, id, method, parameters]

Arduino Side (MCU) - Arduino_RouterBridge Library

#include <Arduino_RouterBridge.h>

void setup() {
    Bridge.begin();  // Initialize Bridge communication

    // Expose functions for Python to call
    Bridge.provide("my_function", myHandler);

    // Use provide_safe() for GPIO operations (thread-safe)
    Bridge.provide_safe("set_led", setLedHandler);
}

// Handler function - receives data from Python
void myHandler(String data) {
    Serial.println("Received: " + data);
}

void setLedHandler(bool state) {
    digitalWrite(LED_BUILTIN, state ? HIGH : LOW);
}

void loop() {
    // Call Python function (synchronous, waits for response)
    int result = 0;
    Bridge.call("python_function", 42).result(result);

    // Notify Python (fire-and-forget, no response)
    Bridge.notify("sensor_data", analogRead(A0));

    delay(100);
}

Python Side (MPU)

from arduino.bridge import Bridge

bridge = Bridge()

# Expose function for MCU to call
@bridge.on_call("python_function")
def handle_call(value):
    return value * 2  # Return value sent back to MCU

# Handle notifications from MCU (no response needed)
@bridge.on_notify("sensor_data")
def handle_sensor(value):
    print(f"Sensor: {value}")

# Call MCU function
bridge.call("my_function", "Hello from Python")

# Notify MCU (fire-and-forget)
bridge.notify("set_led", True)

App.run()  # Required when using bricks

Key Methods

MethodDirectionDescription
provide(name, handler)MCUExpose function for Python to call
provide_safe(name, handler)MCUThread-safe version for GPIO operations
call(name, args...)BothSynchronous call, waits for response
notify(name, args...)BothFire-and-forget, no response

Best Practices

  • Use provide_safe() for handlers that use digitalWrite(), analogRead(), etc.
  • Use notify() for telemetry/status updates (faster than call())
  • Use call() when you need a return value
  • Check call().result(var) return value - returns false on failure

LED Matrix Control (8x13 Built-in Display)

The Arduino UNO Q has a built-in 8x13 LED matrix (monochrome) controlled via the Arduino_LED_Matrix library.

Required Libraries

#include <ArduinoGraphics.h>      // For text rendering
#include <Arduino_LED_Matrix.h>   // For matrix control

Basic Setup

ArduinoLEDMatrix matrix;

void setup() {
    matrix.begin();
}

Core Methods

MethodDescription
matrix.begin()Initialize the LED matrix
matrix.beginDraw() / matrix.endDraw()Frame drawing operations
matrix.clear()Clear all pixels
matrix.set(x, y, r, g, b)Set pixel (any non-zero = on)
matrix.stroke(color)Set drawing color

Text Display Methods (with ArduinoGraphics)

MethodDescription
matrix.textFont(Font_4x6)Set font (Font_4x6, Font_5x7)
matrix.textScrollSpeed(ms)Set scroll speed in milliseconds
matrix.beginText(x, y, color)Start text at position
matrix.println(text)Print text
matrix.endText(SCROLL_LEFT)End text with scroll direction

Example: Scrolling Text Display

#include <ArduinoGraphics.h>
#include <Arduino_LED_Matrix.h>

ArduinoLEDMatrix matrix;

void displayText(String text) {
    matrix.beginDraw();
    matrix.stroke(0xFFFFFFFF);           // White color
    matrix.textScrollSpeed(80);           // 80ms between scroll steps
    matrix.textFont(Font_4x6);            // Small font fits 8px height
    matrix.beginText(0, 1, 0xFFFFFF);     // Start at (0,1)
    matrix.println(text);
    matrix.endText(SCROLL_LEFT);          // Scroll left
    matrix.endDraw();
}

void setup() {
    matrix.begin();
    displayText("Hello!");
}

void loop() {
    delay(100);
}

Scroll Directions

  • SCROLL_LEFT - Text scrolls from right to left
  • SCROLL_RIGHT - Text scrolls from left to right

Complete Example: Python to LED Matrix via Bridge

This example shows how to send OCR results (or any text) from Python to the MCU's LED matrix.

Arduino Sketch (sketch/sketch.ino)

#include <Arduino_RouterBridge.h>
#include <ArduinoGraphics.h>
#include <Arduino_LED_Matrix.h>

ArduinoLEDMatrix matrix;
String currentText = "";
bool newTextAvailable = false;

void displayText(String text) {
    if (text.length() > 0 && text != currentText) {
        currentText = text;
        newTextAvailable = true;
    }
}

void setup() {
    Serial.begin(115200);
    matrix.begin();
    Bridge.begin();
    Bridge.provide("display_text", displayText);

    // Startup message
    matrix.beginDraw();
    matrix.stroke(0xFFFFFFFF);
    matrix.textFont(Font_4x6);
    matrix.beginText(0, 1, 0xFFFFFF);
    matrix.println("Ready");
    matrix.endText(SCROLL_LEFT);
    matrix.endDraw();
}

void loop() {
    if (newTextAvailable) {
        matrix.beginDraw();
        matrix.stroke(0xFFFFFFFF);
        matrix.textScrollSpeed(80);
        matrix.textFont(Font_4x6);
        matrix.beginText(0, 1, 0xFFFFFF);
        matrix.println(currentText);
        matrix.endText(SCROLL_LEFT);
        matrix.endDraw();
        newTextAvailable = false;
    }
    delay(50);
}

Python Code (main.py or web_inference.py)

# Bridge for MCU communication (Arduino UNO Q only)
_bridge = None
_last_text = ""

try:
    from arduino.bridge import Bridge
    _bridge = Bridge()
except ImportError:
    pass  # Not on UNO Q

def send_to_led_matrix(text: str) -> None:
    """Send text to the MCU's LED matrix display."""
    global _last_text
    if _bridge and text and text != _last_text:
        try:
            _bridge.notify("display_text", text)
            _last_text = text
        except Exception as e:
            print(f"Bridge error: {e}")

# Usage in your inference loop:
# send_to_led_matrix("Recognized text here")

Deploying Edge Impulse ML Models

Step 1: Export from Edge Impulse Studio

  1. Train your impulse in Edge Impulse Studio
  2. Go to Deployment
  3. Select the target hardware as Arduino UNO Q
  4. Export as .eim file for:
    • "Arduino UNO Q" (CPU inference)
    • "Linux aarch64" (CPU inference)
    • "Linux Arduino UNO Q (GPU)" (GPU-accelerated inference via Adreno 702)

Step 2: Transfer Model to Device

# Deploy the model via Arduino AppLab as defined here. Follow the instructions
https://docs.edgeimpulse.com/hardware/deployments/run-arduino-app-lab#download-and-select-the-new-custom-model

# This is the old way to move the model via SCP. 
scp your-model.eim arduino@<device-ip>:/home/arduino/.arduino-bricks/ei-models/

Step 3: Make Model Executable

# CRITICAL: In case you copied the .eim files must have execute permission
chmod +x /home/arduino/.arduino-bricks/ei-models/*.eim
# Or for app-local models:
chmod +x /home/arduino/ArduinoApps/my-app/models/*.eim

Step 4: Run

# With App Lab CLI
arduino-app-cli app start user:my-app

# Or directly
cd /home/arduino/ArduinoApps/my-app
python3 main.py
# OR with venv:
source .venv/bin/activate && python3 main.py

Using Edge Impulse SDK Directly (Without Bricks)

For custom Flask apps that bypass the brick system:

from edge_impulse_linux.image import ImageImpulseRunner

runner = ImageImpulseRunner('./models/model.eim')
model_info = runner.init()

# Classify a frame (numpy array, RGB, flattened)
features = frame_rgb.flatten().tolist()
result = runner.classify(features)

# result['result'] contains classification/detection output
runner.stop()

Building Flask Web Apps for App Lab

Architecture Pattern

For custom web UIs (instead of the web_ui brick), use Flask on port 5001:

app-name/
+-- app.yaml              # ports: [5001], bricks: []
+-- python/
|   +-- main.py            # Flask app entry point
|   +-- requirements.txt   # Python dependencies
|   +-- templates/
|   |   +-- index.html     # Web UI
|   |   +-- assets/        # CSS, JS, images
|   +-- utils/
|       +-- mock_dependencies.py  # Dependency mocks (optional)
+-- models/                # .eim model files
+-- assets/                # Images, videos for inference

app.yaml for Flask Apps

name: My Custom Detection App
description: "Real-time detection with Flask web UI"
icon: "icon-emoji-here"
ports: [5001]
bricks: []

Mock Dependencies (Required for edge_impulse_linux)

Create python/utils/mock_dependencies.py to avoid pyaudio/six import issues:

import sys
import builtins

def apply_mocks():
    """Apply mocks for six and pyaudio to avoid dependency issues."""
    class MockSixMovesQueue:
        Queue = None

    class MockSixMoves:
        queue = MockSixMovesQueue()

    class MockSix:
        moves = MockSixMoves()

    sys.modules['six'] = MockSix()
    sys.modules['six.moves'] = MockSixMoves()
    sys.modules['six.moves.queue'] = MockSixMovesQueue()

    class MockPyAudio:
        pass

    sys.modules['pyaudio'] = MockPyAudio()
    builtins.pyaudio = MockPyAudio()

Call apply_mocks() at the top of main.py BEFORE importing edge_impulse_linux.

Requirements.txt for Flask Apps

edge-impulse-linux==1.2.2
opencv-python-headless
flask
numpy
requests

IMPORTANT: Use opencv-python-headless (not opencv-python) on the UNO Q - the headless variant is smaller and doesn't need GUI libraries.

Flask App Template

#!/usr/bin/env python3
"""
Flask web application for Edge Impulse inference on Arduino UNO Q.
"""

# Apply mocks BEFORE importing edge_impulse_linux
from utils.mock_dependencies import apply_mocks
apply_mocks()

import cv2
import numpy as np
from flask import Flask, render_template, Response, jsonify, request
from edge_impulse_linux.image import ImageImpulseRunner

app = Flask(__name__)

# Initialize model
runner = ImageImpulseRunner('./models/model.eim')
model_info = runner.init()

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/video_feed')
def video_feed():
    """MJPEG video stream endpoint."""
    def generate():
        cap = cv2.VideoCapture(2, cv2.CAP_V4L2)  # /dev/video2 for Logitech BRIO RGB
        while True:
            ret, frame = cap.read()
            if not ret:
                break
            _, jpeg = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 80])
            yield (b'--frame\r\nContent-Type: image/jpeg\r\n\r\n' + jpeg.tobytes() + b'\r\n')
    return Response(generate(), mimetype='multipart/x-mixed-replace; boundary=frame')

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5001, threaded=True)

Converting Existing Projects to App Lab Apps

When adapting an existing Python project to run as an Arduino App Lab application, follow these steps:

1. Create App Structure

# On the Arduino UNO Q
arduino-app-cli app new "my-app"
# OR manually:
mkdir -p /home/arduino/ArduinoApps/my-app

2. Create app.yaml

name: My Converted App
description: "Adapted from existing project"
icon: "icon-emoji-here"
ports: [5001]      # If the app has a web UI
bricks: []         # Empty for standalone Flask apps

3. Set Up Python Environment

cd /home/arduino/ArduinoApps/my-app
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

4. Adapt the Code

Key changes needed when converting:

  • Port: Use port 5001 (port 7000 is reserved for App Lab IDE)
  • Host: Bind to 0.0.0.0 (not localhost) so it's accessible over the network
  • Camera: Use V4L2 backend and correct device index (see Camera section below)
  • Models: Ensure .eim files are chmod +x and use linux-aarch64 architecture
  • OpenCV: Use opencv-python-headless instead of opencv-python
  • Dependencies: Add mock_dependencies.py for edge_impulse_linux compatibility
  • Python version: Python 3.10+ syntax (e.g., str | None) is supported on the UNO Q

5. Launch with App Lab CLI

# Start the app
arduino-app-cli app start user:my-app

# Or run directly
cd /home/arduino/ArduinoApps/my-app
source .venv/bin/activate
python3 python/main.py

6. Access the Web UI

Open http://<device-ip>:5001 from any browser on the same network.


Camera & Video Handling on UNO Q

Video Device Mapping

The UNO Q exposes multiple /dev/video* devices. Not all are cameras:

DeviceTypeDescription
/dev/video0EncoderQualcomm Venus video encoder (NOT a camera)
/dev/video1DecoderQualcomm Venus video decoder (NOT a camera)
/dev/video2RGB CameraExternal USB camera main stream (e.g., Logitech BRIO)
/dev/video3MetadataCamera metadata stream
/dev/video4IR CameraInfrared stream (if camera supports it)
/dev/video5MetadataIR metadata stream

Camera Detection Best Practices

  1. Always filter out Venus encoder/decoder devices - they show as video devices but are NOT cameras
  2. Use V4L2 backend on Linux: cv2.VideoCapture(index, cv2.CAP_V4L2)
  3. Check device info with v4l2-ctl --device /dev/videoN --info to identify real cameras
  4. Identify IR cameras - some cameras (like Logitech BRIO) expose both RGB and IR streams
  5. Use /dev/video2 as the typical default for the first USB camera's RGB stream

Camera Detection Code Pattern

import subprocess
import cv2
import glob

def detect_cameras():
    """Detect real cameras, filtering out encoders/decoders."""
    cameras = []
    for device in sorted(glob.glob('/dev/video*')):
        index = int(device.replace('/dev/video', ''))
        try:
            result = subprocess.run(
                ['v4l2-ctl', '--device', device, '--info'],
                capture_output=True, text=True, timeout=2
            )
            output = result.stdout
            # Extract card name
            card_name = None
            for line in output.split('\n'):
                if 'Card type' in line:
                    card_name = line.split(':', 1)[1].strip()

            # Skip encoder/decoder devices
            if card_name and ('venus' in card_name.lower() or
                            'encoder' in card_name.lower() or
                            'decoder' in card_name.lower()):
                continue

            # Must be a Video Capture device
            if 'Video Capture' not in output:
                continue

            # Try to open and read a frame
            cap = cv2.VideoCapture(index, cv2.CAP_V4L2)
            if cap.isOpened():
                ret, frame = cap.read()
                cap.release()
                if ret and frame is not None:
                    cameras.append({'index': index, 'name': card_name or f'Camera {index}'})
        except Exception:
            continue
    return cameras

MJPEG Format for Performance

cap = cv2.VideoCapture(2, cv2.CAP_V4L2)
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'MJPG'))
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)

Storage & Dependency Management

The Arduino UNO Q has limited storage (16GB or 32GB eMMC). The /home/arduino partition is typically 3.6GB.

Freeing Space

# Check disk usage
df -h

# Find large directories
du -sh /home/arduino/* | sort -rh | head -20

# Clean pip cache
pip cache purge

# Clean apt cache
sudo apt clean && sudo apt autoremove -y

# Clean unused Docker images/containers (big space savings)
docker system prune -a
# Or via CLI:
arduino-app-cli system cleanup

# Clean journal logs
sudo journalctl --vacuum-size=50M

If /home Is Full, Use Root Partition

# Create venv on root partition (usually has more space)
sudo mkdir -p /opt/app-venv
sudo chown arduino:arduino /opt/app-venv
python3 -m venv /opt/app-venv
source /opt/app-venv/bin/activate
pip install -r requirements.txt

Dependency Tips

  • Use opencv-python-headless instead of opencv-python (much smaller)
  • Pin edge-impulse-linux==1.2.2 for compatibility
  • Use --no-cache-dir with pip to avoid filling cache: pip install --no-cache-dir -r requirements.txt

Reference Example: Object Detection with Flask

Based on the Edge Impulse Arduino App Lab example:

Project Structure

example-app/
+-- app.yaml
+-- models/
|   +-- model-linux-aarch64.eim
+-- assets/
|   +-- sample-image.jpg
+-- python/
|   +-- main.py
|   +-- requirements.txt
|   +-- templates/
|   |   +-- index.html
|   |   +-- assets/
|   +-- utils/
|       +-- mock_dependencies.py
+-- .python-version          # 3.13.5

app.yaml

name: Custom object detection with Edge Impulse
description: "Real-time object detection using Edge Impulse models and Flask"
icon: "icon-emoji-here"
ports: [5001]
bricks: []

requirements.txt

edge-impulse-linux==1.2.2
opencv-python-headless
flask
numpy
requests

Key Features of the Example

  • Multi-source input: camera, RTSP streams, image files, video files
  • Real-time inference with MJPEG streaming (/video_feed, /inference_feed)
  • Model switching via web UI
  • Frame upload to Edge Impulse Studio with bounding box labels
  • Deployment workflow (build, poll status, download model)
  • Runs on macOS (arm64) and Linux (aarch64)

Running on Arduino UNO Q

# Clone into ArduinoApps
cd /home/arduino/ArduinoApps
git clone https://github.com/edgeimpulse/example-arduino-app-lab-object-detection-using-flask.git my-detection-app
cd my-detection-app

# Make models executable
chmod +x models/*.eim

# Start via CLI
arduino-app-cli app start .

# Or manually with venv
python3 -m venv .venv
source .venv/bin/activate
pip install --no-cache-dir -r python/requirements.txt
python3 python/main.py

Access at http://<device-ip>:5001


Quick Reference Commands

# === Connection ===
ssh arduino@<device-ip>
adb shell

# === App Management ===
arduino-app-cli app new "my-app"
arduino-app-cli app start user:my-app
arduino-app-cli app stop user:my-app
arduino-app-cli app list
arduino-app-cli app logs user:my-app --all

# === Model Deployment ===
scp model.eim arduino@<device-ip>:/home/arduino/ArduinoApps/my-app/models/
ssh arduino@<device-ip> "chmod +x /home/arduino/ArduinoApps/my-app/models/*.eim"

# === Brick Management ===
arduino-app-cli brick list
arduino-app-cli brick details arduino:<brick-name>

# === System ===
arduino-app-cli system update
arduino-app-cli system cleanup
df -h
docker system prune -a

# === Camera ===
v4l2-ctl --list-devices
v4l2-ctl --device /dev/video2 --info

# === Python Environment ===
python3 -m venv .venv
source .venv/bin/activate
pip install --no-cache-dir -r requirements.txt

# === Network ===
hostname -I       # Get device IP

Additional Resources from Community Knowledge Base

Additional Bridge Patterns

Using call() with Return Values (MCU-side handlers)

// MCU: Handler that returns a value
String getValueHandler() {
    return String(analogRead(A0));
}

// Python side:
result = bridge.call("getValueHandler")  # Returns the string value

Async Notifications (Fire-and-Forget)

// MCU: Fire notification (no response needed)
Bridge.notify("buttonPressed", digitalRead(BUTTON_PIN));

// Python side: Handle notifications
@bridge.on_notify("buttonPressed")
def handle_button(value):
    print(f"Button state: {value}")

Multiple Parameters

# Pass multiple parameters as comma-separated string
bridge.call("setPWM", "9,128")  # pin, value

// MCU side: Parse the string
void setPWM(String params) {
    int comma = params.indexOf(',');
    int pin = params.substring(0, comma).toInt();
    int value = params.substring(comma + 1).toInt();
    analogWrite(pin, value);
}

Community Troubleshooting Additions

Common Bridge Communication Issues

IssueCauseSolution
call() returns emptyMCU not initializedAdd 2-3 second delay before first call
call() times outFunction blocks too longKeep handlers non-blocking, use delay(10) in loop
Function not foundName mismatchCheck exact spelling matches between MCU and Python
Serial errorsWrong baud rateVerify 115200 baud on both sides

Non-Blocking Animation Pattern (MCU)

enum AnimationState { IDLE, SCROLLING, ANIMATING };
AnimationState state = IDLE;
String displayText = "";
int scrollOffset = 0;

void loop() {
    switch (state) {
        case SCROLLING:
            // Process ONE step per loop iteration
            scrollOffset++;
            if (scrollOffset > displayText.length() * 6) {
                state = IDLE;
            }
            break;
        case ANIMATING:
            // Animation frame processing
            break;
    }
    Bridge.update();  // Critical: keeps RPC responsive
    delay(50);
}

LED Matrix I2C Troubleshooting

// I2C Scanner - paste in setup() to find device addresses
Wire.begin();
Serial.println("Scanning I2C devices...");
for (byte addr = 1; addr < 127; addr++) {
    Wire.beginTransmission(addr);
    if (Wire.endTransmission() == 0) {
        Serial.print("Found: 0x");
        Serial.println(addr, HEX);
    }
}

Network Troubleshooting

# Check Wi-Fi status
nmcli device wifi list

# Get IP address
hostname -I

# Test connectivity
ping google.com

# View network logs
journalctl -u NetworkManager | tail -20

Deployment via ADB (Alternative to SSH)

# Port forwarding for web UI access
adb forward tcp:7000 tcp:7000

# Push files
adb push local/file /home/arduino/project/

# Shell access
adb shell

# View logs
adb logcat | grep arduino

Resources

Comments (0)

No comments yet. Tried it on real hardware? Tell us here.