M-Code Declarations
M-Code Declarations is the leaf that gives a machine's own M-codes a meaning the parser can act on —
what each machine-specific (OEM or PLC) code does — and it carries beside them the tool-change
trigger mode that decides whether a bare T word changes the tool by itself. It lives on the
General Setup page at /general-setup under the Control-Tree id
equipment/controller/machine/m-codes, grown while the runner snapshot reports a brand controller
parameter table. Both the declaration map and the trigger flag are stored on that one table, so this
leaf appears and disappears together with
Parameters (Native), and the rows declared on it travel
with the project rather than with the runner file, which carries only the preset seed a fresh
project's table is cloned from.
Important
Anatomy makes no claim that any id named here is stable across versions. The migration hop that
keeps an older ?tree= value resolving is described in
Tree Ids and Routes.
The One Object Behind Both Halves
ControllerParameterTableBase implements IMCodeDeclarationConfig and IToolChangeTriggerConfig over its own storage, so the table every brand carries supplies the whole leaf: the declaration map keyed on the code as parsed, and the single trigger boolean. A third face, ISpindleControlConfig, reads the same map for spindle direction alone rather than keeping a second one, which is why one code is never half-recognized by two separate dictionaries.
The tree gate and the panel's own second empty layer probe the same type. The branch builder grows
the node while the snapshot's hasNativeTable flag is set, and the read answers a presence flag
computed from the same resolved parameter table; every shipped brand preset carries one, so in
practice the leaf is unconditional and the "No controller parameter table on the active runner."
line is reachable only on a runner assembled by hand or loaded from XML. Which brand satisfies which
gate is Brand Matrix.
Because the map is a member of the brand parameter table, and that table is the project's per-case copy rather than the runner's, declarations survive installing another runner file of the same brand — see Machine and Controller Plane for which half of a project each value on this plane is written into.
What a Declaration Carries
One row is one MCodeEffects, and a real OEM code is frequently composite, so the row records every effect the code performs rather than a single meaning.
| Column | Effect | What the code expands to |
|---|---|---|
| Tool change | IsToolChange | M06 |
| Spindle | SpindleDirection | M03 for CW, M04 for CCW, M05 for STOP |
| Coolant | CoolantMode | M07 for Mist, M08 for Flood, M09 for Off |
| Not-simulated note | UnmodeledNote | nothing — it raises one informational diagnostic per occurrence |
The two pickers each open on a dash, which is the null option: a declaration that touches neither
the spindle nor the coolant carries neither. The note is free text and is itself an effect — a code
whose real behaviour is a chip conveyor or a door interlock is declared with the note alone, and
every occurrence then reports DeclaredMCode--UnmodeledEffects at message severity instead of the
unknown-code warning. A declaration with no effect and no note is the fourth legal state and means
consume this code silently, which the panel's hint says in as many words: "A declaration with
nothing set consumes its code silently."
That fourth state is what a freshly declared row is. Declare writes a row whose tool-change box is clear, whose two pickers sit on the dash and whose note is empty, so the act of naming a code already silences it; the fields are then filled in against a live row.
How a Declared Code Is Read
MCodeExpansionSyntax rewrites the declared flag into the canonical ISO flags the ordinary consumers already understand, and it runs ahead of them: SpindleSpeedSyntax, CoolantSyntax and ToolChangeSyntax each then see the flag they know. Expanding once, early, is what lets a single composite code feed several consumers without any of them fighting over which removes the original flag.
Three rules of the rewrite are visible in a program:
- The expansion sits at the declared flag's own position, and a repeat of the same declared code
later in the block is dropped. Textual order therefore keeps deciding last-wins conflicts exactly
as it did before any declaration existed — a composite code followed by a raw
M05still ends with the spindle stopped. - A canonical flag that also appears raw and undeclared in the same block is not emitted twice. The raw occurrence keeps its position, so the expansion cannot reorder what the block already said.
- Undeclared codes are untouched. One that no other syntax consumes then reaches
UnconsumedCheckSyntax, which reports it as
Parsing--Unconsumed— the warning this leaf exists to answer.
The spindle-only row takes a different path
A declaration whose sole content is a spindle direction is deliberately not expanded.
IsSpindleDirectionOnly marks it, the expansion skips
it, and SpindleSpeedSyntax resolves it in place through
TryResolveDirection(API).
What separates the two paths is mechanism, not reach. The resolver is consulted before the
built-in ISO mapping, so a spindle-only row overrides a canonical code where the code stands, with
no rewrite at all — declaring M03 as CCW makes M03 turn the spindle counter-clockwise. Add any
second effect to the same row and it leaves that path for the expansion, which arrives at the same
place by rewriting: the declared flag is replaced by the ISO flags its effects name, so declaring
M08 as Mist makes the block emit M07 instead. On either path a declaration over a canonical code wins against that
code's built-in meaning.
The split also reaches the project file. A spindle-only row serializes as the legacy
<SpindleMCode> element so an older reader still resolves it; every other row serializes as
<MCode> with one attribute per effect that is set. A row with nothing set writes an <MCode>
element carrying only its code, so the silent-consume state round-trips.
What the writer refuses
The declare endpoint validates before it stores. An empty code is answered unsuccessfully; so is a
spindle direction that is not CW / CCW / STOP and a coolant mode that is not Mist / Flood /
Off, and those two answers quote the rejected value back alongside the set that was expected. A
typo therefore cannot degrade into a declaration that consumes a code silently. The panel's two
pickers cannot produce such a value; the guard is there for the other writers of the same table.
A coolant mode that names no known mode can still arrive from a project file, because the XML reader
keeps the attribute raw rather than normalizing it and write-then-read stays an identity. The
expansion is where that surfaces: it reports DeclaredMCode--UnknownCoolantMode, skips the coolant
half, and emits the rest of the declaration.
The Tool-Change Trigger Mode
The toggle above the table reads "T word performs the tool change itself (turret / lathe)" and
writes ToolWordTriggersChange. It defaults
off, which is machining-centre behaviour: a T word only pre-selects, the magazine rotates without
moving a feed axis, and the trigger M-code performs the change.
Turned on, ToolChangeSyntax treats a block carrying a T word and
no tool-change flag as the change itself and records T as the triggering term. The test is per
block: a block carrying both a T word and a tool-change flag is unaffected — the flag wins and
M06 is recorded — so no single block is counted twice. Where the two words sit in different blocks
— a T pre-select first, an M06 later — the mode counts the T block as a change on its own, and
the M06 block then records a second one, its tool number arriving modally. The toggle therefore
belongs on a machine whose T word really performs the change; on a machining centre it turns every
pre-select into a change of its own.
On the Siemens machine-data table the flag is not a field of its own. That table binds the
property to its tool-change-mode row,
MdToolChangeMode: present, the row
decides — a stored zero means the T word changes the tool — and the toggle writes back into it as
0 or 1 rather than into the brand-neutral field; absent, the table behaves like every other brand.
The write is skipped when the effective value already matches, so copying a table never rewrites a
raw machine-data value of 2 down to 1. The same row is visible and editable by number on
Parameters (Native), and the two forms are one storage.
The Rows
The read returns the declarations sorted by code as text, case-insensitively, not by numeric
value. M106 therefore sorts ahead of M12, and M6 lands after M331.
Keys are matched case-insensitively too, so m13 and M13 are one declaration and cannot both
exist; re-declaring a code in a different spelling replaces its effects and leaves the spelling
already stored on display. The Code cell is plain bold text with no editor — a code is changed
by declaring the new one and deleting the old.
Only one brand's default parameter table pre-declares anything. The Siemens machine-data default
seeds six auxiliary codes — M12, M13, M22, M23, M330 and M331 — each carrying no effect
and the same note, “machine-specific auxiliary function (OEM/PLC); the exact behavior depends on
the machine”. Those recur often enough in real Siemens programs to be worth silencing with an
explanation rather than a warning, and a machine table that knows their actual effects overrides the
seeded note. On the other four brands the table opens empty.
Editing, Adding and Removing
Every cell commits on its own, and the write is the whole declaration: the handler merges the edited field into the row and sends all four values, so an edit rewrites the row from what the panel is currently showing. The checkbox and the two pickers commit on the click or the pick; the note cell commits through its field's native change event, which is this branch's third text-commit wiring and is recorded as such in Editing Contract — that page also carries the optimistic-write, rollback and toast rules these cells share with the rest of the branch.
The footer holds a single field labelled M-code, hinted M106, and a Declare button. Enter
in that field submits, which no other add-row footer in the branch does. A blank code raises the
toast "M-code must not be empty." locally and never reaches the server. A successful declare clears
the field and re-reads the whole table, so the new row appears in sort position rather than at the
end.
Removal is the trash button at the end of each row. It carries no confirmation dialog, and the row
leaves the list only after the server has answered — the delete is not optimistic. The removed code
returns to what it was before the declaration: Parsing--Unconsumed wherever a program uses it,
unless a shared or brand syntax already knew the code, in which case it goes back to that built-in
meaning.
What This Table Does Not Show
The stored map is not the whole of what a machine honours, and three families of M-code behaviour live outside it.
The canonical ISO codes. M06, M03 / M04 / M05 and M07 / M08 / M09 are consumed by
the shared syntaxes with no declaration at all; this table exists for the codes those consumers do
not already know. Declaring a canonical code is still legal and still honoured — a note-only
declaration on M08, for instance, consumes the raw M08 for its own declaration while a composite
code's flood half continues to emit M08.
A brand's own M-function dialect. The Heidenhain preset's
HeidenhainMFunctionSyntax owns M126 / M127
(shortest-path rotary traverse) and M140 (tool-axis retract) as brand meanings, not as
declarations, so none of them is a row here. That preset's syntax list places the expansion ahead
of the brand syntax precisely so a machine that redefines one of those codes can say so: a
declaration for M126 is expanded and consumed before the brand meaning is reached, and the
declaration wins.
The Siemens tool-change M function. The Siemens machine-data table's
MdToolChangeMCode row names the
M function that performs a tool change, and the table overlays a tool-change declaration for it onto
the view the parsers resolve — merged into a clone of any explicit declaration on the same code, so a
note or coolant half is kept, and never written into the stored map. The panel reads the stored
map, so that overlaid row is not on screen. A Siemens machine whose machine data names a
non-standard trigger honours it in every program while this table lists nothing for it; the value
lives on Parameters (Native) as the machine-data row, in
the raw form an operator reads off the real controller. The overlay's own rules matter when the two
views are compared: the raw number is zero-padded to the parsed flag form, so 6 becomes M06 and
106 becomes M106; a value that is not a positive whole number overlays nothing; and a code already
declared as a tool change here is left exactly as stored.
The trigger toggle above the table does not share that blind spot. It is read through the property the Siemens table overrides, so it shows the machine data's answer, while the rows beside it show only what was declared. The asymmetry is the single most useful thing to know about this leaf: the mode is the effective value, the table is the stored one.
One further consequence of the overlay is invisible in both views. Spindle-only resolution also runs against the overlaid view, so a code that the machine data names as the tool-change trigger and that is also declared here with a spindle direction alone stops being spindle-only: it leaves the in-place resolver and is expanded instead, emitting both the tool change and the direction.
Layout
- General Setup Control Tree — the left dock of
/general-setup- Controller Node Row
- Machine / Controller Group Row
- M-Code Declarations Node Row —
equipment/controller/machine/m-codes
- M-Code Declarations Node Row —
- Machine / Controller Group Row
- Controller Node Row
- Editor Row — M-Code Declarations Panel
- No-Runner Line — "No NC runner — load a project first.", the branch's shared empty state, shown while the snapshot reports no runner
- No-Table Line — "No controller parameter table on the active runner.", shown when the read itself reports no table
- T word performs the tool change itself (turret / lathe) Toggle
- Hint Caption — "Declares machine-specific M-codes: a tool-change trigger other than
M6, composite OEM codes (e.g.M13= spindle CW + flood coolant), or known-but-unsimulated codes (note only, one info message per occurrence). A declaration with nothing set consumes its code silently. Undeclared codes keep the unconsumed warning." - Declaration Table — dense, flat and bordered, one row per declared code
- Header Row — Code, Tool change, Spindle, Coolant, Not-simulated note, and an unlabelled action column
- Code Cell — bold plain text, not editable
- Tool change CheckBox
- Spindle Select — a dash, CW (M03), CCW (M04), STOP (M05)
- Coolant Select — a dash, Mist (M07), Flood (M08), Off (M09)
- Not-simulated note Field — placeholder e.g. chip conveyor forward
- Delete Button — a trash icon, no confirmation
- Add Row Footer
- M-code Field — placeholder
M106; Enter submits - Declare Button
- M-code Field — placeholder
The panel mounts against one node and takes nothing from it: unlike the per-axis leaves it serves a single tree id, so it reads no role off the node's path.
Source Code Path
See HiNC App Anatomy for git repository links.
Web Application
HiNC-2025-webservice (Quasar CLI SPA):
wwwroot-src/src/components/controlTree/SoftNcMCodesPanel.vue— this panel: the two empty layers, the trigger toggle, the two option lists with their dash entry, the whole-declaration write behind every cell, the change-event note cell, the Enter-submitting add footer and the confirmation-free delete.wwwroot-src/src/components/controlTree/SoftNcEmptyState.vue— the shared no-runner line the panel opens with.wwwroot-src/src/components/controlTree/softNcItemTypes.ts— the node id, its label and label key, the snapshot flag it is grown behind, and the item type that binds it to the panel.wwwroot-src/src/api/softNcRunner.ts— the declaration row and snapshot shapes, the reader that coerces an empty string to a null field, the whole-declaration writer, the delete, and the trigger setter.wwwroot-src/src/composables/useSoftNcRunner.ts— the shared runner snapshot the first empty layer reads.wwwroot-src/src/i18n/en/softNc.ts— the node label, the toggle label, the hint, the column headers, the two option label sets, the note placeholder, the empty-code toast and the two empty strings quoted above.Mech/SoftNcRunnerController.cs— the REST surface: the read with its presence flag and its case-insensitive text ordering, the declare endpoint's direction and coolant validation, the delete, the trigger write, and the shared helper that answers a missing dependency with an unsuccessful body rather than an error status.
HiAPI Engine
HiMech/NcParsers/Dependencys/IMCodeDeclarationConfig.cs— the declaration contract: the map, the lookup that returns false for an undeclared code, and the declare and remove mutators whose stored effects are copied rather than aliased.HiMech/NcParsers/Dependencys/MCodeEffects.cs— the four effects, the empty and spindle-only predicates that partition the two consumption paths, and the coolant-mode normalizer.HiMech/NcParsers/Dependencys/IToolChangeTriggerConfig.cs— the trigger contract and the magazine-versus-turret distinction it encodes.HiMech/NcParsers/Dependencys/ISpindleControlConfig.cs— the narrower spindle face over the same storage, and the resolver the spindle-only rows take.HiMech/NcParsers/Dependencys/ControllerParameterTableBase.cs— the storage every brand inherits: the case-insensitive map, the overridable effective view, the virtual trigger property, and the XML round trip that writes a spindle-only row as the legacy element.HiMech/NcParsers/Dependencys/Siemens/SiemensMachineDataTable.cs— the two machine-data rows that change this leaf's meaning: the tool-change mode bound to the trigger property, the tool-change M function overlaid onto the effective view with its zero-padding rule, and the six auxiliary codes the default table pre-declares.HiMech/NcParsers/LogicSyntaxs/MCodeExpansionSyntax.cs— the rewrite: in-place expansion, the duplicate and raw-twin guards, the spindle-only skip, and the two diagnostics a declaration can raise.HiMech/NcParsers/LogicSyntaxs/ToolChangeSyntax.cs— the consumer of the expanded tool change and of the trigger mode, and the term it records for each.HiMech/NcParsers/LogicSyntaxs/SpindleSpeedSyntax.cs— the consumer that tries the machine map before the ISO defaults, which is what lets a spindle-only row remap a canonical code.HiMech/NcParsers/LogicSyntaxs/CoolantSyntax.cs— the consumer of the three expanded coolant flags.HiMech/NcParsers/LogicSyntaxs/Heidenhain/HeidenhainMFunctionSyntax.cs— the brand meanings a declaration can pre-empt on that preset.HiMech/NcParsers/InspectionSyntaxs/UnconsumedCheckSyntax.cs— the warning an undeclared code keeps, and the one a declaration removes.HiMech/NcParsers/Syntaxs/FanucSyntaxUtil.cs,HiMech/NcParsers/Syntaxs/SiemensSyntaxUtil.cs,HiMech/NcParsers/Syntaxs/SyntecSyntaxUtil.cs,HiMech/NcParsers/Syntaxs/MazakSyntaxUtil.cs,HiMech/NcParsers/Syntaxs/HeidenhainSyntaxUtil.cs— where the expansion sits in each brand's syntax list, and why the Heidenhain list anchors it earlier than the rest.HiMech/NcParsers/Keywords/Generic/IsoKeywords.cs— the canonical flag spellings the expansion emits,M06among them.HiMech/NcParsers/Keywords/ICoolantDef.cs— the three coolant constants the picker's values map to.HiGeom/Numerical/SpindleDirection.cs— the direction enum behind the spindle column.
See Also
- Machine and Controller Plane — the plane this leaf sits on, and which half of a project the declaration map is written into
- Brand Matrix — the flag that grows this node, and why it is never false on a shipped brand preset
- Editing Contract — the fetch, commit, rollback and toast rules this panel shares with the branch, and the two places it is recorded as departing from them