#!/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..." # Helper function to escape strings for JSON json_escape() { local str="$1" # Escape backslashes first, then quotes str="${str//\\/\\\\}" str="${str//\"/\\\"}" # Remove any control characters str=$(echo "$str" | tr -d '\n\r\t') echo "$str" } # Parse v4l2-ctl -L output into JSON # Example lines: # brightness 0x00980900 (int) : min=-64 max=64 step=1 default=0 value=0 # exposure_auto 0x009a0901 (menu) : min=0 max=3 default=3 value=3 # 1: Manual Mode # 3: Aperture Priority Mode local controls_json="[" local current_values_json="{" local first_control=true local first_value=true local current_ctrl="" local ctrl_json="" local menu_options="" local in_menu=false while IFS= read -r line || [[ -n "$line" ]]; do # Check if this is a control line (starts with control name) if [[ "$line" =~ ^[[:space:]]*([a-z_][a-z0-9_]*)[[:space:]]+0x[0-9a-f]+[[:space:]]+\(([a-z]+)\)[[:space:]]*:[[:space:]]*(.*) ]]; then # Save previous control if exists if [[ -n "$current_ctrl" ]]; then if [[ "$in_menu" == "true" && -n "$menu_options" ]]; then ctrl_json="${ctrl_json%, }, \"options\": {${menu_options%,}}}" else ctrl_json="${ctrl_json%,}}" fi if [[ "$first_control" == "true" ]]; then first_control=false else controls_json+="," fi controls_json+="$ctrl_json" fi current_ctrl="${BASH_REMATCH[1]}" local ctrl_type="${BASH_REMATCH[2]}" local params="${BASH_REMATCH[3]}" ctrl_json="{\"name\": \"$current_ctrl\", \"type\": \"$ctrl_type\"," menu_options="" in_menu=false # Parse parameters if [[ "$params" =~ min=(-?[0-9]+) ]]; then ctrl_json+=" \"min\": ${BASH_REMATCH[1]}," fi if [[ "$params" =~ max=(-?[0-9]+) ]]; then ctrl_json+=" \"max\": ${BASH_REMATCH[1]}," fi if [[ "$params" =~ step=([0-9]+) ]]; then ctrl_json+=" \"step\": ${BASH_REMATCH[1]}," fi if [[ "$params" =~ default=(-?[0-9]+) ]]; then ctrl_json+=" \"default\": ${BASH_REMATCH[1]}," fi if [[ "$params" =~ value=(-?[0-9]+) ]]; then local current_value="${BASH_REMATCH[1]}" if [[ "$first_value" == "true" ]]; then first_value=false else current_values_json+="," fi current_values_json+="\"$current_ctrl\": $current_value" fi if [[ "$ctrl_type" == "menu" ]]; then in_menu=true fi # Check if this is a menu option line (indented with number: description) elif [[ "$in_menu" == "true" && "$line" =~ ^[[:space:]]+([0-9]+):[[:space:]]+(.+)$ ]]; then local opt_val="${BASH_REMATCH[1]}" local opt_name opt_name=$(json_escape "${BASH_REMATCH[2]}") menu_options+="\"$opt_val\": \"$opt_name\"," fi done < <(v4l2-ctl -d "$VIDEO_DEVICE" -L 2>/dev/null) # Save last control if [[ -n "$current_ctrl" ]]; then if [[ "$in_menu" == "true" && -n "$menu_options" ]]; then ctrl_json="${ctrl_json%, }, \"options\": {${menu_options%,}}}" else ctrl_json="${ctrl_json%,}}" fi if [[ "$first_control" != "true" ]]; then controls_json+="," fi controls_json+="$ctrl_json" fi controls_json+="]" current_values_json+="}" # POST to server local payload="{\"controls\": $controls_json, \"currentValues\": $current_values_json}" 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 count=$(echo "$response" | grep -o '"controlsRegistered":[0-9]*' | cut -d: -f2) 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++)) 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"