E-Stop IP

This commit is contained in:
chk
2026-06-12 18:59:56 +02:00
parent 3e3023fa63
commit bfb84fab50
8 changed files with 78 additions and 12 deletions

View File

@@ -17,7 +17,12 @@
"base": { "ip": "fluidNcBase.local", "port": 2300, "protocol": "telnet", "axes": ["x", "y", "z"], "heartbeatInterval": 5000 },
"elbow": { "ip": "fluidNcEllbow.local", "port": 5000, "protocol": "telnet", "axes": ["a", null, null], "heartbeatInterval": 5000 },
"hand": { "ip": "fluidNcHand.local", "port": 5000, "protocol": "telnet", "axes": ["c", "e", "b"], "heartbeatInterval": 5000 },
"emergencyStop": { "protocol": "shelly", "url": "http://shelly1pmminig4-acebe6f095f4.local/rpc/Switch.Set?id=0&on=false" }
"emergencyStop": {
"protocol": "shelly",
"url": "http://192.168.0.238/rpc/Switch.Set?id=0&on=false",
"urlOn": "http://192.168.0.238/rpc/Switch.Set?id=0&on=true",
"urlStatus": "http://192.168.0.238/rpc/Switch.GetStatus?id=0"
}
},
"vision_config": {"MarkerType": "DICT_4X4_250", "MarkerSize": 0.025},
"renderingInfo": {

View File

@@ -10409,3 +10409,8 @@
2026-06-12T16:46:46.488Z ::ffff:127.0.0.1: M114
2026-06-12T16:46:46.701Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-12T16:46:46.923Z ::ffff:127.0.0.1: G1 X1
2026-06-12T16:56:02.840Z ::ffff:127.0.0.1: M114
2026-06-12T16:56:02.863Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-12T16:56:03.538Z ::ffff:127.0.0.1: M114
2026-06-12T16:56:03.753Z ::ffff:127.0.0.1: G1 X1 Y2 Z3
2026-06-12T16:56:03.985Z ::ffff:127.0.0.1: G1 X1

View File

@@ -14638,3 +14638,5 @@
2026-06-12T16:34:08.427Z ::ffff:127.0.0.1 : Ping
2026-06-12T16:46:45.306Z ::ffff:127.0.0.1 : Ping
2026-06-12T16:46:46.265Z ::ffff:127.0.0.1 : Ping
2026-06-12T16:56:02.816Z ::ffff:127.0.0.1 : Ping
2026-06-12T16:56:03.315Z ::ffff:127.0.0.1 : Ping

View File

@@ -17,8 +17,8 @@ const DEFAULTS = {
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: null → deaktiviert, wenn nicht in robot.json konfiguriert.
emergencyStop: { protocol: 'shelly', url: null }
// url/urlOn/urlStatus: null → deaktiviert, wenn nicht in robot.json konfiguriert.
emergencyStop: { protocol: 'shelly', url: null, urlOn: null, urlStatus: null }
}
};
@@ -87,10 +87,16 @@ function load(fsModule, processEnv, consoleObj) {
const cfg = jsonControllers[key] ?? {};
if (def.protocol === 'shelly') {
// Shelly Smart Plug: nur protocol + url nötig (kein IP/Port/Axes/Heartbeat)
// 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: cfg.url ?? def.url,
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

View File

@@ -25,9 +25,11 @@ module.exports = class ShellyEmergencyStop extends SenderInterface {
constructor(url, options = {}) {
super();
this._offUrl = url || null;
this._onUrl = url ? url.replace('on=false', 'on=true') : null;
// Switch.GetStatus?id=0 — leitet Status-URL aus der Switch.Set-URL ab
this._statusUrl = url ? url.replace(/\/rpc\/.*$/, '/rpc/Switch.GetStatus?id=0') : null;
// Explizite URL-Felder haben Vorrang vor Ableitungslogik.
// Das erlaubt IP-Adressen statt .local-Hostnamen (nötig in Docker-Umgebungen,
// wo mDNS nicht verfügbar ist).
this._onUrl = options.urlOn || (url ? url.replace('on=false', 'on=true') : null);
this._statusUrl = options.urlStatus || (url ? url.replace(/\/rpc\/.*$/, '/rpc/Switch.GetStatus?id=0') : null);
this.url = url || null; // für getStatus / InfoServer-Anzeige
this.state = 'ready';
this.error = null;

View File

@@ -97,7 +97,10 @@ function createApp(options = {}) {
if (ctrl.protocol === 'shelly') {
// Shelly Smart Plug: kein GCode-Empfänger, nur Emergency-Stop-Aktor
const instance = new ShellyClass(ctrl.url);
const instance = new ShellyClass(ctrl.url, {
urlOn: ctrl.urlOn,
urlStatus: ctrl.urlStatus,
});
senders.push({ name, instance, isGCodeReceiver: false });
} else {
// Telnet (FluidNC): Konstruktor erwartet 7 Achsen-Slots (x y z a b c e) vor dem

View File

@@ -182,6 +182,8 @@ 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', () => {
@@ -198,11 +200,39 @@ describe('RobotConfig.load — emergencyStop (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', () => {

View File

@@ -27,12 +27,12 @@ describe('ShellyEmergencyStop — Konstruktor', () => {
expect(s.url).toBe(OFF_URL);
});
test('_onUrl ersetzt on=false durch on=true', () => {
test('_onUrl ersetzt on=false durch on=true (Ableitung)', () => {
const s = new ShellyEmergencyStop(OFF_URL);
expect(s._onUrl).toBe(ON_URL);
});
test('_statusUrl zeigt auf Switch.GetStatus', () => {
test('_statusUrl zeigt auf Switch.GetStatus (Ableitung)', () => {
const s = new ShellyEmergencyStop(OFF_URL);
expect(s._statusUrl).toBe(STATUS_URL);
});
@@ -49,6 +49,19 @@ describe('ShellyEmergencyStop — Konstruktor', () => {
expect(s.state).toBe('ready');
expect(s.error).toBeNull();
});
test('urlOn/urlStatus aus options überschreiben Ableitungslogik (IP statt .local)', () => {
// Typischer Docker-Use-case: url hat noch den .local-Hostnamen,
// aber urlOn/urlStatus zeigen bereits auf die echte IP.
const ipBase = 'http://192.168.0.99';
const s = new ShellyEmergencyStop(OFF_URL, {
urlOn: `${ipBase}/rpc/Switch.Set?id=0&on=true`,
urlStatus: `${ipBase}/rpc/Switch.GetStatus?id=0`,
});
expect(s._offUrl).toBe(OFF_URL); // url bleibt
expect(s._onUrl).toBe(`${ipBase}/rpc/Switch.Set?id=0&on=true`); // explizit
expect(s._statusUrl).toBe(`${ipBase}/rpc/Switch.GetStatus?id=0`); // explizit
});
});
describe('ShellyEmergencyStop — SenderInterface', () => {