Skip to content

Instantly share code, notes, and snippets.

@rm1138
Created April 5, 2026 07:13
Show Gist options
  • Select an option

  • Save rm1138/ef132c3a39f3c1effabf6354e2eca965 to your computer and use it in GitHub Desktop.

Select an option

Save rm1138/ef132c3a39f3c1effabf6354e2eca965 to your computer and use it in GitHub Desktop.
eMeet Pixy (328f:00c0) PTZ and HID control for Linux - reverse-engineered tracking, privacy, gesture, and audio mode commands
#!/usr/bin/env bash
# PTZ and mode control for eMeet Pixy webcam (USB 328f:00c0)
# Uses v4l2-ctl for PTZ and HID for tracking/idle mode
# Usage: cam_ptz.sh <command> [value]
#
# --- How the HID protocol was reverse-engineered ---
#
# The eMeet Pixy exposes two control interfaces:
# 1. UVC (standard) — pan/tilt/zoom/focus via v4l2-ctl
# 2. HID (proprietary) — tracking, audio, gesture, privacy via /dev/hidrawN
#
# The HID protocol was decoded by capturing USB traffic with usbmon + tshark
# while toggling features in EMEET Studio (Windows VM with VirtualBox USB
# passthrough). Captures filtered for device HID interrupt transfers:
# tshark -r capture.pcap \
# -Y "usb.device_address == N && usb.transfer_type == 0x01 && usb.data_len == 32" \
# -T fields -e frame.time_relative -e usb.endpoint_address -e usbhid.data
#
# HID reports are 32 bytes, report ID 0x09. Structure:
# Byte 0: 0x09 (report ID)
# Byte 1: Command group
# Bytes 2+: Subcommand, parameters, value
#
# Decoded commands (OUT = host-to-device on endpoint 0x01):
#
# Tracking mode (group 0x01):
# SET: 09 01 01 00 00 01 00 01 XX XX: 00=off, 01=track, 02=privacy
# ACK: 09 01 01 01
#
# Auto-privacy (group 0x02):
# SET: 09 02 01 00 00 04 00 04 XX XX: timeout in seconds (00=disable)
# ACK: 09 02 01 01
#
# Gesture control (group 0x04, subgroup 0x02):
# SET: 09 04 02 00 00 02 00 02 02 XX XX: 00=off, 01=on
# ACK: 09 04 02 01 00 01 00 01 02
#
# Audio mode (group 0x05):
# SET: 09 05 00 03 00 01 00 01 XX XX: 01=NC, 02=live, 03=original
# QRY: 09 05 00 04
#
# Anti-flicker uses standard UVC control (power_line_frequency) via v4l2-ctl.
#
# --- To decode additional commands ---
#
# 1. VirtualBox VM with Windows 10 LTSC + EMEET Studio is configured in:
# modules/programs/virtualbox.nix (VBox + Extension Pack for USB passthrough)
# modules/core/users.nix (vboxusers group)
# Disable 3D acceleration in VM display settings (EMEET Studio renders transparent otherwise).
#
# 2. Pass the camera to the VM: VBox menu > Devices > USB > EMEET EMEET PIXY
#
# 3. Capture USB traffic with scripts/usb_capture.sh:
# ./scripts/usb_capture.sh capture # guided session, records per-feature pcaps
# ./scripts/usb_capture.sh analyze # extract eMeet HID commands from pcaps
#
# 4. Compare OUT commands (endpoint 0x01) between feature-on and feature-off captures
# to identify which bytes change. The value byte is typically at offset 8 or 9.
set -euo pipefail
DEVICE="/dev/video0"
V4L2_CTL="v4l2-ctl"
STEP_DEG=10
STEP=$((STEP_DEG * 3600))
get() { $V4L2_CTL -d "$DEVICE" --get-ctrl="$1" | cut -d' ' -f2; }
set_ctrl() { $V4L2_CTL -d "$DEVICE" --set-ctrl="$1=$2"; }
clamp() {
local val=$1 min=$2 max=$3
(( val < min )) && val=$min
(( val > max )) && val=$max
echo "$val"
}
find_hidraw() {
for dev in /dev/hidraw*; do
local name
name=$(cat "/sys/class/hidraw/$(basename "$dev")/device/uevent" 2>/dev/null | grep HID_NAME | cut -d= -f2)
if [[ "$name" == *"EMEET"*"PIXY"* ]]; then
echo "$dev"
return 0
fi
done
echo "Error: eMeet Pixy HID device not found" >&2
return 1
}
hid_send() {
local hidraw
hidraw=$(find_hidraw)
# Build 32-byte HID report from hex args
local hex=""
for byte in "$@"; do
hex+="\\x${byte}"
done
# Pad to 32 bytes
local count=$#
for (( i=count; i<32; i++ )); do
hex+="\\x00"
done
printf "$hex" > "$hidraw"
}
set_tracking() {
local mode=$1
hid_send 09 01 01 00 00 01 00 01 "$mode"
sleep 0.2
hid_send 09 01 01 01
}
set_gesture() {
local mode=$1
hid_send 09 04 02 00 00 02 00 02 02 "$mode"
sleep 0.2
hid_send 09 04 02 01 00 01 00 01 02
}
set_audio_mode() {
local mode=$1
hid_send 09 05 00 03 00 01 00 01 "$mode"
sleep 0.2
hid_send 09 05 00 04
}
case "${1:-help}" in
idle)
set_tracking 00
echo "Tracking OFF (idle)"
;;
track)
set_tracking 01
echo "Tracking ON"
;;
privacy)
set_tracking 02
echo "Privacy mode ON"
;;
gesture-on)
set_gesture 01
echo "Gesture control ON"
;;
gesture-off)
set_gesture 00
echo "Gesture control OFF"
;;
audio)
case "${2:?Usage: cam_ptz.sh audio nc|live|org}" in
nc) set_audio_mode 01; echo "Audio: NC mode" ;;
live) set_audio_mode 02; echo "Audio: Live mode" ;;
org) set_audio_mode 03; echo "Audio: Original mode" ;;
*) echo "Usage: cam_ptz.sh audio nc|live|org"; exit 1 ;;
esac
;;
flicker)
case "${2:?Usage: cam_ptz.sh flicker off|50|60}" in
off) set_ctrl power_line_frequency 0; echo "Anti-flicker: disabled" ;;
50) set_ctrl power_line_frequency 1; echo "Anti-flicker: 50Hz" ;;
60) set_ctrl power_line_frequency 2; echo "Anti-flicker: 60Hz" ;;
*) echo "Usage: cam_ptz.sh flicker off|50|60"; exit 1 ;;
esac
;;
auto-privacy)
timeout=${2:-0a}
# Convert decimal to hex if numeric
if [[ "$timeout" =~ ^[0-9]+$ ]]; then
timeout=$(printf '%02x' "$timeout")
fi
hid_send 09 02 01 00 00 04 00 04 "$timeout"
sleep 0.2
hid_send 09 02 01 01
if [ "$timeout" = "00" ]; then
echo "Auto-privacy OFF"
else
echo "Auto-privacy ON (timeout: ${2:-10}s)"
fi
;;
left)
cur=$(get pan_absolute)
set_ctrl pan_absolute "$(clamp $((cur - STEP)) -540000 540000)"
;;
right)
cur=$(get pan_absolute)
set_ctrl pan_absolute "$(clamp $((cur + STEP)) -540000 540000)"
;;
up)
cur=$(get tilt_absolute)
set_ctrl tilt_absolute "$(clamp $((cur + STEP)) -324000 324000)"
;;
down)
cur=$(get tilt_absolute)
set_ctrl tilt_absolute "$(clamp $((cur - STEP)) -324000 324000)"
;;
zoom-in)
cur=$(get zoom_absolute)
set_ctrl zoom_absolute "$(clamp $((cur + 10)) 100 150)"
;;
zoom-out)
cur=$(get zoom_absolute)
set_ctrl zoom_absolute "$(clamp $((cur - 10)) 100 150)"
;;
center)
set_ctrl pan_absolute 0
set_ctrl tilt_absolute 0
set_ctrl zoom_absolute 100
;;
pan)
deg=${2:?Usage: cam_ptz.sh pan <degrees>}
set_ctrl pan_absolute "$(clamp $((deg * 3600)) -540000 540000)"
;;
tilt)
deg=${2:?Usage: cam_ptz.sh tilt <degrees>}
set_ctrl tilt_absolute "$(clamp $((deg * 3600)) -324000 324000)"
;;
zoom)
lvl=${2:?Usage: cam_ptz.sh zoom <100-150>}
set_ctrl zoom_absolute "$(clamp "$lvl" 100 150)"
;;
status)
echo "pan: $(($(get pan_absolute) / 3600)) deg"
echo "tilt: $(($(get tilt_absolute) / 3600)) deg"
echo "zoom: $(get zoom_absolute)"
;;
help|*)
cat <<'EOF'
cam_ptz.sh - eMeet Pixy PTZ control
Movement (10 deg steps):
left, right, up, down
Zoom (10-unit steps):
zoom-in, zoom-out
Absolute:
pan <degrees> -150 to 150
tilt <degrees> -90 to 90
zoom <level> 100 to 150
Tracking:
idle Disable auto-tracking
track Enable auto-tracking
privacy Enable privacy mode
gesture-on Enable gesture control
gesture-off Disable gesture control
audio nc|live|org Set audio mode (noise cancel / live / original)
auto-privacy <sec> Enable auto-privacy with timeout (0 to disable)
flicker off|50|60 Anti-flicker frequency
Other:
center Reset to home position
status Show current position
EOF
;;
esac
@Romonaga

Copy link
Copy Markdown

Thank you very much for the work you have done here. I was able to make use of it and add some of the missing functionality. If you are interested in my follow-up work to yours, you can find it here. It is still a work in progress. https://github.com/Romonaga/PixyPilot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment