Skip to content

Working documentation

This page is generated from the project's working notes. It records a real retrofit in progress, corrections included. Anything marked πŸ”΄ or ⚠️ is unresolved or carries a caveat - read those before acting.

The drill heads and the saw on LinuxCNC

10 September 2026. How the ten vertical drills, three horizontal drills and the saw blade get driven once the Tecnos is gone.


First: these are not axes and they are not tools

⭐ Every one of them is a pneumatic on/off actuator at a fixed offset from the router spindle. There is no Z control per drill, no speed control, no feed. You give it air and it goes down; you take the air away and it comes back.

Outputs Feedback
10 vertical drills OF.10-OF.15, OF.25-OF.28 πŸ”΄ None per drill. One shared "all retracted" input
3 horizontal drills OF.16, OF.17, OF.18 IF.26, IF.27, IF.28 β€” one "retracted" each
Saw blade OF.20 down IF.11 up (-SQ140 magnetic sensor)
Counter-pressure OF.19 none
The drill motor πŸ”΄ OF.9 β€” one 1.5 kW motor spins all ten

πŸ”΄ That last row matters more than it looks. OF.9 is not a per-drill thing. One motor drives the whole block, and the individual outputs are pneumatic selects that push a spinning spindle into the work.


The LinuxCNC mechanism: digital outputs, not tool changes

βœ… This is a solved problem in LinuxCNC and it needs no custom components.

G-code Does
M64 P<n> Turn digital output n on, immediately
M65 P<n> Turn it off, immediately
M62 P<n> / M63 P<n> Same, but queued with motion β€” the output changes at the right point along the path rather than when the interpreter reaches the line
M66 P<n> L3 Q<sec> πŸ”΄ Wait for a digital input, with a timeout
G4 P<sec> Dwell

Each P number maps to a motion.digital-out-NN HAL pin, and that goes straight to a Mesa output. Ten drills is ten M64/M65 pairs.

⭐ Do NOT make them tools

The Tecnos numbers them T1-T10, which is tempting to copy. Resist it. LinuxCNC's tool-change machinery is built around the spindle β€” remapping it for things that are not in the spindle means fighting it for no gain.

βœ… Use a subroutine with the drill number as a parameter instead:

o<drill> sub
  M64 P[#1]        ; fire the drill
  G4 P0.4          ; dwell - the entire interlock, see below
  M65 P[#1]        ; retract
  M66 P10 L3 Q2    ; wait for "aggregates up", 2 s timeout
o<drill> endsub

o<drill> call [3]  ; drill 3

⚠️ The CAM post can still think in tool numbers β€” it just emits a subroutine call rather than a tool change.


πŸ”΄ The thing that makes this machine worth having: fire several at once

A multi-drill head exists so you can drill a whole row in one plunge.

Cabinet work is on the 32 mm system β€” shelf pins, hinge plates and drawer runners all on 32 mm centres. If the drill block is on 32 mm centres too, a row of five holes is one M64, one dwell, one M65.

πŸ”΄ A post-processor that fires them one at a time throws away the machine's entire advantage and turns a two-second operation into ten.

M64 P1
M64 P2
M64 P3          ; three drills selected
G4 P0.4         ; one plunge
M65 P1
M65 P2
M65 P3

⚠️ Check the drill block's actual pitch before designing around this. 32 mm is the convention, but it is worth measuring rather than assuming β€” and it decides how much of your typical job collapses into single plunges.


Timing β€” the dwell IS the interlock

πŸ”΄ The vertical drills have no "down" sensor. There is one input, IF.10, and it is a series chain of six symbols meaning "everything on the head is retracted." Any one unit down breaks it.

So the sequence has to be:

1 M64 β€” fire
2 πŸ”΄ G4 β€” dwell. Nothing confirms the drill went down or reached depth. The dwell is the whole guarantee
3 M65 β€” retract
4 M66 on IF.10 β€” wait for confirmed retraction before moving

βœ… Step 4 is the one that is actually verified, and it is the one that matters: moving the gantry with a drill still down would snap it.

⚠️ The dwell length is one of the "real durations" still on the pre-strip survey. Start generous and tighten it once you have watched the machine.


The drill motor must lead, and stay

πŸ”΄ OF.9 gates all ten drills, and a drill pushed into the work with its motor stopped will jam or snap.

βœ… Simplest safe pattern: turn the drill motor on at program start and leave it on for any program that drills. It is a 1.5 kW motor and starting it repeatedly buys nothing.

⚠️ Make it a program-level thing, not a per-hole thing β€” M64 on the motor output in the preamble, M65 in the epilogue, and a check that it is on before any drill subroutine fires.


The saw

Same pattern, different purpose. OF.20 puts the blade down; IF.11 (-SQ140) confirms it is up.

πŸ”΄ But the saw cuts by moving, not by plunging. The order is:

1 Position at the start of the groove
2 OF.19 counter-pressure on β€” it presses the panel down opposite the cutting unit
3 M64 blade down, G4 dwell
4 πŸ”΄ Feed in X or Y β€” this is the cut
5 M65 blade up, M66 on IF.11
6 Counter-pressure off

⭐ Use M62/M63 rather than M64/M65 for the blade so the motion queue puts the blade movement exactly where the path says, not wherever the interpreter happened to get to.

⚠️ No blade-rotation output appears anywhere in the I/O map. Either the saw is fixed orientation or it is set by hand. Worth confirming before designing grooving operations that assume both axes.


βœ… ANSWERED 10 September β€” the drills ride on Z, so depth is programmable

Grant: "The drill depth of cut can be varied. They are mounted on the Z axis carriage with a pneumatic ram to drive them up and down. Z axis therefore sets the height."

πŸ”΄ This is the good answer. plc-sequences.md has carried the question since the PLC dig, and it decided the whole programming model. Depth is a G-code number, not a spanner job.

   Z axis      -->  sets the HEIGHT of the whole drill block
   pneumatic ram -->  drives it DOWN by a fixed stroke
   depth       =  stroke  -  (drill tip to surface at that Z)

πŸ”΄ Two consequences that shape everything downstream

1. One measurement makes depth calculable. The ram stroke is a constant. Measure it once and:

Z_for_depth(d)  =  surface_Z  +  stroke  -  d

⚠️ Until that constant is measured, no generated program can cut to a stated depth. It is one measurement and it unlocks the whole drilling side.

2. πŸ”΄ Every drill in one plunge cuts to the SAME depth, because they share the Z height.

⭐ That is a hard constraint on gang drilling. Holes can only be ganged if they share a depth and a row and fit the block's pitch. Group by depth first, then optimise within the group.

⚠️ And G81 is not the answer here

The obvious LinuxCNC drilling cycle drives the Z axis down and up. On this machine that would lower the entire carriage β€” all ten drills and the router together β€” rather than firing one pneumatic ram.

| G81 | βœ… Right for the router drilling a hole with a bit in the spindle | | G81 | πŸ”΄ Wrong for the boring head. Z sets height; the ram does the plunging |


What this needs from the Mesa build

17 digital outputs 10 vertical + 3 horizontal + saw + counter-pressure + drill motor + the router ram already counted
5 digital inputs IF.10 aggregates up, IF.11 saw up, IF.26/27/28 horizontal borers retracted
βœ… All already in the landing list Nothing new to wire. Every one of these is an existing OF.n or IF.n with a ribbon way and a terminal already assigned
  • io-map.md β€” every one of these outputs to its wire and terminal
  • landing-list.md β€” where each lands on the Mesa
  • pre-strip-survey.md β€” the durations still to measure
  • mw310-operating.md β€” how the Tecnos numbered and commanded them

πŸ”΄ How to actually generate the G-code

Grant's question, 10 September: what is the best way to create the G-code these tools need?

The short answer

⭐ Split it. Use CAM for the router work, and write a small generator for the drilling and sawing. Do not try to make one tool do both β€” the reason is specific and it decides everything.

Why generic CAM cannot do the drilling

CAM knows about one spindle. Fusion, VCarve, Aspire β€” all of them assume a tool in a spindle, moved by the axes.

πŸ”΄ They have no concept of ten spindles at fixed offsets that fire pneumatically. Point any of them at a row of shelf-pin holes and you get ten separate plunges from the router bit: correct parts, ten times the cycle time, and the boring head never used.

⚠️ You could bend a post-processor into doing it, but the post would have to invent the offset arithmetic and the gang grouping β€” which is a real program, written in a post-processor language, per CAM package. That is the wrong place to put it.

Layer Built with Why
Router work β€” profiles, cutouts, pockets Existing CAM + a LinuxCNC post This is exactly what CAM is good at, and LinuxCNC posts exist for every major package
πŸ”΄ Drilling and sawing A small Python generator Where the machine is special and CAM is blind
Machine handling β€” clamps, stops, vacuum, drill motor LinuxCNC o subroutines Identical every job. Write once

βœ… The Python generator is the master. It emits the preamble, the drilling, the sawing and the epilogue, and calls into the CAM output as a subroutine for the router work. One file per part, one source of truth.

Why Python rather than clever G-code

LinuxCNC has real parametric G-code β€” variables, arithmetic, while, if, o subroutines β€” and the gang-drilling optimiser could be written in it.

⚠️ Do not. The optimiser is a grouping-and-matching problem, and:

βœ… Python is testable without the machine You can run the generator, diff the output, and assert on it
βœ… This repo already has the toolchain A Python build system with tripwires, and linuxcnc-sim/ to run generated programs against
βœ… Mistakes are cheap A bug in a Python optimiser is a failing test. A bug in a G-code while loop is a drill in the wrong place
πŸ”΄ G-code has no test framework And debugging arithmetic inside an interpreter that is also moving a machine is exactly the wrong time to find out

⭐ Keep the G-code dumb and the generator smart. The subroutines on the machine should do one obvious thing each; all the thinking happens before the file is written.

What the generator actually has to do

1. The gang-drilling optimiser

The whole reason for the project. Given a list of holes and the drill block's geometry:

group holes by  (depth, Y position, diameter)
  within each group:
    find sets whose X spacing matches available drill offsets
    emit one plunge per set

⭐ For cabinet work this is usually easy, because the holes are on the 32 mm system and so is the block. Most rows collapse to a single plunge with no cleverness at all.

βœ… Build it in two stages. Get a naive one-drill-at-a-time version working end to end first, then add the grouping. The naive version is correct, just slow β€” and it proves the whole chain before the optimiser exists.

2. The offset arithmetic

Each drill sits at a fixed (dx, dy) from the router spindle. To put drill 3 over a hole at (X, Y), the gantry goes to (X - dx3, Y - dy3).

πŸ”΄ This belongs in the generator, not in G-code and not in the tool table. It is a lookup and a subtraction, and putting it anywhere else makes it invisible.

3. Depth from Z

Z_for_depth(d)  =  surface_Z  +  ram_stroke  -  d

⚠️ ram_stroke is the one measurement still missing. Put it in a config file, not scattered through the code.

4. Sequencing the machine handling

Clamps, stops, vacuum, the drill motor on before any drill fires, counter-pressure around the saw. Same every job, so it lives in the preamble and epilogue subroutines and the generator just calls them.

A concrete starting point

Input: a CSV of holes. One row per hole, because that is what any cabinet design tool can export and what you can also type by hand:

x,y,depth,dia,face
32,50,12,5,top
32,82,12,5,top
32,114,12,5,top

Config: a JSON file with the drill block geometry, the ram stroke, the dwell times and the digital-output numbers. ⭐ Every machine constant in one place, so recalibration is one edit.

Output: a .ngc file, run against linuxcnc-sim before it ever reaches the machine.

βœ… That is a weekend of work for the naive version, and it is immediately useful β€” it will drill correctly, just one hole at a time. The optimiser is a second weekend and it is where the speed comes from.

⚠️ What to be careful about

πŸ”΄ Gang only within one depth Every drill in a plunge shares the Z height. Different depths cannot share a plunge
πŸ”΄ The dwell is the only depth guarantee No sensor confirms a drill went down. Generous first, tightened after watching
πŸ”΄ Never move with a drill down M66 on IF.10 before any motion. The generator must emit it every time, not sometimes
⚠️ Verify in the sim first Every generated program, every time, until the generator has earned trust
  • linuxcnc-sim/ β€” where generated programs get run before the machine sees them
  • pre-strip-survey.md β€” the dwell times still to measure

πŸ”΄ Stopping XY while a drill is down, and diagonals while the saw is down

Grant, 10 September: how do you tell LinuxCNC never to do these things?

⚠️ The honest framing first: "telling LinuxCNC" is the weakest of the three places this can be enforced, and the one everyone reaches for. A rule in the G-code is a convention, not a guard β€” a hand-typed MDI line, a bug in the generator or an edited file all walk straight past it.

⭐ Do all three layers. They fail differently, which is the point.

Layer Catches Weakness
1. The generator never emits it The normal case Anything not produced by the generator
2. A linter rejects the file before it runs Hand edits, generator bugs, files from anywhere Nothing that happens at run time
πŸ”΄ 3. HAL stops the machine anyway Everything, including MDI and jogging Reactive β€” it stops a crash, it does not prevent one

πŸ”΄ Layer 3, the drills β€” and the machine already provides the signal

IF.10 is a series chain of six symbols meaning "every unit on the head is retracted." Any one unit down breaks it.

⭐ That input was built to be an interlock, and this project has so far treated it as information. Use it as the interlock.

      IF.10 true   =  everything retracted
      IF.10 false  =  something is down

πŸ”΄ CORRECTED 10 September β€” that wiring would stop the machine on every hole

⚠️ IF.10 is false for the whole of every legitimate plunge. It means "everything is retracted", so firing a drill makes it false by design. Wiring NOT(IF.10) straight to a motion inhibit would drop the machine to "machine off" on every hole it drilled.

βœ… The interlock is a conjunction, not a signal:

      a drill is down   AND   an axis is actually moving   ->   stop

⭐ Which is the same shape the saw needs, and it means layer 3 is built on velocity pins rather than on the input alone. The input alone says nothing about whether anything is moving.

βœ… Three reasons this is the strong version:

It reads reality, not intent It does not trust that an output went off. It reads whether the units actually came up
It is fail-safe A broken wire reads "not retracted" and inhibits motion
It covers every path Program, MDI, jog, a HAL pin poked by hand. Nothing gets past it

The HAL shape

βœ… The pins are now verified against the LinuxCNC 2.9 documentation and source β€” see linuxcnc-motion-pins.md. Three findings changed the design:

πŸ”΄ motion.motion-inhibit does not exist This page reached for a pin by that shape. A net to a non-existent pin is a silent no-op
πŸ”΄ motion.feed-inhibit is defeatable by M49 Undocumented gating on SS_ENABLED in the 2.9 source. A line of G-code turns the interlock off
πŸ”΄ No feed pin affects jogging "feed-inhibit applies to G-code commands -- not jogs". Jogging with a drill down is exactly the case that needs covering

⭐ So the drill interlock uses motion.enable β€” checked unconditionally, cannot be disabled from a part program β€” plus motion.jog-inhibit and motion.jog-stop for the jog path.

⭐ A hard stop is arguably correct anyway. If a drill is down while the gantry is moving, that is a crash in progress. Stopping hard and needing a reset is the right cost. ⚠️ It is a real cost though: the amp enables drop, the motion queue is cleared, and the stop is a coast rather than a controlled decel.


The saw β€” and it got simpler, not harder

βœ… ANSWERED 10 September β€” one blade, one axis, one direction

Grant: "There is only one saw blade that cuts along the long axis. So the rule is only move along the long axis when saw blade down. And more specifically only go one direction along that axis. I don't know which way yet as I don't have a saw blade mounted at the moment."

⭐ This is much better news than it sounds. The earlier draft of this page treated the saw as harder than the drills, because "do not move diagonally" means catching two axes moving at once rather than any axis moving β€” which needs two velocity thresholds and is inherently reactive.

πŸ”΄ That problem is gone. The rule is now stricter and simpler:

With the blade down
X may move X is the long axis β€” 2903 mm of travel against Y's 1732
πŸ”΄ Y must not move. Z must not move. Not "not at the same time as X". At all
πŸ”΄ X may only move ONE WAY ⚠️ Which way is not established β€” no blade is mounted yet

βœ… And it explains a loose end. io-map.md has no blade-rotation output anywhere, and this page previously flagged that as "worth confirming". There is no rotation output because there is nothing to rotate. The question is closed.

Why the direction matters, and it is not a preference

A spinning blade fed the wrong way climbs the cut. Instead of the teeth pushing the panel down into the bed, they pull it along and up. It snatches the workpiece, or the blade.

⚠️ So cut_direction is a real machine constant, not a convention, and it sits in machine.json as null until a blade goes on and the rotation is seen.

The HAL shape

saw-down  AND  (|Y velocity| > threshold  OR  |Z velocity| > threshold)   ->  stop
saw-down  AND  X velocity of the wrong sign                              ->  stop

⭐ Note what changed. The first line is an OR of two axes that should be at rest, not an AND of two that are both moving. That is a much easier thing to get right, and the threshold is now only guarding against following-error jitter on a stationary axis rather than trying to separate "a slow diagonal" from "a straight cut".

⚠️ The threshold still needs setting against the real machine. Servo following error puts small non-zero numbers on a velocity pin that is nominally at rest. But it is now a one-sided problem: too tight nuisance-trips, too loose lets a slow drift through β€” and there is no legitimate Y or Z motion to accommodate.

βœ… What the generator and linter do with this

A diagonal πŸ”΄ Refused
A groove along Y πŸ”΄ Refused β€” there is one blade and it is fixed. Not "rotate the head first"; it cannot be cut on this machine
Any Y or Z move with the blade down πŸ”΄ Refused
A groove given the wrong way round ⭐ Silently reversed. A slot from A to B is the same slot as from B to A β€” only the feed direction changes, and that is the thing being corrected. The generator reports how many it flipped
A cut the wrong way in a hand-written file πŸ”΄ Refused by the linter, once cut_direction is set
Any saw cut at all, while cut_direction is null πŸ”΄ Refused, with the reason named. --provisional overrides it for sim work and stamps "directions UNCHECKED" into the file

⚠️ Until the blade is mounted, the linter can only warn about direction. It says so explicitly rather than passing silently β€” an unset constant must never read as approval.


⭐ Layer 2: the linter β€” the one most people skip, and it is cheap

A Python script that reads a .ngc file and refuses it. It tracks the modal state β€” which outputs are on, where the tool is β€” and asserts:

Rule
No X or Y move while any drill output is on
No Y or Z move at all while the saw output is on
No X move against the blade's cutting direction
M66 on IF.10 present after every drill retract, before the next move
Drill motor on before any drill fires

βœ… It catches what layer 1 cannot β€” hand edits, a generator bug, a file from somewhere else β€” and it catches it before the machine has the file, which is the only place prevention is actually possible.

⭐ And it is a couple of hundred lines. It parses G-code, not physics. This repo already has the Python toolchain and the build tripwire pattern to hang it off.

πŸ”΄ Run it as a gate, not a suggestion: no .ngc reaches the machine without passing.


⚠️ The thing that gets interlocks bypassed

Setup and maintenance need a drill down while the machine is stationary, and eventually somebody will need to move with one down to free a jam.

πŸ”΄ An interlock with no documented override gets permanently bypassed by whoever is in a hurry. The wire comes off and never goes back.

βœ… Design the override in from the start: a key-switch or a deliberate HAL setting that suspends the interlock, is visible when active, and clears itself on machine-off. Make the safe path the easy one.

  • linuxcnc-motion-pins.md β€” the verified pin semantics, and the three traps
  • io-map.md β€” IF.10, and what its six symbols actually are
  • linuxcnc-sim/ β€” where the linter and the HAL interlock both get tested first

βœ… ANSWERED 10 September β€” "trips when" or "can't come down"? Both, and they are different guards

Grant: "Should we change the wording of the saw guard from trips when blade down and Y or Z moving to blade can't come down if Y or Z moving? Or if blade down is requested wait for Y or Z to stop moving?"

⭐ The instinct is right β€” prevention beats tripping β€” but it cannot replace the trip, because there are two ways to get into trouble and each wording only catches one.

Failure Caught by
The blade comes down while the machine is moving πŸ”΄ Only a permissive: "can't come down unless stopped"
The machine starts moving while the blade is already down πŸ”΄ Only a trip: "stops if Y or Z moves while down"

No single sentence covers both. So the guard is now stated as a pair, and built as a pair:

βœ… The blade cannot begin its descent unless Y and Z are stopped. βœ… If Y or Z moves while the blade is down, the machine stops.

⭐ And "wait" is free β€” HAL is dataflow, not a program

A gate on the output is a wait. The output simply does not energise until the condition clears, and then it does. There is no timer, no state machine, nothing to write.

πŸ”΄ But a silent wait desynchronises the G-code from reality. The program issues M64 P20, dwells 1.2 seconds believing the blade went down, and starts cutting β€” while the gate was still holding the command off. There is no blade-DOWN sensor to catch that.

βœ… So the wait is published as an input the G-code can see, and every descent waits on it explicitly:

o<wait_stationary> call [2.0]     ; M66 P60 L3 - aborts if it never comes
M64 P20

⚠️ CORRECTED 10 September β€” the reason given here was wrong

This section argued that because M64 is documented as acting "immediately as they are received by the motion controller… not synchronized with movement", an M64 after a G0 could fire while that G0 was still decelerating.

πŸ”΄ It cannot. Task gates M64 on WAITING_FOR_MOTION, released only when the motion queue is empty and the machine is in position. The documentation's phrase means the change does not land at a chosen point along a path β€” which is why it breaks blending β€” not that it can overtake the move.

βœ… o<wait_stationary> stays anyway, for a smaller and real reason: commanded-position-reached is not physically-stopped, and the velocity pin the guards use is a commanded value that does not see following error. ⚠️ But a rule kept on a false justification is a rule nobody will trust when it is next questioned, so the justification is corrected rather than quietly left standing. See linuxcnc-motion-pins.md.

⚠️ The saw needed a seal-in, and a plain AND would have been a bad bug

The saw cuts by moving. Gate the blade on "stationary" alone and πŸ”΄ the blade would retract the instant the cut started, mid-kerf.

βœ… The condition applies to going down, not to being down:

   down  =  commanded  AND  (stationary  OR  already down)

The blade cannot begin its descent unless the machine is stopped; once down it stays down for as long as it is commanded, and feeding along X is free. Releasing the command breaks the seal. The counter-pressure beam shares it deliberately β€” it clamps before the blade descends and must hold for the whole cut.

⭐ The drills get the opposite treatment, and the asymmetry is the point

A vertical drill NEVER legitimately moves while down, so it gets a plain AND with no seal-in: if the machine starts moving, the command drops and the drill retracts.

βœ… That is the right response. motion.enable drops too, but the amps take time to stop a moving gantry β€” and the drill is quicker out of the hole than the gantry is to a standstill.

Permissive Seal-in? Because
Vertical drills motor running and stationary no never moves while down β€” dropping out retracts it, which is what you want
Horizontal borers stationary no ⚠️ not gated on the motor β€” which motor serves them is still unconfirmed
Saw + counter-pressure stationary to descend yes cutting is moving
πŸ”΄ Router ram not gated on stationary at all β€” ⭐ the one head that legitimately moves while down. Plunge, then feed β€” that is the cutting cycle. It stays gated on the cut permit instead

What was added

tuchop-io.hal Β§5 The permits, the seal-in, and machine-stationary published to motion.digital-in-60
nc_subs/wait_stationary.ngc The explicit wait, with an abort
ngc_lint.py A new rule: nothing may descend until the machine is proven stopped
gen_drill.py Emits the wait before every plunge β€” one per plunge, not one per program

⚠️ P60 is a deliberate exception to the numbering rule. Everywhere else the LinuxCNC digital I/O index is the machine's own IF.n/OF.n. 60-63 are reserved for signals synthesised in HAL, and are listed in machine.json under inputs.synthetic.


πŸ”΄ CORRECTED 10 September β€” the drill guard was pointing the wrong way

Grant: "There's something backward in the drill wording of the guards for the drills. Instead of retract drills if machine starts moving, the guard should be prevent machine moving until drills are retracted."

⭐ He is right, and the argument I gave for the old behaviour was wrong three times over.

The previous version gated the drill outputs on "the machine is stationary" with no seal-in, so if the machine started moving the command was dropped and the drills retracted. I justified that as "the drill is quicker out of the hole than the gantry is to a standstill."

Why that was wrong
It does not retract fast It is a pneumatic ram. The valve de-energises, air exhausts, the ram returns β€” tens of milliseconds at best, while the gantry accelerates with the drill still in the hole
πŸ”΄ It erases its own evidence Retracting the drills makes IF.10 go true, which clears the very condition the guard trips on. A self-cancelling fault is worse than no fault
πŸ”΄ An interlock must inhibit, not actuate Yanking an actuator in response to a fault makes the guard itself a source of uncommanded machine movement β€” and it changes state under the G-code, which still believes the drill is down and will later command M65 and check IF.10

⭐ The right lever

The drill being down is a legitimate state. Moving is the offence. So block the motion, not the drill.

βœ… The machine will not move while a drill is commanded down.

This is preventive in the strongest sense available: a G0 after a plunge does not start and then get stopped β€” it never starts. The machine waits, and goes when the head is up.

βœ… And it makes the drills and the saw symmetric after all. Both now have a seal-in; both descend only from rest; both stay down once down. The asymmetry I invented was an artefact of the mistake.

Which pin, and why not motion.enable

⚠️ motion.feed-inhibit is right here and motion.enable is not. This is a hold, not a fault:

It releases by itself The moment the drills come up. No reset, no lost homing, no dropped amps
It happens constantly Every hole in every program. Dropping the amp enables every time the machine drills would be absurd, and it is exactly what makes people rip interlocks out

πŸ”΄ But feed-inhibit is silently disabled by M49 β€” which is precisely why the trip in section 5b still exists. ⭐ The hold is the primary and prevents the crash; the trip is the backstop for when the hold is defeated.

πŸ”΄ The trap: IF.10 cannot be used raw for this

IF.10 is one magnetic sensor, -SQ201, and MW310.EQU aliases six symbols onto it β€” IF_AGGR_SU plus IF_MANDR_1_SU through _5_SU, the optional pantograph heads.

⚠️ plc-sequences.md records the original PLC using IF.10 as the head-up confirm around the ROUTER PLUNGE. So if the router head being down breaks IF.10 β€” and the PLC's use of it says it does β€” then holding motion on NOT(IF.10) would make ordinary routing impossible. Plunge the router and then move is the machine's main job.

⚠️ Whether the SAW head also breaks it is not established. The saw has its own up sensor on IF.11 (-SQ140), which suggests not β€” but one sensor answering for six symbols could go either way.

βœ… What IF.10 definitely is NOT β€” it has nothing to do with Z

Grant, 10 September: "IF.10 is independent of Z height. Z height sets the depth of the hole but can't affect IF.10."

-SQ201 senses whether the pneumatic units are retracted RELATIVE TO THE CARRIAGE. Z moves the whole carriage β€” drill block, router head and all β€” so Z position cannot affect IF.10 at any height.

⭐ This is what makes the hold safe to leave permanently armed. It can only ever be asserted by a unit that is actually down. No axis move can provoke it, and no Z move can talk the machine into holding itself still.

⚠️ It does not soften the linter's Z rule, which is a different claim: Z carries the whole block, so moving Z with a drill already down drives that drill deeper. Both are true, and neither implies the other.

βœ… So the hold is built to be correct under both answers:

   hold  =  any drill COMMANDED down
            OR  ( IF.10 low  AND  not the saw  AND  not the router )

⭐ The first term is the one that does the work. It is asserted the instant the G-code asks for a drill, before anything has physically moved, and it does not depend on IF.10 at all. That is what actually implements "do not move until the drills are retracted" β€” M65 drops the command, the hold releases.

The second term is the backstop: a unit physically down without being commanded, or one that has not come up yet after M65. It is why the hold does not release the instant the command drops.

⚠️ If those exemptions never bite, nothing is lost. If they do, routing and sawing still work. Either way the drills are covered β€” and πŸ”΄ whether SQ201 sees the saw and the router head is now a question worth settling at the machine, because it decides whether those exemptions are doing anything.

The saw stays out of the feed hold, deliberately

Feeding along X with the blade down IS the cut. So the saw is in motion.jog-inhibit β€” jogging with a blade down is never intended β€” but not in the feed hold.

⭐ motion.jog-inhibit's checks are unconditional in the source, so unlike feed-inhibit it is not defeatable by M49.


πŸ”΄ CORRECTED 10 September from a photograph of the head

Grant photographed the drill/saw head with the bits and blade out. It corrects the model this page was built on and closes two open questions.

The head is NOT ten drills in a row

Eight vertical drills in a row, on 32 mm centres, aligned with X 1 = 4 mm πŸ”΄ BROKEN OFF Β· 2 = 8 mm reverse Β· 3-8 = six 5 mm
Two more vertical spindles, OFFSET IN Y below the row 9 = 35 mm hinge-cup borer, conventional Β· 10 = 10 mm reverse, directly below the 35
Three horizontal heads, ⭐ two bits each OF.16/17/18 β€” one output per head, so firing one drives two bits
Two dust extraction ports ⚠️ No I/O for either. Extraction is not under machine control

Ten vertical spindles total, matching the ten outputs. βœ… And it reconciles with tabuten.tmp exactly, which is a good sign both are right: T1 4 mm is the broken one, T2 8 mm, T4 35 mm, T5 10 mm, and T3 plus T6-T10 make the six 5 mm.

⭐ It also confirms two assumptions that were guesses: the row really is on 32 mm centres, and it really is aligned with X.

βœ… The saw direction is settled, and the blade is not even fitted

Grant: "From the direction the mechanism spins we can deduce that saw cuts run from x positive towards x negative."

πŸ”΄ cut_direction = "-". Deduced from the arbor rotation rather than measured with a blade on β€” which is why it could be settled with the blade missing. The generator and linter now enforce it, and X was not touched by the axis reversal, so it needs no conversion.

πŸ”΄ Reverse spindles need left-hand bits

All ten run off the one motor (OF.9). Adjacent spindles in a boring head are gear-driven in opposite directions β€” that is normal, not a fault. The red-marked bits are the reverse ones.

⚠️ Fit a right-hand bit to a reverse spindle and it will not cut β€” it rubs and burns. The 8 mm and the 10 mm are recorded as reverse; which of the six 5 mm are reverse is not yet recorded.

What the generator now refuses, and why each is a refusal rather than a guess

Refused Because
Any 4 mm hole Spindle 1 is broken. Planning a plunge onto it would drill nothing, and nobody would find out until the part came off the table
Any 35 mm or 10 mm hole Those two spindles sit off the row and their offsets have never been measured. Arithmetic on an unknown offset puts the hole in the wrong place, which is worse than refusing

⭐ Two measurements unlock the hinge borer β€” the X and Y of each offset spindle relative to the router. That is the single highest-value thing left on the drilling side, because 35 mm hinge cups are most of what a cabinet shop drills.

πŸ”΄ ANSWERED 11 September β€” the horizontal pairs point OPPOSITE ways

Grant: "They point in opposite directions, one each side."

⭐ So this is not a two-holes-at-once capability at all. It is a choice of direction β€” bore the near edge or the far edge without re-fixturing the panel.

πŸ”΄ But one output fires BOTH bits. Only the bit facing material cuts. The other swings out into free air.

⚠️ The bit that is not cutting is the one that breaks things. A head fired with the far bit over a clamp, a fence, or the far side of a narrow panel drives a spinning bit straight into it.

βœ… The original machine guarded exactly this β€” and the guard does not come across

English/ISO.TXT:340-342:

M106  DISABLES THE ANTI-COLLISION FUNCTION OF THE HORIZONTAL DRILLS
M107  ENABLES  THE ANTI-COLLISION FUNCTION OF THE HORIZONTAL DRILLS (default condition)

⭐ On by default, with an explicit code needed to turn it off. Masterwood treated this as a real hazard, not a nicety β€” and that is a strong signal, because they had the machine in front of them and we do not.

πŸ”΄ There is no reference to it anywhere in MW310.PLC. So it was a compiler or board function, and it is in the lost with the PC bucket alongside the spindle-overlap and the entry/exit decel β€” see what-lives-on-the-board.md.

⚠️ pargen.prm:78 has Q_ANTICOLL_V 265 = 2.65 mm, a V-axis quantity, which hints the borers point along Y. But what the function actually guarded against is not established from the capture, and I will not invent it.

What the generator does about it now

It refuses any hole on a face other than top, and says why. Edge boring needs three things this config does not have:

Which axis the pairs point along The vertical row is confirmed aligned with X; the horizontal heads have not been stated
Their offsets from the router spindle Same problem as the two offset vertical spindles
πŸ”΄ A clearance check for the bit that is NOT cutting This is the new one, and it is the one with teeth

⭐ That third item is a genuine piece of retrofit work, not a config value β€” it is the anti-collision function, rebuilt. It belongs in the generator, where the part geometry is known, not in HAL.


The saw aggregate β€” arbor data, and what actually chooses the blade

Grant, 11 September 2026, measured with the blade out:

Arbor thread M20 Γ— 2.5
Arbor diameter 20 mm β€” so the blade bore is fixed at 20 mm
Collets and nut πŸ”΄ Not with the machine. To be sourced
Thread hand βœ… RIGHT hand (Grant, 11 Sep 2026)

βœ… The thread hand is answered, and it was not what this page expected

Order a RIGHT-hand M20 Γ— 2.5 nut.

⚠️ This page previously said left-hand was likely, on the general rule that saw arbor nuts usually are, so that cutting force tightens rather than undoes them. That rule does not hold here. Grant looked at the actual thread. Measurement beats the rule.

⭐ But right-hand is now a free cross-check on the cut direction

The cut direction is still only DEDUCED, from a photograph of which way the mechanism turns. The thread hand is independent physical evidence about the same thing, so the two have to agree.

Under load the blade lags the arbor, and friction drags the nut backwards relative to the arbor. For that to tighten a right-hand nut:

πŸ”΄ The arbor must turn ANTICLOCKWISE, seen from the outer face of the nut.

Check that at commissioning, before a blade ever runs. If it turns the other way, the nut unwinds itself in service and throws the blade, and one of the two observations is wrong: either the thread hand, or the +X towards βˆ’X cut direction that machine.json and the generator are both built on.

⚠️ This reasoning is derived, not measured. It costs one look to settle.

⚠️ The flange set has to suit the arbor (20 mm) and the blade bore together, and there may be a drive pin or keyway to match.

πŸ”΄ Nothing in the capture records a blade size

Searched pargen.prm, all of Param/, and every Defs/*.def. The machine never stored one. It is a tooling choice, not a machine parameter β€” so there is nothing to recover, and nothing lost when the PC goes.

⭐ Three things decide the blade, and only one of them is about what fits

1. Kerf = groove width. This is the one that actually chooses it.

The groove the saw cuts is exactly as wide as the blade is thick.

So the real question is not "what blade fits" but "what do we need to groove?" Drawer bottoms and cabinet backs in 3.6 mm hardboard, or 6 mm ply, want different blades. ⭐ Decide this first β€” it narrows everything else, and it is a shop decision rather than a machine one.

2. Maximum diameter β€” whatever clears the aggregate housing. A physical measurement on the head.

3. Minimum useful diameter β€” the blade has to project far enough below the head, at full ram stroke, to cut through the panel. Depth of cut is the blade radius less the arbor-centre height above the work, so a bigger blade cuts deeper.

⚠️ Two measurements settle 2 and 3: the housing clearance around the arbor, and the arbor centre height above the bed at full stroke. Both are easiest with the cover open β€” ⭐ and the head is open now.


βœ… The offset measurements move to commissioning

Grant, 11 September: "Measuring the 35mm bore offset from router accurately is difficult. My preference is to move this to a commission measurement that we will take after doing a test cut."

⭐ Agreed, and it is the better method rather than just the easier one.

   drill a test hole at a commanded position
   measure where it ACTUALLY landed
   the difference IS the offset

Tape-measuring from a router centreline measures the wrong thing β€” it gives you where the spindle housing is, not where the tool centreline puts a hole. βœ… Deriving it from a cut absorbs tooling runout, any squareness error, and the offset itself, in one number.

⭐ The same technique settles the horizontal head offsets, which have the same problem and were going to be just as awkward.

⚠️ Until it is done the generator refuses any 35 mm or 10 mm hole rather than guessing β€” which is the right state to be in, because a wrong offset does not fail loudly, it just drills in the wrong place.


How each individual ram is actually fired

Grant, 11 September: "The drill block has individual air rams on each of the drills, the horizontal drill heads and saw blades β€” how is each individual ram activated?"

⭐ One output, one solenoid valve, one ram. There is no multiplexing and no selector β€” every ram has its own valve, and every valve has its own wire back to the cabinet. That is why the retrofit is as simple as it is.

The chain, end to end

   M64 P10                        G-code
     -> motion.digital-out-10     LinuxCNC
     -> Mesa output               (replaces the Monoscheda's TD62783)
     -> UF module relay coil      18.5 mA - the module does the switching
     -> relay contact             switches the -G2 24 V DC rail
     -> wire A210                 one wire per output, tagged
     -> -X5 A210                  cabinet terminal
     -> -D pin 3                  Harting, cabinet to machine
     -> -V1 pin 4, YELLOW         the valve bank connector
     -> solenoid Y101             the valve itself
     -> air to drill 1's ram      it goes down

πŸ”΄ De-energise and it spring-returns. Single-coil valves: energised = down, off = up. That is the fail-safe direction β€” lose the output, lose power, lose the controller, and every drill comes up.

The thirteen drill valves are on one bank

-V1 is a 25-pin connector (MASC250002, "CONN. 25P + CAVO ... PER BATTERIA EV 3mt" β€” a batteria di elettrovalvole, a solenoid valve bank, on 3 m of cable).

Valves
Pins 1-3 Horizontal heads 1-3 (Y131-Y133)
Pins 4-13 The ten vertical spindles (Y101-Y110)
Pins 17, 18 Common
Pins 19, 20 PE

⭐ So a valve can be found by colour alone. OF.10 β€” drill 1 β€” is the yellow conductor on pin 4. The full DIN 47100 colour map is in io-map.md.

⚠️ The saw and the router are NOT on that bank

Valve Route
Saw blade down OF.20 Y140 Connector -C pin 13 β†’ -X4 β†’ junction box =BM+X4
Counter-pressure OF.19 Y130 -C pin 12 β†’ -X4
Router ram OF.23/OF.24 Y201A / Y201B -C pins 14, 15 β†’ -X4

πŸ”΄ Y201 is ONE valve with TWO coils β€” A raises, B lowers. It is not spring-return: de-energise both and the router head stays wherever it was put. That is why tuchop-io.hal never drives them together and why the retract is deliberately ungated.

What this means for the retrofit

βœ… Nothing at the valve end changes. Mesa drives the module relay exactly as the Monoscheda did; the relay switches the valve; the valve bank, the 3 m cable, the Harting connectors and every solenoid stay untouched.

⭐ That is the whole reason the retrofit is eight connectors on the Housing's front panel rather than a rewire β€” see retrofit-boundary.md. The machine's pneumatics were already one-wire-per-function, and the Tecnos was only ever a switch on the end of each wire.


πŸ”΄ IF.10 DID NOT MOVE WHEN EVERY HEAD FIRED (31 October 2026)

This page's central interlock claim is contradicted by observation and must be re-examined before the HAL is trusted.

reset-sequence.md captured the machine's own self-test at 56 fps. Three times over, OF.10-15, OF.25-28, OF.16-18 and OF.20 all fired together - ten vertical drills, three horizontal heads and the saw. Each time, IF.11 (LAMA SU) dropped and returned, and IF.26/27 (horizontal units) dropped and returned.

πŸ”΄ IF.10 never changed state once.

This page states as the design's founding fact that "IF.10 goes FALSE on every legitimate plunge - it means 'everything is retracted', so firing a drill makes it false by design", and tuchop-io.hal section 5c builds the motion hold on it.

Possible readings, none confirmed:

SQ201 watches something else The router head or an aggregate carrier, not the drill block
The heads never physically moved Commanded dry; if nothing descended, no sensor would change
The series chain held it high IF.10 carries six symbols on one point; another member may dominate

⚠️ The hold's first term - "any drill COMMANDED down" - does not depend on IF.10 and is unaffected. It was written that way deliberately, which now looks fortunate rather than merely careful. What is in doubt is the second term and every claim on this page about what IF.10 means.

πŸ”΄ Cheapest test: with the air on, fire one drill from the MW310 diagnostic screen and watch IF.10. One minute, and it settles the design.


⭐ Param/Offset.prm β€” the head geometry, measured, in the machine's own file

Found 12 September 2026, while answering "what exactly are we reading off the HMI?". The answer turned out to be much less than had been claimed, because most of it was already on disk.

The drill block, all ten spindles

Rows 0-9, decoded (hundredths of a mm, old frame, offsets relative to tool 1):

Row Tool X Y Bit
0 T1 0.00 0.00 4 mm (broken)
1 T2 +32.00 0.00 8 mm reverse
2 T3 +64.00 0.00 (no entry in tabuten.tmp or TAB01.DAT)
3 T4 +32.00 +32.00 35 mm hinge borer
4 T5 +32.00 +64.00 10 mm reverse
5 T6 βˆ’32.00 0.00 5 mm
6 T7 βˆ’64.00 0.00 5 mm
7 T8 βˆ’96.00 0.00 5 mm
8 T9 βˆ’128.00 0.00 5 mm
9 T10 βˆ’160.00 0.00 5 mm

βœ… The 32 mm pitch is no longer an assumption. It is 3200 in the machine's own parameter file, on eight spindles.

⭐ And the 35 mm hinge borer offset is here β€” +32.00, +32.00 β€” the measurement deferred to commissioning as "difficult to measure accurately". It may not need measuring at all, only confirming.

All ten carry Z 27140 = 271.40 mm, a single shared head Z reference.

πŸ”΄ But it CONFLICTS with the head photograph, and the conflict is not resolvable from here

Ordered by X, left to right in the old frame (X+ = operator's right):

T10   T9    T8    T7    T6    T1      T2     T3
5mm   5mm   5mm   5mm   5mm   4mm*    8mm    (none)
                              BROKEN

Grant's photograph, left to right: "The first drill on the left is broken off, the second drill from the left is 8 mm and it's a reverse drill. The remaining six are all 5 mm."

File Photo
Position of the 8 mm 2nd from one end 2nd from the left
Position of the broken one 6th 1st

⚠️ Reading the file from the other end does not fix it either β€” that puts the 8 mm before the broken one, and the photo has them the other way round.

πŸ”΄ Do not guess which is right. This decides which physical spindle each T number fires. Get it wrong and every drilled hole lands one or more 32 mm steps off, and the program will look perfectly correct. machine.json's drill map is therefore left as it was rather than being rewritten from this file.

It is one look at the machine to settle: stand at a known side, count from a known end, and note which spindle is the broken one and which is the 8 mm. Everything else follows.

⭐ Every head, NAMED β€” and the router offset is in the same table

The type numbers resolve to English names, through Defs/tipoute.txt (type β†’ message id) and English/messaggi.txt. ⚠️ The message file is off by one: message n is line n+1. Checked against Magazzino.def, where QUOTA_CARICO|565 lands on "Coupling value" only with the offset applied.

Type Name
1 Vertical drill
2 / 3 Front / Rear horizontal drill
4 / 5 Left / Right horizontal drill
6 Saw blade on X
98 Tool changer router

The whole head, in one table (old frame, mm, relative to the T1 origin):

Row Head X Y Z
0-9 Vertical drill Γ—10 see above 271.40
16 Left horizontal drill +63.40 βˆ’32.00 207.60
17 Right horizontal drill βˆ’2.00 βˆ’32.00 207.60
18 Left horizontal drill +63.40 βˆ’64.00 207.60
19 Right horizontal drill βˆ’2.00 βˆ’64.00 207.60
20 Front horizontal drill βˆ’4.44 +87.70 208.60
21 Rear horizontal drill βˆ’4.44 +148.00 208.60
22 πŸ”΄ Saw blade on X +32.00 βˆ’168.00 260.50
24 πŸ”΄ Tool changer router βˆ’89.10 βˆ’294.60 282.10

Row 24 also carries 24000 rpm (the HSD M200's rating) and magazine 25 in its trailing columns, tying it to the carousel. Two independent confirmations that row 24 is the router.

πŸ”΄ The drill-to-router offset

Everything is in one table with one origin, so the offset between any two heads is just the difference between their rows. That holds regardless of where the origin actually is β€” which matters, because the T1 identity is still contested (above).

Router relative to vertical drill T1:

Old frame (as stored) βœ… Retrofit convention
X βˆ’89.10 βˆ’89.10
Y βˆ’294.60 +294.60
Ξ”Z +10.70 (router lower) βˆ’10.70

Saw relative to T1:

Old frame βœ… Retrofit convention
X +32.00 +32.00
Y βˆ’168.00 +168.00
Ξ”Z βˆ’10.90 (saw higher) +10.90

βœ… X and Y are solid β€” they are in-plane positions in a single consistent frame, and the frame conversion is a sign flip on Y.

⚠️ The Z column is the datum question again. All ten drills share 271.40 and the heads differ from it by ±11 mm, which reads as a per-head reference height rather than a tool tip. Treat the Z differences as real and the absolute values as unresolved until one tool is touched off.

⭐ This also settles which way the horizontal heads bore

machine.json has horizontal_heads.axis: null. The names answer it, and they match Grant's count of three heads, two bits each pointing opposite ways:

Head Bores along The two bits
Y βˆ’32.00 X Left at X +63.40, Right at X βˆ’2.00 β€” 65.40 mm apart
Y βˆ’64.00 X same pair, 32 mm further along Y
X βˆ’4.44 Y Front at Y +87.70, Rear at Y +148.00 β€” 60.30 mm apart

πŸ”΄ So it is not one axis for all three. Two heads bore along X and one bores along Y. A generator that assumed a single horizontal axis would drive the third head into the wrong face.

C 1  6  -432   5000  25 1 0
C 2  6  -432  10000  25 1 0
...
C 7  6  -432  35000  25 1 0
Q 6  5360  0

⭐ Q 6 5360 0 independently confirms the tool-change heights already taken from P001.SBL (VAGGANCIOW = 53.60 coupling, VSGANCIOW = 0 release). Two unrelated files, same numbers. machine.json's tool_change block is corroborated, not just asserted.

UtensileCU.def lists exactly seven fields, and each C row carries exactly seven values, in order:

NUMERO TIPO QUOTA1 Value 1 QUOTA2 Value 2 ANGOLO EN_BAND LANCIO
0 6 βˆ’4.32 0.00 25 1 0
1 6 βˆ’4.32 50.00 25 1 0
2 6 βˆ’4.32 100.00 25 1 0
3 6 βˆ’4.32 150.00 25 1 0
4 6 βˆ’4.32 200.00 25 1 0
5 6 βˆ’4.32 250.00 25 1 0
6 6 βˆ’4.32 300.00 25 1 0
7 6 βˆ’4.32 350.00 25 1 0

βœ… Eight pockets, numbered 0-7. That is a third independent agreement with EN_CU_6 = 8 and with Grant's physical count of 8 holders.

βœ… ROTARY, confirmed two ways (Grant, 12 September)

Grant: "It is a rotary tool changer. The name Carousel as well as visual inspection gives two confirmation points."

That kills a speculation and promotes a finding.

⚠️ Dead: the suggestion that the 50.00 mm step per pocket implied a linear traversing magazine. It does not.

⭐ Promoted: carousel-indexer.md derives an 80:1 worm from 100000 pulses per pocket at 10000 pulses per motor revolution, and calls it "an entirely ordinary worm ratio" β€” a plausibility argument. Rotary makes it arithmetic. Eight pockets Γ— 10 motor revolutions = 80 motor revolutions per carousel revolution, and that quantity only means anything if the thing revolves. The ratio is now forced by the numbers, not merely typical of them.

πŸ”΄ But rotary RE-OPENS what Value 2 is

A rotary carousel presents every tool at ONE fixed pickup point. The head therefore goes to the same place for every tool change. So:

Value 1 = βˆ’4.32, constant βœ… Consistent, and now corroborated. It equals VQVFRESA exactly β€” VV.59, "quota V fresa da montare", the Y coordinate for the cutter being mounted. The fixed pickup point, and rotary is why it does not vary
πŸ”΄ Value 2 = 0 to 350 in 50.00 steps Cannot be a per-pocket head position, because there is only one pickup point

⚠️ So this page's earlier reading of the C rows as "pocket positions" was half right. Value 1 is a position. Value 2 is something else, and most likely the carousel's own commanded position in the HMI's units rather than anything the gantry goes to.

⭐ One arithmetic check available now: 5000 raw units per pocket against 100000 indexer pulses per pocket is exactly 20 pulses per unit. If Value 2 is the indexer setpoint in HMI units, that ratio should hold across all eight rows β€” and it does, because both series are exact. Suggestive, not proof: the units themselves are still unidentified.

πŸ”΄ Do not treat Value 2 as a coordinate until this is settled. It is not on the critical path β€” the tool change needs the fixed pickup point, which is known twice over β€” but it is currently mislabelled.

πŸ”΄ Correction to an earlier draft of this page, which said "seven pocket positions". That was a filtering mistake on my part β€” C 0 was dropped because its pocket number is zero. There is no missing eighth pocket and there never was.

πŸ”΄ And Value 1 / Value 2 are NOT coupling and releasing heights

The to-do list described them as "per-tool coupling and releasing heights". They are not. Value 1 is constant at βˆ’4.32 across every pocket and Value 2 steps a uniform 50.00 mm. They are the pocket's position, not a height, and they have nothing to do with the tool change Z moves.

⭐ The coupling and releasing heights are machine-level only β€” 53.60 and 0, from P001.SBL and Q 6 5360 0. There is no per-tool pair, so the tool change is built on the machine-level pair alone, and that was the open question.

πŸ”΄ What this leaves for the HMI

Much less than was on the to-do list. Of the numbers that trip was supposed to fetch:

Status
Coupling height 53.60 mm βœ… Known, two independent sources
Release height 0 βœ… Known, two independent sources
Carousel Y position βˆ’4.32 mm βœ… Known, two independent sources
Carousel pocket spacing βœ… On disk, 50 mm
Drill offsets and pitch βœ… On disk (but see the conflict above)
Tool lengths and diameters βœ… On disk in TAB01.DAT
Per-pocket Value 1 / Value 2 βœ… On disk β€” the C rows above. Pocket positions, not heights
VRot.10V, the spindle analog scale βœ… On disk β€” see below

⭐ VRot.10V is in the same row, and it closes the last HMI item

MandrinoCU.def lists 16 fields; row 24 carries them in order (with ASSEINDEX, which the file does not store, absent):

Field Value
TIPO 98 β€” Tool changer router
OFF_U / OFF_V / OFF_W βˆ’89.10 / βˆ’294.60 / 282.10
VMAX 24000
πŸ”΄ V10V πŸ”΄ 24000
UTE_SIM 25 β€” magazine 25, the carousel

πŸ”΄ So 10 V commands 24,000 rpm. That is exactly the number the LinuxCNC spindle analog scaling needs, and it was listed as living only on the HMI.

βœ… Three things corroborate it. VMAX and V10V agree, so the scale is linear to full speed; 24,000 rpm is the HSD M200's rated maximum; and UTE_SIM = 25 ties the row to the carousel, confirming it is the router spindle and not something else.

βœ… The vertical drill rows carry VMAX = V10V = 0, which is right β€” the drill block runs off one fixed-speed motor with no inverter. A mis-parse would not produce that pattern.

πŸ”΄ So nothing is now known to need the HMI

Every number the "before the PC goes" items were chasing is in the capture. ⚠️ That is not a reason to be careless with the rp5800 β€” it is proof that the capture was worth doing, and there may still be things nobody has thought to look for.