From 81f28fcea65571b6e2b2742e523b3e07fed004ef Mon Sep 17 00:00:00 2001 From: chk <79915315+ChKendel@users.noreply.github.com> Date: Wed, 10 Jun 2026 09:36:43 +0200 Subject: [PATCH] =?UTF-8?q?API=20f=C3=BCr=20.npz=20empfang?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 47 +++++++++++++++++++++++++++++++++- server.js | 4 ++- src/snapshotService.js | 58 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 104 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e871805..bbf46cb 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Vollständige Kamera-Metadaten. Primärer Einstiegspunkt für andere Container. } ``` -`calibrationUrl` fehlt, wenn keine `.npz` unter `data/calibration/{id}/` liegt. +`calibrationUrl` fehlt, solange noch keine `.npz` für diese Kamera vorhanden ist. `encode`: `"copybsf"` (MJPEG-Copy, Default) | `"mjpeg"` (Re-Encode) | `"h264"` (GPU, MSE). `mseCodec`: nur bei `encode="h264"` gesetzt, z.B. `"avc1.4D001F"`. @@ -87,6 +87,51 @@ K, D = d["camera_matrix"], d["dist_coeffs"] --- +### `PUT /api/cameras/{id}/calibration` + +Nimmt eine neue `.npz`-Datei entgegen (Homing-Prozess → Webcam-Service). +Schreibt zwei Dateien nach `data/calibration/{id}/`: + +| Datei | Zweck | +|---|---| +| `calibration_YYYYMMDD_HHMMSS.npz` | Archiv (bleibt erhalten) | +| `calibration.npz` | Aktuell — wird bei jedem PUT überschrieben | + +Existiert beim ersten Aufruf nur `calibration.npz` (noch kein Archiv), wird sie +anhand ihres `mtime` automatisch in `calibration_.npz` umbenannt, bevor die +neue Datei abgelegt wird. + +``` +Content-Type: application/octet-stream +Body: rohe .npz-Bytes +``` + +```json +// Response 200 +{ "id": "cam0", "saved": "calibration_20260610_143022.npz", "size": 1284, "calibrationUrl": "/api/cameras/cam0/calibration" } +``` + +Der In-Memory-Buffer wird sofort aktualisiert — `GET /api/cameras/cam0/calibration` +liefert ab diesem Moment die neue Datei, ohne Server-Neustart. + +400 bei leerem Body, 404 bei unbekannter Kamera-ID. + +Aufruf aus Python (Homing): + +```python +import requests +with open("cam0_calibration.npz", "rb") as f: + r = requests.put( + "http://thinkcentre.local:8444/api/cameras/cam0/calibration", + data=f, + headers={"Content-Type": "application/octet-stream"}, + ) +r.raise_for_status() +print(r.json()) # {'id': 'cam0', 'saved': 'calibration_20260610_143022.npz', ...} +``` + +--- + ### `GET /api/snapshot/{id}` Letztes Live-JPEG (Live-Auflösung, z.B. 640×480) aus dem RAM-Puffer. diff --git a/server.js b/server.js index ce802c8..34e6845 100644 --- a/server.js +++ b/server.js @@ -113,7 +113,9 @@ app.use(express.json()); // POST /api/config liest JSON-Body // ── 1. Eigene Endpunkte ─────────────────────────────────────────────────────── app.use('/api/snapshot', createSnapshotRouter(switches, camsMeta)); app.use('/api/stream', createStreamRouter(switches)); -app.use('/api/cameras', createCamerasRouter(camsMeta, calibrations)); +app.use('/api/cameras', createCamerasRouter(camsMeta, calibrations, { + calibDir: path.join(__dirname, 'data', 'calibration'), +})); app.use('/api/config', createConfigRouter({ switches, camsMeta, getCamerasJson: () => camerasJson, diff --git a/src/snapshotService.js b/src/snapshotService.js index eba9e51..f078a39 100644 --- a/src/snapshotService.js +++ b/src/snapshotService.js @@ -1,8 +1,16 @@ 'use strict'; const express = require('express'); +const fs = require('fs'); +const path = require('path'); const { readJpegWidth } = require('./cameraSwitch'); +// Datum → "YYYYMMDD_HHMMSS" +function formatTs(date) { + const p = n => String(n).padStart(2, '0'); + return `${date.getFullYear()}${p(date.getMonth() + 1)}${p(date.getDate())}_${p(date.getHours())}${p(date.getMinutes())}${p(date.getSeconds())}`; +} + // Stabile Schnittstellen für Viewer und Homing-Projekt – lesen NUR aus den // CameraSwitch-Instanzen (RAM-Puffer + Event-Stream). Kein Gerätezugriff hier, // keine go2rtc-Abhängigkeit mehr. @@ -190,9 +198,10 @@ function createStreamRouter(switches) { return router; } -// GET /api/cameras → Kamera-Metadaten (ohne device-Pfad) -// GET /api/cameras/:id/calibration → .npz aus RAM (geladen beim Start) -function createCamerasRouter(cameras, calibrations = {}) { +// GET /api/cameras → Kamera-Metadaten (ohne device-Pfad) +// GET /api/cameras/:id/calibration → .npz aus RAM (geladen beim Start) +// PUT /api/cameras/:id/calibration → neue .npz ablegen; schreibt Archiv + aktuell +function createCamerasRouter(cameras, calibrations = {}, { calibDir = null } = {}) { const router = express.Router(); router.get('/', (_req, res) => { @@ -216,6 +225,49 @@ function createCamerasRouter(cameras, calibrations = {}) { res.end(buf); }); + // PUT: Homing-Prozess liefert neue Kalibrierung. + // Speichert: calibration_YYYYMMDD_HHMMSS.npz (Archiv) + // + calibration.npz (aktuell, Kopie des Archivs) + // Migration: existiert nur calibration.npz (noch kein Archiv), wird es + // anhand seines mtime in calibration_.npz umbenannt. + router.put('/:id/calibration', + express.raw({ type: 'application/octet-stream', limit: '10mb' }), + (req, res) => { + const { id } = req.params; + if (!cameras.some(c => c.id === id)) { + return res.status(404).json({ error: `Unbekannte Kamera: ${id}` }); + } + const buf = req.body; + if (!Buffer.isBuffer(buf) || buf.length === 0) { + return res.status(400).json({ error: 'Body muss eine .npz-Datei als application/octet-stream sein' }); + } + if (!calibDir) { + return res.status(500).json({ error: 'calibDir nicht konfiguriert' }); + } + + const dir = path.join(calibDir, id); + const mainPath = path.join(dir, 'calibration.npz'); + fs.mkdirSync(dir, { recursive: true }); + + // Migration: vorhandenes calibration.npz ohne Timestamp ins Archiv überführen + const hasArchive = fs.readdirSync(dir).some(f => /^calibration_\d{8}_\d{6}\.npz$/.test(f)); + if (!hasArchive && fs.existsSync(mainPath)) { + const mtime = fs.statSync(mainPath).mtime; + fs.renameSync(mainPath, path.join(dir, `calibration_${formatTs(mtime)}.npz`)); + } + + // Neue Datei: Archiv-Kopie + aktuell + const tsName = `calibration_${formatTs(new Date())}.npz`; + fs.writeFileSync(path.join(dir, tsName), buf); + fs.writeFileSync(mainPath, buf); + + // In-Memory sofort aktualisieren → GET liefert sofort die neue Datei + calibrations[id] = buf; + + res.json({ id, saved: tsName, size: buf.length, calibrationUrl: `/api/cameras/${id}/calibration` }); + }, + ); + return router; }