API für .npz empfang
This commit is contained in:
47
README.md
47
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_<ts>.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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
@@ -192,7 +200,8 @@ function createStreamRouter(switches) {
|
||||
|
||||
// GET /api/cameras → Kamera-Metadaten (ohne device-Pfad)
|
||||
// GET /api/cameras/:id/calibration → .npz aus RAM (geladen beim Start)
|
||||
function createCamerasRouter(cameras, calibrations = {}) {
|
||||
// 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_<ts>.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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user