Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
78c72a6
interp+canon: G28.2 (home) / G28.3 (unhome) G-codes
greatEndian Jun 13, 2026
11397cf
task: allow queued JOINT_HOME/UNHOME in emcTaskCheckPreconditions
greatEndian Jun 13, 2026
23e346a
motion: permit homing when idle (not only in free mode)
greatEndian Jun 13, 2026
c23e83d
GCODE_HOMING: plain G28 references the machine first ([RS274NGC]GCODE…
greatEndian Jun 16, 2026
0a5b085
docs+tests: G28.2/G28.3 and GCODE_HOMING
greatEndian Jun 16, 2026
a945dca
G28.2/G28.3: add Pn per-joint home/unhome, fix homing sequencing
greatEndian Jul 2, 2026
4135216
fix(g28): restore traj mode when G28.2/G28.3 rejects the request outr…
greatEndian Jul 2, 2026
fd0f360
docs+tests: document G28.2/G28.3 Pn, add sequencing and flush-order t…
greatEndian Jul 2, 2026
2e64c5e
fix(g28): don't restore a coordinated mode a partially-homed machine …
greatEndian Aug 18, 2026
4997c0d
fix(g28): homing_active completion test, scope sequencing to the queu…
greatEndian Aug 18, 2026
d6d7241
g28: split GCODE_HOMING plain-G28 out of this PR
greatEndian Aug 18, 2026
99aa401
fix(g28): range-check G28.2/G28.3 Pn against the configured joint count
greatEndian Aug 19, 2026
99803c4
docs(g28): correct the reason for reading kinematics_type from status
greatEndian Aug 19, 2026
f9edd58
g28: drop G28.3 unhome, keep G28.2 home only
greatEndian Aug 19, 2026
42bc5ca
fix(g28): initialize and serialize the homing_active status member
greatEndian Aug 19, 2026
4f582cf
fix(g28): stop offering a joint sentinel the P word cannot express
greatEndian Aug 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions docs/src/gcode/g-code.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ as the 'L number', and so on for any other letter.
|<<gcode:g17-g19.1,G17 - G19.1>> |Plane Select
|<<gcode:g20-g21,G20 G21>> |Set Units of Measure
|<<gcode:g28-g28.1,G28 - G28.1>> |Go to Predefined Position
|<<gcode:g28.2,G28.2>> |Home from G-code
|<<gcode:g30-g30.1,G30 - G30.1>> |Go to Predefined Position
|<<gcode:g33,G33>> |Spindle Synchronized Motion
|<<gcode:g33.1,G33.1>> |Rigid Tapping
Expand Down Expand Up @@ -997,6 +998,48 @@ It is an error if :

* Cutter Compensation is turned on

[[gcode:g28.2]]
== G28.2 Home from G-code(((G28.2 Home from G-code)))

This non-modal code lets a program or MDI line reference the machine
instead of requiring the operator to use the GUI's *Home All* button. It
follows the same modal-group-0 pattern as `G28.1`/`G30.1` and takes no axis
words.

* 'G28.2' - runs the homing cycle on all joints, in `HOME_SEQUENCE` order
(the same operation as the GUI *Home All*).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What should the bare form do on a config without HOME_SEQUENCE? do_home_joint(-1) clears H[0].homed (homing.c:577) while the sequence machine excludes joints with the unrealizable default sequence, so on a sequence-less config this errors with "did not complete for joint ALL" and leaves joint 0 unhomed and the machine in FREE (verified on sim). That is Home All parity, but 39 in-tree sims and all five test configs in this PR are that class, and only Pn is tested. Worth stating the HOME_SEQUENCE requirement here, and adding one bare-form test with sequences set?

* 'G28.2 Pn' - runs the homing cycle on joint 'n' only, where 'n' is the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Worth also stating the positive shared-HOME_SEQUENCE case here: Pn on a negative pair homes both joints, but on a positive shared sequence it homes only the named joint (the PR description covers this, the docs should too).

0-based joint number matching its `[JOINT_n]` INI section (the same
numbering used by `HOME_SEQUENCE` and by joint jogging). Other joints are
left as they are. On a synchronized (negative `HOME_SEQUENCE`) joint pair,
Pn on either joint homes both.

.G28.2 Example Lines
[source,ngc]
----
G28.2 (home all joints, in HOME_SEQUENCE order)
G28.2 P1 (home joint 1 only)
----

A queued `G28.2` dips motion into free mode for the duration of the homing
cycle and restores whatever mode (manual/MDI/auto) was active once it
finishes, so the mode dip is invisible at the task level. Motion still
enforces its own safety: the home is honored only when the machine is idle
(in position with no queued motion) or in joint mode, and a home is refused
mid-motion. Homing inhibits and per-joint limit handling are unchanged.

[NOTE]
There is no G-code unhome. Clearing a joint's reference is done from the
GUI, halui or linuxcncrsh.

[NOTE]
`G28.2` is a LinuxCNC extension; there is no standard Fanuc equivalent.

It is an error if :

* Cutter Compensation is turned on
* 'Pn' names a joint number that does not exist on the machine

[[gcode:g30-g30.1]]
== G30, G30.1 Go/Set Predefined Position(((G30 Go/Set Predefined Position)))

Expand Down
9 changes: 6 additions & 3 deletions src/emc/motion/command.c
Original file line number Diff line number Diff line change
Expand Up @@ -1494,9 +1494,12 @@ void emcmotCommandHandler_locked(void *arg, long servo_period)
rtapi_print_msg(RTAPI_MSG_DBG, "JOINT_HOME");
rtapi_print_msg(RTAPI_MSG_DBG, " %d", joint_num);

if (emcmotStatus->motion_state != EMCMOT_MOTION_FREE) {
/* can't home unless in free mode */
reportError(_("must be in joint mode to home"));
/* Normally homing requires free (joint) mode. Allow it also when
* motion is otherwise IDLE (in position, nothing queued) so a
* G-code-triggered home (G28.2) works from MDI / a program. */
if (emcmotStatus->motion_state != EMCMOT_MOTION_FREE
&& !(GET_MOTION_INPOS_FLAG() && emcmotStatus->depth == 0)) {
reportError(_("must be in joint mode (or idle) to home"));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This now accepts an immediate home whenever motion is idle, but do_homing() only advances in FREE, and manual mode on an all-homed machine is TELEOP (emcTaskSetMode(MANUAL) picks TELEOP when all_homed()). A/B on sim: an immediate home (halui, linuxcncrsh, c.home(n)) on a homed machine in manual mode prints must be in joint mode to home before this PR, and is silently accepted and never runs after it. Should the idle case also require a FREE transition to be pending (!emcmotInternal->teleoperating && !emcmotInternal->coordinating), so the relaxation covers only the queued dip it was added for?

return;
}
if (hal_get_bool(emcmot_hal_data->homing_inhibit)) {
Expand Down
1 change: 1 addition & 0 deletions src/emc/motion/control.c
Original file line number Diff line number Diff line change
Expand Up @@ -2196,6 +2196,7 @@ static void update_status(void)
}

emcmotStatus->jogging_active = hal_get_bool(emcmot_hal_data->jog_is_active);
emcmotStatus->homing_active = get_homing_is_active();

/*! \todo FIXME - the rest of this function is stuff that was apparently
dropped in the initial move from emcmot.c to control.c. I
Expand Down
4 changes: 4 additions & 0 deletions src/emc/motion/motion.h
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,10 @@ Suggestion: Split this in to an Error and a Status flag register..
int numExtraJoints;
int stepping;
bool jogging_active;
bool homing_active; /* homing state machine is running (get_homing_is_active()).
Aggregate: stays true across the gap between
HOME_SEQUENCE groups, when every joint's per-joint
.homing flag is momentarily false. */
} emcmot_status_t;

/*********************************
Expand Down
7 changes: 7 additions & 0 deletions src/emc/nml_intf/canon.hh
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,13 @@ extern void SET_G92_OFFSET(double x, double y, double z,

extern void SET_XY_ROTATION(double t);

/* G28.2: trigger the machine homing cycle from G-code (bare form = all
* joints, in HOME_SEQUENCE order). Maps to EMC_JOINT_HOME(-1). */
extern void HOME_CYCLE(void);
/* G28.2 Pn: home a single joint by its 0-based joint number (matching
* [JOINT_n] INI section numbering). Maps to EMC_JOINT_HOME(joint). */
extern void HOME_CYCLE_JOINT(int joint);

/* Offset the origin to the point with absolute coordinates x, y, z,
a, b, c, u, v, and w. Values of x, y, z, a, b, c, u, v, and w are real
numbers. The units are whatever length units are being used at the time
Expand Down
1 change: 1 addition & 0 deletions src/emc/nml_intf/emc.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1850,6 +1850,7 @@ void EMC_MOTION_STAT::update(CMS * cms)
EmcPose_update(cms, &eoffset_pose);
cms->update(numExtraJoints);
cms->update(jogging_active);
cms->update(homing_active);
cms->update(heartbeat);
}

Expand Down
3 changes: 2 additions & 1 deletion src/emc/nml_intf/emc.hh
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,8 @@ enum class EMC_TASK_EXEC {
WAITING_FOR_MOTION_AND_IO = 7,
WAITING_FOR_DELAY = 8,
WAITING_FOR_SYSTEM_CMD = 9,
WAITING_FOR_SPINDLE_ORIENTED = 10
WAITING_FOR_SPINDLE_ORIENTED = 10,
WAITING_FOR_HOMING = 11
};

// types for EMC_TASK interpState
Expand Down
6 changes: 6 additions & 0 deletions src/emc/nml_intf/emc_nml.hh
Original file line number Diff line number Diff line change
Expand Up @@ -1166,6 +1166,12 @@ class EMC_MOTION_STAT:public EMC_MOTION_STAT_MSG {
EmcPose eoffset_pose;
int numExtraJoints;
bool jogging_active;
// Aggregate "the homing state machine is running", from
// get_homing_is_active() in motion. Unlike the per-joint EMC_JOINT_STAT
// .homing flags, this stays true across the gap between HOME_SEQUENCE
// groups, where every joint momentarily reads .homing == false while the
// machine is still homing (see the race note in motion/homing.c).
bool homing_active;
uint64_t heartbeat; // motion controller's heartbeat counter
};

Expand Down
1 change: 1 addition & 0 deletions src/emc/nml_intf/emcops.cc
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ EMC_MOTION_STAT::EMC_MOTION_STAT()
eoffset_pose{},
numExtraJoints(0),
jogging_active(0),
homing_active(false),
heartbeat(0)
{
}
Expand Down
3 changes: 3 additions & 0 deletions src/emc/rs274ngc/gcodemodule.cc
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,9 @@ void SELECT_PLANE(CANON_PLANE pl) {
Py_XDECREF(result);
}

void HOME_CYCLE(void) {}
void HOME_CYCLE_JOINT(int) {}

void SET_TRAVERSE_RATE(double rate) {
maybe_new_line();
if(interp_error) return;
Expand Down
2 changes: 1 addition & 1 deletion src/emc/rs274ngc/interp_array.cc
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ const int Interp::gees[] = {
/* 220 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
/* 240 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
/* 260 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
/* 280 */ 0, 0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
/* 280 */ 0, 0, 0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, // 282=G28.2
/* 300 */ 0, 0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
/* 320 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1, 1, 1,-1,-1,-1,-1,-1,-1,-1,-1,
/* 340 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
Expand Down
3 changes: 3 additions & 0 deletions src/emc/rs274ngc/interp_check.cc
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ int Interp::check_g_codes(block_pointer block, //!< pointer to a block to be c
} else if (mode1 == G_5_2){
} else if (mode1 == G_6_2){
} else if (mode0 == G_28_1 || mode0 == G_30_1) {
} else if (mode0 == G_28_2) { // G-code homing
} else if (mode0 == G_52) {
} else if (mode0 == G_53) {
CHKS(((block->motion_to_be != G_0) && (block->motion_to_be != G_1)),
Expand Down Expand Up @@ -326,12 +327,14 @@ int Interp::check_other_codes(block_pointer block) //!< pointer to a block
(motion != G_6) && (motion != G_6_2) &&
(motion != G_2) && (motion != G_3) &&
(motion != G_74) && (motion != G_84) &&
(block->g_modes[GM_MODAL_0] != G_28_2) &&
(block->m_modes[9] != 50) && (block->m_modes[9] != 51) && (block->m_modes[9] != 52) &&
(block->m_modes[9] != 53) && (block->m_modes[5] != 62) && (block->m_modes[5] != 63) &&
(block->m_modes[5] != 64) && (block->m_modes[5] != 65) && (block->m_modes[5] != 66) &&
(block->m_modes[7] != 19) && (block->user_m != 1) &&
(block->o_type != M_98)),
_("P word with no G2 G3 G4 G10 G64 G5 G5.2 G6, G6.2, G76 G82 G86 G88 G89"
" G28.2"
" or M50 M51 M52 M53 M62 M63 M64 M65 M66 M98 "
"or user M code to use it"));
int p_value = round_to_int(block->p_number);
Expand Down
59 changes: 59 additions & 0 deletions src/emc/rs274ngc/interp_convert.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3168,6 +3168,63 @@ Called by: convert_modal_0.

*/

/*! convert_home_cycle

Handles G28.2 (run the homing cycle) from a G-code line, so machines can
reference themselves from MDI or a program instead of only from the GUI's
*Home All* button. The bare form homes all joints, in HOME_SEQUENCE order.

An optional Pn word homes a single joint by its 0-based joint number
(matching [JOINT_n] INI section numbering, e.g. P1 -> JOINT_1). This is the
primitive Sigma1912 asked for in the PR #4172 discussion for re-homing a
joint that is switched between rotary-axis and spindle use mid-program
(https://github.com/LinuxCNC/linuxcnc/pull/4172) -- it reuses the existing
EMC_JOINT_HOME 'joint' field, so it needs no NML change and works
identically on any kinematics (per grandixximo's review comment on that PR).
Axis-letter forms (G28.2 X) are deliberately NOT supported: resolving an
axis letter to a joint needs the kinematics coordinate map and isn't
trivial even on trivkins (duplicate letters on gantries) -- andypugh's
review also objected that homing is a joint concept, not an axis one.

There is deliberately no G-code unhome. A G28.3 was part of the original
proposal and was dropped during review of PR #4172: neither reviewer could
name a use for it that a numbered parameter would not serve better, and it
was the one operation able to leave a running program on an unreferenced
machine -- the state behind the real-hardware failure Sigma1912 reported.
The GUI, halui and linuxcncrsh keep their existing unhome.

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

Motion still enforces its own safety (idle / not on limits). The joint
number is range-checked against the machine's configured joint count in
task (emcJointHome(), taskintf.cc), which is where that count is known --
the interpreter has no joint count in its state.
*/
int Interp::convert_home_cycle(block_pointer block,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The interpreter keeps its pre-home current point across the home. Verified: G0 X2 / G28.2 P0 (HOME=0) / G91 G0 X1 lands at X3, not X1, and IJ arc centers have the same problem. Absolute moves and the re-home-in-place use case are unaffected. Acceptable if documented, or should completion resync the position model?

setup_pointer settings)
{
CHKS((settings->cutter_comp_side != CUTTER_COMP::OFF),
"Cannot home (G28.2) with cutter radius compensation on");

int joint = -1;
if (block->p_flag) {
CHKS(((block->p_number < 0.0) ||
(block->p_number != round_to_int(block->p_number))),
"P value for G28.2 must be a non-negative whole joint number"
" (omit P to home every joint)");
joint = round_to_int(block->p_number);
}

if (joint < 0) {
HOME_CYCLE();
} else {
HOME_CYCLE_JOINT(joint);
}
return INTERP_OK;
}

int Interp::convert_home(int move, //!< G-code, must be G_28 or G_30
block_pointer block, //!< pointer to a block of RS274 instructions
setup_pointer settings) //!< pointer to machine settings
Expand Down Expand Up @@ -4347,6 +4404,8 @@ int Interp::convert_modal_0(int code, //!< G-code, must be from group 0
CHP(convert_home(code, block, settings));
} else if ((code == G_28_1) || (code == G_30_1)) {
CHP(convert_savehome(code, block, settings));
} else if (code == G_28_2) {
CHP(convert_home_cycle(block, settings));
} else if ((code == G_52) || (code == G_92)) {
CHP(convert_axis_offsets(code, block, settings));
} else if ((code == G_5_3)||(code == G_6_3)) { // jjf
Expand Down
1 change: 1 addition & 0 deletions src/emc/rs274ngc/interp_internal.hh
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ enum GCodes
G_21 = 210,
G_28 = 280,
G_28_1 = 281,
G_28_2 = 282, /* G-code homing cycle (home one/all joints) */
G_30 = 300,
G_30_1 = 301,
G_33 = 330,
Expand Down
2 changes: 2 additions & 0 deletions src/emc/rs274ngc/rs274ngc_interp.hh
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,8 @@ public:
setup_pointer settings);
int convert_savehome(int move, block_pointer block,
setup_pointer settings);
int convert_home_cycle(block_pointer block, // G28.2
setup_pointer settings);
int convert_length_units(int g_code, setup_pointer settings);
int convert_m(block_pointer block, setup_pointer settings);
int convert_modal_0(int code, block_pointer block,
Expand Down
3 changes: 3 additions & 0 deletions src/emc/sai/saicanon.cc
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ void SET_XY_ROTATION(double t) {
ECHO_WITH_ARGS("%.4f", t);
}

void HOME_CYCLE(void) { ECHO_WITH_ARGS(""); }
void HOME_CYCLE_JOINT(int joint) { ECHO_WITH_ARGS("%d", joint); }

void SET_G5X_OFFSET(int index,
double x, double y, double z,
double a, double b, double c,
Expand Down
25 changes: 25 additions & 0 deletions src/emc/task/emccanon.cc
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,31 @@ void SET_XY_ROTATION(double t) {
canon.xy_rotation = t;
}


void HOME_CYCLE(void)
{
// STRAIGHT_FEED/STRAIGHT_TRAVERSE buffer points into chained_points for
// arc-blend lookahead and only append to interp_list on flush (see
// see_segment()/flush_segments()). Without flushing here first, any
// motion queued just before this G28.2 would get silently reordered to
// execute AFTER the home instead of before it.
flush_segments();
auto msg = std::make_unique<EMC_JOINT_HOME>();
msg->joint = -1; // -1 = all joints (HOME_SEQUENCE order)
interp_list.append(std::move(msg));
}

/* G28.2 Pn -- home a single joint. joint is the interp's already-validated
* (non-negative) P value; task range-checks it against the machine's
* configured joint count in emcJointHome() (taskintf.cc). */
void HOME_CYCLE_JOINT(int joint)
{
flush_segments(); // see HOME_CYCLE
auto msg = std::make_unique<EMC_JOINT_HOME>();
msg->joint = joint;
interp_list.append(std::move(msg));
}

void SET_G5X_OFFSET(int index,
double x, double y, double z,
double a, double b, double c,
Expand Down
Loading