'use strict'; const ROBOT_JSON_PATH = 'data/robot/robot.json'; // Backward-compat env-var names für Controller-IPs (werden in Env als GRBL_ELLBOW_IP gespeichert) const ENV_IP_MAP = { base: 'GRBL_BASE_IP', elbow: 'GRBL_ELLBOW_IP', hand: 'GRBL_HAND_IP' }; const DEFAULTS = { kinematics: { type: 'arm3segmentlinearx', l1: 250, l2: 264, l3: 100 }, motion: { defaultFeedrate: 1000, speedMode: 'legacy', useSpeedCalc: false }, controllers: { base: { ip: 'fluidNcBase.local', port: 2300, protocol: 'telnet', axes: ['x', 'y', 'z'], heartbeatInterval: 10000 }, elbow: { ip: 'fluidNcEllbow.local', port: 5000, protocol: 'telnet', axes: ['a', null, null], heartbeatInterval: 10000 }, hand: { ip: 'fluidNcHand.local', port: 5000, protocol: 'telnet', axes: ['c', 'e', 'b'], heartbeatInterval: 10000 }, // Shelly Smart Plug: schaltet Strom für Emergency Stop. // url/urlOn/urlStatus: null → deaktiviert, wenn nicht in robot.json konfiguriert. emergencyStop: { protocol: 'shelly', url: null, urlOn: null, urlStatus: null } } }; function deriveKinematicParams(links) { if (!links) return {}; const result = {}; const l1raw = links.Arm1?.skeleton?.to?.[1]; const l2raw = links.Arm2?.skeleton?.to?.[1]; if (l1raw != null) result.l1 = Math.abs(l1raw); if (l2raw != null) result.l2 = Math.abs(l2raw); // l3 = Endeffektor-Länge (Handgelenk → Fingerspitze) = Hand-Segment + Finger. // FRÜHER fälschlich aus Ellbow.skeleton.to[0] abgeleitet — das ist der seitliche // Ellbogen-Versatz, nicht die Hand/Finger-Länge (siehe doc/Info_Koordinaten.md, Phase 2). const handLen = links.Hand?.skeleton?.to?.[1]; // Hand-Segment (z. B. -35) const fingerLen = links.FingerA?.skeleton?.to?.[1]; // Finger (z. B. -60) if (handLen != null || fingerLen != null) { result.l3 = Math.abs(handLen ?? 0) + Math.abs(fingerLen ?? 0); } return result; } /** * Liest robot.json synchron und gibt einen typisierten Config-Record zurück. * Env-Variablen haben Vorrang vor robot.json (nützlich für Tests und Notfall-Overrides). * * @param {Object} [fsModule] - Abhängigkeit (Default: require('fs')) * @param {Object} [processEnv] - Abhängigkeit (Default: process.env) * @param {Object} [consoleObj] - Abhängigkeit (Default: console) * @returns {{ kinematics, motion, controllers, axesByController }} */ function load(fsModule, processEnv, consoleObj) { const fs_ = fsModule ?? require('fs'); const env_ = processEnv ?? process.env; const log_ = consoleObj ?? console; let json = null; try { const raw = fs_.readFileSync(ROBOT_JSON_PATH, 'utf8'); json = JSON.parse(raw); } catch { log_.warn('[RobotConfig] data/robot/robot.json nicht lesbar — nutze Defaults'); } // Kinematik-Typ und Armlängen (aus links abgeleitet) const linkParams = deriveKinematicParams(json?.links); // Explizite kinematics.l1/l2/l3 in robot.json haben Vorrang vor der links-Ableitung // (zum Kalibrieren der realen Längen, z. B. l3 an die gemessene Reichweite anpassen). const kinematics = { type: json?.kinematics?.type ?? DEFAULTS.kinematics.type, l1: json?.kinematics?.l1 ?? linkParams.l1 ?? DEFAULTS.kinematics.l1, l2: json?.kinematics?.l2 ?? linkParams.l2 ?? DEFAULTS.kinematics.l2, l3: json?.kinematics?.l3 ?? linkParams.l3 ?? DEFAULTS.kinematics.l3 }; // Bewegungs-Defaults — Env hat Vorrang, dann robot.json, dann Hard-Default const jsonMotion = json?.motion ?? {}; const rawFeedrate = env_.ROBOT_DEFAULT_FEEDRATE; const rawMode = env_.ROBOT_SPEED_MODE; const rawCalc = env_.ROBOT_USE_SPEED_CALC; const speedMode = (rawMode || jsonMotion.speedMode || DEFAULTS.motion.speedMode).toLowerCase(); const motion = { defaultFeedrate: rawFeedrate ? Number(rawFeedrate) : (jsonMotion.defaultFeedrate ?? DEFAULTS.motion.defaultFeedrate), speedMode, useSpeedCalc: speedMode === 'correct' || rawCalc === 'true' || rawCalc === '1' }; // Controller-Endpunkte — robot.json überschreibt Defaults, Env überschreibt IPs const jsonControllers = json?.controllers ?? {}; const controllers = {}; for (const key of Object.keys(DEFAULTS.controllers)) { const def = DEFAULTS.controllers[key]; const cfg = jsonControllers[key] ?? {}; if (def.protocol === 'shelly') { // Shelly Smart Plug: protocol + drei URLs (off / on / status). // SHELLY_URL überschreibt url aus robot.json (analog zu GRBL_BASE_IP). // urlOn/urlStatus: explizit konfigurierbar; fehlen sie, leitet ShellyEmergencyStop // sie aus url ab (url.replace('on=false','on=true') bzw. /rpc/Switch.GetStatus). const envUrl = env_.SHELLY_URL; controllers[key] = { protocol: cfg.protocol ?? def.protocol, url: envUrl ?? cfg.url ?? def.url, urlOn: cfg.urlOn ?? def.urlOn, urlStatus: cfg.urlStatus ?? def.urlStatus, }; } else { // Telnet (FluidNC): IP + Port + Achsen + Heartbeat const envIpKey = ENV_IP_MAP[key]; controllers[key] = { ip: env_[envIpKey] ?? cfg.ip ?? def.ip, port: cfg.port ?? def.port, protocol: cfg.protocol ?? def.protocol, axes: cfg.axes ?? def.axes, // Heartbeat-Intervall in ms: wie oft '?' gesendet wird. // deadTimeout = 2 × heartbeatInterval (zwei verpasste Heartbeats → Verbindung tot). heartbeatInterval: cfg.heartbeatInterval ?? def.heartbeatInterval, }; } } function axesByController(key) { return controllers[key]?.axes ?? []; } return { kinematics, motion, controllers, axesByController }; } module.exports = { load, DEFAULTS };