Table of Contents

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 M05 still 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
  • 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

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, M06 among 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