// WebService.js - robust WebSocket wrapper (drop-in replacement) (function () { const startTime = Date.now(); console.log("Meine Document-Location:", document.location); const socketUrl = (location.protocol === "https:" ? "wss://" : "ws://") + window.location.host + "/echo"; console.log("WebSocket URL:", socketUrl); // Expose global socket object immediately so other scripts never see "socket is not defined" window.socket = { // internal _ws: null, _queue: [], _retry: 0, _pingIntervalId: null, _lastPingRequest: null, _connectedOnce: false, // user hooks (can be assigned by UI code) onMessage: null, onOpen: null, onClose: null, // ------ API: send / sendCommand (legacy & recommended) ------ // raw send (backwards compatible) send(msg) { this._enqueueOrSend(msg); }, // preferred semantic method for commands sendCommand(cmd) { // you can add validation/normalization here this._enqueueOrSend(cmd); }, // convenience robot command moveZ(value, feed = 100) { // positive value moves up; negative moves down depending on your G-code semantics this.sendCommand(`G91 G1 Z${value} F${feed}`); }, // manual control to connect/disconnect connect() { _connect(this); }, disconnect() { _disconnect(this); }, isOpen() { return this._ws && this._ws.readyState === WebSocket.OPEN; }, // internal helper: enqueue if not open, otherwise send immediately _enqueueOrSend(msg) { try { if (this._ws && this._ws.readyState === WebSocket.OPEN) { this._ws.send(msg); console.log("WS SEND:", msg); } else { // queue to be flushed on open console.warn("WS not ready → queueing:", msg); this._queue.push(msg); } } catch (e) { console.error("WS send failed:", e, " (queued )"); this._queue.push(msg); } } }; // ---------- internal functions ---------- function _flushQueue(sock) { while (sock._queue.length) { const m = sock._queue.shift(); try { if (sock._ws && sock._ws.readyState === WebSocket.OPEN) { sock._ws.send(m); console.log("Flushed queued msg:", m); } else { // push back and stop flushing sock._queue.unshift(m); break; } } catch (e) { console.warn("Failed flushing queued msg, requeueing", e); sock._queue.unshift(m); break; } } } function _startPing(sock) { if (sock._pingIntervalId) return; sock._pingIntervalId = setInterval(() => { if (sock._ws && sock._ws.readyState === WebSocket.OPEN) { sock._lastPingRequest = Date.now(); sock._enqueueOrSend("Ping"); } }, 5000); } function _stopPing(sock) { if (sock._pingIntervalId) { clearInterval(sock._pingIntervalId); sock._pingIntervalId = null; } } function _connect(sock) { // prevent duplicate connects if (sock._ws && (sock._ws.readyState === WebSocket.CONNECTING || sock._ws.readyState === WebSocket.OPEN)) { console.log("WebSocket already connecting/open - skipping connect()"); return; } try { console.log("WebSocket: connecting to", socketUrl); const ws = new WebSocket(socketUrl); sock._ws = ws; ws.onopen = () => { console.log("Connected to WS Server"); sock._connectedOnce = true; sock._retry = 0; // reset retries _flushQueue(sock); _startPing(sock); if (typeof sock.onOpen === "function") { try { sock.onOpen(); } catch (e) { console.error("onOpen handler error", e); } } }; ws.onmessage = (event) => { const data = event.data ? event.data.toString() : ""; // built-in handling (keeps backward compatible behaviour) if (data === "Ping") { if (sock._lastPingRequest) { console.log("Ping:", Date.now() - sock._lastPingRequest, "ms"); } else { console.log("Ping (no timestamp)"); } } else if (data.includes("position")) { console.log("Position:", data); } else if (data.includes("XYZ__FShow__XYZ")) { const content = data.split("XYZ__FShow__XYZ")[1] || ""; console.log("FShow content received, length", content.length); const el = document.querySelector("textarea#GCodeWindow.editor-look"); if (el) el.value = content; } else if (data.startsWith("{")) { try { const json = JSON.parse(data); if (json.programId === "log" && Array.isArray(json.lines)) { console.log("FShow JSON received, lines:", json.lines.length, "cursor:", json.cursor); const el = document.getElementById("GCodeWindow"); if (el) { el.value = json.lines.join("\n"); // Scroll to cursor line const lineHeight = el.scrollHeight / json.lines.length; el.scrollTop = Math.max(0, (json.cursor - 5)) * lineHeight; } } } catch (e) { console.log("DATA:", Date.now() - startTime, data); } } else { console.log("DATA:", Date.now() - startTime, data); } // call user onMessage hook if present if (typeof sock.onMessage === "function") { try { sock.onMessage(data); } catch (e) { console.error("onMessage handler error", e); } } }; ws.onclose = (event) => { const reason = event.reason || event.code || "closed"; console.log(event.wasClean ? "Disconnected" : "Connection break: " + reason); _stopPing(sock); if (typeof sock.onClose === "function") { try { sock.onClose(event); } catch (e) { console.error("onClose handler error", e); } } // exponential backoff with small jitter sock._retry = (sock._retry || 0) + 1; const base = 1000; const delay = Math.min(30000, base * Math.pow(2, sock._retry - 1)) + Math.floor(Math.random() * 300); console.log("Reconnect in", delay, "ms (attempt", sock._retry, ")"); setTimeout(() => _connect(sock), delay); }; ws.onerror = (err) => { // some browsers call onerror right before onclose; log only console.error("WebSocket error:", err && (err.message || err)); }; } catch (e) { console.error("WebSocket create failed", e); // schedule reconnect sock._retry = (sock._retry || 0) + 1; const delay = Math.min(30000, 1000 * Math.pow(2, sock._retry - 1)) + Math.floor(Math.random() * 300); setTimeout(() => _connect(sock), delay); } } function _disconnect(sock) { try { _stopPing(sock); if (sock._ws) { try { sock._ws.close(); } catch (e) { /* ignore */ } sock._ws = null; } } catch (e) { console.error("Error during disconnect", e); } } // Kick off initial connection window.socket.connect(); // Expose some helpful debug tools in the console window.__socket_debug = { getQueue: () => window.socket._queue.slice(), getState: () => (window.socket._ws ? window.socket._ws.readyState : null), flush: () => _flushQueue(window.socket), reconnect: () => { _disconnect(window.socket); window.socket._retry = 0; window.socket.connect(); } }; // Optional: if you want a simple rate limiter later, you can add code here. // Keep GamePad traffic in mind — don't flood the queue on reconnect. })();