'use strict'; const { load, DEFAULTS } = require('../robot/RobotConfig'); function makeFs(content) { return { readFileSync: jest.fn(() => content) }; } function makeFailFs() { return { readFileSync: jest.fn(() => { throw new Error('ENOENT'); }) }; } const log = { warn: jest.fn(), log: jest.fn(), error: jest.fn() }; const FULL_ROBOT_JSON = { kinematics: { type: 'arm3segmentlinearx' }, motion: { defaultFeedrate: 2300, speedMode: 'legacy', speedModeOptions: ['legacy', 'correct'] }, controllers: { base: { ip: 'fluidNcBase.local', port: 2300, protocol: 'telnet', axes: ['x', 'y', 'z'] }, elbow: { ip: 'fluidNcEllbow.local', port: 5000, protocol: 'telnet', axes: ['a', null, null] }, hand: { ip: 'fluidNcHand.local', port: 5000, protocol: 'telnet', axes: ['c', 'e', 'b'] } }, links: { Arm1: { skeleton: { from: [0,0,0], to: [0,-250,0] } }, Arm2: { skeleton: { from: [0,0,0], to: [0,-250,0] } }, Ellbow: { skeleton: { from: [0,0,0], to: [90,0,0] } }, Hand: { skeleton: { from: [0,0,0], to: [0,-35,0] } }, FingerA: { skeleton: { from: [0,0,0], to: [0,-60,0] } } } }; describe('RobotConfig.load — Vollständige robot.json', () => { let cfg; beforeEach(() => { cfg = load(makeFs(JSON.stringify(FULL_ROBOT_JSON)), {}, log); }); test('kinematics.type aus robot.json', () => { expect(cfg.kinematics.type).toBe('arm3segmentlinearx'); }); test('l1/l2 aus links.Arm1/Arm2.skeleton.to[1] (Betrag)', () => { expect(cfg.kinematics.l1).toBe(250); expect(cfg.kinematics.l2).toBe(250); }); test('l3 aus Hand + Finger (Handgelenk → Fingerspitze), NICHT Ellbow-Versatz', () => { expect(cfg.kinematics.l3).toBe(95); // |Hand.to[1]|=35 + |FingerA.to[1]|=60 }); test('kinematics.l1/l2/l3 explizit in robot.json überschreiben die links-Ableitung', () => { const json = { ...FULL_ROBOT_JSON, kinematics: { type: 'arm3segmentlinearx', l1: 260, l2: 270, l3: 50 } }; const c = load(makeFs(JSON.stringify(json)), {}, log); expect(c.kinematics.l1).toBe(260); expect(c.kinematics.l2).toBe(270); expect(c.kinematics.l3).toBe(50); }); test('motion.defaultFeedrate aus robot.json', () => { expect(cfg.motion.defaultFeedrate).toBe(2300); }); test('motion.speedMode aus robot.json', () => { expect(cfg.motion.speedMode).toBe('legacy'); }); test('motion.useSpeedCalc false für legacy', () => { expect(cfg.motion.useSpeedCalc).toBe(false); }); test('controllers enthält alle drei Endpunkte', () => { expect(cfg.controllers.base.ip).toBe('fluidNcBase.local'); expect(cfg.controllers.base.port).toBe(2300); expect(cfg.controllers.base.protocol).toBe('telnet'); expect(cfg.controllers.elbow.port).toBe(5000); expect(cfg.controllers.hand.ip).toBe('fluidNcHand.local'); }); test('heartbeatInterval Default wenn nicht in robot.json', () => { expect(cfg.controllers.base.heartbeatInterval).toBe(DEFAULTS.controllers.base.heartbeatInterval); expect(cfg.controllers.elbow.heartbeatInterval).toBe(DEFAULTS.controllers.elbow.heartbeatInterval); expect(cfg.controllers.hand.heartbeatInterval).toBe(DEFAULTS.controllers.hand.heartbeatInterval); }); test('axesByController gibt korrektes Array zurück', () => { expect(cfg.axesByController('base')).toEqual(['x', 'y', 'z']); expect(cfg.axesByController('elbow')).toEqual(['a', null, null]); expect(cfg.axesByController('hand')).toEqual(['c', 'e', 'b']); }); test('axesByController gibt [] für unbekannten Key zurück', () => { expect(cfg.axesByController('unknown')).toEqual([]); }); }); describe('RobotConfig.load — Fehlerbehandlung', () => { test('fehlende robot.json → Defaults', () => { const cfg = load(makeFailFs(), {}, log); expect(cfg.kinematics.l1).toBe(DEFAULTS.kinematics.l1); expect(cfg.motion.defaultFeedrate).toBe(DEFAULTS.motion.defaultFeedrate); expect(cfg.controllers.base.ip).toBe(DEFAULTS.controllers.base.ip); }); test('ungültiges JSON → Defaults', () => { const cfg = load(makeFs('{ not valid json }'), {}, log); expect(cfg.kinematics.type).toBe(DEFAULTS.kinematics.type); }); test('fehlende links → kinematics-Defaults', () => { const json = { ...FULL_ROBOT_JSON }; delete json.links; const cfg = load(makeFs(JSON.stringify(json)), {}, log); expect(cfg.kinematics.l1).toBe(DEFAULTS.kinematics.l1); expect(cfg.kinematics.l3).toBe(DEFAULTS.kinematics.l3); }); }); describe('RobotConfig.load — Env-Override', () => { test('ROBOT_DEFAULT_FEEDRATE überschreibt robot.json', () => { const env = { ROBOT_DEFAULT_FEEDRATE: '5000' }; const cfg = load(makeFs(JSON.stringify(FULL_ROBOT_JSON)), env, log); expect(cfg.motion.defaultFeedrate).toBe(5000); }); test('ROBOT_SPEED_MODE=correct setzt useSpeedCalc=true', () => { const env = { ROBOT_SPEED_MODE: 'correct' }; const cfg = load(makeFs(JSON.stringify(FULL_ROBOT_JSON)), env, log); expect(cfg.motion.speedMode).toBe('correct'); expect(cfg.motion.useSpeedCalc).toBe(true); }); test('GRBL_BASE_IP überschreibt controller.base.ip', () => { const env = { GRBL_BASE_IP: '192.168.1.10' }; const cfg = load(makeFs(JSON.stringify(FULL_ROBOT_JSON)), env, log); expect(cfg.controllers.base.ip).toBe('192.168.1.10'); }); test('GRBL_ELLBOW_IP überschreibt controller.elbow.ip', () => { const env = { GRBL_ELLBOW_IP: '192.168.1.11' }; const cfg = load(makeFs(JSON.stringify(FULL_ROBOT_JSON)), env, log); expect(cfg.controllers.elbow.ip).toBe('192.168.1.11'); }); test('GRBL_HAND_IP überschreibt controller.hand.ip', () => { const env = { GRBL_HAND_IP: '192.168.1.12' }; const cfg = load(makeFs(JSON.stringify(FULL_ROBOT_JSON)), env, log); expect(cfg.controllers.hand.ip).toBe('192.168.1.12'); }); }); describe('RobotConfig.load — speedMode correct', () => { test('speedMode correct aus robot.json setzt useSpeedCalc=true', () => { const json = { ...FULL_ROBOT_JSON, motion: { ...FULL_ROBOT_JSON.motion, speedMode: 'correct' } }; const cfg = load(makeFs(JSON.stringify(json)), {}, log); expect(cfg.motion.useSpeedCalc).toBe(true); }); test('ROBOT_USE_SPEED_CALC=true setzt useSpeedCalc', () => { const env = { ROBOT_USE_SPEED_CALC: 'true' }; const cfg = load(makeFs(JSON.stringify(FULL_ROBOT_JSON)), env, log); expect(cfg.motion.useSpeedCalc).toBe(true); }); }); describe('RobotConfig.load — heartbeatInterval', () => { test('heartbeatInterval aus robot.json überschreibt Default', () => { const json = { ...FULL_ROBOT_JSON, controllers: { ...FULL_ROBOT_JSON.controllers, base: { ...FULL_ROBOT_JSON.controllers.base, heartbeatInterval: 5000 }, elbow: { ...FULL_ROBOT_JSON.controllers.elbow, heartbeatInterval: 30000 }, } }; const cfg = load(makeFs(JSON.stringify(json)), {}, log); expect(cfg.controllers.base.heartbeatInterval).toBe(5000); expect(cfg.controllers.elbow.heartbeatInterval).toBe(30000); // hand nicht gesetzt → Default expect(cfg.controllers.hand.heartbeatInterval).toBe(DEFAULTS.controllers.hand.heartbeatInterval); }); test('fehlende heartbeatInterval → Default (10 000 ms)', () => { const cfg = load(makeFailFs(), {}, log); expect(cfg.controllers.base.heartbeatInterval).toBe(10000); }); }); describe('RobotConfig.load — emergencyStop (Shelly)', () => { test('DEFAULTS.controllers.emergencyStop hat protocol=shelly und url=null', () => { expect(DEFAULTS.controllers.emergencyStop.protocol).toBe('shelly'); expect(DEFAULTS.controllers.emergencyStop.url).toBeNull(); expect(DEFAULTS.controllers.emergencyStop.urlOn).toBeNull(); expect(DEFAULTS.controllers.emergencyStop.urlStatus).toBeNull(); }); test('emergencyStop.url aus robot.json wird übernommen', () => { const shellyUrl = 'http://shelly.local/rpc/Switch.Set?id=0&on=false'; const json = { ...FULL_ROBOT_JSON, controllers: { ...FULL_ROBOT_JSON.controllers, emergencyStop: { protocol: 'shelly', url: shellyUrl } } }; const cfg = load(makeFs(JSON.stringify(json)), {}, log); expect(cfg.controllers.emergencyStop.protocol).toBe('shelly'); expect(cfg.controllers.emergencyStop.url).toBe(shellyUrl); }); test('emergencyStop.urlOn + urlStatus aus robot.json werden übernommen (IP statt .local)', () => { const base = 'http://192.168.0.99'; const json = { ...FULL_ROBOT_JSON, controllers: { ...FULL_ROBOT_JSON.controllers, emergencyStop: { protocol: 'shelly', url: `${base}/rpc/Switch.Set?id=0&on=false`, urlOn: `${base}/rpc/Switch.Set?id=0&on=true`, urlStatus: `${base}/rpc/Switch.GetStatus?id=0`, } } }; const cfg = load(makeFs(JSON.stringify(json)), {}, log); expect(cfg.controllers.emergencyStop.url).toBe(`${base}/rpc/Switch.Set?id=0&on=false`); expect(cfg.controllers.emergencyStop.urlOn).toBe(`${base}/rpc/Switch.Set?id=0&on=true`); expect(cfg.controllers.emergencyStop.urlStatus).toBe(`${base}/rpc/Switch.GetStatus?id=0`); }); test('SHELLY_URL Env-Variable überschreibt url aus robot.json', () => { const envUrl = 'http://192.168.0.99/rpc/Switch.Set?id=0&on=false'; const cfg = load(makeFs(JSON.stringify(FULL_ROBOT_JSON)), { SHELLY_URL: envUrl }, log); expect(cfg.controllers.emergencyStop.url).toBe(envUrl); }); test('fehlendes emergencyStop in robot.json → url=null (Default)', () => { // FULL_ROBOT_JSON hat kein emergencyStop → fällt auf Default zurück const cfg = load(makeFs(JSON.stringify(FULL_ROBOT_JSON)), {}, log); expect(cfg.controllers.emergencyStop.protocol).toBe('shelly'); expect(cfg.controllers.emergencyStop.url).toBeNull(); expect(cfg.controllers.emergencyStop.urlOn).toBeNull(); expect(cfg.controllers.emergencyStop.urlStatus).toBeNull(); }); test('emergencyStop hat keine ip/port/axes/heartbeatInterval Felder', () => { const cfg = load(makeFailFs(), {}, log); expect(cfg.controllers.emergencyStop.ip).toBeUndefined(); expect(cfg.controllers.emergencyStop.port).toBeUndefined(); expect(cfg.controllers.emergencyStop.axes).toBeUndefined(); expect(cfg.controllers.emergencyStop.heartbeatInterval).toBeUndefined(); }); });