71
MachPro General Discussion / Re: MachPro upgrade questions
« Last post by paul.stephens on September 17, 2026, 11:42:34 AM »Hi,
That is correct! We Ideally want to keep any custom User logic abstracted from the main framework. It is the same way for MCodes as well. We have a UserMCodeModule that is used for MCodes. The process is slightly different for them, but the results are the same.
I'll run through the basics of the MCode as well..
How to make a new MCode:
What is an MCode?
An MCode is a command in a part program (G-code file) that makes the machine do something other than move; turn a spindle on, open a clamp, run a probe routine, and so on. MCodes look like M3 (spindle clockwise) or M30 (end of program).
Mach comes with many MCodes already built in. You can also create your own, for example, M500 could turn on a vacuum pump, open a door, or run any sequence your machine needs. This guide walks through creating M500 from scratch. The same steps work for any new MCode, just pick a number that isn't already used (numbers 500 and above are the safest choices for custom codes).
The Big Picture (two files, two jobs)
A custom MCode needs two files. Keeping them straight is the whole trick:
Think of it this way: the machine can't call you unless you have a doorbell (m500.mcs). The doorbell's only job is to ring you. What you do when you answer is written in the brain (UserMCodeModule.mcs).
Adding a Custom MCode:
Step 1: Create the doorbell: m500.mcs
Open the folder Mach\Profiles\<your profile>\Macros\.
Create a new plain-text file named exactly m500.mcs (all lowercase; not M500.txt, not m500.mcs.txt - Windows sometimes hides the extension, so double-check).
Paste this in, exactly as shown, and save:
If someday you add M501, M502, etc., each gets its own copy of this file with only the number changed (m501.mcs containing function m501(...) / m.InvokeMacro('m501', ...)).
Step 2: Create the brain; UserMCodeModule.mcs
Open the folder Mach\Profiles\<your profile>\Macros\Scripts\.
Check whether UserMCodeModule.mcs already exists:
If it exists: open it in a text editor (or the Mach script editor) and skip to step 4 - you'll just add to it.
If it does not exist: create it and paste in the starter template below.
Starter template for a brand-new UserMCodeModule.mcs:
What the parts mean:
function UserMCodeModule.m500(hVars); the name must match the MCode: m500 for M500, m501 for M501, and so on. This is how the doorbell finds the right instructions.
The middle is where you put your actual machine logic: turn outputs on/off, wait for inputs, jog an axis, write to registers, show a message, anything the Mach scripting API can do.
return true, true, w.FunctionCompleted(); the standard "I finished successfully" sign-off. Leave it as the last line.
If something can go wrong, report it like this instead of the success line:
Save the file.
Step 3: Try it
Restart Mach (or reload the screen script) so it picks up the new files.
Go to the MDI line, type M500, and press Enter.
Your instructions run. In the example above, output #4 turns on.
If nothing happens
Check these in order:
Optional extras (when you're comfortable)
Safety lockout. If M500 must not run unless a guard is closed, check that input first and return nil, false, "message" to abort with an alarm before doing anything else.
Where the GUI module fits. You may also have a UserGUIModule.mcs file in the same Scripts folder. That file is for screen behavior (buttons, the PLC script, LEDs) - not MCodes. MCode logic always belongs in UserMCodeModule.mcs. If your M500 needs something visible on screen (like an indicator), the screen part goes in UserGUIModule and the machine action stays in UserMCodeModule.
How to modify an existing MCode
First: The Golden Rule
Never edit the stock macro files. Every built-in MCode (M3, M5, M6, M8, M30, …) already has a file in Mach\Profiles\<your profile>\Macros\ - like m3.mcs - plus built-in logic inside Mach's modules. Do not edit those files, and do not edit Mach's modules. Why:
Updates will overwrite your changes. When MachPro software is updated, stock files get replaced and your edits silently vanish.
You don't need to. Mach has a built-in extension system - called hooks - designed exactly for this. Your changes go in your file: UserMCodeModule.mcs in Mach\Profiles\<your profile>\Macros\Scripts\. That file is yours; updates never touch it.
The Big Picture: Hooks
Every built-in MCode runs its job in stages, and at specific points it pauses and asks: "Does the user have anything to say about this?" Those asking-points are hooks. For example, when M3 (spindle forward) runs, it asks at these moments, in this order:
If you wrote a function named UserMCodeModule.m3 in your UserMCodeModule.mcs file, it gets called at that first hook point. If you wrote UserMCodeModule.m3End, it gets called at the second. If you wrote neither, Mach shrugs and carries on; hooks are purely optional.
Most MCodes follow this pattern: a hook named after the code (m3, m8, m6…) that runs before the built-in job, and an …End hook (m3End, m8End, m6End…) that runs after it.
Your Two Choices:
When a hook is called, your function answers one question: should the built-in job still run? Your answer is the first value you return:
Recipe 1: Add something before the stock job (most common)
Example: you want M3 to also turn on a "spindle running" lamp (output #5).
Open Mach\Profiles\<your profile>\Macros\Scripts\UserMCodeModule.mcs.
If it doesn't exist, create it with this starter:
Add a function named after the MCode; this runs before the spindle starts:
Save and select "Compile Scripts". In rare cases, you will need to restart Mach. Now every M3 - from a program, MDI, or a screen button - lights the lamp first, then starts the spindle as usual.
Recipe 2: Add something after the stock job
Use the …End hook instead. Example: after M5 (spindle stop) finishes, turn the lamp off:
Recipe 3: Block the MCode when a condition isn't met
Example: don't allow M3 unless the guard door is closed (input #2).
Returning false as the second value aborts the whole MCode and shows your message to the operator. Nothing else runs; the spindle never starts.
Recipe 4: Replace the stock behavior entirely
Return false as the first value. Mach skips its built-in job and considers the MCode done.
Example: your machine has no physical spindle relay; M3 should only set a register for your VFD over Modbus, and never run the stock spindle logic:
Which hook names exist?
m<code> runs before the built-in job: m3, m4, m5, m6, m8, m9, m30, …
m<code>End runs after the built-in job: m3End, m5End, …
Some complex operations have extra named hooks; tool change, for example, offers PreMainToolChangeCycle and PostMainToolChangeCycle.
If you're not sure which hooks a particular MCode supports, look at that MCode's Default file (read-only!) in Mach\Profiles\<your profile>\Macros\ and search Mach's modules for RunHooks("… every name found there is a hook you can use. You can also search in:
https://support.machmotion.com/docs/current/dir_f2541a3b18981391fa76fac5599e978a.html
Testing and Troubleshooting
Select "Compile Scripts" or restart Mach - hooks are loaded at startup.
Trigger the MCode from the MDI line.
If your hook doesn't seem to run, check:
Quick reference
Quote
Can I also assume that any functionality that I would normally include in the Mach4 ScreenLoad script I can include in UserGUIModule.mcs?
That is correct! We Ideally want to keep any custom User logic abstracted from the main framework. It is the same way for MCodes as well. We have a UserMCodeModule that is used for MCodes. The process is slightly different for them, but the results are the same.
I'll run through the basics of the MCode as well..
How to make a new MCode:
What is an MCode?
An MCode is a command in a part program (G-code file) that makes the machine do something other than move; turn a spindle on, open a clamp, run a probe routine, and so on. MCodes look like M3 (spindle clockwise) or M30 (end of program).
Mach comes with many MCodes already built in. You can also create your own, for example, M500 could turn on a vacuum pump, open a door, or run any sequence your machine needs. This guide walks through creating M500 from scratch. The same steps work for any new MCode, just pick a number that isn't already used (numbers 500 and above are the safest choices for custom codes).
The Big Picture (two files, two jobs)
A custom MCode needs two files. Keeping them straight is the whole trick:
| File | Folder Path | Job / Description |
| m500.mcs | Mach\Profiles\<your profile>\Macros\ | The doorbell. When the machine sees M500 in a program, it looks for and runs this file. Without this file, nothing happens, ever. |
| UserMCodeModule.mcs | Mach\Profiles\<your profile>\Macros\Scripts\ | The brain. This is where you write what M500 actually does. |
Think of it this way: the machine can't call you unless you have a doorbell (m500.mcs). The doorbell's only job is to ring you. What you do when you answer is written in the brain (UserMCodeModule.mcs).
Adding a Custom MCode:
Step 1: Create the doorbell: m500.mcs
Open the folder Mach\Profiles\<your profile>\Macros\.
Create a new plain-text file named exactly m500.mcs (all lowercase; not M500.txt, not m500.mcs.txt - Windows sometimes hides the extension, so double-check).
Paste this in, exactly as shown, and save:
Code: [Select]
function m500(hVars, GcodeComment)
return m.InvokeMacro('m500', hVars, GcodeComment)
end
if (mc.mcInEditor() == 1) then
m500()
endYou never need to edit this file again. It simply says: "When M500 runs, go find my instructions in UserMCodeModule."If someday you add M501, M502, etc., each gets its own copy of this file with only the number changed (m501.mcs containing function m501(...) / m.InvokeMacro('m501', ...)).
Step 2: Create the brain; UserMCodeModule.mcs
Open the folder Mach\Profiles\<your profile>\Macros\Scripts\.
Check whether UserMCodeModule.mcs already exists:
If it exists: open it in a text editor (or the Mach script editor) and skip to step 4 - you'll just add to it.
If it does not exist: create it and paste in the starter template below.
Starter template for a brand-new UserMCodeModule.mcs:
Code: [Select]
local UserMCodeModule = {}
-- Custom MCode functions go here (Step 4).
return UserMCodeModuleNow write what M500 should do. Add this function inside the file (between the two lines of the starter template):Code: [Select]
function UserMCodeModule.m500(hVars)
------------------ Your instructions go below this line ------------------
-- Example: turn OUTPUT #4 on.
local hReg, rc = mc.mcRegGetHandle(inst, "mc.OSIG_OUTPUT4")
if rc == mc.MERROR_NOERROR then
mc.mcRegSetValue(hReg, 1)
end
------------------ Don't edit below this line ------------------
return true, true, w.FunctionCompleted()
endWhat the parts mean:
function UserMCodeModule.m500(hVars); the name must match the MCode: m500 for M500, m501 for M501, and so on. This is how the doorbell finds the right instructions.
The middle is where you put your actual machine logic: turn outputs on/off, wait for inputs, jog an axis, write to registers, show a message, anything the Mach scripting API can do.
return true, true, w.FunctionCompleted(); the standard "I finished successfully" sign-off. Leave it as the last line.
If something can go wrong, report it like this instead of the success line:
Code: [Select]
return nil, false, "M500 failed: vacuum sensor never turned on"Returning false as the second value stops the program and shows your message to the operator as an alarm, which is exactly what you want when a safety check fails.Save the file.
Step 3: Try it
Restart Mach (or reload the screen script) so it picks up the new files.
Go to the MDI line, type M500, and press Enter.
Your instructions run. In the example above, output #4 turns on.
If nothing happens
Check these in order:
- m500.mcs exists in Profiles<your profile>\Macros\ - spelled exactly, lowercase, with the .mcs extension, not .txt. This is the most common mistake. Without the doorbell file, the brain never gets called.
- The function inside UserMCodeModule is named m500 exactly; lowercase m, matching the number.
- UserMCodeModule.mcs starts with local UserMCodeModule = {} and ends with return UserMCodeModule.
- You restarted Mach or compiled scripts after saving.
- You edited the files in the profile you are actually running, not a different profile's folder.
Optional extras (when you're comfortable)
Code: [Select]
Passing a value to your MCode. You can write M500 P1 in a program and read the P value inside your function:
function UserMCodeModule.m500(hVars)
local pValue, ok, errMsg = m.GetMCodeLocalVar(hVars, mc.SV_P)
if ok ~= true then
pValue = 0 -- P word wasn't given; use a default
end
-- ... use pValue to decide what to do ...
return true, true, w.FunctionCompleted()
endSafety lockout. If M500 must not run unless a guard is closed, check that input first and return nil, false, "message" to abort with an alarm before doing anything else.
Where the GUI module fits. You may also have a UserGUIModule.mcs file in the same Scripts folder. That file is for screen behavior (buttons, the PLC script, LEDs) - not MCodes. MCode logic always belongs in UserMCodeModule.mcs. If your M500 needs something visible on screen (like an indicator), the screen part goes in UserGUIModule and the machine action stays in UserMCodeModule.
How to modify an existing MCode
First: The Golden Rule
Never edit the stock macro files. Every built-in MCode (M3, M5, M6, M8, M30, …) already has a file in Mach\Profiles\<your profile>\Macros\ - like m3.mcs - plus built-in logic inside Mach's modules. Do not edit those files, and do not edit Mach's modules. Why:
Updates will overwrite your changes. When MachPro software is updated, stock files get replaced and your edits silently vanish.
You don't need to. Mach has a built-in extension system - called hooks - designed exactly for this. Your changes go in your file: UserMCodeModule.mcs in Mach\Profiles\<your profile>\Macros\Scripts\. That file is yours; updates never touch it.
The Big Picture: Hooks
Every built-in MCode runs its job in stages, and at specific points it pauses and asks: "Does the user have anything to say about this?" Those asking-points are hooks. For example, when M3 (spindle forward) runs, it asks at these moments, in this order:
Code: [Select]
M3 starts
→ hook "m3" (before anything happens)
→ the built-in job (start the spindle)
→ hook "m3End" (after the job finished)If you wrote a function named UserMCodeModule.m3 in your UserMCodeModule.mcs file, it gets called at that first hook point. If you wrote UserMCodeModule.m3End, it gets called at the second. If you wrote neither, Mach shrugs and carries on; hooks are purely optional.
Most MCodes follow this pattern: a hook named after the code (m3, m8, m6…) that runs before the built-in job, and an …End hook (m3End, m8End, m6End…) that runs after it.
Your Two Choices:
When a hook is called, your function answers one question: should the built-in job still run? Your answer is the first value you return:
| Your first return value | What happens | Use this to… |
| return true, true, ... | Built-in job runs normally | Add behavior before/after the stock job (log it, turn on an extra output, check a condition) |
| return false, true, ... | Built-in job is skipped | Replace the stock behavior entirely with your own |
| return nil, false, "message" | Everything stops, operator sees your message as an alarm | Block the MCode when a safety condition fails |
Recipe 1: Add something before the stock job (most common)
Example: you want M3 to also turn on a "spindle running" lamp (output #5).
Open Mach\Profiles\<your profile>\Macros\Scripts\UserMCodeModule.mcs.
If it doesn't exist, create it with this starter:
Code: [Select]
local UserMCodeModule = {}
return UserMCodeModuleAdd a function named after the MCode; this runs before the spindle starts:
Code: [Select]
function UserMCodeModule.m3(hVars)
-- Turn on the spindle lamp.
local hReg, rc = mc.mcRegGetHandle(inst, "mc.OSIG_OUTPUT5")
if rc == mc.MERROR_NOERROR then
mc.mcRegSetValue(hReg, 1)
end
return true, true, w.FunctionCompleted() -- "true" = now run the normal M3 job
endSave and select "Compile Scripts". In rare cases, you will need to restart Mach. Now every M3 - from a program, MDI, or a screen button - lights the lamp first, then starts the spindle as usual.
Recipe 2: Add something after the stock job
Use the …End hook instead. Example: after M5 (spindle stop) finishes, turn the lamp off:
Code: [Select]
function UserMCodeModule.m5End(hVars)
local hReg, rc = mc.mcRegGetHandle(inst, "mc.OSIG_OUTPUT5")
if rc == mc.MERROR_NOERROR then
mc.mcRegSetValue(hReg, 0)
end
return true, true, w.FunctionCompleted()
endRecipe 3: Block the MCode when a condition isn't met
Example: don't allow M3 unless the guard door is closed (input #2).
Code: [Select]
function UserMCodeModule.m3(hVars)
local hReg, rc = mc.mcRegGetHandle(inst, "mc.ISIG_INPUT2")
if rc == mc.MERROR_NOERROR then
local doorClosed = mc.mcRegGetValue(hReg)
if doorClosed ~= 1 then
return nil, false, "M3 refused: guard door is open"
end
end
return true, true, w.FunctionCompleted()
endReturning false as the second value aborts the whole MCode and shows your message to the operator. Nothing else runs; the spindle never starts.
Recipe 4: Replace the stock behavior entirely
Return false as the first value. Mach skips its built-in job and considers the MCode done.
Example: your machine has no physical spindle relay; M3 should only set a register for your VFD over Modbus, and never run the stock spindle logic:
Code: [Select]
function UserMCodeModule.m3(hVars)
-- My own spindle-start logic here (VFD register writes, etc.)
-- ...
return false, true, w.FunctionCompleted() -- "false" = skip Mach's built-in M3 job
endBe careful with this one. The stock job often does bookkeeping you can't see (state tracking, status displays). Replacing M6 (tool change) or M30 (end of program) this way, for instance, can leave the control's idea of "what's happening" out of sync. Prefer Recipes 1–3 unless you're sure.Which hook names exist?
m<code> runs before the built-in job: m3, m4, m5, m6, m8, m9, m30, …
m<code>End runs after the built-in job: m3End, m5End, …
Some complex operations have extra named hooks; tool change, for example, offers PreMainToolChangeCycle and PostMainToolChangeCycle.
If you're not sure which hooks a particular MCode supports, look at that MCode's Default file (read-only!) in Mach\Profiles\<your profile>\Macros\ and search Mach's modules for RunHooks("… every name found there is a hook you can use. You can also search in:
https://support.machmotion.com/docs/current/dir_f2541a3b18981391fa76fac5599e978a.html
Testing and Troubleshooting
Select "Compile Scripts" or restart Mach - hooks are loaded at startup.
Trigger the MCode from the MDI line.
If your hook doesn't seem to run, check:
- The file is UserMCodeModule.mcs, in Macros\Scripts\ of the profile you're actually running.
- The file starts with local UserMCodeModule = {} and ends with return UserMCodeModule.
- The function name matches the hook exactly: UserMCodeModule.m3 (lowercase "m", correct number) or UserMCodeModule.m3End.
- You restarted Mach or compiled after saving.
Quick reference
| You want to… | Hook name | Return |
| Add before stock job | UserMCodeModule.m3 | true, true, w.FunctionCompleted() |
| Add after stock job | UserMCodeModule.m3End | true, true, w.FunctionCompleted() |
| Block with alarm | UserMCodeModule.m3 | nil, false, "message" |
| Replace stock job | UserMCodeModule.m3 | false, true, w.FunctionCompleted() |

Recent Posts