#!/bin/bash # capture-upload.sh - Capture from USB webcam and upload to PicUpper # For Raspberry Pi 3 with Logitech USB webcam set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CONFIG_FILE="${SCRIPT_DIR}/picupper.conf" # ============================================================================== # Load Configuration # ============================================================================== if [[ ! -f "$CONFIG_FILE" ]]; then echo "ERROR: Configuration file not found: $CONFIG_FILE" echo "Please copy picupper.conf.example to picupper.conf and configure it." exit 1 fi # shellcheck source=/dev/null source "$CONFIG_FILE" # Validate required settings if [[ -z "${API_KEY:-}" || "$API_KEY" == "your-api-key-here" ]]; then echo "ERROR: API_KEY not configured in $CONFIG_FILE" exit 1 fi # Set defaults for optional settings API_URL="${API_URL:-https://dev.seedheads.de/picUploadApi/upload}" CAMERA_ID="${CAMERA_ID:-rpi-webcam}" VIDEO_DEVICE="${VIDEO_DEVICE:-/dev/video0}" RESOLUTION="${RESOLUTION:-1920x1080}" CAPTURE_METHOD="${CAPTURE_METHOD:-fswebcam}" SKIP_FRAMES="${SKIP_FRAMES:-5}" TEMP_DIR="${TEMP_DIR:-/tmp}" LOG_FILE="${LOG_FILE:-${SCRIPT_DIR}/picupper.log}" TIMEOUT="${TIMEOUT:-30}" MAX_RETRIES="${MAX_RETRIES:-3}" RETRY_DELAY="${RETRY_DELAY:-5}" # ============================================================================== # Helper Functions # ============================================================================== log() { local timestamp timestamp=$(date '+%Y-%m-%d %H:%M:%S') local msg="[$timestamp] $*" echo "$msg" echo "$msg" >> "$LOG_FILE" 2>/dev/null || true } log_error() { log "ERROR: $*" >&2 } cleanup() { rm -f "${TEMP_FILE:-}" 2>/dev/null || true } trap cleanup EXIT # ============================================================================== # Test Mode # ============================================================================== if [[ "${1:-}" == "--test" ]]; then echo "=== PicUpper Test Mode ===" echo "Config file: $CONFIG_FILE" echo "API URL: $API_URL" echo "Camera ID: $CAMERA_ID" echo "Video device: $VIDEO_DEVICE" echo "Resolution: $RESOLUTION" echo "Capture Method: $CAPTURE_METHOD" echo "" # Check video device (only if using fswebcam or if device is specified for rpicam) if [[ "$CAPTURE_METHOD" == "fswebcam" ]]; then if [[ ! -e "$VIDEO_DEVICE" ]]; then echo "❌ Video device not found: $VIDEO_DEVICE" echo " Available devices:" ls -la /dev/video* 2>/dev/null || echo " No video devices found" exit 1 fi echo "✓ Video device exists: $VIDEO_DEVICE" # Check fswebcam if ! command -v fswebcam &>/dev/null; then echo "❌ fswebcam not installed. Run: sudo apt install fswebcam" exit 1 fi echo "✓ fswebcam installed" elif [[ "$CAPTURE_METHOD" == "rpicam-still" ]] || [[ "$CAPTURE_METHOD" == "libcamera-still" ]]; then # Check rpicam-still/libcamera-still CMD_NAME="rpicam-still" if ! command -v rpicam-still &>/dev/null; then if command -v libcamera-still &>/dev/null; then CMD_NAME="libcamera-still" else echo "❌ rpicam-still (or libcamera-still) not installed." exit 1 fi fi echo "✓ $CMD_NAME installed" else echo "❌ Unknown CAPTURE_METHOD: $CAPTURE_METHOD" exit 1 fi # Try capture # Get System Info for Overlay SYSTEM_HOSTNAME=$(hostname) SYSTEM_IP=$(hostname -I 2>/dev/null | awk '{print $1}') || SYSTEM_IP="unknown" echo "Capturing test image to: $TEST_FILE" if [[ "$CAPTURE_METHOD" == "rpicam-still" ]] || [[ "$CAPTURE_METHOD" == "libcamera-still" ]]; then # Parse resolution WIDTH=$(echo "$RESOLUTION" | cut -d'x' -f1) HEIGHT=$(echo "$RESOLUTION" | cut -d'x' -f2) CMD="rpicam-still" command -v libcamera-still &>/dev/null && CMD="libcamera-still" if $CMD -o "$TEST_FILE" --width "$WIDTH" --height "$HEIGHT" --nopreview --timeout 2000; then FILE_SIZE=$(stat -f%z "$TEST_FILE" 2>/dev/null || stat -c%s "$TEST_FILE" 2>/dev/null) echo "✓ Capture successful: $TEST_FILE (${FILE_SIZE} bytes)" else echo "❌ Capture failed." exit 1 fi else # fswebcam default if fswebcam -d "$VIDEO_DEVICE" -r "$RESOLUTION" -S "$SKIP_FRAMES" \ --title "$CAMERA_ID" \ --subtitle "$SYSTEM_IP" \ --banner-colour '#AA000000' \ --line-colour '#FF000000' \ --text-colour '#FFFFFF' \ --font "sans:24" \ --timestamp "%Y-%m-%d %H:%M:%S" \ "$TEST_FILE" 2>/dev/null; then FILE_SIZE=$(stat -f%z "$TEST_FILE" 2>/dev/null || stat -c%s "$TEST_FILE" 2>/dev/null) echo "✓ Capture successful: $TEST_FILE (${FILE_SIZE} bytes)" echo "" echo "To view the image: gpicview $TEST_FILE" else echo "❌ Capture failed. Check webcam connection and permissions." exit 1 fi fi exit 0 fi # ============================================================================== # Main Capture & Upload # ============================================================================== TEMP_FILE="${TEMP_DIR}/picupper_${CAMERA_ID}_$(date +%s).jpg" # Check video device (only for fswebcam) if [[ "$CAPTURE_METHOD" == "fswebcam" ]]; then if [[ ! -e "$VIDEO_DEVICE" ]]; then log_error "Video device not found: $VIDEO_DEVICE" exit 1 fi fi # Discover available v4l2 controls and report them to server discover_and_report_controls() { if ! command -v v4l2-ctl &>/dev/null; then log "v4l2-ctl not installed, skipping control discovery" return fi local AVAILABLE_URL="${API_URL%/upload}/settings/$CAMERA_ID/available" log "Discovering available camera controls..." # Get v4l2-ctl output local v4l2_output v4l2_output=$(v4l2-ctl -d "$VIDEO_DEVICE" -L 2>/dev/null) || true if [[ -z "$v4l2_output" ]]; then log "No v4l2 controls found" return fi local payload if command -v jq &>/dev/null; then # Use jq for proper JSON generation local controls_array="[]" local values_obj="{}" local current_ctrl="" local current_type="" local current_min="" local current_max="" local current_step="" local current_default="" local current_value="" local menu_opts="{}" while IFS= read -r line; do # Control line pattern: name 0xhex (type) : params if [[ "$line" =~ ^[[:space:]]*([a-z_][a-z0-9_]*)[[:space:]]+0x[0-9a-f]+[[:space:]]+\(([a-z]+)\)[[:space:]]*:[[:space:]]*(.*) ]]; then # Save previous control if [[ -n "$current_ctrl" ]]; then local ctrl_obj ctrl_obj=$(jq -n \ --arg name "$current_ctrl" \ --arg type "$current_type" \ --argjson min "${current_min:-null}" \ --argjson max "${current_max:-null}" \ --argjson step "${current_step:-null}" \ --argjson default "${current_default:-null}" \ --argjson opts "$menu_opts" \ '{name: $name, type: $type} + (if $min != null then {min: $min} else {} end) + (if $max != null then {max: $max} else {} end) + (if $step != null then {step: $step} else {} end) + (if $default != null then {default: $default} else {} end) + (if ($opts | length) > 0 then {options: $opts} else {} end)') controls_array=$(echo "$controls_array" | jq --argjson ctrl "$ctrl_obj" '. + [$ctrl]') if [[ -n "$current_value" ]]; then values_obj=$(echo "$values_obj" | jq --arg k "$current_ctrl" --argjson v "$current_value" '. + {($k): $v}') fi fi current_ctrl="${BASH_REMATCH[1]}" current_type="${BASH_REMATCH[2]}" local params="${BASH_REMATCH[3]}" menu_opts="{}" current_min="" current_max="" current_step="" current_default="" current_value="" [[ "$params" =~ min=(-?[0-9]+) ]] && current_min="${BASH_REMATCH[1]}" [[ "$params" =~ max=(-?[0-9]+) ]] && current_max="${BASH_REMATCH[1]}" [[ "$params" =~ step=([0-9]+) ]] && current_step="${BASH_REMATCH[1]}" [[ "$params" =~ default=(-?[0-9]+) ]] && current_default="${BASH_REMATCH[1]}" [[ "$params" =~ value=(-?[0-9]+) ]] && current_value="${BASH_REMATCH[1]}" # Menu option line elif [[ "$current_type" == "menu" && "$line" =~ ^[[:space:]]+([0-9]+):[[:space:]]+(.+)$ ]]; then local opt_key="${BASH_REMATCH[1]}" local opt_val="${BASH_REMATCH[2]}" menu_opts=$(echo "$menu_opts" | jq --arg k "$opt_key" --arg v "$opt_val" '. + {($k): $v}') fi done <<< "$v4l2_output" # Save last control if [[ -n "$current_ctrl" ]]; then local ctrl_obj ctrl_obj=$(jq -n \ --arg name "$current_ctrl" \ --arg type "$current_type" \ --argjson min "${current_min:-null}" \ --argjson max "${current_max:-null}" \ --argjson step "${current_step:-null}" \ --argjson default "${current_default:-null}" \ --argjson opts "$menu_opts" \ '{name: $name, type: $type} + (if $min != null then {min: $min} else {} end) + (if $max != null then {max: $max} else {} end) + (if $step != null then {step: $step} else {} end) + (if $default != null then {default: $default} else {} end) + (if ($opts | length) > 0 then {options: $opts} else {} end)') controls_array=$(echo "$controls_array" | jq --argjson ctrl "$ctrl_obj" '. + [$ctrl]') if [[ -n "$current_value" ]]; then values_obj=$(echo "$values_obj" | jq --arg k "$current_ctrl" --argjson v "$current_value" '. + {($k): $v}') fi fi payload=$(jq -n --argjson controls "$controls_array" --argjson values "$values_obj" \ '{controls: $controls, currentValues: $values}') else # Fallback: simple approach without jq (no menu options, basic controls only) log "jq not available, using simple control discovery" local controls="[" local values="{" local first_ctrl=true local first_val=true while IFS= read -r line; do if [[ "$line" =~ ^[[:space:]]*([a-z_][a-z0-9_]*)[[:space:]]+0x[0-9a-f]+[[:space:]]+\(([a-z]+)\)[[:space:]]*:.*value=(-?[0-9]+) ]]; then local name="${BASH_REMATCH[1]}" local type="${BASH_REMATCH[2]}" local value="${BASH_REMATCH[3]}" [[ "$first_ctrl" == "true" ]] && first_ctrl=false || controls+="," controls+="{\"name\":\"$name\",\"type\":\"$type\"}" [[ "$first_val" == "true" ]] && first_val=false || values+="," values+="\"$name\":$value" fi done <<< "$v4l2_output" controls+="]" values+="}" payload="{\"controls\":$controls,\"currentValues\":$values}" fi # POST to server local response response=$(curl -s --max-time 10 \ -X POST \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d "$payload" \ "$AVAILABLE_URL" 2>/dev/null) || true if echo "$response" | grep -q '"success"'; then local count if command -v jq &>/dev/null; then count=$(echo "$response" | jq -r '.controlsRegistered // "?"') else count=$(echo "$response" | grep -o '"controlsRegistered":[0-9]*' | cut -d: -f2) fi log "Reported $count available controls to server" else log "Failed to report available controls: $response" fi } # Fetch and apply camera settings from server apply_camera_settings() { if ! command -v v4l2-ctl &>/dev/null; then log "v4l2-ctl not installed, skipping camera settings" return fi local SETTINGS_URL="${API_URL%/upload}/settings/$CAMERA_ID" log "Fetching camera settings from server..." local SETTINGS_RESPONSE SETTINGS_RESPONSE=$(curl -s --max-time 10 \ -H "X-API-Key: $API_KEY" \ "$SETTINGS_URL" 2>/dev/null) || true if [[ -z "$SETTINGS_RESPONSE" ]]; then log "Could not fetch settings, discovering and reporting controls..." discover_and_report_controls return fi # Check if server has available controls registered local has_controls=false if command -v jq &>/dev/null; then local ctrl_count ctrl_count=$(echo "$SETTINGS_RESPONSE" | jq -r '.availableControls | length // 0') if [[ "$ctrl_count" -gt 0 ]]; then has_controls=true fi else if echo "$SETTINGS_RESPONSE" | grep -q '"availableControls":\[{'; then has_controls=true fi fi if [[ "$has_controls" == "false" ]]; then log "No controls registered, discovering and reporting..." discover_and_report_controls return fi # Apply all values from server dynamically if command -v jq &>/dev/null; then local values values=$(echo "$SETTINGS_RESPONSE" | jq -r '.values // {}') local keys keys=$(echo "$values" | jq -r 'keys[]' 2>/dev/null) || true local applied=0 for ctrl in $keys; do local value value=$(echo "$values" | jq -r ".\"$ctrl\" // empty") if [[ -n "$value" && "$value" != "null" ]]; then if v4l2-ctl -d "$VIDEO_DEVICE" --set-ctrl="${ctrl}=${value}" 2>/dev/null; then ((applied++)) || true fi fi done log "Applied $applied camera settings from server" else # Fallback: grep-based parsing for values local values_block values_block=$(echo "$SETTINGS_RESPONSE" | grep -o '"values":{[^}]*}' | sed 's/"values"://') if [[ -n "$values_block" ]]; then # Extract key:value pairs while [[ "$values_block" =~ \"([a-z_]+)\":(-?[0-9]+) ]]; do local ctrl="${BASH_REMATCH[1]}" local value="${BASH_REMATCH[2]}" v4l2-ctl -d "$VIDEO_DEVICE" --set-ctrl="${ctrl}=${value}" 2>/dev/null || true values_block="${values_block#*${BASH_REMATCH[0]}}" done log "Applied camera settings from server" fi fi } # Only handle settings for fswebcam/v4l2 if [[ "$CAPTURE_METHOD" == "fswebcam" ]]; then apply_camera_settings fi # Capture image log "Capturing using $CAPTURE_METHOD ($RESOLUTION)" # Get System Info for Overlay SYSTEM_HOSTNAME=$(hostname) # Get first non-loopback IP SYSTEM_IP=$(hostname -I 2>/dev/null | awk '{print $1}') || SYSTEM_IP="unknown" if [[ "$CAPTURE_METHOD" == "rpicam-still" ]] || [[ "$CAPTURE_METHOD" == "libcamera-still" ]]; then WIDTH=$(echo "$RESOLUTION" | cut -d'x' -f1) HEIGHT=$(echo "$RESOLUTION" | cut -d'x' -f2) CMD="rpicam-still" command -v libcamera-still &>/dev/null && CMD="libcamera-still" # Note: Text overlay on rpicam-still is not as straightforward as fswebcam (requires --post-process-file or similar), # so we are skipping overlay for now to keep it simple, or we could use imagemagick later if requested. if ! $CMD -o "$TEMP_FILE" --width "$WIDTH" --height "$HEIGHT" --nopreview --timeout 2000 >/dev/null 2>&1; then log_error "Capture failed with $CMD" exit 1 fi else # fswebcam logic if ! fswebcam -d "$VIDEO_DEVICE" -r "$RESOLUTION" -S "$SKIP_FRAMES" \ --title "$CAMERA_ID" \ --subtitle "$SYSTEM_IP" \ --banner-colour '#AA000000' \ --line-colour '#FF000000' \ --text-colour '#FFFFFF' \ --font "sans:24" \ --timestamp "%Y-%m-%d %H:%M:%S" \ "$TEMP_FILE" 2>/dev/null; then log_error "Capture failed" exit 1 fi fi FILE_SIZE=$(stat -c%s "$TEMP_FILE" 2>/dev/null || echo "unknown") log "Captured: $TEMP_FILE ($FILE_SIZE bytes)" # Upload with retry upload_success=false attempt=1 while [[ $attempt -le $MAX_RETRIES ]]; do log "Upload attempt $attempt/$MAX_RETRIES" HTTP_RESPONSE=$(curl -s -w "\n%{http_code}" \ --max-time "$TIMEOUT" \ -X POST \ -H "X-API-Key: $API_KEY" \ -F "image=@$TEMP_FILE" \ -F "cameraId=$CAMERA_ID" \ "$API_URL" 2>/dev/null) || true HTTP_CODE=$(echo "$HTTP_RESPONSE" | tail -n1) RESPONSE_BODY=$(echo "$HTTP_RESPONSE" | sed '$d') if [[ "$HTTP_CODE" == "200" ]]; then log "Upload successful (HTTP $HTTP_CODE)" upload_success=true break else log_error "Upload failed (HTTP $HTTP_CODE): $RESPONSE_BODY" if [[ $attempt -lt $MAX_RETRIES ]]; then log "Retrying in $RETRY_DELAY seconds..." sleep "$RETRY_DELAY" fi fi ((attempt++)) done if [[ "$upload_success" != "true" ]]; then log_error "All upload attempts failed" exit 1 fi log "Done"