In this part of the 3018 Desktop Router project, I setup a permanent home for CNCjs on a Dell Wyse 3040 thin client. I’m running CNCjs as the CNC control software and G-code sender (the CNC’s grbl controller is actually doing the motion control). I’m using mjpg-streamer to add a USB webcam to the CNCjs web UI, with nearly no load on the CPU to encode. And I’ve setup a script to launch ffmpeg to record the mjpeg stream when g-code is started and stopped (also using nearly no load on the CPU to transcode). As a cherry on top, I’m running all of this with systemd services so everything autostarts properly on boot without the hacky cron suggestions of the CNCjs docs.

Any Linux single board computer will of course work, including a raspberry pi, but the 3040s are really cheap and tiny, and come with a case and storage for less than the cost of a bare Pi.

This video is the part of the CNC Router Megaproject.



Click the thumbnail to view the video on Youtube Thumbnail


The actual web UI I’ve chosen is CNCjs, a node.js based web app for controlling CNC machines which use an open source control board based on grbl, Smoothieware, or TinyG. It’s actually just a fancy G-code sender, interpretation of the G-code and motion control is done by the router’s control board (which in many cases runs grbl).


CNCjs is written in node.js and available through the node package manager. To install it though, we first must install node.js (package nodejs) and the node package manager (npm) - this will take a very long time:

sudo apt install nodejs npm

Once complete, we can use npm to install cncjs using node:

sudo npm install -g cncjs --unsafe-perms

CNCjs Test Run

To test cncjs, we should be able to just run it as the current user - try this command:

cncjs --allow-remote-access -p 8080

Then we can edit .cncrc to add allowRemoteAccess so we don’t need to pass it as an argument any more. See my final .cncrc at the end of this post for the configuration I ended up with.

To give us permissions to run on port 80 without root, we could delegate this capability to the cncjs binary (/usr/local/bin/cncjs, which is actually a symlink into /usr/local/lib/node_modules/). But, actually, we need to give this to the node.js binary, since that’s the process that ultimately will run and needs the permissions.

sudo setcap CAP_NET_BIND_SERVICE=+eip /usr/bin/node

Also, to give permissions to use the serial port, we need to add our local user to the group dialout:

sudo usermod -a -G dialout discovery

CNCjs Systemd Service

This will make CNCjs run as a system service. We can do the usual stuff with systemctl, which is how services should be managed on distros which use systemd. So, let’s create the service file and edit it:

sudo touch /etc/systemd/system/cncjs.service
sudo chmod 664 /etc/systemd/system/cncjs.service
sudo nano /etc/systemd/system/cncjs.service

And now the service contents. Notably, running as our local user in the local user’s home directory.

Description=CNC Controller Web UI

ExecStart=cncjs -p 80


Now, we can start and enable the service:

sudo systemctl daemon-reload
sudo systemctl start cncjs
sudo systemctl enable cncjs

Webcam Streaming

We have a few pieces of software to install. Notably, v4l2-ctl and mjpg-streamer along with dependencies.


If you want to see the capabilities of your camera or identify which one is which, you can use v4l2-ctl to do so. If you only have one camera and assume it supports MJPEG and is /dev/video0, you can skip this if you’d like.

Install it using apt:

sudo apt install v4l-utils

List devices - sudo v4l2ctl --list-devices

If you have more than one device (not /dev/video* nodes, actual devices) you can specify the device using -d /dev/videoX in the following commands. If you have multiple cameras, then do this for each.

Then list formats - sudo v4l2-ctl --list-formats - it should show MJPEG as an option. If it doesn’t, all will still work fine, but the CPU will encode the images as JPEG and it will put more load on the processor.

You can also do sudo v4l2-ctl --all to see everything. This should include the maximum resolution for the camera. You’ll need to note that for later.


mjpg-streamer is a project which was originally designed to allow extremely low resource Linux systems to host camera streams, by relying entirly on the webcam hardware to JPEG-encode each image (resulting in a motion-JPEG stream). mjpg-streamer then only has to deal with shuffling bytes around to the HTTP clients. It’s been updated to support the Pi camera (if you want to use that), and can also encode to MJPEG if the camera doesn’t support it (at the cost of higher CPU usage). Since we need an HTTP MJPEG stream for the CNCjs camera widget, we can’t use h.264 here, so relying on the camera’s MJPEG codec is the lowest resource way to achieve this. The bitrate is fairly high (~35mbps for my setup) which will make the recordings bigger, but you can always transcode the recordings before keeping them.

Anyway, let’s install it. In this case, it’s not available from apt, so we are going to install dependencies through apt and then build it from source.

sudo apt install cmake libjpeg8-dev
git clone
cd mjpg-streamer/mjpg-streamer-experimental
sudo make install

Webcam Test Run

mjpg-streamer has different modules to deal with input and output, so in this case we are using input_uvc which deals with v4l2 devices and output_http which provides an http webserver for streams and snapshots. You can pass arguments to both modules, in this case I’m passing a device and resolution (make sure to choose something your device supports!).

sudo mjpg_streamer -i " -d /dev/video0 -r 1920x1080" -o " -p 8080"

You can leave out the device option if you only have one camera. We need to use sudo to gain permissions to access the /dev/video devices, since we aren’t adding ourselves to the video group. Systemd will run the service as root by default anyway.

Webcam Systemd Service

First, create the script - you can replace webcamd with whatever you want, i.e. if you are using multiple cameras

sudo touch /etc/systemd/system/webcamd.service
sudo chmod 664 /etc/systemd/system/webcamd.service
sudo nano /etc/systemd/system/webcamd.service

And now the service contents. Make sure your devices supports the resolution you specify!

Description=Webcam Stream

ExecStart=mjpg_streamer -i " -d /dev/video0 -r 1920x1080" -o " -p 8080"


Now, we can start and enable the service:

sudo systemctl daemon-reload
sudo systemctl start webcamd
sudo systemctl enable webcamd


Finally, nearing the end of this big software dump. The final step is to start and stop recordings from CNCjs, preferably automatically, in a location we can access easily, and also with the ability to manually start and stop recordings.


We will record the stream with ffmpeg. Depending on the GPU you are running on, you may be able to use libva hardware acceleration. In my setup, I am using mjpg passthrough, so the resulting file is a .mjpeg and is not transcoding to a more space efficient codec. This reduces CPU usage dramatically if libva doesn’t support both MJPEG decoding and h.264 encoding on your hardware. Not many programs can play back the .mjpeg file directly, but you can always transcode it to h.264 in ffmpeg or Handbrake for a more universal format (and you probably should to keep file sizes reasonable).

ffmpeg -i "http://localhost:8080/?action=stream" -codec copy /mnt/cnc/test1.mjpeg

Network Mount using Autofs

I’ve chosen to network mount my recordings directory using autofs. You can use a directory on the system if you have enough space. My 3040 thin client has 8GB of emmc, so it can’t really fit any recordings. Here’s the basic process to setup autofs for this use case:

  1. Install autofs and the cifs (samba) client: sudo apt install autofs cifs-utils
  2. Create a mount point: sudo mkdir /mnt/cnc
  3. Edit /etc/auto.master using sudo and add the following line: /- /etc/auto.smb.shares --timeout 15 browse
  4. Add a new share in that new file, again as root:
/mnt/cnc -fstype=cifs,rw,username=<user>,password=<password>,noperm ://server/share/directory
  1. Then enable and start autofs
sudo systemctl enable autofs
sudo systemctl restart autofs
  1. You should be able to see the network share - ls /mnt/cnc

Start/Stop Scripts

Now that we have a place to store the recordings and a command to record, we need to be able to spawn this from cncjs on command. Here are the scripts to do that with ~/

#Date/time stamp for new recording
fname=$(date +%F_%H-%M-%S)
#Call ffmpeg with filename cnc_<date>.mjpeg in the background
ffmpeg -i "http://localhost:8080/?action=stream" -codec copy /mnt/cnc/cnc_$fname.mjpeg -nostats &
#End terminal session

And to stop, we kill ffmpeg with ~/ and it will clean up itself nicely:

killall ffmpeg

Final CNCjs Configuration

Here is my final configuration file for CNCjs, in case you’d like to copy anything from it. All of this can be configured via the web UI if you like as well.

    "state": {
        "checkForUpdates": true,
        "controller": {
            "exception": {
                "ignoreErrors": false
    "secret": "$2a$10$FBerJXHRoBaO0YD4nVZUM.",
    "allowRemoteAccess": true,
    "commands": [
            "id": "727d8fa0-0790-44f0-a9bb-6fcb992fab2d",
            "mtime": 1665444504169,
            "enabled": true,
            "title": "Start Record",
            "commands": "/home/discovery/"
            "id": "ec02236a-529d-4fba-a575-70fbce559222",
            "mtime": 1665444509575,
            "enabled": true,
            "title": "Stop Record",
            "commands": "/home/discovery/"
    "events": [
            "id": "80a0b85d-bcab-414a-9c22-0d8a25a83368",
            "mtime": 1665444515639,
            "enabled": true,
            "event": "gcode:start",
            "trigger": "system",
            "commands": "/home/discovery/"
            "id": "811d3570-9200-4b9f-88db-38b518f870f6",
            "mtime": 1665444519136,
            "enabled": true,
            "event": "gcode:stop",
            "trigger": "system",
            "commands": "/home/discovery/"

Parts and Links

Some of these may be affiliate links, which may earn a commission for me.