Skip to content

About

Turn the LEDs of the 3DO Nozzle Camera V2 (Sony 4K) on and off from Klipper/Mainsail - no firmware flashing, no extra wiring. Survives reboots and USB re-plugs, with a toggle button right in the webcam panel.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

3DO Nozzle Camera V2 — Turn the FPC LEDs On and Off (Klipper / Mainsail)

The LEDs on the 3DO nozzle camera's FPC cable are software-controllable. No firmware flashing, no extra wiring, no soldering. They can be switched from a shell command, kept off across reboots and USB re-plugs, and driven from a toggle button placed directly in the webcam panel of your dashboard.

Hardware this was built and verified on: 3DO Nozzle Camera Kit V2 — Sony 4K (3do.dk, product 2681), glued fixed-focus variant, LED FPC cable, on a Voron 2.4 with Klipper + Mainsail + crowsnest on a Raspberry Pi 5.

LEDs off LEDs on
Webcam panel, LEDs off Webcam panel, LEDs on

The same webcam panel, one click apart. Left: backlight_compensation=0, the nozzle is lit only by ambient light. Right: =2, the two FPC LEDs light the print surface. The toggle sits in the panel's footer bar. (UI shown in French: "LED éteintes" = LEDs off, "LED allumées" = LEDs on.)


Step 0 — Install the camera first

This guide assumes you already have a working 3DO nozzle camera streaming in Mainsail. It only covers the LEDs. For the camera itself — mounting, FPC routing, DSP board, wiring, firmware and the basic crowsnest setup — follow the manufacturer's documentation first:

Three points from that documentation deserve to be repeated, because getting them wrong is expensive:

  • FPC orientation is not optional. The white silkscreen dots on the sensor board and on the DSP board must face each other. Plugging the flat cable in backwards damages the camera. Check twice before powering up.
  • Bend the FPC at most once, 180°, at a single spot. On a moving toolhead, prefer a wide curve over a fold, and take up the slack by positioning the DSP board rather than coiling the cable.
  • The glued variant is fixed focus at 34.5 mm, measured from the back of the sensor PCB to the subject. There is no adjustment after the fact — the mount has to place it right.

Once the stream shows up in Mainsail, come back here and the LEDs take about five minutes.

⚠️ Use 3DO's documentation, but do not contact them about this guide. They are the right address for the camera, its firmware and their own instructions — not for anything written here, which they neither wrote nor reviewed.


The problem

You install the LED version of the FPC cable, and the LEDs are on permanently — during prints, at night, forever. Nothing in the Mainsail UI turns them off.

The reason is a combination of two defaults, and neither is a bug:

  1. The camera firmware exposes a control called backlight_compensation whose default value is 1, which means "LEDs on while the video stream is open".
  2. crowsnest / ustreamer keeps that stream open 24/7.

So "on while streaming" silently becomes "on all the time".

The control

The camera is a standard UVC device (Sunplus DSP, USB ID 1bcf:28c4). The LEDs are driven through a regular V4L2 control:

backlight_compensation Effect
0 LEDs off
1 LEDs on while the stream is open (firmware default)
2 LEDs always on

This mapping is documented by 3DO in the official V2 repository — it has been public since June 2024, buried in a table of twelve V4L2 controls with no explanation, which is why almost nobody knows it exists.

⚠️ Don't generalise from the focus controls. On the glued fixed-focus variant, focus_absolute and focus_automatic_continuous are exposed by the firmware but do nothing — they are dummies. It is tempting to conclude that the LED control is a dummy too. It is not. Each control has to be tested on its own.


TL;DR — one command

# Resolve your device once. The by-id name contains US or EU depending on the
# model, so the * wildcard saves you from typing it -- but check what it resolved
# to: if nothing matches, the shell passes the literal string through and v4l2-ctl
# reports a confusing error. With two 3DO cameras, pick the right one by hand.
DEV=$(ls /dev/v4l/by-id/usb-3DO_*-video-index0)
echo "$DEV"
# /dev/v4l/by-id/usb-3DO_3DO_4K_USB_CAMERA_US_3DO-video-index0

v4l2-ctl -d "$DEV" --set-ctrl=backlight_compensation=0

The LEDs go off immediately, without interrupting the video stream. That is the whole mechanism. Everything below is about making it survive reboots, survive re-plugging the camera, and giving you a button.

Your user must be in the video group (id $USER). On a standard MainsailOS/Klipper install it already is — no sudo needed.


What this repo installs

Piece Purpose
scripts/cam_led.sh on / off / auto — the single place where "how" is implemented
scripts/camled_api.py + systemd/camled.service tiny local HTTP API on 127.0.0.1:8099, deliberately independent of Klipper
nginx/camled.location.conf exposes it as /camled/ next to Mainsail
panel/index.html the webcam page with the LED toggle in its footer bar
udev/99-3do-cam-led.rules + scripts/cam_led_udev.sh re-applies "off" whenever the camera re-appears on USB
a line in crowsnest.conf applies "off" at boot

Why a dedicated service instead of Klipper macros? Because the LEDs must answer even when Klipper is in shutdown or error — which is exactly when you want to look at the nozzle. A gcode_shell_command macro dies with Klipper; a small local service does not. It also keeps the macro list in Mainsail clean.


Before you start — back up, and pick your moment

You are about to edit configuration files on the machine that runs your printer, and to restart services it depends on. Back up every file before you touch it, with a timestamp so you can always tell which is which:

TS=$(date +%Y%m%d_%H%M%S)

# crowsnest configuration (Step 1)
cp ~/printer_data/config/crowsnest.conf ~/printer_data/config/crowsnest.conf.bak_$TS

# the webcam page, if you already have one (Step 4)
cp ~/cam_panel/index.html ~/cam_panel/index.html.bak_$TS 2>/dev/null

# the nginx site (Step 3) -- NOTE THE DESTINATION
mkdir -p ~/backups_nginx
sudo cp /etc/nginx/sites-enabled/mainsail ~/backups_nginx/mainsail.bak_$TS

⚠️ The nginx backup must land outside sites-enabled/. nginx loads every file in that directory, so a mainsail.bak sitting next to the original becomes a second server block and nginx -t fails with a duplicate default server for 0.0.0.0:80. We made that exact mistake while writing this guide.

Do this while the printer is idle, never mid-print. Steps 1 and 3 restart crowsnest and reload nginx, which briefly interrupts the video stream. Check first:

curl -s "http://localhost:7125/printer/objects/query?print_stats" | grep -o '"state": *"[a-z]*"'
# "state": "standby"

Every change in this guide is reversible: restore the file from its .bak_, restart the service, and you are back where you started. Nothing here writes to the camera's firmware.


Step 1 — Off by default at boot

crowsnest can apply V4L2 controls when it starts the streamer. Edit ~/printer_data/config/crowsnest.conf, in your [cam 1] section:

[cam 1]
mode: ustreamer
port: 8080
device: /dev/v4l/by-id/usb-3DO_3DO_4K_USB_CAMERA_US_3DO-video-index0
resolution: 1920x1080
max_fps: 30
v4l2ctl: backlight_compensation=0        # <-- LEDs off at every start

Restart crowsnest, then verify:

sudo systemctl restart crowsnest
v4l2-ctl -d "$DEV" --get-ctrl=backlight_compensation
# backlight_compensation: 0

Verified on a real reboot: the camera enumerates ~4 s after boot, crowsnest writes the control ~19 s after boot — a 15 s margin, no race. Older crowsnest versions had a bug where v4l2ctl was not applied at boot (issue #281); if you are on an old release, check this step specifically instead of assuming.

Step 2 — The local API

mkdir -p ~/printer_data/scripts
cp scripts/cam_led.sh scripts/camled_api.py ~/printer_data/scripts/
chmod +x ~/printer_data/scripts/cam_led.sh ~/printer_data/scripts/camled_api.py

sudo cp systemd/camled.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now camled.service

curl http://127.0.0.1:8099/state
# {"ok": true, "value": 0, "on": false, "error": null}

Edit the DEV= line at the top of both scripts if your by-id path differs (US vs EU).

Step 3 — Expose it through nginx

Add the location to your Mainsail server block (/etc/nginx/sites-enabled/mainsail), next to your other custom locations:

location /camled/ { proxy_pass http://127.0.0.1:8099/; proxy_http_version 1.1; proxy_set_header Host $http_host; proxy_read_timeout 10s; access_log off; }
sudo nginx -t && sudo systemctl reload nginx
curl http://<PRINTER_IP>/camled/state

⚠️ Never leave a backup copy of the config inside sites-enabled/. nginx loads every file in that directory, so mainsail.bak becomes a second server block and you get a duplicate default server for 0.0.0.0:80. Keep backups elsewhere, and always run nginx -t before reloading — that is what turns a mistake into a non-event.

Step 4 — The button in the webcam panel

panel/index.html is a self-contained page: the MJPEG stream, plus a footer bar with the LED toggle. It reads the real state on load and re-syncs every 10 s, so the switch never lies if the value is changed elsewhere.

mkdir -p ~/cam_panel
cp panel/index.html ~/cam_panel/

Serve it and declare it as a webcam of service type iframe:

location /cam/ { alias /home/pi/cam_panel/; index index.html; add_header Cache-Control "no-store"; }

Then in Mainsail: Settings → Webcams → Add, service iframe, URL /cam/.

Two independent webcam panels in Mainsail

Two webcam panels side by side. The top one shows another iframe dashboard (blurred — unrelated to this guide), the bottom one is the nozzle camera page with the LED toggle in its footer.

💡 Want each camera to get its own dashboard panel instead of one panel with a dropdown? See Nitrooxyde/mainsail-multi-webcam-panels — a Mainsail fork that creates one independent panel per webcam (Webcam, Webcam 2, …), scaling automatically with the number of cameras, installable through Moonraker's update_manager. That is how the toggle shown here ends up sitting in its own panel next to the others.

Step 5 — Survive re-plugging the camera

This is the step most people will miss, and it is the one that actually bites.

When the camera loses power — you unplug it, or the printer is switched off at the wall — the firmware comes back on its default 1. crowsnest logs Watchdog: Lost Device but does not restart, so its v4l2ctl line is never replayed: the LEDs come back on and stay on.

sudo cp udev/99-3do-cam-led.rules /etc/udev/rules.d/
cp scripts/cam_led_udev.sh ~/printer_data/scripts/ && chmod +x ~/printer_data/scripts/cam_led_udev.sh
sudo udevadm control --reload-rules

Three details in that rule matter:

  • ATTR{index}=="0" — the module exposes both /dev/video0 and /dev/video1; without this filter the rule fires twice.
  • systemd-run --no-block — udev must never wait for a RUN to finish.
  • The script waits for the control to answer (0.5 s polling, 10 s max) instead of a fixed sleep: the /dev/videoX node can exist before the DSP is ready to talk.

Verified by physically unplugging the camera — dmesg showing a genuine USB disconnect / new high-speed USB device, and the script's own log:

udev: LED reset to 0 (was 1, now 0)

was 1 is the proof the firmware really had reverted to its default, and that the rule caught it.


Verifying it works

# read
curl http://<PRINTER_IP>/camled/state

# switch
curl -X POST "http://<PRINTER_IP>/camled/set?v=2"   # on
curl -X POST "http://<PRINTER_IP>/camled/set?v=0"   # off

# out-of-range values are rejected
curl -X POST "http://<PRINTER_IP>/camled/set?v=9"   # HTTP 400

If you are unsure whether the LEDs physically respond, blink them and watch the camera head — this beats any measurement:

DEV=$(ls /dev/v4l/by-id/usb-3DO_*-video-index0)   # same variable as above

for i in 1 2 3 4 5; do
  v4l2-ctl -d "$DEV" --set-ctrl=backlight_compensation=2; sleep 3
  v4l2-ctl -d "$DEV" --set-ctrl=backlight_compensation=0; sleep 3
done

Troubleshooting / gotchas we hit

pgrep ustreamer returns 0 while ustreamer is running. Its process comm is truncated to main. Use ps -eo args | grep ustreamer, ss -ltnp | grep 8080, or curl http://127.0.0.1:8080/state instead. We wasted a diagnosis on this.

The stream looks frozen in the browser after re-plugging, and the toggle seems dead. A dying MJPEG stream does not always fire the <img> onerror event, so the page keeps showing the last frame. Zombie connections can also saturate the browser's per-origin HTTP/1.1 connection pool, leaving fetch() calls pending — which makes the button look broken when it is not. Reload the page. The camera and the control are fine; verify with curl .../camled/state from a shell before suspecting the hardware.

unbind/bind in sysfs is not equivalent to unplugging. It does not cut the 5 V rail, so the DSP keeps its internal state and does not revert to the firmware default. It is fine for testing that a udev rule fires; it is useless for testing default-value behaviour. Only a physical unplug (or a power cut) reproduces that.

A soft reboot does not cut USB power either — the camera keeps its control value across systemctl reboot.

No brightness/dimming. The LEDs are SK6812 WWCW parts, so the hardware could dim, but the UVC firmware exposes on/off only. The DSP declares two undocumented UVC Extension Units; probing them by writing blind into DSP registers risks bricking the module, and 3DO no longer publishes a firmware tool (it was removed from their repository in June 2026). Not recommended.


Authorship

  • Idea, requirement, hardware and real-world testing — Nitrooxyde. The original question ("can these LEDs be controlled at all?"), the direction taken at every step, the physical tests that no software can do — unplugging the camera, watching the LEDs blink, rebooting the printer host — and the review that caught real mistakes in this guide before publication.
  • Implementation and write-up — Claude Opus 5 (Anthropic). All of the code and documentation here: the shell scripts, the local HTTP API and its systemd unit, the udev rule, the webcam panel page, and this README.

The findings themselves came from reading 3DO's own documentation and verifying every claim on a live machine, never from assumption.

Credits & sources

Disclaimer

Provided as is, with no warranty of any kind — see LICENSE. Use it at your own risk.

  • Not affiliated with, nor endorsed by, 3DO, the Mainsail project or the Klipper project. 3DO's own documentation is linked above and remains the authority on the camera itself.
  • Do not contact 3DO about this repository. They did not write it, did not review it, and are in no way responsible for it. Their support covers their hardware and their own documentation — sending them questions about a third-party guide only wastes their time and yours. The same goes for the Mainsail, crowsnest and Klipper teams: nothing here is theirs.
  • Verified on exactly one setup: 3DO Nozzle Camera Kit V2 (Sony 4K, glued fixed-focus, LED FPC cable), Raspberry Pi 5, MainsailOS, crowsnest v5 (Python), Klipper + Moonraker, on a Voron 2.4. Other camera revisions, distributions, crowsnest versions or streamer backends may behave differently — verify each step on your own machine rather than assuming.
  • You are editing system files (nginx, systemd, udev) on the host that drives your printer. Back up first (see above), and never run these steps during a print.
  • Do not probe the camera's undocumented UVC Extension Units looking for brightness control. That means writing blind into DSP registers, 3DO no longer publishes a firmware tool, and a bricked module cannot be recovered.

No support. This is a write-up of something that worked here, published in case it saves someone a few hours. It is not a maintained project: issues and pull requests are not monitored, and no help can be provided. Fork it, adapt it, redistribute it — the MIT license is there for that.

License

MIT — see LICENSE.

About

Turn the LEDs of the 3DO Nozzle Camera V2 (Sony 4K) on and off from Klipper/Mainsail - no firmware flashing, no extra wiring. Survives reboots and USB re-plugs, with a toggle button right in the webcam panel.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages