490 lines
18 KiB
Bash
Executable File
490 lines
18 KiB
Bash
Executable File
#!/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++))
|
|
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"
|