feat: Implement server-managed camera settings with new API endpoints, client-side application, and updated documentation.

This commit is contained in:
sebseb7
2025-12-18 14:55:23 +01:00
parent f7663c04c6
commit 0a93ee6713
4 changed files with 282 additions and 0 deletions

View File

@@ -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 ## Storage Structure
Images are stored in a hierarchical directory structure optimized for time-lapse processing: Images are stored in a hierarchical directory structure optimized for time-lapse processing:

View File

@@ -76,6 +76,58 @@ VIDEO_DEVICE="/dev/video0"
./capture-upload.sh ./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 ## Troubleshooting
### Check if webcam is detected ### Check if webcam is detected

View File

@@ -118,6 +118,125 @@ if [[ ! -e "$VIDEO_DEVICE" ]]; then
exit 1 exit 1
fi 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 # Capture image
log "Capturing from $VIDEO_DEVICE ($RESOLUTION)" log "Capturing from $VIDEO_DEVICE ($RESOLUTION)"
if ! fswebcam -d "$VIDEO_DEVICE" -r "$RESOLUTION" -S "$SKIP_FRAMES" --no-banner "$TEMP_FILE" 2>/dev/null; then if ! fswebcam -d "$VIDEO_DEVICE" -r "$RESOLUTION" -S "$SKIP_FRAMES" --no-banner "$TEMP_FILE" 2>/dev/null; then

View File

@@ -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 // Error handling middleware
app.use((error, req, res, next) => { app.use((error, req, res, next) => {
if (error instanceof multer.MulterError) { if (error instanceof multer.MulterError) {