Skip to content

Repository files navigation

Wireless Dual-Laser Turret Controller

MicroPython firmware for an ESP32-S3-N16R8 (16 MB flash, 8 MB octal PSRAM) that drives two independent pan-and-tilt laser turrets over WiFi. Use the browser to prepare jobs (text, images, SVGs, freehand drawings, or generative patterns), upload them to the device, and play them back on a glow-in-the-dark canvas — all wirelessly.


Features

  • Dual independent turrets — two pan/tilt steppers, each with an on/off laser diode. Mid-tones are achieved by spatial dithering (no PWM).

  • Job types:

    • Text engraving — type text, pick a font size, raster to G-code.
    • Image dithering — 4 modes: Threshold (binary), Bayer 4×4 (crosshatch), Blue noise (film grain), Floyd–Steinberg (error diffusion). The slider adjusts brightness bias in dither modes.
    • Freehand draw — draw directly on the preview canvas.
    • Generative patterns — spiral, wave, spirograph, Lissajous, rose, butterfly, harmonograph, epitrochoid.
    • SVG files — drop in any vector graphic; parsed client-side by a pool of Web Workers for fast, accurate path sampling with arc‑length LUTs.
  • Job buffer system — upload large jobs to the device's PSRAM; playback continues even if WiFi drops. REPEAT re-runs the last job without re-uploading.

  • Save / Load jobs — download jobs as .laserjob files and reload them later, restoring all settings.

  • Calibration — browser wizard measures each turret's beam at the four physical canvas corners; persisted to the device as a 4-point map (identity until set). Raw jog bypasses the map while measuring.

  • RGB LED indicator (GPIO 48) — colour-coded status at a glance, with adjustable mode and brightness from the web UI:

    Color Meaning
    Red Boot / WebSocket disconnected
    Blue WebSocket connected
    Rainbow cycle Job uploading
    Green Drawing (job running)
    Blue (or red if WS dropped) Job complete
  • Canvas — independent width and height sliders (101–501 px, step 2).

  • Emergency stop — one-click CLEAR that aborts the job, turns off lasers, and releases motors.


Hardware Requirements

Component Note
ESP32-S3-DevKitC-1 N16R8 16 MB flash, 8 MB octal PSRAM. The PSRAM is required for the job buffer system.
2× pan stepper motors 4-pin unipolar (e.g., 28BYJ-48 with ULN2003 driver).
2× tilt stepper motors Same as above.
2× laser modules Any GPIO-switched laser (driven via transistor or relay as needed).
2× stepper driver boards ULN2003 for 5V unipolar; or adapt for A4988/DRV8825 if using bipolar.
Power supply Adequate for your motors and lasers.

Pin Wiring

Each turret has 4 pan pins, 4 tilt pins, and 1 laser pin. The pins drive the motor phases directly using an 8-step half-step sequence, so the driver board must accept 4 direct coil inputs (ULN2003-style).

Pin assignments live in the Turret(...) constructor calls in main.py. To remap pins, edit those two lines and the LED_PIN constant in led.py.

Turret A (Top)

ESP32-S3 GPIO Connects To
GPIO 4 Pan coil 1 (driver IN1)
GPIO 5 Pan coil 2 (driver IN2)
GPIO 6 Pan coil 3 (driver IN3)
GPIO 7 Pan coil 4 (driver IN4)
GPIO 15 Tilt coil 1 (driver IN1)
GPIO 16 Tilt coil 2 (driver IN2)
GPIO 17 Tilt coil 3 (driver IN3)
GPIO 18 Tilt coil 4 (driver IN4)
GPIO 13 Laser trigger (via transistor/relay)

Turret B (Bottom)

ESP32-S3 GPIO Connects To
GPIO 9 Pan coil 1 (driver IN1)
GPIO 10 Pan coil 2 (driver IN2)
GPIO 11 Pan coil 3 (driver IN3)
GPIO 12 Pan coil 4 (driver IN4)
GPIO 42 Tilt coil 1 (driver IN1)
GPIO 41 Tilt coil 2 (driver IN2)
GPIO 40 Tilt coil 3 (driver IN3)
GPIO 39 Tilt coil 4 (driver IN4)
GPIO 14 Laser trigger (via transistor/relay)

On-board

GPIO Device
GPIO 48 Built-in WS2812 RGB LED

Quick Start

1. Install host tools

pip install -r requirements.txt

This installs mpremote (file transfer + reset) and esptool (firmware flashing).

2. Flash MicroPython (SPIRAM build)

Download the octal-PSRAM firmware from micropython.org/download/ESP32_GENERIC_S3 (file name contains SPIRAM_OCT). Connect the ESP32-S3 via USB, then:

esptool.py --chip esp32s3 --port <port> erase_flash
esptool.py --chip esp32s3 --port <port> --baud 460800 write_flash -z 0 \
    ESP32_GENERIC_S3-SPIRAM_OCT-*.bin

Finding the port:

  • Linux: ls /dev/ttyACM* after connecting the device
  • Windows: Device Manager → Ports (COM & LPT) → USB Serial Device (e.g. COM3)
  • macOS: ls /dev/cu.usbmodem* or /dev/cu.usbserial*

3. Set WiFi credentials

Create netconf.py in the repo root (this file is gitignored):

SSID = "your_wifi_name"
PASSWORD = "your_wifi_password"

4. Upload the application

# Linux / macOS
bash tools/flash_firmware.sh

# Windows (or any OS with Python)
python tools/flash_firmware.py

This builds index.html from the web sources, uploads all firmware files, and resets the device.

The script will ask two questions:

  • Upload config.json? — Answer y only on first-time setup. This file holds turret calibration; re-uploading it will overwrite any calibration adjustments you've made from the web UI.
  • Upload netconf.py? — Answer y only the first time (or when you change WiFi networks). It contains your SSID and password and rarely changes after initial setup.

5. Find the IP & open the web UI

mpremote connect <port> exec 'import network; print(network.WLAN(network.STA_IF).ifconfig()[0])'

Or check your router's DHCP table. Open the IP in a browser:

http://<esp32_ip>/

Web UI Tour

The interface is split into panels on a single page:

Panel What it does
Input Source Switch between Text, Image, SVG, Freehand Draw, Generative Patterns, and Live Calibrate modes. Adjust canvas width/height, dither mode, brightness bias, zoom, and invert.
Visualizer Shows the current artwork or toolpath preview with a centre crosshair and coordinate grid.
Controls Step speed slider, STREAM JOB (upload + play), REPEAT LAST JOB, EMERGENCY STOP, SAVE JOB / LOAD JOB (.laserjob files), and a live status message.
Manual Console Send G-code commands to turret A, B, or both: laser on/off (M3/M5), jog (G0), go home (G0 X0 Y0), set home (HOME), unlock (UNLOCK).
Calibration Wizard Measures each corner (SET HOME + four corner locks) to build the per-turret 4-corner map, persisted on the device. RAW mode bypasses the map for accurate measurements.
LED Status Mode dropdown (Auto / Off / Rainbow) and brightness slider (0–100%). Settings persist across WebSocket reconnects.

Calibration

Calibration makes the software's canvas coordinates line up with the physical glow canvas. Each turret records how many motor steps it takes to reach the four physical corners of the canvas, measured relative to the shared center ("Home"). The device builds a map from those four corner measurements and interpolates between them, so every drawing coordinate lands on the right spot on the real canvas. Until at least one corner is measured, the map is a no-op and raw coordinates are used.

Calibrating step by step

  1. Position the canvas so both beams can reach all four corners. Mark the center and the corners with small reference dots if you can — it makes aiming much easier.
  2. In the web UI, switch the Input Source to Live Calibrate.
  3. In the Manual Console, set the target to Turret A and press START RAW. RAW mode makes manual jog moves bypass the map, so you measure true motor offsets instead of warped ones.
  4. Jog Turret A's beam onto the physical center (click the grid, or send G0 X0 Y0), then jog Turret B's beam onto the same center spot. Now press SET HOME for A and for B — this just zeroes both counters at the same physical point; it does not move the motors.
  5. For each corner in the wizard table (Top-Right, Top-Left, Bottom-Left, Bottom-Right):
    • Jog Turret A's beam onto the corner dot and press Save A.
    • Jog Turret B's beam onto the same dot and press Save B.
    • Always approach the dot from the same direction (for example, always from the left). Stepper backlash makes the beam land slightly differently depending on which way you approached.
  6. Press FINISH — RAW mode turns off and the map applies to all drawing from now on.
  7. Press RUN CAL TEST. Both lasers should trace the same box, diagonals, and center cross, perfectly superimposed. If the beams separate, re-check the corners they diverge near.

Tips

  • Calibration is saved to config.json on the device, so it survives power cycles and reboots.
  • If the beams match at the corners but drift apart at the center, redo the SET HOME step so both turrets share the same center anchor.
  • If the beams diverge at one corner, just redo that corner (approaching from the same direction again).
  • The map is normalized by the canvas size — calibrate with the canvas size you plan to draw with.

WebSocket Protocol Reference

The device listens on port 80 and upgrades to WebSocket when it detects an Upgrade: websocket header. Clients that bypass the web UI can drive the turrets directly using this protocol.

Text commands (opcode 0x1)

From client Effect
V|<int> Set step interval in µs. Clamped to 200–100000.
GET_CFG Reply CFG|<json> with current calibration.
SETCAL|<turret>|<corner> Read the turret's current stepper position and persist it as that corner (tr/tl/bl/br), reply CFG|<json>.
SETCANVAS|<w>|<h> Store the canvas half-extents used by the calibration map. No reply.
RAW|<0/1> Toggle raw jog — manual moves bypass the calibration map while measuring corners.
LED|<mode>|<brightness> Set LED mode (auto/off/rainbow) and brightness (0–100). No reply.
BEGIN|<total> Allocate PSRAM buffers for <total> move-pairs, enter LOADING state. Reply ACK|0 or STAT|ERROR|mem (OOM).
GO Start drawing from the buffer. Only valid from READY state (all chunks loaded).
REPEAT Re-run the last completed job from its existing buffer without re-uploading. Only valid from DONE state.
M|<target>:<gcode> Manual control. target = A / B / BOTH. G-codes: M3 laser on, M5 laser off, G0/G1 X.. Y.. jog, HOME zero+clear, UNLOCK stop + release all motors (both turrets).
CLEAR Abort everything: stop turrets, clear queues and buffers, lasers off, reply ACK|-1.

Binary frames (opcode 0x2)

After BEGIN, move data is uploaded as binary frames. Each frame packs multiple moves into a compact byte layout:

Header (7 bytes):
  byte 0:       version = 0x01
  uint32 LE:    starting sequence number
  uint16 LE:    move count N (typically 500)

Payload (N × 9 bytes per move):
  int16 LE:     turret A X
  int16 LE:     turret A Y
  int16 LE:     turret B X
  int16 LE:     turret B Y
  byte:         bit 0 = laser A, bit 1 = laser B

The device replies with ACK|<next_expected_seq> on success, NACK|<expected> on a gap, or re-ACK|<expected> on a duplicate (dedup).

Device replies (text frames)

Reply When
ACK|<seq> Chunk accepted, next expected sequence.
NACK|<expected> Gap detected — client should backtrack.
CFG|<json> Response to GET_CFG, `SETCAL
STAT|LOADING|<n>|<total> Upload progress.
STAT|RUNNING|<n>|<total> Drawing progress.
STAT|DONE|<n>|<total> Job complete (sent once per connection).
STAT|ERROR|mem Out-of-memory on BEGIN.

Building the Web UI

python3 tools/build_html.py

This inlines web/style.css and concatenates the JS modules into index.html. Do not hand-edit index.html — always rebuild from sources.


Project Structure

main.py          config_store.py  calib.py         state.py
turret.py        ws.py            server.py        job.py
led.py           netconf.py       config.json      index.html
web/             tools/
  • web/ — browser UI sources (canvas.js, dither.js, raster.js, toolpath.js, stream.js, protocol.js, calib.js, svg-worker.js, index.html, style.css)
  • tools/ — build_html.py, flash_firmware.sh, flash_firmware.py, gen_bluenoise.py

See AGENTS.md for detailed firmware architecture and internals.


License

MIT (see LICENSE).

About

dual laser turrets firmware

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages