Skip to content
Merged
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ The interactive TUI exposes the following keybindings:
| <kbd>c</kbd> | coordinate systems (activate / move-to / overwrite) |
| <kbd>m</kbd> | Move-To picker (presets + User Specify numeric entry) |
| <kbd>t</kbd> | tool diameter offsets |
| <kbd>b</kbd> | Z0 sensor probe dialog (sets the Z origin of User, EXOFS or G54–G59 using probe) |
| <kbd>q</kbd> | quit |

**Cut panel** (only when `--file` is given)
Expand All @@ -88,7 +89,6 @@ Gotchas/Notes using the mill
* Single-stepping nc-code _partially works_, but the MDX complains about being starved of NC code and lights the View-LED when you reach a cutting operation.
* The progress display during a cutting job is that of filling the machine buffer, rather than the actual block being executed, which usually runs a few blocks behind. It's a shame there are not two counters so that this could be made 100% accurate.
* There might be an issue with storing A-axis values in workspace offsets and recalling them, as sometimes I get an extra +360° rotation on recall.
* I need a Z-origin sensor to test the sensor-based Z-origin setting, and decode the thickness settings — NYI.
* The geometry of the mill means that without tooling, it is fairly difficult (but not impossible) to crash. The rotary axis makes this much easier as the firmware allow the collet to hit the vice area.
* Be warned that bad, terrible things could happen driving your hardware with this experimental software, due to known or unknown bugs or defects, for which you entirely agree to take all risks in operating and hold the contributors blameless.

Expand Down
2 changes: 1 addition & 1 deletion docs/machine-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ struct.unpack_from('<I', data)[0]
| 20 | `0x00100000` | ERROR_MASK | Device error condition |
| 13 | `0x00002000` | ACK_JOG | Transient acknowledgement for `SET 0x4f5` (relative jog) |
| 12 | `0x00001000` | ACK_ABS | Transient acknowledgement for `SET 0x4f7` (absolute move) |
| 21 | `0x00200000` | CMD_ACK | Clears after `SET 0x3901` / `SET 0x2425` / `SET 0x3107` / axis param writes |
| 21 | `0x00200000` | CMD_ACK | Clears after `SET 0x3901` (Z0 sensor thickness) / `SET 0x2425` / `SET 0x3107` / axis param writes |

Bit 21 appears in both `BUSY_MASK` (combined with bit 2) and `CMD_ACK` (alone); the two names reflect different uses of the same physical bit.

Expand Down
26 changes: 21 additions & 5 deletions docs/usb-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,17 @@ All status/config queries use Pattern B. The polling step is mandatory — firin
|--------|------|----------|-------|
| `0x3003` | `get_uint32_0x3003` | 4 bytes | Current spindle/feed speed; clamped to `[0x3005.min, 0x3005.max]` |
| `0x3005` | `get_speed_range_0x3005` | 8 bytes | Spindle speed range `[min_speed, max_speed]`; stored at `obj[0x21]`/`obj[0x22]` |
| `0x3900` | `get_uint32_0x3900` | 4 bytes BE | Configured spindle target RPM; range 4500–15000 |
| `0x2405` | `get_status_struct_0x2405` | 16 bytes | `uint32[0]` = total spindle rotation time in seconds |

### Z0 sensor

| wValue | Name | Data | Notes |
|--------|------|------|-------|
| `0x3900` | `get_uint32_0x3900` | 4 bytes BE (Pattern B) | Configured **thickness of the Z0 sensor**, 1/1000 mm (`15000` = 15 mm). Setup dialog → Modeling Machine → "Thickness of Z0 Sensor:" (dialog resource 4050, edit control `0x0FD9`) |
| `0x2001` | `trigger_read_0x2001` | 2 bytes (Pattern B) | Sensor status: bit 2 = connected, bit 3 = in contact |

See [z-probing.md](z-probing.md) for the `SET 0x3902` operation that uses the z-origin sensor.

### Coordinate systems / WCS

| wValue | Name | Response | Notes |
Expand All @@ -97,8 +105,8 @@ All status/config queries use Pattern B. The polling step is mandatory — firin
| `0x3700` | `get_uint32_0x3700` | 4 bytes | Single int32 - unknown purpose read 0x3700 write 0x3701, related to rotary axis/jig detect |
| `0x3801` | `get_rotary_axis_centreline_0x3801` | 12 bytes | 3×uint32 = stored rotary A-axis centreline `[X, Y, Z]` in 1/1000 mm. Only `Y, Z` define the line (X is "along" the rotation axis, so its value is informational only). Written by the jig-detect routine via `SET 0x3803`. Read by the "Current Jig" indicator in the main panel — see [rotary-jig-alignment.md](rotary-jig-alignment.md#current-jig-indicator). |
| `0x3804` | `get_rotary_axis_angle_correction` | 24 bytes | 6 × int32 BE in 1/1000 mm: `[refX1, ofsY1, ofsZ1, refX2, ofsY2, ofsZ2]` — rotary A-axis two-point angle correction. See [machine-calibration.md](machine-calibration.md#rotary-axis-calibration). |
| `0x3a02` | `get_2uint32_0x3a02` | 8 bytes | 2×uint32 |
| `0x3a05` | `get_uint32_0x3a05` | 4 bytes | Used in spindle-stop to override Z for positioning |
| `0x3a02` | `get_2uint32_0x3a02` | 8 bytes (Pattern B) | Tool-sensor X, Y location on the rotary unit (1/1000 mm) |
| `0x3a05` | `get_uint32_0x3a05` | 4 bytes | Z value read by Jig Detection (`start_jig_detect_dialog`) and used as the Z of both detection points |
| `0x05f0` | `get_XYZ_axis_scaling` | 32 bytes | 8 × uint32 BE = 4 numerators then 4 denominators (axis order X, Y, Z, A). Effective scale per axis = `numerator / denominator`. See [machine-calibration.md](machine-calibration.md#axis-scaling). |

---
Expand Down Expand Up @@ -131,9 +139,17 @@ Payload layouts and dispatch detail live in [move-commands.md](move-commands.md)
| `0x3008` | 1 byte (10–200) | Spindle speed override % |
| `0x3009` | (bare) | Spindle stop; followed by `wait_busy_bits_clear` in drilling sequences |
| `0x3809` | `<HH` — [1, 0xFFFF] start / [0, 0] stop | Rotary A-axis drilling mode (continuous slow rotation) |
| `0x3901` | `>I` — RPM uint32 | Write spindle target RPM (4500–15000); poll ping bit 21 clear after |
| `0x2425` | (bare) | Reset spindle rotation time counter to zero; poll ping bit 21 clear after |

### Z0 sensor

| wValue | Payload | Notes |
|--------|---------|-------|
| `0x3901` | `>I` — thickness uint32, 1/1000 mm | Write Z0 sensor thickness (Setup dialog OK; VPanel limits it to 14000–16000); poll ping bit 21 clear after. |
| `0x3902` | `>HHH` — slot, -1, -1 | Probe for Z0 sensor, then update Z-axis origin in coordinate system slot. |

See [z-probing.md](z-probing.md) for the `SET 0x3902` operation that uses the z-origin sensor.

### Feed rate

| wValue | Payload | Notes |
Expand Down Expand Up @@ -211,7 +227,7 @@ GET 0x0002 (machine type — expect high word 0x1234)
SET 0x03f5 b'\x00' (keepalive)
SET 0x05f0 → GET 0x0003 (axis scaling — 8×uint32, see machine-calibration.md)
SET 0x3804 → GET 0x0003 (rotary angle correction — 6×int32)
SET 0x3900 → GET 0x0003 (read configured spindle RPM)
SET 0x3900 → GET 0x0003 (read configured Z0 sensor thickness, 1/1000 mm)
```

---
Expand Down
85 changes: 85 additions & 0 deletions docs/z-probing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Z0 sensor probing

Locate the material/tool contact surface on the Z-axis.

A conductive disk is positioned on the workpiece below the tool, which is wired back to the machine. The operation slowly lowers the Z-axis until the cutting bit touches the pad completing a circuit, which stops motion and causes a retreat. The measured Z-height is then adjusted down by the stored height of the conductive pad (14-16mm). The calculated surface height is stored to the device as the selected coordinate system's Z axis origin.

The conductive disk is connected to the rear "Z-origin" sensor port, which is a 3.5mm Jack socket at the rear of the machine. The 3.5mm jack 'tip' *alone* must be connected to the pad, the supplied cable uses a banana plug to make contact with the disk.

The height of the disk can be precisely measured (with calipers) and saved into the firmware - VPanel Setup dialog limits the thickness to 14.000–16.000 mm; the firmware's own limits are not known.

## User notes

Select the coordinate system whose Z origin you want to set with `c` (**User (RML-1)**,
**EXOFS** or **G54–G59**), activate it, then close the coordinate dialog.
Then press **b** to open the Z-origin dialog.
Place the clean connected sensor on the workpiece, position the tool directly
above it, turn off the spindle, and close the cover. The **Z0 Sensor Probe**
dialog shows the target coordinate system and the sensor thickness stored in the machine: **Enter** starts the
descent/retract cycle and **Esc** dismisses the dialog, or requests an
immediate stop during motion. The TUI keeps polling and displaying Z/feed. Other machine commands are blocked until a fresh idle reply arrives.

The firmware updates the target's Z origin; the application then reads that slot again
and refreshes its coordinate display. A cancellation, device error, lost status,
unobserved motion or timeout is reported as an unverified result, never success.
Quit during an active cycle requests stop; quit again after idle to exit.

You cannot start the Z-origin probing for machine coordinates (MCS - slot 0) since that
offset is fixed at zero. VPanel only offers EXOFS and G54–G59 when the command set is NC Code or
automatic, so the dialog warns when the command set is RML-1; whether the firmware
accepts those modes then is not known. The cycle uses the machine's stored sensor
thickness (GET 0x3900 / SET 0x3901, the Setup dialog's "Thickness of Z0 Sensor",
1/1000 mm), which is read at connect, shown in the dialog and in the TUI state pane.

Measure your sensor and check it matches: a wrong value shifts the Z origin the cycle
sets by the same amount. To change it, type the new value in the dialog
(14.000–16.000 mm, as in VPanel's Setup dialog) and press
**H**; the value is written, read back and verified, and **Enter** is refused while an
edited value has not been set.

## Setting probe thickness

The cycle subtracts the thickness of the conductive pad, which is stored in the machine:

| wValue | Payload | Notes |
|---|---|---|
| `0x3900` | reply `>I`, 1/1000 mm | Read (Pattern B): `SET 0x3900`, poll ping, `GET 0x0003`. `15000` = 15 mm |
| `0x3901` | `>I`, 1/1000 mm | Write, inside the `SET 0x1109` `00` … `FF` bracket; then poll ping bit 21 (`0x00200000`) until it clears (3 s timeout) |

To set 15.030 mm: `SET 0x1109 00`, `SET 0x3901 00 00 3A B6`, wait for the ack, `SET 0x1109 FF`, then read `0x3900`
back to check it (`00 00 3A B6`). VPanel limits the value to 14.000–16.000 mm; the firmware's own limits are not
known. Writing a changed value and reading it back was confirmed on a real MDX-40A.

## Z-probe cycle

1. SET 0x03F5 payload 02, operation begin SET 0x1109 payload 00.
2. Stop spindle: SET 0x3006 uint32 big-endian zero.
3. Twice: Pattern B query 0x2001, two-byte reply 00 14. Bit 2 of byte 1
satisfies the connected check; bit 3 is the contact condition.
4. SET **0x3902**, payload **00 01 FF FF FF FF** (`>HHH`, slot, -1, -1).
5. Direct GET 0x0100 32-byte status polls during descent/retraction. During
this phase the TUI does not send its normal idle heartbeat/secondary reads.
6. Motion flags clear, then re-read the User origin with Pattern B 0x030B.

Operation cleanup restores 0x03F5/0x1109 to FF, matching VPanel's operation exit.

The captured cycle descended at 60 mm/min, retracted at 1800 mm/min and ended
in about 16.9 seconds. Those speeds are firmware-controlled, not hard-coded
motion commands. A lowest sampled Z of -79.880 mm preceded reversal; exact
contact instant was between samples. No text success response is sent.

### Target coordinate system (slot)

The first word of `SET 0x3902` is the WCS slot index (the same numbering as
`WCS_SLOT_NAMES` and the `wcs_code` of `SET 0x3501`).
From VPanel's `MDx3902_GiantDispatch` (`0x403b10`, operation 6, called from
`set_origin_using_sensor_button_gd6`), whose `cs` is the "Set Origin Point" dropdown's
item ID (`dialog_poulate_coordinatesys_dropdown`, `0x413e80`):

| Slot (`cs`) | Coordinate system | First word | VPanel encoder |
|---|---|---|---|
| 0 | MCS | not allowed | — |
| 1 | User (RML-1) | `0001` | `0x41b270` |
| 2 | EXOFS | `0002` | `0x41b2b0` |
| 3–8 | G54–G59 | `0003`–`0008` | `0x41b2f0(cs + 0x33)`: `(G − 54) + 3` |
| 10–310 | G54.1 P1–P300 | `cs − 1` | `0x41b340(cs − 9)`: `(n − 1) + 9`; not implemented here |
17 changes: 10 additions & 7 deletions rollingmill/decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,11 @@
0x3009: 'spindle_stop', # SET bare trigger (used in drilling sequences)
0x3808: 'nc_spindle_speed', # SET 2×uint16 [RPM, mode=2]; NC job only
0x3809: 'rotatary_drill', # SET 2×uint16 [1,0xFFFF]=start / [0,0]=stop (A-axis, Drill Workpiece dialog)
0x3900: 'spindle_rpm_read', # Pattern B; GET 0x0003 → 1×uint32 RPM
0x3901: 'spindle_rpm_write', # SET 1×uint32 RPM; waits ping bit21
# ── Z0 probing ───────────────────────────────────────────────────────────
0x2001: 'z0_sensor_status',
0x3900: 'z0_sensor_thickness_read', # Pattern B; GET 0x0003 → 1×uint32, 1/1000 mm
0x3901: 'z0_sensor_thickness_write', # SET 1×uint32, 1/1000 mm; waits ping bit21
0x3902: 'z0_probe',
# ── Operation bracket ────────────────────────────────────────────────────
0x1109: 'op_bracket', # SET 1 byte: 0x00=begin, 0xff=end
# ── Coordinate system selection ──────────────────────────────────────────
Expand Down Expand Up @@ -114,7 +117,7 @@
# SET commands that prime the device; the next GET 0x0003 is their response.
# Tracked by parse_lines so decode_line can dispatch 0x0003 correctly.
_PATTERN_B: frozenset = frozenset({
0x3804, 0x2405,
0x2001, 0x3804, 0x2405,
0x3005, 0x3003, 0x3800, 0x3b01,
0x3900,
0x0101,
Expand Down Expand Up @@ -267,8 +270,8 @@ def decode_line(
h, m = secs // 3600, (secs // 60) % 60
return f"{ts} SPINDLE_TIME {h}h {m:02d}m ({secs} s)"
if last_trigger == 0x3900 and len(data) >= 4:
rpm = struct.unpack_from('>I', data)[0]
return f"{ts} SPINDLE_RPM_CFG {rpm} RPM"
um = struct.unpack_from('>I', data)[0]
return f"{ts} Z0_SENSOR_THICKNESS {um / 1000:.3f} mm"
if last_trigger is not None and (
last_trigger == 0x030b or 0x3202 <= last_trigger <= 0x320a
):
Expand Down Expand Up @@ -322,8 +325,8 @@ def decode_line(
return f"{ts} SPINDLE_STOP (bare)"

if wv == 0x3901 and direction == '>' and len(data) >= 4:
rpm = struct.unpack_from('>I', data)[0]
return f"{ts} SPINDLE_RPM_WRITE {rpm} RPM"
um = struct.unpack_from('>I', data)[0]
return f"{ts} Z0_SENSOR_THICKNESS_WRITE {um / 1000:.3f} mm"

if wv == 0x3808 and direction == '>' and len(data) >= 4:
rpm, mode = struct.unpack_from('>HH', data)
Expand Down
Loading
Loading