Created
April 5, 2026 07:13
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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