Skip to content

Add Z0 sensor probing (b key), extracted from #3 - #8

Merged
shuckc merged 9 commits into
mainfrom
z-probe
Nov 9, 2026
Merged

shuckc merged 9 commits into
mainfrom
z-probe

Conversation

@shuckc

@shuckc shuckc commented Nov 7, 2026 •

Copy link
Copy Markdown
Member

Z0 sensor probing lifted out of #3 by @btingle so it can be tested and reviewed separately from the jogging / move-to changes. The commit keeps btingle as author.

What it does

  • b in the TUI, then Y to confirm, runs the firmware's Z0 sensor cycle for the User (RML-1) coordinate system; Esc requests a stop.
  • New rollingmill/probing.py (ZProbe) drives the cycle from a captured VPanel session: sensor pre-checks via 0x2001, then SET 0x3902. The TUI keeps polling; a cancelled, lost or timed-out cycle is reported as unverified rather than success.
  • decode.py knows the two new wValues; docs/z-probing.md records the protocol evidence; README documents the key.

What is deliberately not included

Everything else in #3: the pulse/hold jogging rework, keyrelease.py, the move-to rewrite (move_active etc.) and the setuptools<81 pin. The TUI changes here are only the probe hooks (key handling, polling, dialog, cleanup on exit).

Test plan

  • pytest -q: new tests/test_probing.py replays the captured status samples and checks the exact USB bytes and TUI interlocks
  • Hardware test with a Z0 sensor (not yet run, per the docs)

Credit: @btingle, #3.

🤖 Generated with Claude Code

Press b, confirm with Y, and the firmware runs its Z0 sensor cycle for the
User (RML-1) coordinate system, based on a captured VPanel session
(SET 0x3902 with the 0x2001 sensor pre-checks). The TUI keeps polling during
the cycle, Esc requests a stop, and any cancelled/lost/timed-out cycle is
reported as unverified.

Extracted from the Z-probe parts of PR #3 by btingle (probing.py, tests,
docs, decode entries and the TUI hooks); the jogging and move-to changes in
that PR are left out.

Co-authored-by: Chris Shucksmith <chris@shucksmith.co.uk>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@shuckc

shuckc commented Nov 8, 2026 •

Copy link
Copy Markdown
Member Author

Op 0x3900 / 0x3901 hold the Z0 sensor thickness, not the spindle RPM.

To verify I parsed the Setup page's dialog template out of VP_MDX40A.exe (resource 4050, the "Modeling Machine" tab):

  • The "Thickness of Z0 Sensor:" label sits at y=147.
  • The edit box directly below it is ID 0x0FD9, at y=157.
  • The unit label next to it is 0x0FDA. It reads "inch" in the template, and setup_dlg_on_init sets it to "mm" or "inch".
  • The data-exchange routine at 0x402c10 isn't defined as a function in Ghidra, so I decoded its bytes by hand.
    • It binds 0xFD9 to this+0xC0.
    • It range-checks the value against this+0xB8 (14000) and this+0xBC (16000).
    • It applies a mm/inch flag from this+0xDC.

What fills and saves it:

  • Open the dialog read_nc_mode_tool_radius_settings calls get_uint32_0x3900 and stores it in +0xC0. Pattern B: SET 0x3900, then GET 0x0003, 4 bytes, big-endian uint32
  • Click OK write_ncode_settings calls MdxAction_x3901_p1(+0xC0). On the wire: SET 0x3901, big-endian uint32, then wait for ping bit 21 to clear
  • Units and range: the value is in 1/1000 mm. Default is 15000, which matches the 15 in your screenshot. The dialog limits it to 14.000 to 16.000 mm.
  • No other users: the Setup page loader is the only reader of 0x3900, and the OK path is the only writer of 0x3901.
  • Not verified: I didn't decode the exact mm/inch conversion helper. The 1/1000 mm scaling is inferred from the default and the range, so it's worth one live read to confirm.
  • Not verified: the firmware's own accepted range. The 14000–16000 limit is enforced by the dialog, not necessarily by the machine.

What this means for rollingmill:

  • The labels are wrong. docs/usb-protocol.md, machine.py, tui.py and decode.py all call 0x3900 / 0x3901 the "spindle target RPM". The "range 4500–15000" in our docs came from observing a stored value of 15000. That was the thickness.
  • Nothing writes the setting. The TUI never sends 0x3901. spindle_on_rpm only has a wrong docstring, and it sends the speed with SET 0x3006.
  • There is a read bug. On connect, get_spindle_rpm() reads 0x3900 and accepts it as the target RPM because 15000 is inside 4500 to 15000. A mill with a 15 mm sensor therefore starts with a 15000 RPM target. The trace decoder also labels SET 0x3901 as SPINDLE_RPM_WRITE.

Proposed changes

  1. Rename to get_z0_sensor_thickness_mm(), with no RPM semantics and the connect-time RPM read removed. That changes the initial target RPM to SPINDLE_RPM_MIN.
  2. Fix the docs and the decode.py labels.
  3. Show the stored thickness in the probe confirm dialog (PR Add Z0 sensor probing (b key), extracted from #3 #8).
  4. Add a guarded setter limited to 14.0–16.0 mm, but only after you've tried it on the machine.

@shuckc

shuckc commented Nov 8, 2026 •

Copy link
Copy Markdown
Member Author

I've verified the first argument to op 3902 is the coordinate system slot number, so it was easy enough to extend this to work for the G54-G59 coordinates, which I verified in hardware.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Failed stop commands are not retried after cancellation, allowing the probe to keep moving despite a stop request, while stale origin and thickness caches can misrepresent machine state.

3 open issues
What changed in this PR

Adds firmware-driven Z0 sensor probing to Rollingmill’s TUI, extracted from #3 without the jogging and move-to changes. It also adds sensor-thickness editing and corrects registers previously identified as spindle RPM settings.

Changes:

  • Adds a probe dialog with target selection, thickness editing, polling, cancellation and command interlocks.
  • Implements the probe cycle and corrects sensor-thickness handling in the machine API and trace decoder.
  • Adds captured-status replay tests, failure-path coverage and protocol documentation; hardware validation remains pending.
File Description
tests/​test_z0_thickness.py Tests thickness reads, writes, validation and dialog controls.
tests/​test_probing.py Tests probe packets, cycle handling and TUI interlocks.
tests/​fixtures/​mdx40a_zprobe_status.json Supplies captured probe status samples for replay.
rollingmill/​tui.py Adds probe controls, thickness editing and polling integration.
rollingmill/​probing.py Implements probe initiation, monitoring, cancellation and cleanup.
rollingmill/​machine.py Adds thickness accessors and corrects spindle-target initialization.
rollingmill/​decode.py Recognizes probe commands and decodes thickness values.
README.md Documents the probe keybinding and feature availability.
docs/​z-probing.md Describes operation, protocol evidence and validation limits.
docs/​usb-protocol.md Corrects register descriptions and documents sensor commands.
docs/​machine-state.md Corrects the thickness-command acknowledgement description.

Review effort: Balanced

Comment thread rollingmill/probing.py Resolved
Comment thread rollingmill/machine.py Resolved
Comment thread rollingmill/probing.py Resolved
@shuckc
shuckc force-pushed the z-probe branch 2 times, most recently from a9ee59c to e6959f6 Compare November 8, 2026 19:23
shuckc and others added 6 commits November 8, 2026 19:52
Decoding VP_MDX40A.exe: the Setup dialog's "Thickness of Z0 Sensor:" edit
box (dialog 4050, control 0x0FD9) is loaded from GET 0x3900 and saved with
SET 0x3901, a big-endian uint32 in 1/1000 mm (default 15000, VPanel limits
it to 14000-16000). Nothing else reads or writes them, so they were never
a spindle RPM.

- machine: get_spindle_rpm -> get_z0_sensor_thickness_mm, cached at connect
  as z0_sensor_thickness_mm; stop taking it as the target RPM, which now
  starts at SPINDLE_RPM_MIN; fix the spindle_on_rpm docstring (SET 0x3006)
- decode: label the read/write as Z0_SENSOR_THICKNESS
- docs: add a Z0 sensor section to usb-protocol.md, fix 0x3901 and 0x3a05

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
machine: set_z0_sensor_thickness_mm() writes SET 0x3901 (big-endian uint32,
1/1000 mm) inside the 0x1109 operation bracket, waits for the ping bit 21
ack like the Setup dialog's OK handler, then reads the value back; it is
limited to VPanel's 14-16 mm and returns True only if the read-back matches.

tui: the 'b' confirmation becomes a Z0 Sensor Probe dialog styled like the
others (title, key summary at the bottom). It shows the stored thickness
(no re-read), lets it be edited, H sets it, Enter starts the probe cycle
(refused while an edited value is unset) and Esc cancels. Y no longer
confirms.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
VPanel's MDx3902_GiantDispatch encodes the target coordinate system as the
first word of SET 0x3902: 1 = User (RML-1), 2 = EXOFS, 3-8 = G54-G59, which is
the WCS slot index rollingmill already uses (wcs_code in SET 0x3501,
WCS_SLOT_NAMES, get_wcs_origin). Only slot 1 has a live capture.

- probing: probe the active slot (1-8), send the slot as the first word and
  refresh that slot's origin afterwards; MCS and slots 9+ are rejected
- tui: the dialog shows the target, refuses Enter for an unsupported target
  and warns when the command set is RML-1 (VPanel only offers G54-G59 and
  EXOFS in NC Code or automatic mode)
- docs: record the encoder evidence in z-probing.md

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…arning

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Under `python -m rollingmill.tui`, getLogger(__name__) is "__main__", so the
probe warnings showed with the wrong context and missed the rollingmill
logger's level setting.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@shuckc
shuckc force-pushed the z-probe branch 3 times, most recently from da8f769 to 851e3d0 Compare November 8, 2026 20:55
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The key summary uses the ↵ symbol and "Esc close" like the coordinates box, the three
descriptive lines are grey instead of cyan, and there is a blank row before "Target".

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@shuckc
shuckc merged commit f0c8cd4 into main Nov 9, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants