Heartbeat
This commit is contained in:
@@ -13,9 +13,9 @@ 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'] },
|
||||
elbow: { ip: 'fluidNcEllbow.local', port: 5000, protocol: 'telnet', axes: ['a', null, null] },
|
||||
hand: { ip: 'fluidNcHand.local', port: 5000, protocol: 'telnet', axes: ['c', 'e', 'b'] }
|
||||
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 }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -84,10 +84,13 @@ function load(fsModule, processEnv, consoleObj) {
|
||||
const cfg = jsonControllers[key] ?? {};
|
||||
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
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -36,8 +36,18 @@ module.exports = class TelnetSenderGRBL extends SenderInterface {
|
||||
this.TelnetSocketClass = options.TelnetSocketClass || TelnetSocket;
|
||||
this.setTimeoutFn = options.setTimeoutFn || setTimeout;
|
||||
this.clearTimeoutFn = options.clearTimeoutFn || clearTimeout;
|
||||
this.setIntervalFn = options.setIntervalFn || setInterval;
|
||||
this.clearIntervalFn = options.clearIntervalFn || clearInterval;
|
||||
this.reconnectDelay = Number.isFinite(options.reconnectDelay) ? options.reconnectDelay : 1000;
|
||||
this.maxReconnectDelay = Number.isFinite(options.maxReconnectDelay) ? options.maxReconnectDelay : 30000;
|
||||
// Heartbeat: erkennt tote Verbindungen (z.B. nach NotAus).
|
||||
// Sendet alle heartbeatInterval ms '?' an den Controller.
|
||||
// Kommt deadTimeout ms lang keine Antwort, gilt die Verbindung als tot.
|
||||
this.heartbeatInterval = Number.isFinite(options.heartbeatInterval) ? options.heartbeatInterval : 10000;
|
||||
this.deadTimeout = Number.isFinite(options.deadTimeout) ? options.deadTimeout : 20000;
|
||||
this._heartbeatTimer = null;
|
||||
this._lastDataAt = 0;
|
||||
this._rawSocket = null;
|
||||
this.reconnectAttempt = 0;
|
||||
this.reconnectTimer = null;
|
||||
this.shouldReconnect = true;
|
||||
@@ -108,10 +118,18 @@ module.exports = class TelnetSenderGRBL extends SenderInterface {
|
||||
return;
|
||||
}
|
||||
|
||||
this._rawSocket = socket;
|
||||
|
||||
// TCP-Keepalive aktivieren: OS sendet Keepalive-Proben auf Netzwerk-Ebene.
|
||||
// Schützt als Fallback, falls der Heartbeat-Timer ausfällt.
|
||||
socket.setKeepAlive(true, 2000);
|
||||
|
||||
this.tSocket = new this.TelnetSocketClass(socket);
|
||||
this.tSocket.on('close', () => {
|
||||
console.log("Telnet Closed " + this.urlGRBLstr);
|
||||
this._stopHeartbeat();
|
||||
this.tSocket = null;
|
||||
this._rawSocket = null;
|
||||
if (this.shouldReconnect) {
|
||||
this.state = 'reconnecting';
|
||||
this.scheduleReconnect();
|
||||
@@ -120,7 +138,9 @@ module.exports = class TelnetSenderGRBL extends SenderInterface {
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('data', () => {});
|
||||
// Zeitstempel jeder eingehenden Nachricht festhalten (für Heartbeat-Timeout).
|
||||
socket.on('data', () => { this._lastDataAt = Date.now(); });
|
||||
|
||||
this.state = 'connected';
|
||||
this.error = null;
|
||||
this.reconnectAttempt = 0;
|
||||
@@ -130,6 +150,9 @@ module.exports = class TelnetSenderGRBL extends SenderInterface {
|
||||
this.connectRejecter = null;
|
||||
}
|
||||
this.connectPromise = null;
|
||||
|
||||
// Heartbeat starten: erkennt NotAus / tote Verbindungen.
|
||||
this._startHeartbeat();
|
||||
});
|
||||
|
||||
socket.on('error', (error) => {
|
||||
@@ -167,6 +190,54 @@ module.exports = class TelnetSenderGRBL extends SenderInterface {
|
||||
}, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Startet den Heartbeat-Timer.
|
||||
*
|
||||
* Sendet alle `heartbeatInterval` ms den FluidNC-Realtime-Command '?' und
|
||||
* prüft, ob seit `deadTimeout` ms keine Daten mehr empfangen wurden.
|
||||
* Bleibt die Antwort aus (z.B. nach NotAus), wird der Socket zerstört —
|
||||
* der bestehende 'close'-Handler leitet danach den Reconnect ein.
|
||||
* @private
|
||||
*/
|
||||
_startHeartbeat() {
|
||||
this._stopHeartbeat();
|
||||
this._lastDataAt = Date.now();
|
||||
|
||||
this._heartbeatTimer = this.setIntervalFn(() => {
|
||||
if (!this.tSocket) {
|
||||
this._stopHeartbeat();
|
||||
return;
|
||||
}
|
||||
|
||||
const silent = Date.now() - this._lastDataAt;
|
||||
if (silent >= this.deadTimeout) {
|
||||
console.log(
|
||||
`[TelnetSenderGRBL] ${this.urlGRBLstr}: ` +
|
||||
`keine Daten seit ${silent}ms (deadTimeout=${this.deadTimeout}ms) — ` +
|
||||
`Verbindung wird als tot eingestuft und beendet`
|
||||
);
|
||||
this._stopHeartbeat();
|
||||
// rawSocket.destroy() löst 'close' auf tSocket aus → bestehende Reconnect-Logik
|
||||
if (this._rawSocket) this._rawSocket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
// Leichtgewichtiger Probe: FluidNC antwortet mit <Idle|…> oder <Run|…>
|
||||
try { this.tSocket.write('?'); } catch { /* Fehler kommt über 'error'-Event */ }
|
||||
}, this.heartbeatInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stoppt den Heartbeat-Timer.
|
||||
* @private
|
||||
*/
|
||||
_stopHeartbeat() {
|
||||
if (this._heartbeatTimer) {
|
||||
this.clearIntervalFn(this._heartbeatTimer);
|
||||
this._heartbeatTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
send(command) {
|
||||
if (!this.tSocket || typeof this.tSocket.write !== 'function') {
|
||||
return false;
|
||||
@@ -193,8 +264,11 @@ module.exports = class TelnetSenderGRBL extends SenderInterface {
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this._stopHeartbeat();
|
||||
|
||||
if (this.isTestMode) {
|
||||
this.tSocket = null;
|
||||
this._rawSocket = null;
|
||||
this.state = 'disconnected';
|
||||
this.shouldReconnect = false;
|
||||
if (this.reconnectTimer) {
|
||||
|
||||
Reference in New Issue
Block a user