Skip to content

G28.2: home the machine from G-code (all joints, or Pn per joint) - #4172

Open
greatEndian wants to merge 16 commits into
LinuxCNC:masterfrom
greatEndian:g28
Open

G28.2: home the machine from G-code (all joints, or Pn per joint)#4172
greatEndian wants to merge 16 commits into
LinuxCNC:masterfrom
greatEndian:g28

Conversation

@greatEndian

@greatEndian greatEndian commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

G28.2 — home the machine from G-code

Lets a program or MDI line reference the machine, instead of requiring the
operator to press Home All in the GUI.

  • G28.2 — run the homing cycle on all joints, in HOME_SEQUENCE order
    (the same operation as the GUI's Home All)
  • G28.2 Pn — run the homing cycle on joint n only, where n is the
    0-based joint number matching its [JOINT_n] INI section

G28.2 is non-modal (modal group 0), following the existing G28.1 /
G30.1 pattern. It is a LinuxCNC extension; there is no standard Fanuc
equivalent.

What it's for

  • Unattended / scripted power-up — a program or an automation layer can
    reference the machine without a human at the GUI.
  • Re-homing a joint mid-program@Sigma1912's case in the discussion
    below: a joint switched between rotary-axis and spindle use, whose
    reference is no longer valid once it has run as a spindle. G28.2 Pn
    re-establishes it in place, which also avoids the workaround that
    issue Connecting joint.n.index-enable to spindle.n.index-enable breaks spindle-synchronized motion #3556 currently forces.
  • Operators who would rather type a reference command than click one.

Pn details

The joint index rides the joint field that EMC_JOINT_HOME already
carries, so this needs no NML change and no motion change, and behaves
identically on any kinematics.

On a synchronized (negative HOME_SEQUENCE) joint pair, Pn on either joint
homes both — motion's existing gantry behavior. On a positive shared
sequence, Pn homes only the named joint; use the bare form to home both.

Pn is range-checked in task against the machine's configured joint count,
and an out-of-range number is refused before anything else happens, naming
the valid range:

Cannot home invalid joint 5 (valid: 0..4, or -1 for all)

There is deliberately no axis-letter form (G28.2 X): resolving an axis
letter to a joint needs the kinematics coordinate map and is ambiguous even
on trivkins (duplicate letters on gantries), and as @andypugh noted, homing
is a joint concept rather than an axis one.

How it works

  1. interp convert_home_cycle() emits the canon op HOME_CYCLE(), or
    HOME_CYCLE_JOINT(n) when a P word is present.
  2. canon flushes queued segments and appends EMC_JOINT_HOME with the
    joint number (-1 for all).
  3. task executes it in program order and waits for the cycle to finish
    before letting the program continue.

Homing only advances while motion is in FREE mode (do_homing() is called
from there), so a home queued from a program or MDI running in TELEOP/COORD
would otherwise stall silently. Task dips motion into FREE for the duration
and restores the previous trajectory mode when the cycle completes —
invisibly to the task-level MANUAL/MDI/AUTO state.

Three details of that sequencing are worth calling out for review:

  • Completion is tested against motion's aggregate homing_active, not
    against an OR of the per-joint .homing flags. On a machine that homes in
    several HOME_SEQUENCE groups, the sequence machine finishes one group and
    can spend a cycle or more before the next raises .homing, so there is a
    window where every joint reads .homing == false while the machine is
    still homing. Task samples far coarser than the servo cycle and can land in
    it. motion/homing.c documents the same deassertion lag in its own words.
    This is what the new homing_active status field is for.
  • The mode restore is gated on all_homed() for non-identity
    kinematics, mirroring the condition motion itself applies in
    switch_to_teleop_mode() and the EMCMOT_COORD case. Restoring
    unconditionally would have task report DONE while motion refused the
    transition, leaving the machine in FREE with the GUI's mode controls dead.
  • The sequencing is scoped to the queued path only. Immediate homes —
    the GUI Home button, halui, linuxcncrsh — pass straight through exactly
    as before, since nothing calls emcTaskCheckPostconditions() for them and
    a mode dip taken there would never be undone.

Supporting fixes

Both are needed for a queued home to reach motion at all:

  • task: EMC_JOINT_HOME_TYPE was missing from
    emcTaskCheckPreconditions(), so a queued home hit the default case and
    was silently dropped before ever reaching motion. It now returns
    WAITING_FOR_MOTION (drain prior motion, then home).
  • motion: homing required motion_state == FREE; it is now also
    permitted when motion is otherwise idle (in position, queue empty), so a
    home issued from MDI or a program is honored. Refusing mid-motion still
    holds, as do the homing inhibit and per-joint limit guards.

Also included: homing_active added to emcmot_status_t and
EMC_MOTION_STAT (with its constructor initializer and NML serializer
entry), and a new EMC_TASK_EXEC::WAITING_FOR_HOMING state.

Deliberately not in this PR

  • G28.3 (unhome) was dropped after review. No reviewer could name a use
    a numbered parameter would not serve better under NO_FORCE_HOMING=1, and
    it was the only operation here able to leave a running program on an
    unreferenced machine. Unhoming remains available from the GUI, halui and
    linuxcncrsh.
  • [RS274NGC]GCODE_HOMING=1 (plain G28 references the machine first)
    was split out and will be proposed separately. It needs real-machine
    validation of the home-then-coordinated-return sequence that sim cannot
    fully exercise.

Testing

Five test cases under tests/interp/gcode-homing/: joint-pword,
sequencing, flush-order, invalid-pword, immediate-unhome-mode.

  • tests/interp 88/88; gcode-homing + motion-logger + motion 13/13.
  • Real hardware (@Sigma1912, Mesa 7I95T, gantry): g28.2 homes all
    joints; g28.2 p0 homes the gantry pair; g28.2 p4 homes the named joint;
    and a program of the form g0 x100 / g01 a-5 / g28.2 p0 / g01 x50 runs the
    first moves, re-homes the gantry, then completes — all correct.

Credits

Developed in collaboration with @grandixximo, who shaped the requirements and
drove the real-world homing-from-G-code use case, and with @Sigma1912, whose
Pn proposal became the per-joint form and whose hardware testing found the
out-of-range bug. Review input from @andypugh on the joint-vs-axis question.

@Sigma1912

Copy link
Copy Markdown
Contributor

Would it be possible to add a parameter for homing individual joints?

Example:

{G28.2, G28.3} P1 to home/ unhome joint 1

This would be very useful for configurations with joints that switch between rotary and spindle use. It would also circumvent #3556 that complicates the current way of 'rehoming'

@greatEndian

Copy link
Copy Markdown
Contributor Author

Home individual axis-

G28.2XC will force home axis X and C ...
G28.3YZ will force unhome axis YZ ...

for non trivial kinematics we need to start discussion how to handle it per joint..

@Sigma1912

Copy link
Copy Markdown
Contributor

What happens if we try to run a program in unhomed state ( will that trigger an interpreter error?):

Example 1 :

G28.3
G1 x2

How are these commands handled when used inside a gcode program? (I presume it is like a queue buster command that halts the read ahead until all queued command have been executed) :

Example 2 :

G1 x1
G28.3
G28.2
G1 x2

@greatEndian

greatEndian commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Example 1 :
force unhome
if NO_FORCE_HOMING==1 program run
else message to home again

Example 2 :
program run
force unhome
force home
program run

@Sigma1912

Copy link
Copy Markdown
Contributor

for non trivial kinematics we need to start discussion how to handle it per joint..

Using axes is probably enough for the majority of use cases and most machine operators would likely not even know about the joint-axis mappings on their particular machines.
However, it might be useful for more exotic configurations (eg for homing/unhoming extra joints that do not have axis letters assigned).
It seems an easy enough addition to introduce an optional parameter word with the joint number (but I might be wrong about that) ;)

@grandixximo

Copy link
Copy Markdown
Contributor

I think the cleanest direction is to keep the G-code surface dead simple and put the real effort into execution. For per-joint homing, Sigma's Pn is the right primitive: the joint index rides the field that EMC_JOINT_HOME already carries, so G28.2 P1 needs no NML or motion change and behaves the same on any kinematics. Bare G28.2 stays home-all. Gantries need no extra word since Pn reuses the existing single-joint homing behavior, but that's worth a doc note: on a synchronized (negative HOME_SEQUENCE) pair, homing either joint brings its partner, while on a positive shared sequence Pn homes only the named joint and you'd use bare G28.2 to do both. I'd leave the axis-letter form (G28.2 X) out for now, since resolving axis letters to joints needs the kinematics coordinates map and isn't actually trivial even on trivkins (duplicate letters for gantries), so it's better as its own later piece.

The part that really needs rethinking isn't the syntax, it's how a queued home executes. Homing only runs in free mode, and nothing currently flips the machine into free for a G-code-triggered home, so just relaxing the motion guard means the command can silently stall in teleop. It needs to be sequenced in task: drain motion, switch to free, home, wait for it to actually finish, then restore the prior mode. And if homing fails, the program has to abort rather than carry on unreferenced. A nice consequence is the trivkins question answers itself: home-all works everywhere, but a partial home can only resume coordinated motion on identity kinematics, because non-trivkins won't re-enter teleop until everything's homed.

There's also the unhome-then-keep-running hole Sigma raised. The existing force-homing check only fires at program start, so G28.3 mid-program followed by a move slips through. Rather than checking every move, we re-apply that same NO_FORCE_HOMING gate at the home/unhome sync point the command already forces, so it's governed by exactly the same policy as starting a program, with no cost on the motion path.

Given all that, I'd split it: land the explicit G28.2/G28.3 (with Pn) first, and hold GCODE_HOMING plain-G28, which adds the home-then-return move on top, until it's been signed off on real hardware.

@andypugh

Copy link
Copy Markdown
Collaborator

"Homing" is 100% a Joint thing, not an axis thing, so I don't think that I support the G28.2 X C style.
(Also, it is a break in G-code syntax to use an axis letter without a number, I think?)

Using P1 to home a single joint seems reasonable to me. The command can be repeated to home another joint.

However, is is already possible to home through the linuxcncrsh and similar interfaces, so I am not entirely sure there is a use-case for this.

One for discussion at a developer meeting, I think.

@Sigma1912

Copy link
Copy Markdown
Contributor

However, is is already possible to home through the linuxcncrsh and similar interfaces, so I am not entirely sure there is a use-case for this.

As I pointed out above, one use case is the rehoming of joints that are switched between rotary and spindle modes. This needs to be done inside a gcode program and currently requires a rather complex hal setup plus a workaround for #3556 (which is a serious bug)

greatEndian added a commit to greatEndian/linuxcnc that referenced this pull request Jul 2, 2026
Implements the direction from PR LinuxCNC#4172's review discussion instead of
the axis-letter form originally sketched there:

- Adds an optional Pn word to G28.2/G28.3 to home/unhome a single joint
  by its 0-based joint number (matching [JOINT_n] INI numbering), e.g.
  G28.2 P1. Bare G28.2/G28.3 (no P) are unchanged (home/unhome all
  joints). Reuses the joint field EMC_JOINT_HOME/EMC_JOINT_UNHOME
  already carry, so it needs no NML change and works identically on
  any kinematics -- exactly the primitive grandixximo's review comment
  argued for. The axis-letter form (G28.2 X) is deliberately NOT
  implemented: resolving an axis letter to a joint needs the
  kinematics coordinate map and isn't trivial even on trivkins
  (duplicate letters on gantries), and andypugh's review also objected
  that homing is a joint concept, not an axis one. G28.2/G28.3 needed
  adding to the P-word whitelist in interp_check.cc (checked against
  g_modes[GM_MODAL_0], since they are modal-group-0 codes like G10/G4,
  not motion-group codes).

- Fixes the real gap grandixximo's review identified: do_homing()
  (control.c) only ever advances while motion is in FREE mode, so a
  home/unhome issued from a running program or MDI while in
  TELEOP/COORD would previously either be rejected by a motion-side
  guard or silently never progress. Task now sequences it properly: a
  new EMC_TASK_EXEC::WAITING_FOR_HOMING state (modeled on the existing
  WAITING_FOR_SPINDLE_ORIENTED state) saves the current trajectory
  mode, dips into FREE, waits for the actual per-joint .homing/.homed
  status to reach the expected end state, then restores the prior mode
  -- invisibly to the task-level MDI/AUTO/MANUAL state, the same
  principle multichannel-DESIGN.txt uses for the analogous
  per-channel-homing problem. If homing/unhoming does not reach the
  expected end state, the program aborts (execState = ERROR) rather
  than continuing unreferenced, and the machine is left in FREE for an
  operator to intervene from rather than snapped back to a mode an
  unhomed machine may not legally run coordinated motion in.

  HOME and UNHOME are not symmetric at the motion level: EMCMOT_JOINT_HOME
  is a genuine state machine (.homing goes true while running), but
  EMCMOT_JOINT_UNHOME (command.c) is synchronous -- set_unhomed() just
  clears .homed immediately, .homing is never touched. The sequencing
  wait branches accordingly: UNHOME checks the target .homed state
  directly, HOME waits for the full start-then-finish cycle.

- Re-applies [TRAJ]NO_FORCE_HOMING at the point a home/unhome command
  already forces a sync, closing the hole Sigma1912 raised in the PR
  discussion (G28.3 mid-program followed by a move with no re-home).
  NO_FORCE_HOMING=0 already refuses to *start* MDI/AUTO on an unhomed
  machine, but only at program/MDI start, not per line, so this
  specific gap needed its own check -- at no cost to the motion path,
  since it only runs at a sync point the command already forces.

- Fixes two bugs found in the process that predate this commit and
  affect the base G28.2/G28.3/GCODE_HOMING feature, not just Pn:

  * HOME_CYCLE()/UNHOME_AXES()/HOME_CYCLE_IF_UNHOMED() (emccanon.cc)
    never flushed pending chained motion segments before appending
    their own command. STRAIGHT_FEED/STRAIGHT_TRAVERSE buffer points
    for arc-blend lookahead and only reach interp_list on a flush, so
    a queued move immediately before a G28.2/G28.3/homing-G28 could
    silently execute AFTER the home instead of before it. Fixed by
    calling flush_segments() first in all five home/unhome canon
    functions (the three pre-existing ones too).

  * emcJointHome()/emcJointUnhome() (taskintf.cc) returned 0 (success)
    for an out-of-range joint number, so an invalid Pn would silently
    report success instead of an error.

- Adds stub implementations of the two new canon calls to
  gcodemodule.cc (the Python gcode module bindings), the third canon
  backend alongside emccanon.cc and saicanon.cc.

Validated live in headless sim: the exact rehoming-a-shared-joint use
case Sigma1912 described (cold-start home-all, move, mid-program
unhome one joint, rehome it, move again) completes cleanly with zero
errors; a NO_FORCE_HOMING=0 config confirms an unreferenced move after
an unhome is correctly blocked with the intended error message; a
plain move-then-home-all program confirms the flush_segments() ordering
fix. Interp-level regression (tests/interp/gcode-homing/*,
tests/interp/rotation/g28) passes 4/4, including a new joint-pword
test case for the Pn parsing.

Signed-off-by: chabron94 <chabron94@gmail.com>
@grandixximo

Copy link
Copy Markdown
Contributor

Big step in the right direction. The free-mode dip and restore, failure-abort-leave-in-FREE, the HOME/UNHOME asymmetry, the flush_segments ordering fix and the out-of-range Pn fix are all solid.

Main concern is the HOME completion check. You infer "homing stopped" from the per-joint .homing OR going false, then test all_target_homed. On a machine that homes in separate HOME_SEQUENCE steps, the sequence machine finishes one group and spends a cycle or more before the next group sets .homing, so there is a window where every joint reads .homing == false while not yet fully homed. Task samples coarser than the servo cycle, lands in that gap, and aborts a good home-all with "did not complete". Single-joint Pn and all-in-one-sequence are fine; multi-sequence home-all (bare G28.2 and GCODE_HOMING plain-G28) is exposed. The clean fix is the aggregate signal that already exists in RT, get_homing_is_active() / homing_active (homing.c), plumbed into status, rather than edge-detecting the per-joint OR.

Non-trivkins teleop restore: success and restore are gated on the target joint, not all_homed(). On non-identity kins switch_to_teleop_mode() refuses unless the whole machine is homed, so a Pn home that succeeds for its joint while the machine is not fully homed (e.g. after an earlier G28.3 Pk) leaves motion silently stuck in FREE while task marks it DONE and runs the next line, and the coordinated move then has no valid frame. Gate the teleop restore on all_homed() for non-identity kins (or verify the mode took) and abort if it can't.

Two smaller ones. The sequencing now wraps every EMC_JOINT_HOME/UNHOME, including GUI Home-All and unhome, so those hit the same path and race; worth scoping to the queued path. And the UNHOME success test (!any_target_homed over all joints) mis-scores a volatile unhome (joint == -2), which leaves non-volatile joints homed; G28.3 never sends -2 but this path now catches all unhomes, so guard it.

Last, GCODE_HOMING plain-G28 is still bundled and rides the same multi-sequence home-all path, on sim-only validation. I'd land the explicit G28.2/G28.3 (with Pn) first and hold plain-G28 until it has a hardware sign-off.

@grandixximo

Copy link
Copy Markdown
Contributor

@Sigma1912 on GCODE_HOMING plain-G28 specifically, the one part still lacking real-machine validation is the home-then-return sequence (home-all, cross into teleop, then the coordinated return move), which sim can't fully exercise. Since this is your use case, are you able to test it on real hardware, or do you know someone who can? That would let us keep it in rather than defer it.

@Sigma1912

Sigma1912 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

a plain G28 references the machine first, before its normal return move, only when the machine is not already fully homed

How is this supposed to work as I cannot possibly command G28 from either MDI or Program when not fully homed?

  1. test with real hardware (Mesa 7I95T):
    Machine homed, MDI g28.2 -> correctly homes all joints
    Machine homed, MDI g28.2 p0 -> correctly homes joints 0 and 1 in my gantry setup
    Machine homed, MDI g28.2 p4 -> correctly homes joint 4

This seems to work !

  1. test with real hardware (Mesa 7I95T):
    Machine homed, MDI g28.3 p0 -> joint_0 is unhomed, error : all joints must be homed before going into coordinated mode
    Manual Control tab shows joints and Menu 'Machine' . 'Homing', 'Unhoming', 'Zero Coordinate System' are grayed out.
    No further command is possible until the controller is switched off and on again (F2)

Not sure if this is the intended behavior but I don't quite see an application for this.

Just so we are on the same page, I cloned and built the 'g28' branch, git log --minimal shows this (which seems current):

Screenshot from 2026-07-17 07-38-19

@Sigma1912

Copy link
Copy Markdown
Contributor

Tried this gcode:

g0 x10
g28.3 p2
g28 x160
g0 x100
m2

with [RS274NGC] GCODE_HOMING = 1

Machine moves to X10, then rehomes all joints and exits:

all joints must be homed before going into coordinated mode
USRMOT: ERROR: invalid command
need to be enabled, in coord mode for linear move

@grandixximo

grandixximo commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

I think, as far as I understand the code, in the INI file if you set

[RS274NGC]
GCODE_HOMING=1

You should be able to actually run a gcode that starts with G28

Edit:
Cross posted, then seems like possibly the race I spotted, or another race maybe hitting.

@grandixximo

Copy link
Copy Markdown
Contributor

Correction to my last note: on a closer read it isn't the race I flagged. The race would print "G28.2 home did not complete", and none of Sigma's errors say that. His failures are all mode errors, so this is the other finding, the non-trivkins mode-restore gap.

The tell is all joints must be homed before going into coordinated mode, which only fires from motion's COORD guard on non-identity kinematics, so the gantry trips it. g28.3 p0 unhomes one joint, the machine is no longer fully homed, and on success the code restores the prior COORD/TELEOP mode gated on the target joint rather than all_homed(). On non-identity kins the restore is refused, with no recovery, hence the F2 wedge. Test 3 is the same bug cascading: the g28.3 p2 breaks the mode state, then g28 home-all can't re-enter coord, so the return and the following g0 fail with "need to be enabled, in coord mode".

Fix: after a (un)home that leaves the machine not fully homed, don't restore a coordinated mode. Leave it in FREE and abort with a clear error instead of wedging.

And Sigma's opening point is the real one for GCODE_HOMING: with NO_FORCE_HOMING=0 you can't start a program unhomed, so a homed machine's G28 is just the pure return; the home-first branch is only reachable by unhoming mid-program, which is exactly the path that breaks here. I'd land explicit G28.2/G28.3 (with Pn) now and drop GCODE_HOMING plain-G28.

@Sigma1912

Copy link
Copy Markdown
Contributor

I'd land explicit G28.2/G28.3 (with Pn) now

Given that G28.2 (Pn) can be called without the need to unhome first what is the intended use case for G28.3 (Pn)?

@grandixximo

grandixximo commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

I guess that's for @greatEndian to answer, I've been thinking about it, unless it's about compatibility with something out of tree that only supports G28.3 in combination with G28.2 I don't see a good argument to keep it either...

@grandixximo
grandixximo marked this pull request as draft August 16, 2026 09:17
@grandixximo

Copy link
Copy Markdown
Contributor

@greatEndian please can you have another look at the questions here and illuminate with some answers?

@greatEndian

Copy link
Copy Markdown
Contributor Author

@Sigma1912 @grandixximo @andypugh thanks for the real-hardware run..

@Sigma1912 — that was worth more than the sim work. Both failures you hit are real bugs, and I can name both. Answers in order.

  1. What is G28.3 Pn actually for?

It is not a precursor to G28.2 — you are right that homing does not need it. It is a way to declare "this joint's reference is no longer physically valid".

Your own case is the clearest one: a joint that switches between rotary-axis and spindle use. While it runs as a spindle, the homed flag is a lie — the encoder count no longer corresponds to a known machine position, but nothing in the controller knows that. G28.3 Pn marks it unreferenced, and with NO_FORCE_HOMING=0 the machine then refuses further coordinated motion until G28.2 Pn re-references it. The same applies to anything that invalidates a reference without moving the joint through a normal cycle: a decoupled indexer, a released brake or clutch, a re-gripped chuck.

So the pair is: G28.3 Pn = "stop trusting this joint", G28.2 Pn = "trust it again". If that argument does not convince you, I have no objection to landing G28.2 Pn alone and dropping G28.3 — it is the weaker half.

  1. The F2 wedge (your tests 2 and 3) — confirmed bug

@grandixximo's second diagnosis is correct, it is the mode-restore gap and not the completion race. On success the code restores the prior trajectory mode gated on the target joint only:

} else if (success) {
emcStatus->task.execState = EMC_TASK_EXEC::DONE;
if (homingPriorMode != EMC_TRAJ_MODE::FREE) {
emcTrajSetMode(homingPriorMode); // <-- not gated on all_homed()
}
}

On your gantry (non-identity kinematics) switch_to_teleop_mode() refuses unless the whole machine is homed, so after g28.3 p0 the restore is rejected by motion — that is your all joints must be homed before going into coordinated mode — task still marks the command DONE, and nothing recovers the mode. Hence the grayed-out controls and the F2. Test 3 is the same bug cascading: the mode state is already broken before g28 runs.

Fix: never restore a coordinated mode when the machine is not fully homed. Leave motion in FREE and abort with a clear operator error instead of wedging. I will also verify the mode actually took rather than assuming emcTrajSetMode() succeeded.

  1. The home-completion race — confirmed, and upstream documents it

Accepted. Inferring "homing stopped" from the per-joint .homing OR is unsafe, and homing.c says so itself at line 546:

"The homing status variable turns false before homing_active state turns false. This means that a new homing command on a joint might fail due to the homing state machine being active while all joints already are in the 'not homing' state."

That is exactly the window my poll can land in on a multi-HOME_SEQUENCE home-all, producing a spurious "did not complete" abort on a perfectly good home. Single-joint Pn and single-sequence machines are not exposed, which is why the sim tests and Sigma's test 1 passed.

Fix as you suggested: use the aggregate get_homing_is_active() (homing.h:65), plumbed through emcmot status into emcStatus, instead of edge-detecting the per-joint OR. It is not currently in NML status, so that is a small status-field addition.

  1. Volatile unhome and GUI scoping — both accepteddesign leaves non-volatile joints homed and would be reported as a failure. G28.3 never sends -2, but since this path now catches every unhome it needs the guard.
  • The sequencing currently intercepts EMC_JOINT_HOME_TYPE / EMC_JOINT_UNHOME_TYPE unconditionally in emcTaskIssueCommand, so GUI Home-All and Unhome ride the same mode-dip path. That was not intended — I will scope it to the queued/program-order path.
  1. GCODE_HOMING plain-G28 — I agree, drop it from this PR

@Sigma1912's opening question settles it. With NO_FORCE_HOMING=0 a program cannot start unhomed, so on a homed machine G28 is just the ordinary return move and the home-first branch is unreachable. The only way to reach it is to unhome mid-program — which is precisely the path that broke on your machine. A feature whose only reachable path is the broken one does not belong in this PR.

It is interleaved with the Pn work in 3d238db and touches ~15 files, so this is a rebase rather than a revert, but it is the right split: land explicit G28.2 / G28.3 (with Pn), hold plain-G28 for its own PR with hardware sign-off.

  1. Axis letters

@andypugh — already dropped. The implemented form is Pn only, for the reasons you and @grandixximo both gave: homing is a joint concept, and an axis letter without a number is a syntax break. There is no G28.2 X C in the branch. Happy to have it on a developer-meeting agenda.

Next push

  1. homing_active into status, replace the completion check
  2. Gate mode restore on all_homed(), abort-in-FREE instead of wedging
  3. Guard the volatile-unhome scoring
  4. Scope sequencing to the queued path
  5. Separate out GCODE_HOMING

@Sigma1912, if you are willing to re-run tests 2 and 3 after that push, that closes the loop on the part sim cannot reach.

Best regards

greatEndian added a commit to greatEndian/linuxcnc that referenced this pull request Aug 18, 2026
…can't hold

Addresses the first of grandixximo's findings on PR LinuxCNC#4172, and the failure
Sigma1912 hit on real hardware (Mesa 7I95T gantry): "g28.3 p0" reported
"all joints must be homed before going into coordinated mode", greyed out
the GUI's mode controls, and left the machine needing F2 to recover.

The G28.2/G28.3 sequencing dips motion into FREE (do_homing() only advances
there) and restores the previous trajectory mode when the command finishes.
That restore was gated only on the command having succeeded for its *target*
joint. But a per-joint G28.2 Pn / G28.3 Pn can succeed for its own joint
while leaving the machine as a whole unreferenced, and motion refuses to
(re-)enter TELEOP or COORD in that state on non-identity kinematics --
switch_to_teleop_mode() (motion.c) and the EMCMOT_COORD case (command.c)
both gate on "kinType != KINEMATICS_IDENTITY && !get_allhomed()".

So the restore was silently rejected while task still reported DONE: the
program advanced to the next line, motion had no valid frame for it, and the
machine sat stranded in FREE. Sigma's third test is the same bug cascading --
the g28.3 p2 breaks the mode state, then the following g28 and g0 fail with
"need to be enabled, in coord mode".

Mirror motion's own condition before restoring, and fail the command cleanly
(staying in FREE, with an operator error) instead of reporting success and
stranding the operator. Identity kinematics are unaffected: motion permits
the restore there, so the behaviour is unchanged for trivkins.

Note this only reaches the buggy path with NO_FORCE_HOMING=1. With the
default 0, the pre-existing NO_FORCE_HOMING re-check catches a partial
unhome first -- which, together with the existing tests using trivkins, is
why neither the sim tests nor Sigma's first test caught it.

The kinematics type is read from emcStatus->motion.traj.kinematics_type
rather than this file's static emcmotConfig: that copy is filled in once
just before the main loop and never refreshed, so it goes stale as soon as
switchkins changes kinematics at runtime (G43.4/G43.5). taskintf.cc re-reads
the motion config whenever config_num changes and republishes it in status.

Tests: adds gcode-homing/nonidentity-restore, which needs both knobs the
existing coverage lacks -- corexykins (KINEMATICS_BOTH) and
NO_FORCE_HOMING=1. The program is "G28.3 P0 / M64 P0 / M2"; the digital
output is the witness, since it needs no coordinated motion and so would
still run with the machine stuck in FREE. Verified the test actually catches
the regression by temporarily reverting the fix and confirming it fails --
the interpreter never returns to idle, with dout0=1 proving the program had
carried on past the G28.3 -- then restored the fix and confirmed it passes.

Verified: tests/interp/gcode-homing (6/6), and the full tests/interp +
tests/motion-logger suite (89/89, 1 pre-existing skip). One flush-order
failure seen in an earlier sweep did not reproduce (89/89 on re-run, 8/8
in isolation including under load); it uses trivkins, where this change is
a no-op by construction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
greatEndian added a commit to greatEndian/linuxcnc that referenced this pull request Aug 18, 2026
…ed path

Addresses grandixximo's three remaining findings on PR LinuxCNC#4172.

1. Completion test (his main concern)

The HOME completion test inferred "homing has stopped" by OR-ing the
per-joint .homing flags. On a machine that homes in several HOME_SEQUENCE
groups, the sequence machine finishes one group and can spend a cycle or
more before the next group raises .homing, so there is a window in which
every joint reads .homing == false while the machine is still homing. Task
samples far coarser than the servo cycle, lands in that window, and scores a
perfectly good home-all as "did not complete".

motion/homing.c documents the same lag in its own words -- "The homing status
variable turns false before homing_active state turns false" -- and guards
against it internally for exactly this reason.

Use motion's aggregate get_homing_is_active() instead. It was not published
anywhere, so this plumbs it through as emcmot_status_t.homing_active ->
EMC_MOTION_STAT::homing_active, mirroring jogging_active field for field. The
per-joint OR is kept as a belt-and-braces term, since it can only extend the
"still running" window, never shorten it. EMC_STAT is 8064 bytes against the
20480-byte emcStatus NML buffer, so the added field is free.

Single-joint Pn and single-sequence machines never hit the gap, which is why
neither the sim tests nor Sigma1912's hardware test 1 caught it.

2. Sequencing applied to immediate commands as well as queued ones

EMC_TASK_EXEC::WAITING_FOR_HOMING is only ever reached through
emcTaskCheckPostconditions(), which task calls only for commands taken off
the interp_list. The GUI's Home and Unhome buttons, halui and linuxcncrsh all
send immediate commands: they reach emcTaskIssueCommand() but nothing follows
up. Applying the FREE-mode dip to them was a regression in two ways:

- the dip was never undone, silently stranding the machine in joint mode; and
- because the dip runs before the command is issued, an immediate unhome
  started succeeding from teleop, where motion deliberately refuses it
  ("must be in joint mode or disabled to unhome", EMCMOT_JOINT_UNHOME in
  command.c). The sequencing was quietly granting a permission upstream
  denies.

Scope the whole sequencing to the queued path via issuingQueuedCommand, so
immediate home/unhome behaves exactly as it did before this branch.

3. Volatile unhome scoring

A volatile unhome (joint == -2) clears only the joints configured
VOLATILE_HOME and leaves every other joint homed, so "no joint in range is
still homed" would score a correct volatile unhome as a failure. Task cannot
narrow the check to just the volatile joints: volatile_home lives in motion's
private homing state (H[jno].volatile_home) and is not published in joint
status -- the volatile_home in emc_nml.hh belongs to
EMC_JOINT_SET_HOMING_PARAMS, a command message, not to EMC_JOINT_STAT.

Guarded, but note this is defensive rather than a live fix: G28.3 only ever
emits Pn >= 0 or -1, and with the sequencing now scoped to the queued path an
immediate unhome(-2) does not reach the scoring either. It is here so a
future queued command carrying -2 cannot be silently scored as a failure.

Tests: adds gcode-homing/immediate-unhome-mode, asserting an immediate unhome
from teleop is refused with the mode untouched, and an immediate home leaves
the mode untouched. Verified it catches the regression by neutralising the
queued-path scoping and confirming it fails ("immediate unhome from teleop
went through (homed=[0, 1, 1])"), then restoring it and confirming it passes.

Verified: tests/interp/gcode-homing (7/7) and the full tests/interp +
tests/motion-logger + tests/abort suite (94/94, 1 pre-existing skip).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
greatEndian added a commit to greatEndian/linuxcnc that referenced this pull request Aug 18, 2026
Per the review discussion on PR LinuxCNC#4172: land the explicit G28.2 / G28.3
(with Pn) now, and hold the GCODE_HOMING plain-G28 behaviour for its own PR
once it has a real-machine sign-off.

Sigma1912's opening question settled it. With NO_FORCE_HOMING=0 a program
cannot be started on an unhomed machine at all, so on a homed machine a plain
G28 is just the ordinary return move and the home-first branch is unreachable.
The only way to reach it is to unhome mid-program -- which is precisely the
path that broke on his gantry. A feature whose only reachable path is the
broken one does not belong in this PR.

Removes the flag and everything gated on it:

- interpreter: FEATURE_GCODE_HOMING, the [RS274NGC]GCODE_HOMING INI read, and
  the G28 home-first branch in convert_home()
- canon: HOME_CYCLE_IF_UNHOMED() -- the declaration plus the implementation in
  all three backends (emccanon.cc, saicanon.cc, gcodemodule.cc)
- NML: the EMC_HOME_ALL_IF_UNHOMED (-3) sentinel
- task: the sentinel resolution in EMC_JOINT_HOME_TYPE (target_joint is now
  const), and the stale comment in emcTaskCheckPostconditions()
- docs: the g-code.adoc NOTE, the GCODE_HOMING cross-reference in the
  G28.2/G28.3 note, and the ini-config.adoc entry
- tests: gcode-homing/homing-on and gcode-homing/homing-off, which existed
  only to cover the two settings of the flag, plus stale mentions in
  joint-pword and flush-order

G28.2 / G28.3 (with Pn) are untouched -- they were always independent of the
flag. `git grep` for GCODE_HOMING, EMC_HOME_ALL_IF_UNHOMED and
HOME_CYCLE_IF_UNHOMED now returns nothing.

Done as a removal on top rather than by rewriting history: a merge commit sits
partway along this branch, so unpicking 86c58f6 in place would mean
replaying the series and would churn commits the reviewers have already read.
The net diff of the PR is the same either way. The removed work is preserved
intact on the g28-gcode-homing branch for its own PR.

Verified: full tests/interp + tests/motion-logger + tests/abort suite
(92/92, 1 pre-existing skip). 92 rather than 94 because the two flag tests
above are the ones removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Sigma1912

Sigma1912 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

So the pair is: G28.3 Pn = "stop trusting this joint", G28.2 Pn = "trust it again". If that argument does not convince you, I have no objection to landing G28.2 Pn alone and dropping G28.3 — it is the weaker half.

I'm just having trouble seeing the actual use case in a program. If I reach a state in the program execution where I 'don't trust' a joint would I not just simply use G28.2 to home it?
If there is anything that impedes it from homing then the homing process will fail anyway.
If I switch a rotary from spindle mode to rotary mode, why would I use G28.3 to unhome it from the program just to have to home it from the gui when I could use G28.2 instead and just be done?

TESTRESULTS:

  1. test with real hardware (Mesa 7I95T):
    Machine homed, MDI g28.2 -> correctly homes all joints
    Machine homed, MDI g28.2 p0 -> correctly homes joints 0 and 1 in my gantry setup
    Machine homed, MDI g28.2 p4 -> correctly homes joint 4

However it doesn't like when an unconfigured joint number is requested:
Machine homed, MDI g28.2 p5 ->Gui goes into joint mode and shows error :

 G28.2 home did not start -- check machine mode, motion.homing-inhibit, and whether a homing cycle is already in progress
Screenshot from 2026-08-18 16-11-52

Jogging is now in joint mode but recovers on next gcode execution (either MDI command or program start). That is a bug

  1. test with real hardware (Mesa 7I95T):
    Machine homed, MDI g28.3 p0 -> error message:
 G28.3 unhome succeeded but left the machine not fully homed -- staying in joint mode, as non-identity kinematics cannot re-enter coordinated motion until every joint is homed
  1. test with real hardware (Mesa 7I95T):
    Running gcode program:
g0 x100
g01 a-5 f1000
g28.2 p0
g01 x50 f500
m2

Runs as expected. executes the first two lines then rehomes x gantry and executes the last move.

Conclusion:

  • G28.2 needs to check if an unconfigured joint is requested.
  • G28.3 still not sure what to think about a gcode that ejects the controller into a state that requires the user to rehome through the gui. Other opinions?

@grandixximo

grandixximo commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

The only way I find G28.3 having a use case, is when these codes live in some conditionals that depend on external output, and IMO you must have NO_FORCE_HOMING = 1 for G28.3 to make sense at all, and they can be easily replaced by simple flags like #<_axis_n_is_unreferenced> = 1 and that would be valid and easy to use with or without NO_FORCE_HOMING.
Also in an earlier sentence you wrote g28.3 is not a precursor to G28.2, and in the following example you propose, it is exactly that.
No real argument to keep it, I'm also for removing it, unless a real use case is proposed.

greatEndian added a commit to greatEndian/linuxcnc that referenced this pull request Aug 19, 2026
Implements the direction from PR LinuxCNC#4172's review discussion instead of
the axis-letter form originally sketched there:

- Adds an optional Pn word to G28.2/G28.3 to home/unhome a single joint
  by its 0-based joint number (matching [JOINT_n] INI numbering), e.g.
  G28.2 P1. Bare G28.2/G28.3 (no P) are unchanged (home/unhome all
  joints). Reuses the joint field EMC_JOINT_HOME/EMC_JOINT_UNHOME
  already carry, so it needs no NML change and works identically on
  any kinematics -- exactly the primitive grandixximo's review comment
  argued for. The axis-letter form (G28.2 X) is deliberately NOT
  implemented: resolving an axis letter to a joint needs the
  kinematics coordinate map and isn't trivial even on trivkins
  (duplicate letters on gantries), and andypugh's review also objected
  that homing is a joint concept, not an axis one. G28.2/G28.3 needed
  adding to the P-word whitelist in interp_check.cc (checked against
  g_modes[GM_MODAL_0], since they are modal-group-0 codes like G10/G4,
  not motion-group codes).

- Fixes the real gap grandixximo's review identified: do_homing()
  (control.c) only ever advances while motion is in FREE mode, so a
  home/unhome issued from a running program or MDI while in
  TELEOP/COORD would previously either be rejected by a motion-side
  guard or silently never progress. Task now sequences it properly: a
  new EMC_TASK_EXEC::WAITING_FOR_HOMING state (modeled on the existing
  WAITING_FOR_SPINDLE_ORIENTED state) saves the current trajectory
  mode, dips into FREE, waits for the actual per-joint .homing/.homed
  status to reach the expected end state, then restores the prior mode
  -- invisibly to the task-level MDI/AUTO/MANUAL state, the same
  principle multichannel-DESIGN.txt uses for the analogous
  per-channel-homing problem. If homing/unhoming does not reach the
  expected end state, the program aborts (execState = ERROR) rather
  than continuing unreferenced, and the machine is left in FREE for an
  operator to intervene from rather than snapped back to a mode an
  unhomed machine may not legally run coordinated motion in.

  HOME and UNHOME are not symmetric at the motion level: EMCMOT_JOINT_HOME
  is a genuine state machine (.homing goes true while running), but
  EMCMOT_JOINT_UNHOME (command.c) is synchronous -- set_unhomed() just
  clears .homed immediately, .homing is never touched. The sequencing
  wait branches accordingly: UNHOME checks the target .homed state
  directly, HOME waits for the full start-then-finish cycle.

- Re-applies [TRAJ]NO_FORCE_HOMING at the point a home/unhome command
  already forces a sync, closing the hole Sigma1912 raised in the PR
  discussion (G28.3 mid-program followed by a move with no re-home).
  NO_FORCE_HOMING=0 already refuses to *start* MDI/AUTO on an unhomed
  machine, but only at program/MDI start, not per line, so this
  specific gap needed its own check -- at no cost to the motion path,
  since it only runs at a sync point the command already forces.

- Fixes two bugs found in the process that predate this commit and
  affect the base G28.2/G28.3/GCODE_HOMING feature, not just Pn:

  * HOME_CYCLE()/UNHOME_AXES()/HOME_CYCLE_IF_UNHOMED() (emccanon.cc)
    never flushed pending chained motion segments before appending
    their own command. STRAIGHT_FEED/STRAIGHT_TRAVERSE buffer points
    for arc-blend lookahead and only reach interp_list on a flush, so
    a queued move immediately before a G28.2/G28.3/homing-G28 could
    silently execute AFTER the home instead of before it. Fixed by
    calling flush_segments() first in all five home/unhome canon
    functions (the three pre-existing ones too).

  * emcJointHome()/emcJointUnhome() (taskintf.cc) returned 0 (success)
    for an out-of-range joint number, so an invalid Pn would silently
    report success instead of an error.

- Adds stub implementations of the two new canon calls to
  gcodemodule.cc (the Python gcode module bindings), the third canon
  backend alongside emccanon.cc and saicanon.cc.

Validated live in headless sim: the exact rehoming-a-shared-joint use
case Sigma1912 described (cold-start home-all, move, mid-program
unhome one joint, rehome it, move again) completes cleanly with zero
errors; a NO_FORCE_HOMING=0 config confirms an unreferenced move after
an unhome is correctly blocked with the intended error message; a
plain move-then-home-all program confirms the flush_segments() ordering
fix. Interp-level regression (tests/interp/gcode-homing/*,
tests/interp/rotation/g28) passes 4/4, including a new joint-pword
test case for the Pn parsing.

Signed-off-by: chabron94 <chabron94@gmail.com>
greatEndian added a commit to greatEndian/linuxcnc that referenced this pull request Aug 19, 2026
…can't hold

Addresses the first of grandixximo's findings on PR LinuxCNC#4172, and the failure
Sigma1912 hit on real hardware (Mesa 7I95T gantry): "g28.3 p0" reported
"all joints must be homed before going into coordinated mode", greyed out
the GUI's mode controls, and left the machine needing F2 to recover.

The G28.2/G28.3 sequencing dips motion into FREE (do_homing() only advances
there) and restores the previous trajectory mode when the command finishes.
That restore was gated only on the command having succeeded for its *target*
joint. But a per-joint G28.2 Pn / G28.3 Pn can succeed for its own joint
while leaving the machine as a whole unreferenced, and motion refuses to
(re-)enter TELEOP or COORD in that state on non-identity kinematics --
switch_to_teleop_mode() (motion.c) and the EMCMOT_COORD case (command.c)
both gate on "kinType != KINEMATICS_IDENTITY && !get_allhomed()".

So the restore was silently rejected while task still reported DONE: the
program advanced to the next line, motion had no valid frame for it, and the
machine sat stranded in FREE. Sigma's third test is the same bug cascading --
the g28.3 p2 breaks the mode state, then the following g28 and g0 fail with
"need to be enabled, in coord mode".

Mirror motion's own condition before restoring, and fail the command cleanly
(staying in FREE, with an operator error) instead of reporting success and
stranding the operator. Identity kinematics are unaffected: motion permits
the restore there, so the behaviour is unchanged for trivkins.

Note this only reaches the buggy path with NO_FORCE_HOMING=1. With the
default 0, the pre-existing NO_FORCE_HOMING re-check catches a partial
unhome first -- which, together with the existing tests using trivkins, is
why neither the sim tests nor Sigma's first test caught it.

The kinematics type is read from emcStatus->motion.traj.kinematics_type
rather than this file's static emcmotConfig: that copy is filled in once
just before the main loop and never refreshed, so it goes stale as soon as
switchkins changes kinematics at runtime (G43.4/G43.5). taskintf.cc re-reads
the motion config whenever config_num changes and republishes it in status.

Tests: adds gcode-homing/nonidentity-restore, which needs both knobs the
existing coverage lacks -- corexykins (KINEMATICS_BOTH) and
NO_FORCE_HOMING=1. The program is "G28.3 P0 / M64 P0 / M2"; the digital
output is the witness, since it needs no coordinated motion and so would
still run with the machine stuck in FREE. Verified the test actually catches
the regression by temporarily reverting the fix and confirming it fails --
the interpreter never returns to idle, with dout0=1 proving the program had
carried on past the G28.3 -- then restored the fix and confirmed it passes.

Verified: tests/interp/gcode-homing (6/6), and the full tests/interp +
tests/motion-logger suite (89/89, 1 pre-existing skip). One flush-order
failure seen in an earlier sweep did not reproduce (89/89 on re-run, 8/8
in isolation including under load); it uses trivkins, where this change is
a no-op by construction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
greatEndian added a commit to greatEndian/linuxcnc that referenced this pull request Aug 19, 2026
…ed path

Addresses grandixximo's three remaining findings on PR LinuxCNC#4172.

1. Completion test (his main concern)

The HOME completion test inferred "homing has stopped" by OR-ing the
per-joint .homing flags. On a machine that homes in several HOME_SEQUENCE
groups, the sequence machine finishes one group and can spend a cycle or
more before the next group raises .homing, so there is a window in which
every joint reads .homing == false while the machine is still homing. Task
samples far coarser than the servo cycle, lands in that window, and scores a
perfectly good home-all as "did not complete".

motion/homing.c documents the same lag in its own words -- "The homing status
variable turns false before homing_active state turns false" -- and guards
against it internally for exactly this reason.

Use motion's aggregate get_homing_is_active() instead. It was not published
anywhere, so this plumbs it through as emcmot_status_t.homing_active ->
EMC_MOTION_STAT::homing_active, mirroring jogging_active field for field. The
per-joint OR is kept as a belt-and-braces term, since it can only extend the
"still running" window, never shorten it. EMC_STAT is 8064 bytes against the
20480-byte emcStatus NML buffer, so the added field is free.

Single-joint Pn and single-sequence machines never hit the gap, which is why
neither the sim tests nor Sigma1912's hardware test 1 caught it.

2. Sequencing applied to immediate commands as well as queued ones

EMC_TASK_EXEC::WAITING_FOR_HOMING is only ever reached through
emcTaskCheckPostconditions(), which task calls only for commands taken off
the interp_list. The GUI's Home and Unhome buttons, halui and linuxcncrsh all
send immediate commands: they reach emcTaskIssueCommand() but nothing follows
up. Applying the FREE-mode dip to them was a regression in two ways:

- the dip was never undone, silently stranding the machine in joint mode; and
- because the dip runs before the command is issued, an immediate unhome
  started succeeding from teleop, where motion deliberately refuses it
  ("must be in joint mode or disabled to unhome", EMCMOT_JOINT_UNHOME in
  command.c). The sequencing was quietly granting a permission upstream
  denies.

Scope the whole sequencing to the queued path via issuingQueuedCommand, so
immediate home/unhome behaves exactly as it did before this branch.

3. Volatile unhome scoring

A volatile unhome (joint == -2) clears only the joints configured
VOLATILE_HOME and leaves every other joint homed, so "no joint in range is
still homed" would score a correct volatile unhome as a failure. Task cannot
narrow the check to just the volatile joints: volatile_home lives in motion's
private homing state (H[jno].volatile_home) and is not published in joint
status -- the volatile_home in emc_nml.hh belongs to
EMC_JOINT_SET_HOMING_PARAMS, a command message, not to EMC_JOINT_STAT.

Guarded, but note this is defensive rather than a live fix: G28.3 only ever
emits Pn >= 0 or -1, and with the sequencing now scoped to the queued path an
immediate unhome(-2) does not reach the scoring either. It is here so a
future queued command carrying -2 cannot be silently scored as a failure.

Tests: adds gcode-homing/immediate-unhome-mode, asserting an immediate unhome
from teleop is refused with the mode untouched, and an immediate home leaves
the mode untouched. Verified it catches the regression by neutralising the
queued-path scoping and confirming it fails ("immediate unhome from teleop
went through (homed=[0, 1, 1])"), then restoring it and confirming it passes.

Verified: tests/interp/gcode-homing (7/7) and the full tests/interp +
tests/motion-logger + tests/abort suite (94/94, 1 pre-existing skip).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
greatEndian added a commit to greatEndian/linuxcnc that referenced this pull request Aug 19, 2026
Implements the direction from PR LinuxCNC#4172's review discussion instead of
the axis-letter form originally sketched there:

- Adds an optional Pn word to G28.2/G28.3 to home/unhome a single joint
  by its 0-based joint number (matching [JOINT_n] INI numbering), e.g.
  G28.2 P1. Bare G28.2/G28.3 (no P) are unchanged (home/unhome all
  joints). Reuses the joint field EMC_JOINT_HOME/EMC_JOINT_UNHOME
  already carry, so it needs no NML change and works identically on
  any kinematics -- exactly the primitive grandixximo's review comment
  argued for. The axis-letter form (G28.2 X) is deliberately NOT
  implemented: resolving an axis letter to a joint needs the
  kinematics coordinate map and isn't trivial even on trivkins
  (duplicate letters on gantries), and andypugh's review also objected
  that homing is a joint concept, not an axis one. G28.2/G28.3 needed
  adding to the P-word whitelist in interp_check.cc (checked against
  g_modes[GM_MODAL_0], since they are modal-group-0 codes like G10/G4,
  not motion-group codes).

- Fixes the real gap grandixximo's review identified: do_homing()
  (control.c) only ever advances while motion is in FREE mode, so a
  home/unhome issued from a running program or MDI while in
  TELEOP/COORD would previously either be rejected by a motion-side
  guard or silently never progress. Task now sequences it properly: a
  new EMC_TASK_EXEC::WAITING_FOR_HOMING state (modeled on the existing
  WAITING_FOR_SPINDLE_ORIENTED state) saves the current trajectory
  mode, dips into FREE, waits for the actual per-joint .homing/.homed
  status to reach the expected end state, then restores the prior mode
  -- invisibly to the task-level MDI/AUTO/MANUAL state, the same
  principle multichannel-DESIGN.txt uses for the analogous
  per-channel-homing problem. If homing/unhoming does not reach the
  expected end state, the program aborts (execState = ERROR) rather
  than continuing unreferenced, and the machine is left in FREE for an
  operator to intervene from rather than snapped back to a mode an
  unhomed machine may not legally run coordinated motion in.

  HOME and UNHOME are not symmetric at the motion level: EMCMOT_JOINT_HOME
  is a genuine state machine (.homing goes true while running), but
  EMCMOT_JOINT_UNHOME (command.c) is synchronous -- set_unhomed() just
  clears .homed immediately, .homing is never touched. The sequencing
  wait branches accordingly: UNHOME checks the target .homed state
  directly, HOME waits for the full start-then-finish cycle.

- Re-applies [TRAJ]NO_FORCE_HOMING at the point a home/unhome command
  already forces a sync, closing the hole Sigma1912 raised in the PR
  discussion (G28.3 mid-program followed by a move with no re-home).
  NO_FORCE_HOMING=0 already refuses to *start* MDI/AUTO on an unhomed
  machine, but only at program/MDI start, not per line, so this
  specific gap needed its own check -- at no cost to the motion path,
  since it only runs at a sync point the command already forces.

- Fixes two bugs found in the process that predate this commit and
  affect the base G28.2/G28.3/GCODE_HOMING feature, not just Pn:

  * HOME_CYCLE()/UNHOME_AXES()/HOME_CYCLE_IF_UNHOMED() (emccanon.cc)
    never flushed pending chained motion segments before appending
    their own command. STRAIGHT_FEED/STRAIGHT_TRAVERSE buffer points
    for arc-blend lookahead and only reach interp_list on a flush, so
    a queued move immediately before a G28.2/G28.3/homing-G28 could
    silently execute AFTER the home instead of before it. Fixed by
    calling flush_segments() first in all five home/unhome canon
    functions (the three pre-existing ones too).

  * emcJointHome()/emcJointUnhome() (taskintf.cc) returned 0 (success)
    for an out-of-range joint number, so an invalid Pn would silently
    report success instead of an error.

- Adds stub implementations of the two new canon calls to
  gcodemodule.cc (the Python gcode module bindings), the third canon
  backend alongside emccanon.cc and saicanon.cc.

Validated live in headless sim: the exact rehoming-a-shared-joint use
case Sigma1912 described (cold-start home-all, move, mid-program
unhome one joint, rehome it, move again) completes cleanly with zero
errors; a NO_FORCE_HOMING=0 config confirms an unreferenced move after
an unhome is correctly blocked with the intended error message; a
plain move-then-home-all program confirms the flush_segments() ordering
fix. Interp-level regression (tests/interp/gcode-homing/*,
tests/interp/rotation/g28) passes 4/4, including a new joint-pword
test case for the Pn parsing.

Signed-off-by: chabron94 <chabron94@gmail.com>
greatEndian added a commit to greatEndian/linuxcnc that referenced this pull request Aug 19, 2026
…can't hold

Addresses the first of grandixximo's findings on PR LinuxCNC#4172, and the failure
Sigma1912 hit on real hardware (Mesa 7I95T gantry): "g28.3 p0" reported
"all joints must be homed before going into coordinated mode", greyed out
the GUI's mode controls, and left the machine needing F2 to recover.

The G28.2/G28.3 sequencing dips motion into FREE (do_homing() only advances
there) and restores the previous trajectory mode when the command finishes.
That restore was gated only on the command having succeeded for its *target*
joint. But a per-joint G28.2 Pn / G28.3 Pn can succeed for its own joint
while leaving the machine as a whole unreferenced, and motion refuses to
(re-)enter TELEOP or COORD in that state on non-identity kinematics --
switch_to_teleop_mode() (motion.c) and the EMCMOT_COORD case (command.c)
both gate on "kinType != KINEMATICS_IDENTITY && !get_allhomed()".

So the restore was silently rejected while task still reported DONE: the
program advanced to the next line, motion had no valid frame for it, and the
machine sat stranded in FREE. Sigma's third test is the same bug cascading --
the g28.3 p2 breaks the mode state, then the following g28 and g0 fail with
"need to be enabled, in coord mode".

Mirror motion's own condition before restoring, and fail the command cleanly
(staying in FREE, with an operator error) instead of reporting success and
stranding the operator. Identity kinematics are unaffected: motion permits
the restore there, so the behaviour is unchanged for trivkins.

Note this only reaches the buggy path with NO_FORCE_HOMING=1. With the
default 0, the pre-existing NO_FORCE_HOMING re-check catches a partial
unhome first -- which, together with the existing tests using trivkins, is
why neither the sim tests nor Sigma's first test caught it.

The kinematics type is read from emcStatus->motion.traj.kinematics_type
rather than this file's static emcmotConfig: that copy is filled in once
just before the main loop and never refreshed, so it goes stale as soon as
switchkins changes kinematics at runtime (G43.4/G43.5). taskintf.cc re-reads
the motion config whenever config_num changes and republishes it in status.

Tests: adds gcode-homing/nonidentity-restore, which needs both knobs the
existing coverage lacks -- corexykins (KINEMATICS_BOTH) and
NO_FORCE_HOMING=1. The program is "G28.3 P0 / M64 P0 / M2"; the digital
output is the witness, since it needs no coordinated motion and so would
still run with the machine stuck in FREE. Verified the test actually catches
the regression by temporarily reverting the fix and confirming it fails --
the interpreter never returns to idle, with dout0=1 proving the program had
carried on past the G28.3 -- then restored the fix and confirmed it passes.

Verified: tests/interp/gcode-homing (6/6), and the full tests/interp +
tests/motion-logger suite (89/89, 1 pre-existing skip). One flush-order
failure seen in an earlier sweep did not reproduce (89/89 on re-run, 8/8
in isolation including under load); it uses trivkins, where this change is
a no-op by construction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
greatEndian added a commit to greatEndian/linuxcnc that referenced this pull request Aug 19, 2026
…ed path

Addresses grandixximo's three remaining findings on PR LinuxCNC#4172.

1. Completion test (his main concern)

The HOME completion test inferred "homing has stopped" by OR-ing the
per-joint .homing flags. On a machine that homes in several HOME_SEQUENCE
groups, the sequence machine finishes one group and can spend a cycle or
more before the next group raises .homing, so there is a window in which
every joint reads .homing == false while the machine is still homing. Task
samples far coarser than the servo cycle, lands in that window, and scores a
perfectly good home-all as "did not complete".

motion/homing.c documents the same lag in its own words -- "The homing status
variable turns false before homing_active state turns false" -- and guards
against it internally for exactly this reason.

Use motion's aggregate get_homing_is_active() instead. It was not published
anywhere, so this plumbs it through as emcmot_status_t.homing_active ->
EMC_MOTION_STAT::homing_active, mirroring jogging_active field for field. The
per-joint OR is kept as a belt-and-braces term, since it can only extend the
"still running" window, never shorten it. EMC_STAT is 8064 bytes against the
20480-byte emcStatus NML buffer, so the added field is free.

Single-joint Pn and single-sequence machines never hit the gap, which is why
neither the sim tests nor Sigma1912's hardware test 1 caught it.

2. Sequencing applied to immediate commands as well as queued ones

EMC_TASK_EXEC::WAITING_FOR_HOMING is only ever reached through
emcTaskCheckPostconditions(), which task calls only for commands taken off
the interp_list. The GUI's Home and Unhome buttons, halui and linuxcncrsh all
send immediate commands: they reach emcTaskIssueCommand() but nothing follows
up. Applying the FREE-mode dip to them was a regression in two ways:

- the dip was never undone, silently stranding the machine in joint mode; and
- because the dip runs before the command is issued, an immediate unhome
  started succeeding from teleop, where motion deliberately refuses it
  ("must be in joint mode or disabled to unhome", EMCMOT_JOINT_UNHOME in
  command.c). The sequencing was quietly granting a permission upstream
  denies.

Scope the whole sequencing to the queued path via issuingQueuedCommand, so
immediate home/unhome behaves exactly as it did before this branch.

3. Volatile unhome scoring

A volatile unhome (joint == -2) clears only the joints configured
VOLATILE_HOME and leaves every other joint homed, so "no joint in range is
still homed" would score a correct volatile unhome as a failure. Task cannot
narrow the check to just the volatile joints: volatile_home lives in motion's
private homing state (H[jno].volatile_home) and is not published in joint
status -- the volatile_home in emc_nml.hh belongs to
EMC_JOINT_SET_HOMING_PARAMS, a command message, not to EMC_JOINT_STAT.

Guarded, but note this is defensive rather than a live fix: G28.3 only ever
emits Pn >= 0 or -1, and with the sequencing now scoped to the queued path an
immediate unhome(-2) does not reach the scoring either. It is here so a
future queued command carrying -2 cannot be silently scored as a failure.

Tests: adds gcode-homing/immediate-unhome-mode, asserting an immediate unhome
from teleop is refused with the mode untouched, and an immediate home leaves
the mode untouched. Verified it catches the regression by neutralising the
queued-path scoping and confirming it fails ("immediate unhome from teleop
went through (homed=[0, 1, 1])"), then restoring it and confirming it passes.

Verified: tests/interp/gcode-homing (7/7) and the full tests/interp +
tests/motion-logger + tests/abort suite (94/94, 1 pre-existing skip).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
greatEndian added a commit to greatEndian/linuxcnc that referenced this pull request Aug 19, 2026
Per the review discussion on PR LinuxCNC#4172: land the explicit G28.2 / G28.3
(with Pn) now, and hold the GCODE_HOMING plain-G28 behaviour for its own PR
once it has a real-machine sign-off.

Sigma1912's opening question settled it. With NO_FORCE_HOMING=0 a program
cannot be started on an unhomed machine at all, so on a homed machine a plain
G28 is just the ordinary return move and the home-first branch is unreachable.
The only way to reach it is to unhome mid-program -- which is precisely the
path that broke on his gantry. A feature whose only reachable path is the
broken one does not belong in this PR.

Removes the flag and everything gated on it:

- interpreter: FEATURE_GCODE_HOMING, the [RS274NGC]GCODE_HOMING INI read, and
  the G28 home-first branch in convert_home()
- canon: HOME_CYCLE_IF_UNHOMED() -- the declaration plus the implementation in
  all three backends (emccanon.cc, saicanon.cc, gcodemodule.cc)
- NML: the EMC_HOME_ALL_IF_UNHOMED (-3) sentinel
- task: the sentinel resolution in EMC_JOINT_HOME_TYPE (target_joint is now
  const), and the stale comment in emcTaskCheckPostconditions()
- docs: the g-code.adoc NOTE, the GCODE_HOMING cross-reference in the
  G28.2/G28.3 note, and the ini-config.adoc entry
- tests: gcode-homing/homing-on and gcode-homing/homing-off, which existed
  only to cover the two settings of the flag, plus stale mentions in
  joint-pword and flush-order

G28.2 / G28.3 (with Pn) are untouched -- they were always independent of the
flag. `git grep` for GCODE_HOMING, EMC_HOME_ALL_IF_UNHOMED and
HOME_CYCLE_IF_UNHOMED now returns nothing.

Done as a removal on top rather than by rewriting history: a merge commit sits
partway along this branch, so unpicking 86c58f6 in place would mean
replaying the series and would churn commits the reviewers have already read.
The net diff of the PR is the same either way. The removed work is preserved
intact on the g28-gcode-homing branch for its own PR.

Verified: full tests/interp + tests/motion-logger + tests/abort suite
(92/92, 1 pre-existing skip). 92 rather than 94 because the two flag tests
above are the ones removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
greatEndian added a commit to greatEndian/linuxcnc that referenced this pull request Aug 19, 2026
emcJointHome()/emcJointUnhome() bounded the joint number by
EMCMOT_MAX_JOINTS, the compile-time size of joints[], instead of by the
machine's configured joint count. A number in between the two -- P5 on a
five-joint machine -- therefore passed task, reached motion, and was
silently dropped: do_home_joint() finds no joint of that number to start
and reports nothing at all.

Task then waited for a homing cycle that could never begin. With the
machine parked in the FREE-mode dip taken for homing sequencing, it sat
out the whole two-second start timeout -- the GUI jogging in joint mode
meanwhile -- and finally reported the generic "G28.2 home did not start",
which names neither the joint nor the real problem. Reported on real
hardware (Mesa 7I95T gantry) in PR LinuxCNC#4172 and reproduced exactly in sim:
motion_mode read FREE for 2.0s, then COORD again.

Bound both entry points by TrajConfig.Joints and report through
emcOperatorError() rather than rcs_print(), so an operator who typed
"G28.2 P5" sees which numbers the machine actually has. Checked in task
rather than in the interpreter because the joint count is not part of
interpreter state, and because task covers every caller -- G-code, the
GUI Home button, halui, linuxcncrsh -- not just G-code.

The unhome path had a check in motion, but as "jno > all_joints", so the
first unconfigured joint number fell through it into an unrelated
complaint about extra joints; and because an unconfigured joint reads as
not homed, task's synchronous "no joint in range is still homed" test
would have scored that refusal as a successful unhome.

New test tests/interp/gcode-homing/invalid-pword: on a fully homed
corexykins machine, G28.2 P3 and G28.3 P7 must each be refused with an
error naming the joint, within the start timeout rather than after it,
leaving the trajectory mode untouched -- and a valid G28.2 P1 on the same
machine must still home. Verified to fail without this fix (2.03s,
generic message). Suite 6/6; tests/interp + tests/motion-logger 92/92.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
greatEndian added a commit to greatEndian/linuxcnc that referenced this pull request Aug 19, 2026
No reviewer of PR LinuxCNC#4172 could name a use for a G-code unhome that
something simpler does not already serve. Sigma1912 asked twice what a
program would do with it -- if a joint is no longer trusted, G28.2
re-homes it directly, and the homing cycle fails on its own if anything
is wrong -- and grandixximo concluded it only makes sense under
[TRAJ]NO_FORCE_HOMING=1, where a numbered parameter carries the same
"this joint is unreferenced" flag with no new G-code and no controller
state change.

It was also the only operation in this PR able to leave a running
program on an unreferenced machine. Sigma's "g28.3 p0" on a real gantry
unhomed a joint mid-program and left the controller in joint mode with
the coordinated modes refused, recoverable only through the GUI. The
machinery that made that safe -- the unhome branch of the completion
test, the NO_FORCE_HOMING re-check at the sync point, the volatile
unhome (-2) guard -- exists solely to contain a hazard the feature
itself introduces, so dropping the feature drops all of it.

G28.2, bare and with Pn, is unchanged. Unhoming stays available from
the GUI, halui and linuxcncrsh, none of which can strand a program
mid-run.

Removed: G_28_3 and its gees[] slot, the UNHOME_AXES()/UNHOME_JOINT()
canon ops and their three implementations, EMC_JOINT_UNHOME_TYPE from
the queued-command precondition and postcondition paths, homingIsUnhome
and the unhome half of WAITING_FOR_HOMING, and the nonidentity-restore
test, which tested the wedge above. EMC_JOINT_UNHOME takes the
immediate path only, as it did before this PR.

tests/interp 88/88; gcode-homing + motion-logger + motion 13/13.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@greatEndian greatEndian changed the title G28 with features G28.2: home the machine from G-code (all joints, or Pn per joint) Aug 19, 2026
@greatEndian

Copy link
Copy Markdown
Contributor Author

@Sigma1912 thanks for taking it back to the machine — both of the things you
hit are fixed on the branch (42bc5cafaf). I have also rewritten the PR
description and title, which were still describing the original three-part
proposal; what follows is what is actually in it now.

1. g28.2 p5 — a real bug, and a good catch

emcJointHome() range-checked the joint number against EMCMOT_MAX_JOINTS,
the compile-time size of joints[], instead of against the machine's
configured joint count. P5 on your five-joint machine falls between the
two: it passed task, reached motion, and was then dropped without a word —
do_home_joint() has no joint of that number to start, and reports nothing.

That also explains the part you flagged as the bug. A queued home takes a
deliberate dip into FREE mode for the duration of the cycle (homing only
advances there) and undoes it when the cycle finishes. With a cycle that
could never begin, task sat out the full two-second start timeout — the GUI
jogging in joint mode meanwhile — and then reported the generic "did not
start", which names neither the joint nor the real problem. Nothing restored
the mode until your next MDI line or program run.

Both ends are fixed: the number is now bounded by TrajConfig.Joints, the
request is refused before the mode dip is taken, and it is reported through
emcOperatorError() so you see what the machine actually has —

Cannot home invalid joint 5 (valid: 0..4, or -1 for all)

Checked in task rather than in the interpreter, because the joint count is
not part of interpreter state and because task covers every caller — the same
hole was reachable from the GUI Home button, halui and linuxcncrsh, not just
from G-code. New test tests/interp/gcode-homing/invalid-pword, verified to
fail without the fix (2.03 s, generic message).

I reproduced your case in sim first — motion_mode read FREE for 2.0 s, then
COORD again — so if you get a chance to re-run g28.2 p5 on the 7I95T, I would
like to hear whether it now refuses immediately and leaves your mode alone.

2. G28.3 — dropped

@Sigma1912 @grandixximo @andypugh — agreed, and removed. Neither of you could
name a use a numbered parameter would not serve better under
NO_FORCE_HOMING=1, and I cannot either: if a joint is not trusted, G28.2
re-homes it directly, and the cycle fails on its own if something is wrong.

It was also the one operation here able to leave a running program on an
unreferenced machine — your g28.3 p0 on the gantry, which needed the GUI to
recover. Everything that existed to contain that hazard (the unhome branch of
the completion test, the NO_FORCE_HOMING re-check at the sync point, the
volatile-unhome guard) went with it, so the PR is meaningfully smaller.

G28.2, bare and with Pn, is unchanged. Unhoming stays where it was — GUI,
halui, linuxcncrsh — none of which can strand a program mid-run.

3. Also in this push

The one red CI job (cppcheck) was mine: the homing_active status member
added for the home-completion test was missing from the EMC_MOTION_STAT
constructor. Fixing that turned up a second gap in the same change — it was
also missing from EMC_MOTION_STAT::update(), the NML serializer, which
cannot fail locally because task both writes and reads the field through the
same shared-memory copy of the struct. Both fixed.

Where the PR stands

G28.2 only, bare and with Pn. Plain-G28 GCODE_HOMING is split out and
will be proposed separately, once the home-then-coordinated-return sequence
has real-machine validation. Local: tests/interp 88/88, plus gcode-homing,
motion-logger and motion 13/13.

@Sigma1912 your passing hardware results — g28.2, g28.2 p0 on the gantry
pair, g28.2 p4, and the mid-program g28.2 p0 — are the first real-machine
evidence this feature has, and they are now cited in the PR description.
Thank you. The p5 re-test is the last thing I know of that is outstanding.

@greatEndian
greatEndian marked this pull request as ready for review August 19, 2026 12:45
greatEndian and others added 15 commits August 19, 2026 09:48
Add two non-modal (group 0) G-codes to trigger the machine homing cycle
from G-code, so a machine can reference / clear references from MDI or a
program instead of only from the GUI:

  G28.2   run the homing cycle (all joints, in HOME_SEQUENCE order)
  G28.3   unhome (all joints)

- interp: enum G_28_2/G_28_3, modal group 0, accepted in check_g_codes;
  convert_modal_0 -> convert_home_cycle (cutter-comp guard)
- canon: HOME_CYCLE()/UNHOME_AXES() -> EMC_JOINT_HOME/UNHOME(joint=-1);
  saicanon/gcodemodule stubs

Bare form only (no axis words). Useful for unattended power-up and for
operators who prefer a typed reference command.
EMC_JOINT_HOME_TYPE and EMC_JOINT_UNHOME_TYPE were missing from
emcTaskCheckPreconditions(), which is called for every command pulled
off the interp_list. A homing command that arrives via the queue (e.g.
from a G-code like G28.2, or any front-end that queues it) therefore
hit the 'default' case and returned EMC_TASK_EXEC::ERROR - the command
was dropped and never reached motion. Add both types returning
WAITING_FOR_MOTION (drain prior motion, then home).

Bug fix; independent of the G28.2/G28.3 codes (any queued home benefits).
EMCMOT_JOINT_HOME required motion_state == FREE. Allow it also when
motion is otherwise idle (in position, queue empty) so a homing command
issued from MDI or a program (G28.2) is honored, while still refusing
to home mid-motion. No change when already in free mode.
…_HOMING=1)

With [RS274NGC]GCODE_HOMING=1 (default 0 = stock), a plain G28 runs the
machine homing cycle before its natural return move whenever the machine
is not already fully homed; a fully-homed machine sees a pure legacy G28
(return only). G30 and G28.2/G28.3 are unchanged.

Lets a machine reference itself from MDI or from the top of a program
(e.g. unattended power-up) instead of only from the GUI, while a homed
machine pays nothing.

Flow: interp convert_home (FEATURE_GCODE_HOMING + G_28) emits a new canon
op HOME_CYCLE_IF_UNHOMED() before the waypoint/return moves. Canon queues
EMC_JOINT_HOME carrying the EMC_HOME_ALL_IF_UNHOMED (-3) sentinel in its
'joint' field. Task resolves the sentinel at execution time: if all_homed()
the home is dropped (the queued return alone = pure legacy G28), otherwise
it issues the normal home-all (emcJointHome(-1)) - the same path G28.2 and
the GUI Home-All already use. No new command field, no motion / homing.c /
NML-layout change.

Single-channel; built on the G28.2/G28.3 home-cycle G-codes.

Verified at the interpreter (rs274 -i): GCODE_HOMING=1 plain G28 emits
HOME_CYCLE_IF_UNHOMED() before the return traverse; G30 does not home;
G28.2 still homes unconditionally; default-off G28 is bit-identical to
stock (no homing emitted). Full build (interp/task/motion) clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document the G-code machine-homing feature and add interpreter-level
regression tests for it.

Docs:
- g-code.adoc: new "G28.2, G28.3 Home, Unhome from G-code" section, a
  GCODE_HOMING note in the G28 section, and a quick-reference entry.
- ini-config.adoc: GCODE_HOMING entry in the [RS274NGC] section.

Tests (tests/interp/gcode-homing, rs274 canon-level):
- homing-on: with [RS274NGC]GCODE_HOMING=1, a plain G28 emits
  HOME_CYCLE_IF_UNHOMED() before its return; G28.2 -> HOME_CYCLE(),
  G28.3 -> UNHOME_AXES().
- homing-off: default (flag off) a plain G28 emits no homing op, while
  G28.2/G28.3 still home/unhome (flag-independent).

Verified: new tests pass; tests/interp + tests/abort = 86/86 (1 skipped),
build clean.

Co-authored-by: Luca Toniolo <10792599+grandixximo@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements the direction from PR LinuxCNC#4172's review discussion instead of
the axis-letter form originally sketched there:

- Adds an optional Pn word to G28.2/G28.3 to home/unhome a single joint
  by its 0-based joint number (matching [JOINT_n] INI numbering), e.g.
  G28.2 P1. Bare G28.2/G28.3 (no P) are unchanged (home/unhome all
  joints). Reuses the joint field EMC_JOINT_HOME/EMC_JOINT_UNHOME
  already carry, so it needs no NML change and works identically on
  any kinematics -- exactly the primitive grandixximo's review comment
  argued for. The axis-letter form (G28.2 X) is deliberately NOT
  implemented: resolving an axis letter to a joint needs the
  kinematics coordinate map and isn't trivial even on trivkins
  (duplicate letters on gantries), and andypugh's review also objected
  that homing is a joint concept, not an axis one. G28.2/G28.3 needed
  adding to the P-word whitelist in interp_check.cc (checked against
  g_modes[GM_MODAL_0], since they are modal-group-0 codes like G10/G4,
  not motion-group codes).

- Fixes the real gap grandixximo's review identified: do_homing()
  (control.c) only ever advances while motion is in FREE mode, so a
  home/unhome issued from a running program or MDI while in
  TELEOP/COORD would previously either be rejected by a motion-side
  guard or silently never progress. Task now sequences it properly: a
  new EMC_TASK_EXEC::WAITING_FOR_HOMING state (modeled on the existing
  WAITING_FOR_SPINDLE_ORIENTED state) saves the current trajectory
  mode, dips into FREE, waits for the actual per-joint .homing/.homed
  status to reach the expected end state, then restores the prior mode
  -- invisibly to the task-level MDI/AUTO/MANUAL state, the same
  principle multichannel-DESIGN.txt uses for the analogous
  per-channel-homing problem. If homing/unhoming does not reach the
  expected end state, the program aborts (execState = ERROR) rather
  than continuing unreferenced, and the machine is left in FREE for an
  operator to intervene from rather than snapped back to a mode an
  unhomed machine may not legally run coordinated motion in.

  HOME and UNHOME are not symmetric at the motion level: EMCMOT_JOINT_HOME
  is a genuine state machine (.homing goes true while running), but
  EMCMOT_JOINT_UNHOME (command.c) is synchronous -- set_unhomed() just
  clears .homed immediately, .homing is never touched. The sequencing
  wait branches accordingly: UNHOME checks the target .homed state
  directly, HOME waits for the full start-then-finish cycle.

- Re-applies [TRAJ]NO_FORCE_HOMING at the point a home/unhome command
  already forces a sync, closing the hole Sigma1912 raised in the PR
  discussion (G28.3 mid-program followed by a move with no re-home).
  NO_FORCE_HOMING=0 already refuses to *start* MDI/AUTO on an unhomed
  machine, but only at program/MDI start, not per line, so this
  specific gap needed its own check -- at no cost to the motion path,
  since it only runs at a sync point the command already forces.

- Fixes two bugs found in the process that predate this commit and
  affect the base G28.2/G28.3/GCODE_HOMING feature, not just Pn:

  * HOME_CYCLE()/UNHOME_AXES()/HOME_CYCLE_IF_UNHOMED() (emccanon.cc)
    never flushed pending chained motion segments before appending
    their own command. STRAIGHT_FEED/STRAIGHT_TRAVERSE buffer points
    for arc-blend lookahead and only reach interp_list on a flush, so
    a queued move immediately before a G28.2/G28.3/homing-G28 could
    silently execute AFTER the home instead of before it. Fixed by
    calling flush_segments() first in all five home/unhome canon
    functions (the three pre-existing ones too).

  * emcJointHome()/emcJointUnhome() (taskintf.cc) returned 0 (success)
    for an out-of-range joint number, so an invalid Pn would silently
    report success instead of an error.

- Adds stub implementations of the two new canon calls to
  gcodemodule.cc (the Python gcode module bindings), the third canon
  backend alongside emccanon.cc and saicanon.cc.

Validated live in headless sim: the exact rehoming-a-shared-joint use
case Sigma1912 described (cold-start home-all, move, mid-program
unhome one joint, rehome it, move again) completes cleanly with zero
errors; a NO_FORCE_HOMING=0 config confirms an unreferenced move after
an unhome is correctly blocked with the intended error message; a
plain move-then-home-all program confirms the flush_segments() ordering
fix. Interp-level regression (tests/interp/gcode-homing/*,
tests/interp/rotation/g28) passes 4/4, including a new joint-pword
test case for the Pn parsing.

Signed-off-by: chabron94 <chabron94@gmail.com>
…ight

emcJointHome()/emcJointUnhome() can fail immediately (e.g. an invalid
joint number from a bad Pn word) after the FREE-mode dip added for
homing sequencing has already been applied. Since homing never starts
in that case, the WAITING_FOR_HOMING poll that normally restores the
prior traj mode never runs, leaving traj.mode stuck at FREE. Because
determineMode() derives task.mode from traj.mode, this makes task.mode
read as MANUAL indefinitely -- silently breaking all subsequent MDI
and AUTO commands until the operator manually cycles mode again.

Found via the same isolated/chained-style critical-review stress
testing used on feat/rotary-tangent: G28.2 P<invalid-joint> followed by
any other MDI command reproduced it every time, while a generic
interpreter error (e.g. an out-of-range G-code) did not, confirming
this was specific to the new homing-sequencing dip rather than
general MDI error handling.

Fix: check emcJointHome()/emcJointUnhome()'s return value immediately
and undo the mode dip and homingWaiting flag right there if the
request was rejected outright, instead of leaving them for a
resolution path that will never run.

Signed-off-by: chabron94 <chabron94@gmail.com>
…ests

Docs (docs/src/gcode/g-code.adoc): the G28.2/G28.3 section still said
"take no axis words; they always act on all joints", predating the Pn
work in 3d238db. Documents the Pn word (0-based joint number, matching
[JOINT_n] INI numbering), examples, the NO_FORCE_HOMING gate re-check
on unhome, and the invalid-joint error condition.

Tests: the existing joint-pword test only exercises canon-call
generation at the interpreter level (rs274 -g); nothing covered the
actual task-level sequencing behavior added in 3d238db/65447329e9,
or the flush_segments() ordering fix (which needs the real emccanon.cc
buffering, not reachable via the interp-only saicanon.cc backend rs274
uses). Adds two live headless tests using the DISPLAY-script pattern
from writing-tests.adoc:

- gcode-homing/sequencing: G28.2 Pn's mode dip is invisible at the task
  level; a G28.3 Pn that leaves the machine not fully homed trips the
  pre-existing NO_FORCE_HOMING gate and blocks further MDI (recovery
  goes through the classic c.home() NML call, since that gate has no
  exemption for a homing command submitted as MDI text); an invalid Pn
  is rejected without leaving task_mode stuck (regression test for the
  bug fixed in 6544732).

- gcode-homing/flush-order: a queued move immediately before G28.2 must
  complete before the home, not after. Verified this test actually
  catches the regression by temporarily reverting the flush_segments()
  call in HOME_CYCLE_JOINT() and confirming it fails (X still at 0.008
  when homed flips true), then restored the fix and confirmed it passes.

Verified: tests/interp/gcode-homing (5/5) and the full tests/interp +
tests/abort suite (89/89, 1 pre-existing skip) both pass.
…can't hold

Addresses the first of grandixximo's findings on PR LinuxCNC#4172, and the failure
Sigma1912 hit on real hardware (Mesa 7I95T gantry): "g28.3 p0" reported
"all joints must be homed before going into coordinated mode", greyed out
the GUI's mode controls, and left the machine needing F2 to recover.

The G28.2/G28.3 sequencing dips motion into FREE (do_homing() only advances
there) and restores the previous trajectory mode when the command finishes.
That restore was gated only on the command having succeeded for its *target*
joint. But a per-joint G28.2 Pn / G28.3 Pn can succeed for its own joint
while leaving the machine as a whole unreferenced, and motion refuses to
(re-)enter TELEOP or COORD in that state on non-identity kinematics --
switch_to_teleop_mode() (motion.c) and the EMCMOT_COORD case (command.c)
both gate on "kinType != KINEMATICS_IDENTITY && !get_allhomed()".

So the restore was silently rejected while task still reported DONE: the
program advanced to the next line, motion had no valid frame for it, and the
machine sat stranded in FREE. Sigma's third test is the same bug cascading --
the g28.3 p2 breaks the mode state, then the following g28 and g0 fail with
"need to be enabled, in coord mode".

Mirror motion's own condition before restoring, and fail the command cleanly
(staying in FREE, with an operator error) instead of reporting success and
stranding the operator. Identity kinematics are unaffected: motion permits
the restore there, so the behaviour is unchanged for trivkins.

Note this only reaches the buggy path with NO_FORCE_HOMING=1. With the
default 0, the pre-existing NO_FORCE_HOMING re-check catches a partial
unhome first -- which, together with the existing tests using trivkins, is
why neither the sim tests nor Sigma's first test caught it.

The kinematics type is read from emcStatus->motion.traj.kinematics_type
rather than this file's static emcmotConfig: that copy is filled in once
just before the main loop and never refreshed, so it goes stale as soon as
switchkins changes kinematics at runtime (G43.4/G43.5). taskintf.cc re-reads
the motion config whenever config_num changes and republishes it in status.

Tests: adds gcode-homing/nonidentity-restore, which needs both knobs the
existing coverage lacks -- corexykins (KINEMATICS_BOTH) and
NO_FORCE_HOMING=1. The program is "G28.3 P0 / M64 P0 / M2"; the digital
output is the witness, since it needs no coordinated motion and so would
still run with the machine stuck in FREE. Verified the test actually catches
the regression by temporarily reverting the fix and confirming it fails --
the interpreter never returns to idle, with dout0=1 proving the program had
carried on past the G28.3 -- then restored the fix and confirmed it passes.

Verified: tests/interp/gcode-homing (6/6), and the full tests/interp +
tests/motion-logger suite (89/89, 1 pre-existing skip). One flush-order
failure seen in an earlier sweep did not reproduce (89/89 on re-run, 8/8
in isolation including under load); it uses trivkins, where this change is
a no-op by construction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ed path

Addresses grandixximo's three remaining findings on PR LinuxCNC#4172.

1. Completion test (his main concern)

The HOME completion test inferred "homing has stopped" by OR-ing the
per-joint .homing flags. On a machine that homes in several HOME_SEQUENCE
groups, the sequence machine finishes one group and can spend a cycle or
more before the next group raises .homing, so there is a window in which
every joint reads .homing == false while the machine is still homing. Task
samples far coarser than the servo cycle, lands in that window, and scores a
perfectly good home-all as "did not complete".

motion/homing.c documents the same lag in its own words -- "The homing status
variable turns false before homing_active state turns false" -- and guards
against it internally for exactly this reason.

Use motion's aggregate get_homing_is_active() instead. It was not published
anywhere, so this plumbs it through as emcmot_status_t.homing_active ->
EMC_MOTION_STAT::homing_active, mirroring jogging_active field for field. The
per-joint OR is kept as a belt-and-braces term, since it can only extend the
"still running" window, never shorten it. EMC_STAT is 8064 bytes against the
20480-byte emcStatus NML buffer, so the added field is free.

Single-joint Pn and single-sequence machines never hit the gap, which is why
neither the sim tests nor Sigma1912's hardware test 1 caught it.

2. Sequencing applied to immediate commands as well as queued ones

EMC_TASK_EXEC::WAITING_FOR_HOMING is only ever reached through
emcTaskCheckPostconditions(), which task calls only for commands taken off
the interp_list. The GUI's Home and Unhome buttons, halui and linuxcncrsh all
send immediate commands: they reach emcTaskIssueCommand() but nothing follows
up. Applying the FREE-mode dip to them was a regression in two ways:

- the dip was never undone, silently stranding the machine in joint mode; and
- because the dip runs before the command is issued, an immediate unhome
  started succeeding from teleop, where motion deliberately refuses it
  ("must be in joint mode or disabled to unhome", EMCMOT_JOINT_UNHOME in
  command.c). The sequencing was quietly granting a permission upstream
  denies.

Scope the whole sequencing to the queued path via issuingQueuedCommand, so
immediate home/unhome behaves exactly as it did before this branch.

3. Volatile unhome scoring

A volatile unhome (joint == -2) clears only the joints configured
VOLATILE_HOME and leaves every other joint homed, so "no joint in range is
still homed" would score a correct volatile unhome as a failure. Task cannot
narrow the check to just the volatile joints: volatile_home lives in motion's
private homing state (H[jno].volatile_home) and is not published in joint
status -- the volatile_home in emc_nml.hh belongs to
EMC_JOINT_SET_HOMING_PARAMS, a command message, not to EMC_JOINT_STAT.

Guarded, but note this is defensive rather than a live fix: G28.3 only ever
emits Pn >= 0 or -1, and with the sequencing now scoped to the queued path an
immediate unhome(-2) does not reach the scoring either. It is here so a
future queued command carrying -2 cannot be silently scored as a failure.

Tests: adds gcode-homing/immediate-unhome-mode, asserting an immediate unhome
from teleop is refused with the mode untouched, and an immediate home leaves
the mode untouched. Verified it catches the regression by neutralising the
queued-path scoping and confirming it fails ("immediate unhome from teleop
went through (homed=[0, 1, 1])"), then restoring it and confirming it passes.

Verified: tests/interp/gcode-homing (7/7) and the full tests/interp +
tests/motion-logger + tests/abort suite (94/94, 1 pre-existing skip).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Per the review discussion on PR LinuxCNC#4172: land the explicit G28.2 / G28.3
(with Pn) now, and hold the GCODE_HOMING plain-G28 behaviour for its own PR
once it has a real-machine sign-off.

Sigma1912's opening question settled it. With NO_FORCE_HOMING=0 a program
cannot be started on an unhomed machine at all, so on a homed machine a plain
G28 is just the ordinary return move and the home-first branch is unreachable.
The only way to reach it is to unhome mid-program -- which is precisely the
path that broke on his gantry. A feature whose only reachable path is the
broken one does not belong in this PR.

Removes the flag and everything gated on it:

- interpreter: FEATURE_GCODE_HOMING, the [RS274NGC]GCODE_HOMING INI read, and
  the G28 home-first branch in convert_home()
- canon: HOME_CYCLE_IF_UNHOMED() -- the declaration plus the implementation in
  all three backends (emccanon.cc, saicanon.cc, gcodemodule.cc)
- NML: the EMC_HOME_ALL_IF_UNHOMED (-3) sentinel
- task: the sentinel resolution in EMC_JOINT_HOME_TYPE (target_joint is now
  const), and the stale comment in emcTaskCheckPostconditions()
- docs: the g-code.adoc NOTE, the GCODE_HOMING cross-reference in the
  G28.2/G28.3 note, and the ini-config.adoc entry
- tests: gcode-homing/homing-on and gcode-homing/homing-off, which existed
  only to cover the two settings of the flag, plus stale mentions in
  joint-pword and flush-order

G28.2 / G28.3 (with Pn) are untouched -- they were always independent of the
flag. `git grep` for GCODE_HOMING, EMC_HOME_ALL_IF_UNHOMED and
HOME_CYCLE_IF_UNHOMED now returns nothing.

Done as a removal on top rather than by rewriting history: a merge commit sits
partway along this branch, so unpicking 86c58f6 in place would mean
replaying the series and would churn commits the reviewers have already read.
The net diff of the PR is the same either way. The removed work is preserved
intact on the g28-gcode-homing branch for its own PR.

Verified: full tests/interp + tests/motion-logger + tests/abort suite
(92/92, 1 pre-existing skip). 92 rather than 94 because the two flag tests
above are the ones removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
emcJointHome()/emcJointUnhome() bounded the joint number by
EMCMOT_MAX_JOINTS, the compile-time size of joints[], instead of by the
machine's configured joint count. A number in between the two -- P5 on a
five-joint machine -- therefore passed task, reached motion, and was
silently dropped: do_home_joint() finds no joint of that number to start
and reports nothing at all.

Task then waited for a homing cycle that could never begin. With the
machine parked in the FREE-mode dip taken for homing sequencing, it sat
out the whole two-second start timeout -- the GUI jogging in joint mode
meanwhile -- and finally reported the generic "G28.2 home did not start",
which names neither the joint nor the real problem. Reported on real
hardware (Mesa 7I95T gantry) in PR LinuxCNC#4172 and reproduced exactly in sim:
motion_mode read FREE for 2.0s, then COORD again.

Bound both entry points by TrajConfig.Joints and report through
emcOperatorError() rather than rcs_print(), so an operator who typed
"G28.2 P5" sees which numbers the machine actually has. Checked in task
rather than in the interpreter because the joint count is not part of
interpreter state, and because task covers every caller -- G-code, the
GUI Home button, halui, linuxcncrsh -- not just G-code.

The unhome path had a check in motion, but as "jno > all_joints", so the
first unconfigured joint number fell through it into an unrelated
complaint about extra joints; and because an unconfigured joint reads as
not homed, task's synchronous "no joint in range is still homed" test
would have scored that refusal as a successful unhome.

New test tests/interp/gcode-homing/invalid-pword: on a fully homed
corexykins machine, G28.2 P3 and G28.3 P7 must each be refused with an
error naming the joint, within the start timeout rather than after it,
leaving the trajectory mode untouched -- and a valid G28.2 P1 on the same
machine must still home. Verified to fail without this fix (2.03s,
generic message). Suite 6/6; tests/interp + tests/motion-logger 92/92.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comment added with the mode-restore gate claimed the file-static
emcmotConfig copy "goes stale the moment switchkins changes kinematics at
runtime (G43.4/G43.5)". That is not true, and the branch should not carry
a justification that does not hold: emcmotConfig->kinType is written
exactly once, in init_comm_buffers() (motion.c), and a runtime switchkins
change never touches it -- the switchkins user module answers
KINEMATICS_BOTH for every selectable type, and handle_kinematicsSwitch()
(control.c) neither writes kinType nor bumps config_num.

The code is unchanged: traj.kinematics_type is still the right field to
read, being the copy task actually maintains. Only the stated reason for
reading it is corrected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
No reviewer of PR LinuxCNC#4172 could name a use for a G-code unhome that
something simpler does not already serve. Sigma1912 asked twice what a
program would do with it -- if a joint is no longer trusted, G28.2
re-homes it directly, and the homing cycle fails on its own if anything
is wrong -- and grandixximo concluded it only makes sense under
[TRAJ]NO_FORCE_HOMING=1, where a numbered parameter carries the same
"this joint is unreferenced" flag with no new G-code and no controller
state change.

It was also the only operation in this PR able to leave a running
program on an unreferenced machine. Sigma's "g28.3 p0" on a real gantry
unhomed a joint mid-program and left the controller in joint mode with
the coordinated modes refused, recoverable only through the GUI. The
machinery that made that safe -- the unhome branch of the completion
test, the NO_FORCE_HOMING re-check at the sync point, the volatile
unhome (-2) guard -- exists solely to contain a hazard the feature
itself introduces, so dropping the feature drops all of it.

G28.2, bare and with Pn, is unchanged. Unhoming stays available from
the GUI, halui and linuxcncrsh, none of which can strand a program
mid-run.

Removed: G_28_3 and its gees[] slot, the UNHOME_AXES()/UNHOME_JOINT()
canon ops and their three implementations, EMC_JOINT_UNHOME_TYPE from
the queued-command precondition and postcondition paths, homingIsUnhome
and the unhome half of WAITING_FOR_HOMING, and the nonidentity-restore
test, which tested the wedge above. EMC_JOINT_UNHOME takes the
immediate path only, as it did before this PR.

tests/interp 88/88; gcode-homing + motion-logger + motion 13/13.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
EMC_MOTION_STAT gained homing_active for the home-completion test, but
the member was left out of both the constructor's initializer list and
EMC_MOTION_STAT::update(), the NML/CMS serializer where every other
member of the class appears.

Upstream CI catches the first half: cppcheck fails this PR with
"Member variable 'EMC_MOTION_STAT::homing_active' is not initialized in
the constructor [uninitMemberVar]" (emc/nml_intf/emcops.cc:102), the
only red job on an otherwise green run.

The second half cannot fail locally: task writes the field and task
reads it back, both through the same shared-memory copy of the struct,
so neither the sim nor the test suite ever exercises the encoder. A
client receiving neutrally-encoded status over NML would have read
whatever the field happened to decode to.

Both entries are placed in declaration order, between jogging_active
and heartbeat.

cppcheck clean on both files; gcode-homing + motion-logger + motion
13/13.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Sigma1912

Copy link
Copy Markdown
Contributor

Thanks for the quick response.

I have retested and I still have one thing I'd like to flag:

MDI command G28.2 P5 now triggers this error message:

Cannot home invalid joint 5 (valid: 0..4, or -1 for all)

Which suggests that -1 is a valid value for the P word which it isn't because running G28.2 P-1 triggers this error:

emc/task/emctask.cc 70: interp_error: P value for G28.2 must be a non-negative whole joint number P value for G28.2 must be a non-negative whole joint number

Not sure if the first error message is in your control but I think we need to clarify this situation for the user.

@Sigma1912

Copy link
Copy Markdown
Contributor

Maybe G28.2 could simply be made to accept a P value of -1 for a home-all command and make that the default?

@greatEndian

Copy link
Copy Markdown
Contributor Author

@Sigma1912 you are right, and that is a fair catch — the message was
documenting the internal API to someone holding a G-code manual. Fixed in
4f582cfcff.

-1 (all) and -2 (volatile) are the NML convention EMC_JOINT_HOME carries.
They are how the GUI's Home All button, halui and linuxcncrsh ask for those
operations. G-code cannot express them and is not meant to, so naming them in
an operator-facing error sent you straight into the second error you quoted.

The two messages now read:

Cannot home invalid joint 5 (this machine has joints 0..4; omit the joint to home them all)
P value for G28.2 must be a non-negative whole joint number (omit P to home every joint)

Each one now points at the spelling that does what the reader wanted. The
unhome message loses its sentinels for the same reason, even though that path
is only reachable from the interfaces where -1/-2 are legitimate input.

tests/interp/gcode-homing/invalid-pword grew two assertions: the rejection
must name no negative sentinel, and G28.2 P-1 must be refused with an error
that names the bare form. Both were verified to fail against the old
messages — the first reproduces your report exactly.

On P-1 as home-all

I would rather not, and I think the fix above is the better answer to the same
problem. Bare G28.2 is already home-all, and it is what the docs and the
G28.1/G30.1 family lead you to. Adding P-1 gives two spellings for one
operation, and it makes the natural next question "so what does P-2 do?" —
where the honest answer is that -2 is the volatile-unhome sentinel, which is
exactly the kind of internal detail that should not become G-code surface.

If you or @grandixximo still prefer accepting P-1, it is a two-line change in
convert_home_cycle() and I will make it. But I would rather the error text
teach the bare form than the syntax grow a synonym.

Thanks for going back to the machine twice on this.

The out-of-range home error read

    Cannot home invalid joint 5 (valid: 0..4, or -1 for all)

which sends the reader straight into a second error, because the
interpreter refuses a negative P word:

    P value for G28.2 must be a non-negative whole joint number

-1 (all) and -2 (volatile) are an internal NML convention. They are how
the GUI's Home All button, halui and linuxcncrsh ask for those
operations; G-code cannot express them and is not meant to. Naming them
in an operator-facing message was documenting the API to someone holding
a G-code manual.

Report only what the reader can act on -- the joints this machine has --
and point at the spelling that does what they wanted:

    Cannot home invalid joint 5 (this machine has joints 0..4; omit the
    joint to home them all)
    P value for G28.2 must be a non-negative whole joint number (omit P
    to home every joint)

Reported by Sigma1912 on PR LinuxCNC#4172 after re-testing the P5 fix on real
hardware (Mesa 7I95T).

The unhome message loses its sentinels for the same reason, though that
path is reachable only from the interfaces where -1/-2 are valid input.

tests/interp/gcode-homing/invalid-pword now asserts that the rejection
names no negative sentinel, and that G28.2 P-1 is refused with an error
naming the bare form. Both assertions verified to fail against the
previous messages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

4 participants