u
This commit is contained in:
@@ -3,7 +3,9 @@ module.exports = {
|
|||||||
name: 'picupper',
|
name: 'picupper',
|
||||||
script: 'server.js',
|
script: 'server.js',
|
||||||
env: {
|
env: {
|
||||||
PICUPPER_PORT: 3080
|
PICUPPER_PORT: 3080,
|
||||||
|
WSS_SERVER_URL: 'wss://dash.bosewolf.de/agentapi/',
|
||||||
|
WSS_API_KEY: '65771bb3a0a97a22fd9299702c08a43468a230d6432fc20ad36c8463d4c6d816'
|
||||||
},
|
},
|
||||||
watch: false,
|
watch: false,
|
||||||
instances: 1,
|
instances: 1,
|
||||||
|
|||||||
24
package-lock.json
generated
24
package-lock.json
generated
@@ -16,7 +16,8 @@
|
|||||||
"openai": "^6.15.0",
|
"openai": "^6.15.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"tesseract.js": "^7.0.0",
|
"tesseract.js": "^7.0.0",
|
||||||
"uuid": "^10.0.0"
|
"uuid": "^10.0.0",
|
||||||
|
"ws": "^8.18.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
"node_modules/@emnapi/runtime": {
|
||||||
@@ -2078,6 +2079,27 @@
|
|||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"openai": "^6.15.0",
|
"openai": "^6.15.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"tesseract.js": "^7.0.0",
|
"tesseract.js": "^7.0.0",
|
||||||
"uuid": "^10.0.0"
|
"uuid": "^10.0.0",
|
||||||
|
"ws": "^8.18.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
57
server.js
57
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 UPLOAD_DIR = process.env.PICUPPER_UPLOAD_DIR || path.join(__dirname, 'uploads');
|
||||||
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB - typical webcam snapshot size
|
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) {
|
if (!PORT) {
|
||||||
console.error('ERROR: PICUPPER_PORT environment variable is required');
|
console.error('ERROR: PICUPPER_PORT environment variable is required');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -387,6 +399,8 @@ app.post('/upload', authenticate, upload.single('image'), async (req, res) => {
|
|||||||
const user = req.authenticatedUser;
|
const user = req.authenticatedUser;
|
||||||
const storageKey = `${user}:${cameraId}`;
|
const storageKey = `${user}:${cameraId}`;
|
||||||
const settings = loadCameraSettings()[storageKey] || {};
|
const settings = loadCameraSettings()[storageKey] || {};
|
||||||
|
const factor = settings.factor !== undefined ? settings.factor : 1.0;
|
||||||
|
const adjustedBrightness = brightness * factor;
|
||||||
|
|
||||||
if (settings.insertBrightnessToDb) {
|
if (settings.insertBrightnessToDb) {
|
||||||
const dbPath = '/home/seb/src/actest/ac_data.db';
|
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)
|
INSERT INTO readings (dev_name, port, port_name, fan_speed)
|
||||||
VALUES ('Wall', 3, 'Light', ?)
|
VALUES ('Wall', 3, 'Light', ?)
|
||||||
`);
|
`);
|
||||||
stmt.run(brightness);
|
stmt.run(adjustedBrightness);
|
||||||
db.close();
|
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) {
|
} catch (dbErr) {
|
||||||
console.error('Failed to insert brightness into ac_data.db:', 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({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
cameraId,
|
cameraId,
|
||||||
@@ -612,7 +660,7 @@ app.put('/settings/:cameraId', authenticate, express.json(), (req, res) => {
|
|||||||
const existing = allSettings[storageKey] || {};
|
const existing = allSettings[storageKey] || {};
|
||||||
|
|
||||||
// Separate v4l2 control values from config settings
|
// 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 newValues = {};
|
||||||
const newConfig = {};
|
const newConfig = {};
|
||||||
|
|
||||||
@@ -646,7 +694,8 @@ app.put('/settings/:cameraId', authenticate, express.json(), (req, res) => {
|
|||||||
crop: allSettings[storageKey].crop,
|
crop: allSettings[storageKey].crop,
|
||||||
ocr: allSettings[storageKey].ocr,
|
ocr: allSettings[storageKey].ocr,
|
||||||
chartLabel: allSettings[storageKey].chartLabel,
|
chartLabel: allSettings[storageKey].chartLabel,
|
||||||
insertBrightnessToDb: allSettings[storageKey].insertBrightnessToDb
|
insertBrightnessToDb: allSettings[storageKey].insertBrightnessToDb,
|
||||||
|
factor: allSettings[storageKey].factor
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
53
todo___/wss-client-example/example.js
Normal file
53
todo___/wss-client-example/example.js
Normal file
@@ -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);
|
||||||
|
});
|
||||||
130
todo___/wss-client-example/lib.js
Normal file
130
todo___/wss-client-example/lib.js
Normal file
@@ -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<void>} 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<Object>} 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
13
todo___/wss-client-example/package.json
Normal file
13
todo___/wss-client-example/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
156
wss-client.js
Normal file
156
wss-client.js
Normal file
@@ -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<void>} 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<Object>} 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user