From 0a93ee67139a344b790f03d58c914ffe86b3dced Mon Sep 17 00:00:00 2001 From: sebseb7 Date: Thu, 18 Dec 2025 14:55:23 +0100 Subject: [PATCH] feat: Implement server-managed camera settings with new API endpoints, client-side application, and updated documentation. --- README.md | 45 ++++++++++++++++ demo/README.md | 52 ++++++++++++++++++ demo/capture-upload.sh | 119 +++++++++++++++++++++++++++++++++++++++++ server.js | 66 +++++++++++++++++++++++ 4 files changed, 282 insertions(+) diff --git a/README.md b/README.md index 2929edb..c661c40 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,51 @@ curl -H "X-API-Key: your-secret-key" http://localhost:3080/stats/front-door } ``` +### Get Camera Settings + +```bash +GET /settings/:cameraId +``` + +Get v4l2 camera settings (focus, exposure, etc.) for a camera. + +**Example:** +```bash +curl -H "X-API-Key: your-secret-key" http://localhost:3080/settings/front-door +``` + +**Response:** +```json +{ + "cameraId": "front-door", + "settings": { + "focus_automatic_continuous": 0, + "focus_absolute": 30, + "exposure_auto": 1, + "exposure_absolute": 200, + "brightness": 0, + "contrast": 32 + } +} +``` + +### Update Camera Settings + +```bash +PUT /settings/:cameraId +``` + +Update camera settings. Settings are applied by the capture client via v4l2-ctl. + +**Example:** +```bash +curl -X PUT \ + -H "X-API-Key: your-secret-key" \ + -H "Content-Type: application/json" \ + -d '{"focus_absolute": 40, "exposure_absolute": 250}' \ + http://localhost:3080/settings/front-door +``` + ## Storage Structure Images are stored in a hierarchical directory structure optimized for time-lapse processing: diff --git a/demo/README.md b/demo/README.md index 71c4a1f..6c5c203 100644 --- a/demo/README.md +++ b/demo/README.md @@ -76,6 +76,58 @@ VIDEO_DEVICE="/dev/video0" ./capture-upload.sh ``` +--- + +## Camera Settings (Focus & Exposure) + +Camera settings are stored on the PicUpper server and applied before each capture. + +### Set camera settings via API + +```bash +# Set focus and exposure for your camera +curl -X PUT \ + -H "X-API-Key: your-api-key" \ + -H "Content-Type: application/json" \ + -d '{ + "focus_automatic_continuous": 0, + "focus_absolute": 30, + "exposure_auto": 1, + "exposure_absolute": 200, + "brightness": 0, + "contrast": 32 + }' \ + https://dev.seedheads.de/picUploadApi/settings/rpi-webcam-1 +``` + +### Get current settings + +```bash +curl -H "X-API-Key: your-api-key" \ + https://dev.seedheads.de/picUploadApi/settings/rpi-webcam-1 +``` + +### Available settings (for Logitech C922) + +| Setting | Range | Description | +|---------|-------|-------------| +| `focus_automatic_continuous` | 0-1 | 0=manual, 1=auto | +| `focus_absolute` | 0-255 | Lower=closer (manual mode) | +| `exposure_auto` | 1-3 | 1=manual, 3=auto | +| `exposure_absolute` | 3-2047 | Shutter speed (manual mode) | +| `brightness` | -64 to 64 | Image brightness | +| `contrast` | 0-64 | Image contrast | + +### Find your camera's available controls + +```bash +v4l2-ctl -d /dev/video0 --list-ctrls +``` + +**Note:** Install `v4l-utils` on the Pi: `sudo apt install v4l-utils` + +--- + ## Troubleshooting ### Check if webcam is detected diff --git a/demo/capture-upload.sh b/demo/capture-upload.sh index 67a7eed..8fd5fa0 100644 --- a/demo/capture-upload.sh +++ b/demo/capture-upload.sh @@ -118,6 +118,125 @@ if [[ ! -e "$VIDEO_DEVICE" ]]; then exit 1 fi +# Fetch and apply camera settings from server +apply_camera_settings() { + # Check if v4l2-ctl is available + if ! command -v v4l2-ctl &>/dev/null; then + log "v4l2-ctl not installed, skipping camera settings" + return + fi + + # Derive settings URL from upload URL (replace /upload with /settings) + 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, using camera defaults" + return + fi + + # Check if server has custom settings (look for updatedAt field) + local has_custom_settings=false + if command -v jq &>/dev/null; then + local updated_at + updated_at=$(echo "$SETTINGS_RESPONSE" | jq -r '.settings.updatedAt // empty') + if [[ -n "$updated_at" && "$updated_at" != "null" ]]; then + has_custom_settings=true + fi + else + if echo "$SETTINGS_RESPONSE" | grep -q '"updatedAt"'; then + has_custom_settings=true + fi + fi + + if [[ "$has_custom_settings" == "false" ]]; then + # No custom settings on server - upload current camera settings + log "No settings on server, uploading current camera settings..." + upload_current_settings + return + fi + + # Apply settings from server + if command -v jq &>/dev/null; then + local settings + settings=$(echo "$SETTINGS_RESPONSE" | jq -r '.settings // empty') + if [[ -n "$settings" ]]; then + for ctrl in focus_automatic_continuous focus_absolute exposure_auto exposure_absolute brightness contrast; do + local value + value=$(echo "$settings" | jq -r ".$ctrl // empty") + if [[ -n "$value" && "$value" != "null" ]]; then + v4l2-ctl -d "$VIDEO_DEVICE" --set-ctrl="${ctrl}=${value}" 2>/dev/null || true + fi + done + log "Applied camera settings from server" + fi + else + for ctrl in focus_automatic_continuous focus_absolute exposure_auto exposure_absolute brightness contrast; do + local value + value=$(echo "$SETTINGS_RESPONSE" | grep -o "\"${ctrl}\":[0-9-]*" | cut -d: -f2) + if [[ -n "$value" ]]; then + v4l2-ctl -d "$VIDEO_DEVICE" --set-ctrl="${ctrl}=${value}" 2>/dev/null || true + fi + done + log "Applied camera settings from server" + fi +} + +# Read current camera settings and upload to server +upload_current_settings() { + local SETTINGS_URL="${API_URL%/upload}/settings/$CAMERA_ID" + + # Read current values from camera + local focus_auto focus_abs exp_auto exp_abs brightness contrast + focus_auto=$(v4l2-ctl -d "$VIDEO_DEVICE" --get-ctrl=focus_automatic_continuous 2>/dev/null | cut -d: -f2 | tr -d ' ') || focus_auto="" + focus_abs=$(v4l2-ctl -d "$VIDEO_DEVICE" --get-ctrl=focus_absolute 2>/dev/null | cut -d: -f2 | tr -d ' ') || focus_abs="" + exp_auto=$(v4l2-ctl -d "$VIDEO_DEVICE" --get-ctrl=exposure_auto 2>/dev/null | cut -d: -f2 | tr -d ' ') || exp_auto="" + exp_abs=$(v4l2-ctl -d "$VIDEO_DEVICE" --get-ctrl=exposure_absolute 2>/dev/null | cut -d: -f2 | tr -d ' ') || exp_abs="" + brightness=$(v4l2-ctl -d "$VIDEO_DEVICE" --get-ctrl=brightness 2>/dev/null | cut -d: -f2 | tr -d ' ') || brightness="" + contrast=$(v4l2-ctl -d "$VIDEO_DEVICE" --get-ctrl=contrast 2>/dev/null | cut -d: -f2 | tr -d ' ') || contrast="" + + # Build JSON payload + local json_parts=() + [[ -n "$focus_auto" ]] && json_parts+=("\"focus_automatic_continuous\": $focus_auto") + [[ -n "$focus_abs" ]] && json_parts+=("\"focus_absolute\": $focus_abs") + [[ -n "$exp_auto" ]] && json_parts+=("\"exposure_auto\": $exp_auto") + [[ -n "$exp_abs" ]] && json_parts+=("\"exposure_absolute\": $exp_abs") + [[ -n "$brightness" ]] && json_parts+=("\"brightness\": $brightness") + [[ -n "$contrast" ]] && json_parts+=("\"contrast\": $contrast") + + if [[ ${#json_parts[@]} -eq 0 ]]; then + log "Could not read camera settings" + return + fi + + # Join with commas + local IFS=',' + local json_body="{${json_parts[*]}}" + + # Upload to server + local response + response=$(curl -s --max-time 10 \ + -X PUT \ + -H "X-API-Key: $API_KEY" \ + -H "Content-Type: application/json" \ + -d "$json_body" \ + "$SETTINGS_URL" 2>/dev/null) || true + + if echo "$response" | grep -q '"success"'; then + log "Uploaded current camera settings to server" + else + log "Failed to upload camera settings" + fi +} + +apply_camera_settings + + # Capture image log "Capturing from $VIDEO_DEVICE ($RESOLUTION)" if ! fswebcam -d "$VIDEO_DEVICE" -r "$RESOLUTION" -S "$SKIP_FRAMES" --no-banner "$TEMP_FILE" 2>/dev/null; then diff --git a/server.js b/server.js index 796b338..6cd5a59 100644 --- a/server.js +++ b/server.js @@ -194,6 +194,72 @@ app.get('/stats/:cameraId', authenticate, (req, res) => { } }); +// Camera settings storage +const SETTINGS_FILE = process.env.PICUPPER_SETTINGS_FILE || path.join(__dirname, 'camera-settings.json'); + +function loadCameraSettings() { + try { + if (fs.existsSync(SETTINGS_FILE)) { + return JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8')); + } + } catch (error) { + console.error('Error loading camera settings:', error.message); + } + return {}; +} + +function saveCameraSettings(settings) { + fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2)); +} + +// Get camera settings +app.get('/settings/:cameraId', authenticate, (req, res) => { + const { cameraId } = req.params; + const allSettings = loadCameraSettings(); + const settings = allSettings[cameraId] || {}; + + res.json({ + cameraId, + settings: { + focus_automatic_continuous: settings.focus_automatic_continuous ?? 0, + focus_absolute: settings.focus_absolute ?? 30, + exposure_auto: settings.exposure_auto ?? 1, + exposure_absolute: settings.exposure_absolute ?? 200, + brightness: settings.brightness ?? 0, + contrast: settings.contrast ?? 32, + ...settings + } + }); +}); + +// Update camera settings +app.put('/settings/:cameraId', authenticate, express.json(), (req, res) => { + const { cameraId } = req.params; + const newSettings = req.body; + + if (!newSettings || typeof newSettings !== 'object') { + return res.status(400).json({ error: 'Settings object required in request body' }); + } + + const allSettings = loadCameraSettings(); + allSettings[cameraId] = { + ...(allSettings[cameraId] || {}), + ...newSettings, + updatedAt: new Date().toISOString() + }; + + try { + saveCameraSettings(allSettings); + res.json({ + success: true, + cameraId, + settings: allSettings[cameraId] + }); + } catch (error) { + res.status(500).json({ error: 'Failed to save settings', details: error.message }); + } +}); + // Error handling middleware app.use((error, req, res, next) => { if (error instanceof multer.MulterError) {