From ee1955c048e4512ef1032254ae2667b65541af0c Mon Sep 17 00:00:00 2001 From: sebseb7 Date: Sat, 27 Dec 2025 20:09:42 +0100 Subject: [PATCH] u --- ecosystem.config.cjs | 4 +- package-lock.json | 24 +++- package.json | 3 +- server.js | 57 ++++++++- todo___/wss-client-example/example.js | 53 ++++++++ todo___/wss-client-example/lib.js | 130 ++++++++++++++++++++ todo___/wss-client-example/package.json | 13 ++ wss-client.js | 156 ++++++++++++++++++++++++ 8 files changed, 433 insertions(+), 7 deletions(-) create mode 100644 todo___/wss-client-example/example.js create mode 100644 todo___/wss-client-example/lib.js create mode 100644 todo___/wss-client-example/package.json create mode 100644 wss-client.js diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs index a772c56..0bb8041 100644 --- a/ecosystem.config.cjs +++ b/ecosystem.config.cjs @@ -3,7 +3,9 @@ module.exports = { name: 'picupper', script: 'server.js', env: { - PICUPPER_PORT: 3080 + PICUPPER_PORT: 3080, + WSS_SERVER_URL: 'wss://dash.bosewolf.de/agentapi/', + WSS_API_KEY: '65771bb3a0a97a22fd9299702c08a43468a230d6432fc20ad36c8463d4c6d816' }, watch: false, instances: 1, diff --git a/package-lock.json b/package-lock.json index adca3fa..83eef54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,8 @@ "openai": "^6.15.0", "sharp": "^0.34.5", "tesseract.js": "^7.0.0", - "uuid": "^10.0.0" + "uuid": "^10.0.0", + "ws": "^8.18.3" } }, "node_modules/@emnapi/runtime": { @@ -2078,6 +2079,27 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 5598ccb..43604c9 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "openai": "^6.15.0", "sharp": "^0.34.5", "tesseract.js": "^7.0.0", - "uuid": "^10.0.0" + "uuid": "^10.0.0", + "ws": "^8.18.3" } } diff --git a/server.js b/server.js index c605191..f878834 100644 --- a/server.js +++ b/server.js @@ -16,6 +16,18 @@ const PORT = process.env.PICUPPER_PORT; const UPLOAD_DIR = process.env.PICUPPER_UPLOAD_DIR || path.join(__dirname, 'uploads'); const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB - typical webcam snapshot size +// WSS Client Configuration +import { TischlerClient } from './wss-client.js'; +const WSS_URL = process.env.WSS_SERVER_URL || 'wss://dash.bosewolf.de/agentapi/'; +const WSS_KEY = process.env.WSS_API_KEY; + +const wssClient = new TischlerClient(WSS_URL, WSS_KEY); +if (WSS_KEY) { + wssClient.connect().catch(err => console.error('WSS Client connection failed:', err.message)); +} else { + console.log('WSS_API_KEY not set. WSS Client disabled.'); +} + if (!PORT) { console.error('ERROR: PICUPPER_PORT environment variable is required'); process.exit(1); @@ -387,6 +399,8 @@ app.post('/upload', authenticate, upload.single('image'), async (req, res) => { const user = req.authenticatedUser; const storageKey = `${user}:${cameraId}`; const settings = loadCameraSettings()[storageKey] || {}; + const factor = settings.factor !== undefined ? settings.factor : 1.0; + const adjustedBrightness = brightness * factor; if (settings.insertBrightnessToDb) { const dbPath = '/home/seb/src/actest/ac_data.db'; @@ -396,15 +410,49 @@ app.post('/upload', authenticate, upload.single('image'), async (req, res) => { INSERT INTO readings (dev_name, port, port_name, fan_speed) VALUES ('Wall', 3, 'Light', ?) `); - stmt.run(brightness); + stmt.run(adjustedBrightness); db.close(); - console.log(`Brightness ${brightness} inserted into ac_data.db as Light`); + console.log(`Brightness ${brightness} (factored to ${adjustedBrightness}) inserted into ac_data.db as Light`); } } } catch (dbErr) { console.error('Failed to insert brightness into ac_data.db:', dbErr); } + // Send data to WSS API + if (WSS_KEY && wssClient.authenticated) { + try { + const user = req.authenticatedUser; + const storageKey = `${user}:${cameraId}`; + const settings = loadCameraSettings()[storageKey] || {}; + const readings = []; + const factor = settings.factor !== undefined ? settings.factor : 1.0; + const adjustedBrightness = brightness * factor; + + if (settings.ocr && settings.ocr.enabled && ocr_val !== null) { + readings.push({ + device: cameraId, + channel: settings.chartLabel || 'CO2', + value: ocr_val + }); + } + + if (settings.insertBrightnessToDb) { + readings.push({ + device: cameraId, + channel: settings.chartLabel || 'Light', + value: adjustedBrightness + }); + } + + if (readings.length > 0) { + wssClient.sendReadings(readings); + } + } catch (wssErr) { + console.error('Failed to send WSS readings:', wssErr); + } + } + res.json({ success: true, cameraId, @@ -612,7 +660,7 @@ app.put('/settings/:cameraId', authenticate, express.json(), (req, res) => { const existing = allSettings[storageKey] || {}; // Separate v4l2 control values from config settings - const configKeys = ['rotation', 'crop', 'ocr', 'chartLabel', 'insertBrightnessToDb']; + const configKeys = ['rotation', 'crop', 'ocr', 'chartLabel', 'insertBrightnessToDb', 'factor']; const newValues = {}; const newConfig = {}; @@ -646,7 +694,8 @@ app.put('/settings/:cameraId', authenticate, express.json(), (req, res) => { crop: allSettings[storageKey].crop, ocr: allSettings[storageKey].ocr, chartLabel: allSettings[storageKey].chartLabel, - insertBrightnessToDb: allSettings[storageKey].insertBrightnessToDb + insertBrightnessToDb: allSettings[storageKey].insertBrightnessToDb, + factor: allSettings[storageKey].factor } }); } catch (error) { diff --git a/todo___/wss-client-example/example.js b/todo___/wss-client-example/example.js new file mode 100644 index 0000000..45243cb --- /dev/null +++ b/todo___/wss-client-example/example.js @@ -0,0 +1,53 @@ +import { TischlerClient } from './lib.js'; + +// Configuration (Replace with your actual values or set ENV vars) +// Example: SERVER_URL=ws://localhost:3000 API_KEY=k_... node example.js +const SERVER_URL = process.env.SERVER_URL || 'wss://dash.bosewolf.de/agentapi/'; +const API_KEY = process.env.API_KEY || 'YOUR_API_KEY_HERE'; + +if (API_KEY === 'YOUR_API_KEY_HERE') { + console.error('Please set API_KEY environment variable or edit example.js'); + process.exit(1); +} + +const client = new TischlerClient(SERVER_URL, API_KEY); + +client.onAuthenticated = () => { + // Determine random values for demo + const temp = 20 + Math.random() * 5; + const humidity = 40 + Math.random() * 20; + + // Example 1: Numeric Data (e.g. Temperature) + // Note: The 'device' id will be prefixed by the server with your Agent's prefix. + const readings = [ + { + device: 'sensor-1', + channel: 'temperature', + value: temp + }, + { + device: 'sensor-1', + channel: 'humidity', + value: humidity + }, + // Example 2: Generic JSON Data (e.g. Status object) + { + device: 'sensor-1', + channel: 'status', + data: { status: 'ok', battery: '95%', fw: '1.2.0' } // 'data' field for JSON + } + ]; + + client.sendReadings(readings); + + // Close after a standardized delay to ensure ACK is received + setTimeout(() => { + console.log('Done. Closing connection.'); + client.close(); + }, 2000); +}; + +// Start +client.connect().catch(err => { + console.error('Failed to connect:', err); +}); diff --git a/todo___/wss-client-example/lib.js b/todo___/wss-client-example/lib.js new file mode 100644 index 0000000..fd759d3 --- /dev/null +++ b/todo___/wss-client-example/lib.js @@ -0,0 +1,130 @@ +import WebSocket from 'ws'; + +/** + * A simple client for the TischlerCtrl WebSocket API. + */ +export class TischlerClient { + /** + * @param {string} url - The WebSocket server URL (e.g., 'ws://localhost:3000') + * @param {string} apiKey - Your Agent API Key + */ + constructor(url, apiKey) { + this.url = url; + this.apiKey = apiKey; + this.ws = null; + this.authenticated = false; + this.onAuthenticated = null; // Callback + } + + /** + * Connect to the WebSocket server. + * @returns {Promise} Resolves when connected (but not yet authenticated) + */ + connect() { + return new Promise((resolve, reject) => { + this.ws = new WebSocket(this.url); + + this.ws.on('open', () => { + console.log('[Client] Connected to server.'); + this.authenticate(); + resolve(); + }); + + this.ws.on('message', (data) => this.handleMessage(data)); + + this.ws.on('error', (err) => { + console.error('[Client] Connection error:', err.message); + reject(err); + }); + + this.ws.on('close', () => { + console.log('[Client] Disconnected.'); + this.authenticated = false; + }); + }); + } + + /** + * Send authentication message. + */ + authenticate() { + console.log('[Client] Authenticating...'); + this.send({ + type: 'auth', + apiKey: this.apiKey + }); + } + + /** + * Send sensor readings. + * @param {Array} readings - Array of reading objects + * @example + * client.sendReadings([ + * { device: 'temp-sensor-1', channel: 'temp', value: 24.5 }, + * { device: 'temp-sensor-1', channel: 'config', data: { mode: 'eco' } } + * ]); + */ + sendReadings(readings) { + if (!this.authenticated) { + console.warn('[Client] Cannot send data: Not authenticated.'); + return; + } + + console.log(`[Client] Sending ${readings.length} readings...`); + this.send({ + type: 'data', + readings: readings + }); + } + + /** + * Handle incoming messages. + */ + handleMessage(data) { + try { + const message = JSON.parse(data.toString()); + + switch (message.type) { + case 'auth': + if (message.success) { + this.authenticated = true; + console.log(`[Client] Authenticated as "${message.name}" (Prefix: ${message.devicePrefix})`); + if (this.onAuthenticated) this.onAuthenticated(); + } else { + console.error('[Client] Authentication failed:', message.error); + this.ws.close(); + } + break; + + case 'ack': + console.log(`[Client] Server acknowledged ${message.count} readings.`); + break; + + case 'error': + console.error('[Client] Server error:', message.error); + break; + + default: + console.log('[Client] Received:', message); + } + } catch (err) { + console.error('[Client] Failed to parse message:', err); + } + } + + /** + * Helper to send JSON object. + */ + send(obj) { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(obj)); + } + } + + /** + * Close connection. + */ + close() { + if (this.ws) this.ws.close(); + } +} diff --git a/todo___/wss-client-example/package.json b/todo___/wss-client-example/package.json new file mode 100644 index 0000000..ef2084d --- /dev/null +++ b/todo___/wss-client-example/package.json @@ -0,0 +1,13 @@ +{ + "name": "wss-client-example", + "version": "1.0.0", + "description": "Example client for TischlerCtrl WebSocket API", + "main": "lib.js", + "type": "module", + "scripts": { + "start": "node example.js" + }, + "dependencies": { + "ws": "^8.18.0" + } +} \ No newline at end of file diff --git a/wss-client.js b/wss-client.js new file mode 100644 index 0000000..169dd79 --- /dev/null +++ b/wss-client.js @@ -0,0 +1,156 @@ +import WebSocket from 'ws'; + +/** + * A simple client for the TischlerCtrl WebSocket API. + */ +export class TischlerClient { + /** + * @param {string} url - The WebSocket server URL (e.g., 'ws://localhost:3000') + * @param {string} apiKey - Your Agent API Key + */ + constructor(url, apiKey) { + this.url = url; + this.apiKey = apiKey; + this.ws = null; + this.authenticated = false; + this.onAuthenticated = null; // Callback + this.reconnectInterval = 1000; + this.maxReconnectInterval = 30000; + this.isReconnecting = false; + } + + /** + * Connect to the WebSocket server. + * @returns {Promise} Resolves when connected (but not yet authenticated) + */ + connect() { + return new Promise((resolve, reject) => { + this.ws = new WebSocket(this.url); + + this.ws.on('open', () => { + console.log('[Client] Connected to server.'); + this.isReconnecting = false; + this.reconnectInterval = 1000; // Reset backoff + this.authenticate(); + resolve(); + }); + + this.ws.on('message', (data) => this.handleMessage(data)); + + this.ws.on('error', (err) => { + console.error('[Client] Connection error:', err.message); + // Reject only if this is the initial connection attempt and not a reconnection + if (!this.isReconnecting) reject(err); + }); + + this.ws.on('close', () => { + console.log('[Client] Disconnected.'); + this.authenticated = false; + this.scheduleReconnect(); + }); + }); + } + + scheduleReconnect() { + if (this.isReconnecting) return; + this.isReconnecting = true; + + console.log(`[Client] Reconnecting in ${this.reconnectInterval / 1000}s...`); + setTimeout(() => { + // Reset flag BEFORE attempting, so a failed attempt can schedule another + this.isReconnecting = false; + + console.log('[Client] Attempting to reconnect...'); + this.connect().catch(() => { + // Connection failed - close handler will call scheduleReconnect again + }); + + // Increase backoff for next time + this.reconnectInterval = Math.min(this.reconnectInterval * 1.5, this.maxReconnectInterval); + }, this.reconnectInterval); + } + + /** + * Send authentication message. + */ + authenticate() { + console.log('[Client] Authenticating...'); + this.send({ + type: 'auth', + apiKey: this.apiKey + }); + } + + /** + * Send sensor readings. + * @param {Array} readings - Array of reading objects + * @example + * client.sendReadings([ + * { device: 'temp-sensor-1', channel: 'temp', value: 24.5 }, + * { device: 'temp-sensor-1', channel: 'config', data: { mode: 'eco' } } + * ]); + */ + sendReadings(readings) { + if (!this.authenticated) { + console.warn('[Client] Cannot send data: Not authenticated.'); + return; + } + + console.log(`[Client] Sending ${readings.length} readings...`); + this.send({ + type: 'data', + readings: readings + }); + } + + /** + * Handle incoming messages. + */ + handleMessage(data) { + try { + const message = JSON.parse(data.toString()); + + switch (message.type) { + case 'auth': + if (message.success) { + this.authenticated = true; + console.log(`[Client] Authenticated as "${message.name}" (Prefix: ${message.devicePrefix})`); + if (this.onAuthenticated) this.onAuthenticated(); + } else { + console.error('[Client] Authentication failed:', message.error); + this.ws.close(); + } + break; + + case 'ack': + console.log(`[Client] Server acknowledged ${message.count} readings.`); + break; + + case 'error': + console.error('[Client] Server error:', message.error); + break; + + default: + console.log('[Client] Received:', message); + } + } catch (err) { + console.error('[Client] Failed to parse message:', err); + } + } + + /** + * Helper to send JSON object. + */ + send(obj) { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(obj)); + } + } + + /** + * Close connection. + */ + close() { + if (this.ws) this.ws.close(); + } +}