PlatformIO firmware for an ESP32 addon that plugs into OpenPnP as a USB serial GcodeDriver. It provides two Z-sensing methods on one board:
| Sensor | Method | OpenPnP use |
|---|---|---|
| HX711 + load cells | Contact / force | G38.2 in-bed Z touch-off |
| VL53L4CX laser | Non-contact distance | M212 height read → surface Z via OpenPnP delta math |
Serial is 115200 baud. Successful commands end with ok.
Everything needed to wire the hardware, verify sensors, flash firmware, and reach the diagnostics webpage.
- ESP32-WROOM-32 dev board (USB-UART bridge, e.g. CP2102 → often
/dev/ttyUSB0on Linux) - PlatformIO (install guide)
- Adafruit HX711 breakout (#5974) + two 5kg 4-wire bar load cells (e.g. Amazon B09VYSHW16)
- Adafruit VL53L4CX ToF sensor (#5425), mounted on the tool head pointing down
Clone or open this project, then use the commands below from the repo root.
Defined in include/pins.h. On boards labeled D##, use the silkscreen labels (D16 = GPIO 16, etc.).
| Function | GPIO | Connect to |
|---|---|---|
| HX711 DATA | 16 (D16) | Adafruit DATA |
| HX711 SCK | 17 (D17) | Adafruit SCK |
| Status LED | 2 | On-board LED (optional) |
| I2C SDA | 21 (D21) | VL53L4CX SDA |
| I2C SCL | 22 (D22) | VL53L4CX SCL |
| VL53 XSHUT | 18 (D18) | VL53 XSHUT (required) |
| VL53 GPIO | 23 (D23) | VL53 GPIO (data-ready, recommended) |
The Adafruit HX711 has two supply pins:
| Adafruit pin | Connect to | Purpose |
|---|---|---|
| VCC | ESP32 5V | Load cell excitation (E+/E-) |
| VDD | ESP32 3.3V | Digital logic (DATA/SCK levels) |
| GND | GND |
Leave the VIO solder jumper on the back open (default split-supply mode).
Set the board RATE switch to L (10 SPS) for probing — less noise than 80 SPS.
| Adafruit | ESP32 |
|---|---|
| DATA | D16 |
| SCK | D17 |
Wire both cells the same way into the 6-pin terminal:
| Cell wire (typical) | Terminal |
|---|---|
| Red | E+ |
| Black | E- |
| Green | A+ |
| White | A- |
Leave B+/B- empty. Test with one cell first; add the second once M114 responds to force.
Mechanical: Fix the mounting-hole end rigidly; apply downward force on the sensing end. Do not press the rubber boot in the middle of the beam.
| VL53L4CX | ESP32 |
|---|---|
| VIN | 3.3V |
| GND | GND |
| SDA | D21 |
| SCL | D22 |
| XSHUT | D18 |
| GPIO | D23 |
I2C address: 0x29 (7-bit) / 0x52 (8-bit write form used by the driver). Firmware uses Medium distance mode (VL53L4CX does not support Short mode).
STEMMA QT cables carry the same signals if you prefer a harness to the head.
Mount the VL53L4CX on the tool head so the laser points straight down, offset from the top camera along head Y (match the LaserZ head offset in OpenPnP — e.g. 30 mm ahead of the camera center).
Print 3d_models/z_laser_mount.stl and attach the VL53L4CX as shown:
Flash the dedicated test sketches before the full firmware when bringing up new hardware.
VL53L4CX test:
pio run -e esp32dev_vl53_test -t upload --upload-port /dev/ttyUSB0
pio device monitor --port /dev/ttyUSB0 --baud 115200Expect VL53L4CX DETECTED! and live distance readings. Serial: r (read), x (XSHUT reset), ? (help).
HX711 test:
pio run -e esp32dev_hx711_test -t upload --upload-port /dev/ttyUSB0
pio device monitor --port /dev/ttyUSB0 --baud 115200Expect HX711 DETECTED! and changing gram values when you press the pad. Serial: t (tare), c -2150 (cal factor), ? (help).
pio run -e esp32dev -t upload --upload-port /dev/ttyUSB0If upload fails (USB busy or CP2102 quirks), close OpenPnP/Java serial users and try a slower speed in platformio.ini (upload_speed = 115200).
Serial monitor:
pio device monitor --port /dev/ttyUSB0 --baud 115200Typical boot output:
LumenPnP-Addon starting (HX711 + VL53L4CX)
Reset reason: 1
HX711 ready.
Send M115 for info. Ready for OpenPnP GcodeDriver.
ok
Multicore: serial/probe on core 1, laser+web on core 0
If WiFi credentials are stored in flash, you will also see DHCP IP and Web server started on port 80. (Reset reason: 1 is a normal power-on reset.)
WiFi credentials are not hardcoded. Configure once over serial (or the web G-code console after first connect):
M710 S"YourNetwork" P"YourPassword"
| Command | Action |
|---|---|
M710 |
Report SSID, configured flag, connection state, IP |
M710 S"<ssid>" |
Set SSID (quote if it contains spaces) |
M710 P"<password>" |
Set password |
M710 R |
Reconnect with saved credentials |
M710 C |
Clear saved WiFi from flash |
Credentials are stored in NVS and survive reboot. mDNS hostname: http://lumen-probe.local (same LAN).
Web UI:
| URL | Purpose |
|---|---|
/ |
Live diagnostics — force, laser, 10s trends, histogram, datalog, G-code tester |
/settings |
Edit and save probe/laser settings to flash |
Hard-refresh the browser (Ctrl+Shift+R) after firmware updates — the HTML is embedded in the binary.
- HX711 split power: 5V → VCC, 3.3V → VDD, GND common
- Load cells on Channel A; force changes
M114/ test firmware grams - Z laser mount printed and VL53 mounted on head (see Z laser mount)
- VL53 XSHUT wired; test firmware shows live mm
- Production firmware flashes;
M115responds -
M710WiFi configured; diagnostics page loads - OpenPnP LumenPnP-Addon driver +
LaserZhead actuator configured (see Configuration below)
How to calibrate sensors, tune laser/OpenPnP math, and wire actuators in OpenPnP.
The addon uses a second GcodeDriver on its own USB serial port. Your main GcodeDriver (Marlin on the LumenPnP motion controller) still handles all XY/Z motion. The addon driver only sends sensor commands (M212, G38.2, M600, etc.) — it never moves the machine.
| Driver | Port (example) | Role |
|---|---|---|
| GcodeDriver | ttyACM1 |
Marlin motion, vacuum, LEDs |
| LumenPnP-Addon | ttyUSB0 |
HX711 force probe + VL53 laser |
This matches a working LumenPnP machine.xml setup: one ESP32 serial device, one motion-controller serial device.
Machine Setup → Drivers → Add GcodeDriver — name it LumenPnP-Addon (or similar) and point it at the ESP32 port.
| Setting | Value | Why |
|---|---|---|
| Communications | Serial | ESP32 USB-UART |
| Port | ttyUSB0 (Linux) / your COM port |
Addon board |
| Baud | 115200 |
Firmware default |
| Command confirm regex | ^ok.* |
Every command ends with ok |
| Connect command | M115 ; LumenPnP addon ready check |
Confirms firmware alive after USB reset |
| Connect wait | 1500 ms |
ESP32 reboots when USB opens |
| Timeout | 10000 ms |
M212 can take a few seconds |
| Sync initial location | Off | Addon has no machine coordinates |
| Allow unhomed motion | On | Sensor reads work without homing |
| Visual homing | Off | Not applicable |
| Units | Millimeters |
Leave feed rate / acceleration send-on-change disabled — this driver does not issue moves.
Laser height probing in OpenPnP uses a virtual Z axis, not the physical nozzle Z screw.
Machine Setup → Axes → Add axis:
| Field | Value |
|---|---|
| Type | ReferenceVirtualAxis |
| Name | zCamera (or similar) |
| Axis type | Z |
The Top down-looking camera and the LaserZ actuator both bind to this virtual Z axis (axis-Z-id → zCamera). OpenPnP tracks a separate “camera/laser height” coordinate that does not command Marlin during a laser probe read.
Set the Top camera Default Z to your normal focus height (e.g. 7.18 mm in a calibrated LumenPnP config). That value becomes LzRef in the addon firmware — the virtual Z OpenPnP reports for LaserZ at the teach pose.
Create the laser actuator under the Head, not under machine-level actuators.
Machine Setup → Heads → H1 → Actuators → Add
| Field | Value |
|---|---|
| Name | LaserZ |
| Driver | LumenPnP-Addon |
| Value type | Double |
| Axis Z | zCamera (virtual axis) |
| Coordinated before actuate | Wait for stillstand |
| Coordinated before read | Wait for stillstand |
| Head offsets | Set Y to the laser offset from the camera center in head coordinates (e.g. 30.0 mm if the VL53 is 30 mm ahead of the top camera on the Y axis). X/Z as measured. |
Then set the head’s Z probe actuator:
Machine Setup → Heads → H1 → General → Z probe actuator: LaserZ
OpenPnP will use LaserZ for board/feeder Z probing scripts instead of driving the nozzle into the pad.
On the LumenPnP-Addon driver, under Actuator read for LaserZ:
| Mapping | G-code / regex |
|---|---|
| Actuator read command | M212 |
| Actuator read regex | (?i)Laser:(?<Value>[-\d.]+) |
Example response (delta mode, K taught):
Laser:0.125
; raw 42 refined 41 corrected 41 samples 9/9 sigma 0.15 peak R12 centroid 12.34
ok
If K is not calibrated, M212 returns Laser:-1 with an error comment.
With delta mode on (firmware default), M212 returns a delta, not raw standoff mm:
capturedZ = actuatorLocationZ + M212_reading
where:
actuatorLocationZ— currentzCameravirtual Z in OpenPnP (e.g. 7.18 at safe focus)M212_reading—surfaceZ − LzReffrom the firmware
So at the teach pose, Laser: should read near zero. When the board is lower, the delta goes negative; when higher, positive. Raw standoff and surface Z are still on the diagnostics webpage and in M114 / /status JSON.
flowchart LR
subgraph motion [GcodeDriver - Marlin]
XY[XY jog to probe point]
Zcam[zCamera virtual Z at focus]
end
subgraph addon [LumenPnP-Addon - ESP32]
M212[M212 laser read]
delta["Laser: surfaceZ − LzRef"]
end
XY --> Zcam
Zcam --> M212
M212 --> delta
delta --> capture["OpenPnP capturedZ = zCamera + delta"]
Force touch-off uses machine-level actuators on the same LumenPnP-Addon driver.
Machine Setup → Actuators → Add
ForceZProbe (Double):
| Mapping | Value |
|---|---|
| Driver | LumenPnP-Addon |
| Actuator read command | G38.2 Z-80 F80 |
| Actuator read regex | (?i)PRB:(?<Value>[-\d.]+) |
Firmware arms the load cell. On contact:
Probe triggered!
PRB:0.000
ok
Tune Z-80 F80 and M600 S<grams> threshold for your mechanics. Use this actuator in nozzle tip calibration or custom scripts that need a physical touch.
ForceZTare (Boolean) — optional convenience actuator:
| Mapping | Value |
|---|---|
| Actuate boolean command | {True:M600}{False:M999} |
M600 tares the scale; M999 resets probe state.
Useful maintenance commands (serial or web G-code console):
| Command | Purpose |
|---|---|
M600 |
Tare (zero) the scale |
M600 S25 |
Set trigger threshold to 25 g (runtime; default 30 g) |
M600 I1 / M600 I0 |
Invert sign on/off (default on: compression = positive g) |
M114 |
Live grams, threshold, probe state, last laser mm |
M999 / M112 |
Abort / reset probe state |
These fragments are taken from a working LumenPnP OpenPnP config (~/.openpnp2/machine.xml). IDs (AXS…, ACT…, DRV…) are machine-specific — when copying, let OpenPnP generate new IDs or keep yours consistent. Adjust serial port names for your system.
Virtual camera Z axis (Machine → Axes):
<axis class="org.openpnp.machine.reference.axis.ReferenceVirtualAxis"
id="AXS171084434ad04ba5" name="zCamera" type="Z">
<home-coordinate value="0.0" units="Millimeters"/>
</axis>Head: LaserZ actuator + Z probe (Machine → Heads → H1):
<!-- Inside <head> … -->
<actuators>
<actuator class="org.openpnp.machine.reference.ReferenceActuator"
id="ACT202606121201a" name="LaserZ"
value-type="Double" value-type-confirmed="true"
driver-id="DRV202606121200a"
axis-X-id="AXS169824381580efcb"
axis-Y-id="AXS16982438158c4660"
axis-Z-id="AXS171084434ad04ba5"
coordinated-before-actuate-enum="WaitForStillstand"
coordinated-before-read-enum="WaitForStillstand">
<!-- Y offset = laser ahead of top camera in head coordinates -->
<head-offsets units="Millimeters" x="0.0" y="30.0" z="0.0" rotation="0.0"/>
</actuator>
</actuators>
<z-probe-actuator-name>LaserZ</z-probe-actuator-name>Top camera uses the same virtual Z (axis-Z-id="AXS171084434ad04ba5"). Default focus Z should match LzRef:
<default-z value="7.180000000000049" units="Millimeters"/>Machine-level force actuators (Machine → Actuators):
<actuator class="org.openpnp.machine.reference.ReferenceActuator"
id="ACT202606121202a" name="ForceZProbe"
value-type="Double" driver-id="DRV202606121200a" …/>
<actuator class="org.openpnp.machine.reference.ReferenceActuator"
id="ACT202606121203a" name="ForceZTare"
value-type="Boolean" driver-id="DRV202606121200a" …/>LumenPnP-Addon driver (Machine → Drivers — second driver, ESP32 on ttyUSB0):
<driver class="org.openpnp.machine.reference.driver.GcodeDriver"
id="DRV202606121200a" name="LumenPnP-Addon"
communications="serial"
sync-initial-location="false"
allow-unhomed-motion="true"
visual-homing-enabled="false"
connect-wait-time-milliseconds="1500"
timeout-milliseconds="10000"
units="Millimeters">
<serial port-name="ttyUSB0" baud="115200" line-ending-type="LF"
flow-control="Off" …/>
<command type="COMMAND_CONFIRM_REGEX">
<text><![CDATA[^ok.*]]></text>
</command>
<command type="CONNECT_COMMAND">
<text><![CDATA[M115 ; LumenPnP addon ready check]]></text>
</command>
<!-- LaserZ (head actuator ACT202606121201a) -->
<command head-mountable-id="ACT202606121201a" type="ACTUATOR_READ_COMMAND">
<text><![CDATA[M212]]></text>
</command>
<command head-mountable-id="ACT202606121201a" type="ACTUATOR_READ_REGEX">
<text><![CDATA[(?i)Laser:(?<Value>[-\d.]+)]]></text>
</command>
<!-- ForceZProbe -->
<command head-mountable-id="ACT202606121202a" type="ACTUATOR_READ_COMMAND">
<text><![CDATA[G38.2 Z-80 F80]]></text>
</command>
<command head-mountable-id="ACT202606121202a" type="ACTUATOR_READ_REGEX">
<text><![CDATA[(?i)PRB:(?<Value>[-\d.]+)]]></text>
</command>
<!-- ForceZTare -->
<command head-mountable-id="ACT202606121203a" type="ACTUATE_BOOLEAN_COMMAND">
<text><![CDATA[{True:M600}{False:M999}]]></text>
</command>
</driver>The main motion GcodeDriver stays on the LumenPnP controller (e.g. ttyACM1 / Marlin). Only sensor commands go to LumenPnP-Addon.
-
With nothing on the pad:
M600(tare). -
Place a known weight on the pad.
-
Read grams with
M114. -
Compute and set calibration factor:
new_factor = (reading_grams / actual_grams) × current_factor -
Save via Settings page (
/settings) or G-code is not available for cal factor over serial — use the web Settings form or set at runtime in test firmware then copy the value.
Persisted in NVS (hx711 namespace): calibration factor, invert flag.
Sign matters: if compression reads negative, toggle invert (M600 I1) or flip the factor sign.
This is the most important laser setup step. The firmware maps standoff distance to machine surface Z using constant K:
K = machine_surface_z + laser_standoff_mm (at the teach pose)
surface_z = K − standoff_mm
openpnp_delta = surface_z − LzRef
LzRef must equal the zCamera virtual Z OpenPnP shows at the teach pose. On a LumenPnP with Top camera default Z 7.18 mm, that is:
M212 V7.18
(M212 L7.18 is an alias.) This should match Machine Setup → Cameras → Top → Default Z.
-
Home the machine and jog the Top camera over a known flat surface at your normal safe/focus Z (
zCamera≈ 7.18 mm in a typical config). -
Read the machine board surface Z from OpenPnP at that XY (e.g.
5.200mm) — this is physical machine Z of the surface, not the virtual camera Z. -
Send over the addon serial (web G-code console, OpenPnP driver test, or
pio device monitor):M212 Z5.200Firmware reads laser standoff, computes
K = surface_z + distance, saves to flash, and prints the result. -
Set LzRef to the virtual Z from step 1 (check Top → Default Z or the DRO for
zCamera):M212 V7.18 -
Confirm with
M212at the same pose —Laser:should be a small value near 0.000. -
Run a Z probe on that head (board location or test script). OpenPnP should capture a sensible height without the nozzle touching the pad.
Values below were read from a live, taught addon (M115 over USB + GET /api/settings on the diagnostics page). Use them to restore a fresh flash or as a baseline when tuning a new unit. Re-read M115 / /api/settings after any teach pass — K and LzRef are machine-specific.
| Setting | Recommended value | G-code to set |
|---|---|---|
| OpenPnP K | 56.200 |
M212 K56.200 |
| OpenPnP LzRef | 7.180 |
M212 V7.180 |
| Delta mode | on | M212 O1 |
| Avg samples | 9 |
M212 S9 |
| ROI SPADs | 4 |
M212 R4 |
| Hold jump | 1.0 mm |
M212 H1.0 |
| Hold alpha | 0.35 |
M212 A0.35 |
| Outlier gate | 1.5 mm |
M212 U1.5 |
| Max sigma | 2.0 mm |
M212 G2.0 |
| mm per bin | 1.5 |
M212 P1.5 |
| VL53 distance cal | off (0.00 mm offset) |
— (optional: M212 D<known_mm>) |
One-shot restore after flash:
M212 K56.200 V7.180 O1 S9 R4 H1.0 A0.35 U1.5 G2.0 P1.5
| Setting | Recommended value | Notes |
|---|---|---|
| Calibration factor | -1000.00 |
Firmware default — calibrate with a known weight before relying on gram readings |
| Invert sign | on | M600 I1 (saved via Settings page) |
| Trigger threshold | 30.0 g |
Runtime default; M600 S30 (not persisted across reboot) |
Add after the laser restore line if you want the same runtime force-probe threshold:
M600 I1
M600 S30
| OpenPnP item | Value in machine.xml |
|---|---|
Top camera Default Z / units-per-pixel Z |
7.18 mm |
LaserZ head offset Y |
30.0 mm |
| Head Z probe actuator | LaserZ |
| Addon serial port | ttyUSB0 @ 115200 |
| Motion serial port | ttyACM1 @ 115200 |
If you already know K:
M212 K12.345
At a known physical standoff (gauge block, shim stack):
M212 D25.0
Applies a sensor offset so corrected mm matches the reference.
Laser surface Z uses a hold/EMA filter to reject single-frame spikes while tracking real plateaus:
| Parameter | Default | G-code | Meaning |
|---|---|---|---|
| Jump threshold | 1.0 mm | M212 H1.0 |
Ignore jumps larger than this unless sustained |
| EMA alpha | 0.35 | M212 A0.35 |
Smoothing strength for in-range readings |
| Reset hold | — | M212 N |
Clear hold / EMA state |
The diagnostics page shows HELD (smoothed) vs raw instant surface Z. M212 uses the held value for OpenPnP delta output when delta mode is on.
| Parameter | Default | G-code | Notes |
|---|---|---|---|
| Average samples | 9 | M212 S9 |
1–15 samples per read |
| ROI size | 4 | M212 R4 |
Center SPADs per side (4–16) |
| Outlier gate | 1.5 mm | M212 U1.5 |
Reject per-sample spikes |
| Max sigma | 2.0 mm | M212 G2.0 |
Gate noisy reads |
| mm per histogram bin | 1.5 | M212 P1.5 |
Centroid scale |
| Delta mode | on | M212 O1 / M212 O0 |
OpenPnP delta vs raw mm |
| Clear VL53 cal | — | M212 C |
Remove distance offset |
All of the above (except M212 C and M212 N) are saved to flash in the laser NVS namespace.
Open http://<ip>/settings (or /settings on mDNS). Changes write to NVS and survive reboot:
- HX711 threshold, calibration factor, invert, tare
- OpenPnP K, LzRef, delta mode, teach-from-surface
- Laser samples, ROI, outlier/sigma/mm-per-bin, hold jump/alpha
- Actions: tare, reset surface hold, clear laser calibration
Equivalent G-code is listed at the bottom of the settings page.
On the diagnostics page, Drift Datalog records 1 sample/sec for up to 10 minutes while the machine is stationary. Use it to analyze force/laser drift. Download CSV for offline plotting.
| Namespace | Keys | Set via |
|---|---|---|
hx711 |
calibration factor, invert | Settings page |
laser |
K, LzRef, delta, hold H/A, S/R/U/P/G tuning | M212 …, Settings page |
wifi |
SSID, password | M710 … |
Trigger threshold (M600 S) is runtime only unless changed again after reboot (default 30 g).
| Command | Description |
|---|---|
M115 |
Firmware info, calibration, laser tuning, WiFi status |
M114 |
Load cell grams + probe state + laser summary |
M999 / M112 |
Reset / abort probe |
| Unknown | ok ; unknown: <cmd> (keeps OpenPnP moving) |
| Command | Description |
|---|---|
M600 |
Tare scale |
M600 S<g> |
Set trigger threshold (grams) |
M600 I0 / M600 I1 |
Sign invert off / on |
G38.2 / G38.3 |
Arm probe; block until contact or timeout (30 s) |
| Command | Description |
|---|---|
M212 |
Read laser; print Laser:<value> + metadata + ok |
M212 S<n> |
Average sample count (1–15) |
M212 R<n> |
ROI SPADs per side (4–16) |
M212 Z<mm> |
Teach K from known surface Z at current pose |
M212 K<mm> |
Set K directly |
M212 L<mm> / M212 V<mm> |
Set LzRef (OpenPnP virtual Z) |
M212 D<mm> |
Calibrate at known standoff distance |
M212 H<mm> |
Surface hold jump threshold |
M212 A<0-1> |
Surface hold EMA alpha |
M212 N |
Reset surface hold |
M212 O0 / M212 O1 |
Raw mm / OpenPnP delta output |
M212 P<mm/bin> |
Histogram centroid scale |
M212 G<sigma> |
Max per-sample sigma gate (mm) |
M212 U<mm> |
Outlier gate (mm) |
M212 C |
Clear laser distance calibration |
Multiple M212 parameters can be combined on one line, e.g. M212 S9 R4 H0.8.
| Command | Description |
|---|---|
M710 |
WiFi status |
M710 S"<ssid>" |
Set SSID |
M710 P"<password>" |
Set password |
M710 R |
Reconnect |
M710 C |
Clear credentials |
Beyond basic force/laser readouts, the live page includes:
- 10 s trend charts — force; laser BEFORE / AFTER / HELD / INSTANT (toggleable)
- Histogram — photon counts vs timing bins (log/linear scale)
- OpenPnP block — K, LzRef, delta, held vs raw surface Z
- G-code console — send any command, see responses
- Drift datalog — 10 min capture, CSV download, trend analysis plots
Production firmware is modular C++ — main.cpp is a thin entry point; behavior lives in service classes under src/ with headers in include/.
lumenpnp-addons/
├── platformio.ini # esp32dev (production), hx711_test, vl53_test
├── 3d_models/
│ └── z_laser_mount.stl # Printable VL53L4CX head mount
├── photos/
│ ├── z_laser_mount_1.jpg # Mount photos (see Installation)
│ └── z_laser_mount_2.jpg
├── include/
│ ├── pins.h # GPIO pin map
│ ├── addon_config.h # Defaults, task stacks, NVS/datalog limits
│ ├── addon_types.h # LiveStatus, M212Snapshot, DatalogEntry, LaserTuning
│ ├── addon_sync.h # Null-safe FreeRTOS mutex helpers
│ ├── vl53l4cx_sensor.h # VL53L4CX driver wrapper
│ ├── hx711_probe.h # HX711 force probe + G38 state machine
│ ├── laser_service.h # Laser polling, surface hold, OpenPnP K/LzRef math
│ ├── gcode_parser.h # G-code string/parameter parsing (M710 SSID/password)
│ ├── gcode_handler.h # Serial G-code dispatch (M212, M600, G38, M710, …)
│ ├── wifi_manager.h # NVS WiFi credentials + connect/mDNS
│ ├── datalog_recorder.h # 1 Hz drift capture buffer + CSV export
│ ├── diagnostics_web.h # HTTP routes + JSON status/settings
│ ├── lumen_addon_app.h # App wiring, multicore tasks, main loop
│ └── web/
│ ├── diagnostics_page.h # Embedded HTML/JS for `/` (live charts, datalog)
│ └── settings_page.h # Embedded HTML/JS for `/settings`
├── src/
│ ├── main.cpp # setup() / loop() → LumenAddonApp
│ ├── lumen_addon_app.cpp # Core assignment, sensor + web FreeRTOS tasks
│ ├── hx711_probe.cpp
│ ├── laser_service.cpp
│ ├── vl53l4cx_sensor.cpp
│ ├── gcode_parser.cpp
│ ├── gcode_handler.cpp
│ ├── wifi_manager.cpp
│ ├── datalog_recorder.cpp
│ ├── diagnostics_web.cpp
│ ├── hx711_test.cpp # Wiring test only (env: esp32dev_hx711_test)
│ └── vl53l4cx_test.cpp # Wiring test only (env: esp32dev_vl53_test)
└── README.md
| Task / loop | Core | Responsibility |
|---|---|---|
loop() |
1 | USB serial, G-code input, HX711 G38.2 armed polling |
sensor |
0 | VL53L4CX fast poll, surface hold, periodic M212 cache, datalog ticks |
web |
0 | WebServer::handleClients(), WiFi reconnect |
Shared state uses FreeRTOS mutexes per subsystem (HX711, laser, live-status snapshot, datalog).
| Route | Method | Purpose |
|---|---|---|
/ |
GET | Diagnostics page (embedded UI) |
/settings |
GET | Settings page |
/status |
GET | Live JSON for polling (~450 ms in browser) |
/api/settings |
GET/POST | Read/write NVS-backed settings |
/tare, /set, /send |
POST/GET | Tare, threshold, G-code forward |
/api/datalog/start |
POST | Start drift capture |
/api/datalog/stop |
POST | Stop capture |
/api/datalog/download |
GET | CSV download |
| Symptom | Things to check |
|---|---|
| HX711 not ready | Split power wiring; wait_ready vs is_ready — use test firmware; keep wires short |
| Grams wrong sign | M600 I1 or flip calibration factor |
Laser:-1 |
I2C/XSHUT wiring; teach K with M212 Z<surface> |
| OpenPnP delta nonsense | K not taught (M212 Z); LzRef ≠ Top camera Default Z / zCamera at teach pose; delta mode off (M212 O0); wrong head offset on LaserZ |
| Z probe does nothing / wrong height | Head Z probe actuator not set to LaserZ; actuator on wrong driver; LaserZ axis Z not bound to zCamera virtual axis |
| WiFi won't connect | M710 to verify SSID; M710 R; same LAN; serial shows IP on success |
| Web page stale after flash | Hard refresh (Ctrl+Shift+R) |
| Web shows offline (no /status) | Usually a firmware crash/reboot loop — check serial for assert failed or Reset reason: after boot; re-flash latest build |
| USB upload busy | Close OpenPnP serial connection first |
OpenPnP wiki:
Hardware iterations and PRs welcome. Discuss on the LumenPnP / Opulo community (Discord).

