#!/usr/bin/env node /** * Minimal Care Device Agent. * * Demo mode: FAKE_READING=1 posts stub vitals (no serial port). * Production: replace readSerialStub() with a vendor/serial/BLE reader. */ const fs = require('fs'); const path = require('path'); function loadEnvFile() { const envPath = path.join(__dirname, '.env'); if (!fs.existsSync(envPath)) return; for (const line of fs.readFileSync(envPath, 'utf8').split('\n')) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const eq = trimmed.indexOf('='); if (eq <= 0) continue; const key = trimmed.slice(0, eq).trim(); let val = trimmed.slice(eq + 1).trim(); if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { val = val.slice(1, -1); } if (process.env[key] === undefined) process.env[key] = val; } } loadEnvFile(); const CARE_URL = (process.env.CARE_URL || '').replace(/\/$/, ''); const DEVICE_TOKEN = process.env.DEVICE_TOKEN || ''; const CONSULTATION_UUID = process.env.CONSULTATION_UUID || ''; const FAKE_READING = ['1', 'true', 'yes'].includes(String(process.env.FAKE_READING || '').toLowerCase()); const INTERVAL_SECONDS = Math.max(5, Number(process.env.INTERVAL_SECONDS || 30)); const AGENT_VERSION = process.env.AGENT_VERSION || '0.1.0'; const AGENT_HOSTNAME = process.env.AGENT_HOSTNAME || require('os').hostname(); const ONCE = process.argv.includes('--once'); if (!CARE_URL || !DEVICE_TOKEN) { console.error('Set CARE_URL and DEVICE_TOKEN (see .env.example).'); process.exit(1); } async function api(pathname, body) { const res = await fetch(`${CARE_URL}/api/v1/device-agent${pathname}`, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'X-Care-Device-Token': DEVICE_TOKEN, }, body: JSON.stringify(body), }); const text = await res.text(); let data; try { data = text ? JSON.parse(text) : {}; } catch { data = { raw: text }; } if (!res.ok) { const err = new Error(data.message || data.error || `HTTP ${res.status}`); err.status = res.status; err.data = data; throw err; } return data; } /** Stub serial read — replace with real driver for production. */ function readSerialStub() { if (!FAKE_READING) return null; const base = 36.5 + Math.random() * 0.8; return { vitals: { temperature: Math.round(base * 10) / 10, pulse: 60 + Math.floor(Math.random() * 20), spo2: 96 + Math.floor(Math.random() * 4), }, raw: { driver: 'fake', captured_at: new Date().toISOString(), }, }; } async function tick() { const hb = await api('/heartbeat', { agent_version: AGENT_VERSION, hostname: AGENT_HOSTNAME, }); console.log(`[heartbeat] status=${hb.status} last_seen=${hb.last_seen_at}`); const reading = readSerialStub(); if (reading && CONSULTATION_UUID) { const result = await api('/vitals', { consultation_uuid: CONSULTATION_UUID, vitals: reading.vitals, raw: reading.raw, }); console.log(`[vitals] id=${result.vital_sign_id} temp=${reading.vitals.temperature}`); } else if (reading && !CONSULTATION_UUID) { console.log('[vitals] skipped — set CONSULTATION_UUID to post fake readings'); } } async function main() { console.log(`Care Device Agent → ${CARE_URL} (fake=${FAKE_READING ? 'on' : 'off'})`); await tick(); if (ONCE) return; setInterval(() => { tick().catch((err) => console.error('[error]', err.message, err.data || '')); }, INTERVAL_SECONDS * 1000); } main().catch((err) => { console.error(err.message, err.data || ''); process.exit(1); });