Re: CNC Gui Thoughts
I read the original CNC Gui Thoughts post and was late to the game. I'm reviving it with some of my thoughts, based on what I've done. I'd love to see more CNC specific features and customization down the road.
I'm new to CNC with a background in 3D printing and software engineering. I write a lot of website control pages for my primary job, but not in Vue. Initially I planned on customizing the Duet UI from the open-source code; however, the learning curve for editing the Duet3D Web UI was higher than writing my own website, watching the REST and WS networking traffic, and replicating that. I created this WebUI in NodeJS/ReactJS, which I'm more comfortable with than Vue and other people's code.
Here's some of the thoughts I've had with the Duet3D Web UI that inspired this:
- Movement buttons should disable when movement not allowed.
- Layout of the movement buttons puts a strain on my brain
- Less jumping around between UI pages
- Clearer workplace selections
- Less empty space (CSS margins/padding)
- Cellphone layout removes some movement options (5 to 3 buttons of movement)
- Lack of GCode Viewer based on current tool position in a CNC non FDM view
- I see a lot of discussion about plugins, love to know where people get external plugins from
For me, the most useful part of the custom UI is the layout of the movement buttons, clearer workspace coordinates, enabled/disabled buttons based on machine state, and having everything in one page that fits on my cellphone (STOP always available across the top). The layout is design consideration is cellphone first, then tablet, then computer.
The image below is a collage of the UI. Several screenshots of the buttons where the machine position is different and the movement buttons are enabled/disabled based on limits. All information comes from the network traffic, which in-turn comes from the config files. This means there's no hard-coded limits based on my machine. And I wrote the Gcode viewer in ThreeJS, not my favorite thing to do, but I turned out nicely.
Collage of features
I'm still in the CNC building process, and have not cut anything, but have run it as a pen drawing CNC. Most of my UI redesign came from my inexperience with CNC and its learning curve. The primary lesson learned is CRAHSING the CNC by accidentally clicking on the wrong thing during the configuration/build phase of this process. I'm sure I'll understand some features down the road that I've currently misinterpreted and this UI will evolve.
There's some cool (from a hack perspective) benefits to this design; I consider it a benefit at least. For starters, this is it's own website with a back-end and a front-end. It doesn't replace the Duet3D Web UI; instead, it enhances it. The hack is to use Nginx as a proxy redirect based on URL paths and interact with the REST/WS traffic that already exists. From what I can tell, there is CORS requirements to come from the same domain for some network commands; so, here's how this setup works in Nginx.
Update: More specifics on how/why is covered over here: https://forum.duet3d.com/post/310830
Web Server (reused one I already have, could use a RPi or really anything with linux)
- Domain ($12 a year routed to your IP-Address)
- Nginx (port-forward 80/443 from router to Web Server for proxy-redirect)
- Custom CNC UI Website (REST/WS)
SBC with Duet3 6HC
I don't know how many people really care about this, but I've learned a lot from user posts over the years that inspired my own projects, so I'm going all-in on posting this here.
Nginx Server Config File (I've removed the Certbot https config portion as it's not relevant here, but add it to simplify the Chrome HTTPS experience)
File: /etc/nginx/sites-enabled/cnc.DOMAIN-NAME
server {
server_name cnc.DOMAIN-NAME;
access_log /var/log/nginx/cnc.DOMAIN-NAME.access.log main;
listen 80;
client_max_body_size 90M;
allow 192.168.1.0/24; # allow your local subnet
deny all; # prevent external access
# route the Duet3D traffic to the SBC
location / {
proxy_pass http://SBC-IP-ADDRESS/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $http_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 1d;
}
# Route 'server' path traffic to custom website (32222 is some random port I've chosen)
location /server {
proxy_pass http://localhost:32222/server;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $http_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 1d;
}
# route 'development' to my development computer's custom website
location /development{
proxy_pass http://DEVELOPMENT-IP:32222/development;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $http_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 1d;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
This configuration allows the following:
The website's index lives where the domain path root is. For anyone familiar with expressJS, that would look like this
File server.ts
...
import { router } from './routes/rest';
app.use(`/${process.env.URI_PATH}`, router);
...
Where process.env.URI_PATH comes from an environment variable file to use server or development depending on where it's running.
File .env
URI_PATH=server
The networking to control the Duet takes a little click-n-watch of the network traffic and is too much to get into here. Let me know if this kind of 'hack' project is useful to anyone.
If you've read this far, here's some more code for you. This is to relay some REST controls that have CORS domain requirements. The web socket is handled from the browser front-end to the Duet3D SBC as CORS isn't a thing.
File: ./routes/rest.ts
import express from 'express';
export const router = express.Router();
const https = require('https');
router.get('/', (req, res) => {
res.render('index');
});
router.get('/cnc/connect', (req, res) => {
const server = https.get(
'https://cnc.DOMAIN-NAME/machine/connect?password=reprap',
(response) => {
if (response.statusCode !== 200) {
res.sendStatus(503);
return;
}
res.sendStatus(200);
},
);
});
router.get('/cnc/settings', async (req, res) => {
getDuet(
'https://cnc.DOMAIN-NAME/machine/file/0%3A%2Fsys%2Fdwc-settings.json',
res,
);
});
router.get('/files/sys', (req, res) => {
getDuet('https://cnc.DOMAIN-NAME/machine/directory/0%3A%2Fsys', res);
});
router.get('/files/macros', (req, res) => {
getDuet('https://cnc.DOMAIN-NAME/machine/directory/0%3A%2Fmacros%2F', res);
});
router.get('/jobs', (req, res) => {
getDuet('https://cnc.DOMAIN-NAME/machine/directory/0%3A%2Fgcodes%2F', res);
});
router.get('/file/:name', (req, res) => {
let { name } = req.params;
getDuetFile(
`https://cnc.DOMAIN-NAME/machine/file/0%3A%2Fgcodes%2F${name}`,
res,
);
});
router.get('/cnc/move/:value', async (req, res) => {
let { value } = req.params;
let commands = ['M120', 'G91', `G1 ${value} F6000`, 'G90', 'M121'];
let command = formatGcode(commands);
console.log('/cnc/move/:value', command);
const options = {
hostname: 'cnc.DOMAIN-NAME',
port: 443,
path: '/machine/code?async=true',
method: 'POST',
};
const request = https.request(options, (response) => {
console.log('statusCode:', res.statusCode);
console.log('headers:', res.headers);
response.on('data', (d) => {
process.stdout.write(d);
});
});
request.write(command);
request.end();
res.sendStatus(200);
});
router.get('/cnc/stop', async (req, res) => {
let { value } = req.params;
let commands = ['M112', 'M999'];
let command = formatGcode(commands);
console.log('/cnc/stop', command);
const options = {
hostname: 'cnc.DOMAIN-NAME',
port: 443,
path: '/machine/code',
method: 'POST',
};
const request = https.request(options, (response) => {
console.log('statusCode:', res.statusCode);
console.log('headers:', res.headers);
response.on('data', (d) => {
process.stdout.write(d);
});
});
request.write(command);
request.end();
res.sendStatus(200);
});
router.post('/cnc/commands', async (req, res) => {
let commands = req.body;
let command = formatGcode(commands);
console.log('/cnc/commands', command);
const options = {
hostname: 'cnc.DOMAIN-NAME',
port: 443,
path: '/machine/code',
method: 'POST',
};
const request = https.request(options, (response) => {
console.log('statusCode:', res.statusCode);
console.log('headers:', res.headers);
response.on('data', (d) => {
process.stdout.write(d);
});
});
request.write(command);
request.end();
res.sendStatus(200);
});
function formatGcode(commands) {
return commands.join('\n');
}
function getDuet(url, res) {
https.get(url, (response) => {
if (response.statusCode !== 200) {
res.sendStatus(503);
return;
}
let data = '';
response.on('data', (chunk) => {
data += chunk;
});
response.on('close', () => {
console.log('Retrieved all data');
res.send(data);
});
});
}
function getDuetFile(url, res) {
https.get(url, (response) => {
if (response.statusCode !== 200) {
res.sendStatus(503);
return;
}
let data = '';
response.on('data', (chunk) => {
data += chunk;
});
response.on('close', () => {
console.log('Retrieved all data');
res.send({ contents: data });
});
});
}