WS statt Telnet

This commit is contained in:
chk
2026-03-09 20:15:18 +01:00
parent 9480f40233
commit f2a675a652
8 changed files with 221 additions and 327 deletions

3
.gitignore vendored
View File

@@ -43,6 +43,9 @@ build/Release
node_modules/ node_modules/
jspm_packages/ jspm_packages/
# SSH keys
*.pem
# Snowpack dependency directory (https://snowpack.dev/) # Snowpack dependency directory (https://snowpack.dev/)
web_modules/ web_modules/

View File

@@ -1,88 +0,0 @@
const net = require("net");
class TelnetConnection {
constructor(config){
this.host = config.host;
this.port = config.port;
this.reconnectDelay = config.reconnectDelay || 30000;
this.socket = null;
this.dataListeners = [];
this.connectionListeners = [];
this.connect();
}
connect(){
console.log("Connecting to FluidNC:", this.host);
this.socket = net.createConnection({
host: this.host,
port: this.port
});
this.socket.on("connect",()=>{
console.log("FluidNC connected");
this.connectionListeners.forEach(fn=>fn(true));
});
this.socket.on("data",(data)=>{
const msg = data.toString();
this.dataListeners.forEach(fn=>fn(msg));
});
this.socket.on("close",()=>{
console.log("FluidNC disconnected");
this.connectionListeners.forEach(fn=>fn(false));
setTimeout(()=>{
this.connect();
}, this.reconnectDelay);
});
this.socket.on("error",(err)=>{
console.log("Telnet error:",err.message);
});
}
send(cmd){
if(!this.socket) return;
this.socket.write(cmd+"\n");
}
realtime(cmd){
if(!this.socket) return;
this.socket.write(cmd);
}
onData(fn){
this.dataListeners.push(fn);
}
onConnection(fn){
this.connectionListeners.push(fn);
}
}
module.exports = TelnetConnection;

14
package-lock.json generated
View File

@@ -7,9 +7,11 @@
"": { "": {
"name": "fluidnc-webcontrol", "name": "fluidnc-webcontrol",
"version": "1.0.0", "version": "1.0.0",
"license": "ISC",
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.22.1",
"ws": "^8.16.0" "telnet-stream": "^1.1.0",
"ws": "^8.19.0"
} }
}, },
"node_modules/accepts": { "node_modules/accepts": {
@@ -214,6 +216,7 @@
"version": "4.22.1", "version": "4.22.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"license": "MIT",
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
@@ -712,6 +715,12 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/telnet-stream": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/telnet-stream/-/telnet-stream-1.1.0.tgz",
"integrity": "sha512-saVuav/ScOFlrQXSB8xUqwwIi3sifjSfBiiywGMu7B8wJz60duqFCyG8IhcRjoPpl6VGGkBH+yH3Tnbxh36h1Q==",
"license": "AGPL-3.0"
},
"node_modules/toidentifier": { "node_modules/toidentifier": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -760,6 +769,7 @@
"version": "8.19.0", "version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"license": "MIT",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
}, },

View File

@@ -6,7 +6,17 @@
"start": "node server/server.js" "start": "node server/server.js"
}, },
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.22.1",
"ws": "^8.16.0" "telnet-stream": "^1.1.0",
} "ws": "^8.19.0"
},
"description": "WebPage to show X-Z-Options. The FluidNC is connected to this app.",
"repository": {
"type": "git",
"url": "http://thinkcentre.local:3000/ChK/appRobotControlScara.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs"
} }

View File

@@ -1,8 +1,8 @@
module.exports = { module.exports = {
fluidnc: { fluidnc: {
host: "fluidncsilver.local", host: "fluidncred.local",
port: 81, port: 80,
reconnectDelay: 30000 reconnectDelay: 30000
}, },

View File

@@ -1,104 +1,69 @@
const WebSocket = require("ws");
const EventEmitter = require("events");
const config = require("../config/config"); class FluidNCClient extends EventEmitter {
const net = require("net"); constructor(cfg) {
const { resolve } = require("path"); super();
const TelnetSocket = require("telnet-stream");
this.host = cfg.host;
this.port = cfg.port || 81;
this.ws = null;
this.reconnectDelay = 2000;
class FluidNCClient { this.connect();
constructor() {
console.log("[FluidNCClient] Initializing...");
this.url = config.fluidnc.host;
this.port = config.fluidnc.port || 23;
this.tSocket = null;
this.connected = false;
this.state = { x:0, z:0, state:"unknown" };
this.listeners = [];
this._connect();
} }
_connect() { connect() {
console.log(`[FluidNCClient] Connecting to FluidNC: ${this.url}`); const url = `ws://${this.host}:${this.port}`;
console.log("[FluidNC] Connecting to:", url);
let socket = null; this.ws = new WebSocket(url);
new Promise((resolve, reject) => { this.ws.on("open", () => {
socket = net.createConnection({host:this.url, port:this.port}, () => { console.log("[FluidNC] Connected (WS)");
resolve(socket);
}).on("error", reject);
}).then(connection => {
console.log("[FluidNCClient] Telnet socket connected");
this.connected = true;
connection.on("data", data => {
const msg = data.toString().replace(/\r/g,"").trim();
if(!msg) return;
console.log("[FluidNCClient] Received:", msg);
if(msg.startsWith("<") && msg.includes("MPos:")){
const match = msg.match(/MPos:([\d\.\-]+),[\d\.\-]+,([\d\.\-]+)/);
if(match){
this.state.x = parseFloat(match[1]);
this.state.z = parseFloat(match[2]);
this.state.state = msg.split(",")[0].replace("<","");
this._broadcast(this.state);
}
}
}); });
if(socket != null){ this.ws.on("message", (msg) => {
this.tSocket = TelnetSocket(socket); this.emit("message", msg.toString());
});
this.tSocket.on("close", () => { this.ws.on("close", () => {
console.log("[FluidNCClient] Telnet Closed"); console.log("[FluidNC] Disconnected → retry");
this.tSocket = null; setTimeout(() => this.connect(), this.reconnectDelay);
this.connected = false; });
setTimeout(()=>this._connect(), 30000);
this.ws.on("error", (err) => {
console.log("[FluidNC] WS Error:", err.message);
}); });
} }
}, error => { // --- BASIC COMMANDS ---
console.log("[FluidNCClient] Telnet Connection Error:", error.toString()); sendLine(cmd) {
this.tSocket = null; if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.connected = false; this.ws.send(cmd + "\n");
setTimeout(()=>this._connect(), 30000);
});
}
_broadcast(msg){
this.listeners.forEach(fn => fn(msg));
}
onMessage(fn){
this.listeners.push(fn);
}
send(cmd){
if(this.tSocket){
console.log("[FluidNCClient] Sending:", cmd);
this.tSocket.write(cmd + "\r\n");
} }
} }
jog(axis, value){ requestStatus() {
if(!this.connected) return; this.sendLine("?");
this.send("G91");
this.send(`G0 ${axis.toUpperCase()}${value}`);
this.send("G90");
} }
sendGcode(cmd){ jog(axis, value) {
if(!this.connected) return; const cmd = `$J=G91 ${axis}${value} F2000`;
this.send("G90"); this.sendLine(cmd);
this.send(cmd);
} }
setZero(){ sendGcode(cmd) {
if(!this.connected) return; this.sendLine(cmd);
this.send("G10 L20 P1 X0 Z0"); }
setZero() {
this.sendLine("G92 X0 Y0 Z0");
}
onMessage(fn) {
this.on("message", fn);
} }
} }

View File

@@ -9,88 +9,72 @@ const FluidNCClient = require("./fluidnc/FluidNCClient");
const app = express(); const app = express();
app.use(express.static(path.join(__dirname,"../web"))); // Serve frontend
app.use(express.static(path.join(__dirname, "../web")));
const server = https.createServer({ // HTTPS server
key: fs.readFileSync(path.join(__dirname,"../cert/key.pem")), const server = https.createServer(
cert: fs.readFileSync(path.join(__dirname,"../cert/cert.pem")) {
},app); key: fs.readFileSync(path.join(__dirname, "../cert/key.pem")),
cert: fs.readFileSync(path.join(__dirname, "../cert/cert.pem"))
},
app);
const wss = new WebSocket.Server({server}); // Websocket server (browser connections)
const wss = new WebSocket.Server({ server });
const fluid = new FluidNCClient(); // Create FluidNC WebSocket client
const fluid = new FluidNCClient(config.fluidnc);
// Connected browser clients
let clients = []; let clients = [];
wss.on("connection",(ws)=>{ wss.on("connection", (ws) => {
console.log("[WS] Browser connected");
console.log("[WS] Client connected");
clients.push(ws); clients.push(ws);
ws.on("message",(msg)=>{ ws.on("message", (msg) => {
try {
try{
const data = JSON.parse(msg); const data = JSON.parse(msg);
console.log("[WS] Received message:", data); if (data.type === "jog") {
fluid.jog(data.axis, data.value);
if(data.type==="jog"){
fluid.jog(data.axis,data.value);
} }
if(data.type==="gcode"){ if (data.type === "gcode") {
fluid.sendGcode(data.cmd); fluid.sendGcode(data.cmd);
} }
if(data.type==="zero"){ if (data.type === "zero") {
fluid.setZero(); fluid.setZero();
} }
}catch(e){ } catch (e) {
console.log("[WS] Error parsing:", e);
console.log("[WS] Error parsing message:", e);
} }
}); });
ws.on("close",()=>{ ws.on("close", () => {
clients = clients.filter(c => c !== ws);
clients = clients.filter(c=>c!==ws); console.log("[WS] Browser disconnected");
console.log("[WS] Client disconnected");
}); });
}); });
fluid.onMessage((msg)=>{ // FluidNC → Browser broadcasting
fluid.onMessage((msg) => {
const json = JSON.stringify(msg); clients.forEach(c => {
if (c.readyState === WebSocket.OPEN) {
clients.forEach(c=>{ c.send(msg.toString());
if(c.readyState===WebSocket.OPEN){
c.send(json);
} }
}); });
}); });
server.listen(config.server.port,()=>{ // Status polling ("?" every 200ms)
setInterval(() => {
fluid.requestStatus();
}, 200);
server.listen(config.server.port, () => {
console.log("[Server] Running at:"); console.log("[Server] Running at:");
console.log(`https://localhost:${config.server.port}`); console.log(`https://localhost:${config.server.port}`);
}); });

View File

@@ -1,132 +1,142 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="de">
<head> <head>
<meta charset="UTF-8" />
<title>SCARA Robot Control</title>
<style>
body {
background: #202020;
color: #e0e0e0;
font-family: Arial, sans-serif;
text-align: center;
}
<meta charset="UTF-8"> h1 {
<title>FluidNC Control</title> margin-top: 20px;
}
<style> #posBox {
margin: 20px auto;
width: 300px;
padding: 15px;
background: #333;
border-radius: 10px;
font-size: 20px;
}
body{ .btnGrid {
font-family:Arial; display: grid;
margin:40px; grid-template-columns: repeat(3, 120px);
} gap: 10px;
justify-content: center;
margin-top: 30px;
}
button{ button {
width:60px; background: #444;
height:40px; border: none;
margin:3px; padding: 15px;
} font-size: 18px;
color: white;
.axis{ border-radius: 8px;
margin-bottom:30px; cursor: pointer;
} }
.status{
font-size:20px;
margin-bottom:20px;
}
</style>
button:hover {
background: #666;
}
</style>
</head> </head>
<body> <body>
<h2 id="connection">Trying to connect to FluidNC...</h2> <h1>SCARA Robot Control</h1>
<div class="axis"> <div id="posBox">
<div>X: <span id="posX">0.000</span></div>
<div>Z: <span id="posZ">0.000</span></div>
</div>
<h3>X Axis</h3> <h2>Jog Controls</h2>
<button onclick="jog('x',-10)">-10</button> <div class="btnGrid">
<button onclick="jog('x',-1)">-1</button> <button onclick="jog('X', -10)">X -10</button>
<button></button>
<button onclick="jog('X', 10)">X +10</button>
<span id="x">0</span> <button onclick="jog('Z', 10)">Z +10</button>
<button></button>
<button onclick="jog('Z', -10)">Z -10</button>
</div>
<button onclick="jog('x',1)">+1</button> <script>
<button onclick="jog('x',10)">+10</button> let ws;
</div> function connectWS() {
ws = new WebSocket("wss://" + window.location.hostname + ":3000");
<div class="axis"> ws.onopen = () => {
console.log("WS connected");
};
<h3>Z Axis</h3> ws.onclose = () => {
console.warn("WS disconnected → reconnecting...");
setTimeout(connectWS, 1000);
};
<button onclick="jog('z',-10)">-10</button> ws.onmessage = (ev) => {
<button onclick="jog('z',-1)">-1</button> const text = ev.data;
<span id="z">0</span> // GRBL/FluidNC status line?
if (text.startsWith("<")) {
handleStatus(text);
return;
}
<button onclick="jog('z',1)">+1</button> // Try JSON only if valid JSON
<button onclick="jog('z',10)">+10</button> try {
const data = JSON.parse(text);
console.log("JSON:", data);
} catch (e) {
// non JSON → ignore
}
};
</div> ws.onerror = (e) => console.error("WS error:", e);
}
<h3>GCode</h3> connectWS();
<input id="gcode" style="width:300px"/> // --- Jog-Funktion ---
<button onclick="sendGcode()">Send</button> function jog(axis, value) {
ws.send(JSON.stringify({
type: "jog",
axis: axis,
value: value
}));
}
<br><br> // --- GRBL Status Parser ---
function handleStatus(line) {
<button onclick="setZero()">Set Current Position to Zero</button> let match = line.match(/WPos:([^|>]+)/);
if (!match) {
match = line.match(/MPos:([^|>]+)/);
if (!match) return; // Keiner von beiden vorhanden
}
<script>
const ws = new WebSocket("wss://"+location.host); const coords = match[1].split(",").map(Number);
ws.onmessage = (e)=>{ // Nur X (Index 0) und Z (Index 2) anzeigen
const x = coords[0];
const z = coords[2];
const data = JSON.parse(e.data);
if(data.type==="connection"){ document.getElementById("posX").textContent = x.toFixed(3);
document.getElementById("posZ").textContent = z.toFixed(3);
if(data.connected){ }
document.getElementById("connection").innerText="FluidNC connected"; </script>
}else{
document.getElementById("connection").innerText="Trying to connect to FluidNC...";
}
}
if(data.type==="status"){
document.getElementById("x").innerText=data.x.toFixed(2);
document.getElementById("z").innerText=data.z.toFixed(2);
}
};
function jog(axis,val){
ws.send(JSON.stringify({
type:"jog",
axis:axis,
value:val
}));
}
function sendGcode(){
ws.send(JSON.stringify({
type:"gcode",
cmd:document.getElementById("gcode").value
}));
}
function setZero(){
ws.send(JSON.stringify({
type:"zero"
}));
}
</script>
</body> </body>
</html> </html>