MachPro Custom Development
Screen-set customization, profile-specific Lua modules, signal libraries, custom M-codes, existing M-code hooks, and Mach4-to-MachPro setup questions.
Contents
- 1. Screen customization
- 2. Signal-library architecture
- 3. User signal handling and Mach4 migration
- 4. Create a custom M-code
- 5. Modify an existing M-code
- 6. Mach4-to-MachPro setup questions
- 7. Create dashboard user commands
- 8. Supplemental context and technical review
1. Screen customization
1.1 Save and use a custom screen set
The screen is not compiled into MachPro. It is a data file (a .set file) that is loaded at startup. The built-in workflow is described:
- Load the shipped screen.
- Enter screen-edit mode.
- Use Screen → Save As and give it your own name (for example,
MyMill.set).
MachPro remembers that filename in your profile and loads your copy from then on. The shipped file is never touched.
1.2 Why some screen areas cannot be edited
The reason you can add things outside the dashboard but can't change certain existing elements is that those areas (like the DRO block) are single, self-contained controls rather than groups of individually editable pieces. The editor can show them and let you attach code, but it can't let you rearrange what is inside them. There is nothing to grab onto.
The practical workaround where it matters: place your own buttons as separate controls overlapping or immediately adjacent to a sealed block, with your own code behind them. To the operator, the result looks and behaves the same as if the button were part of the block.
1.3 Change screen behavior with UserGUIModule
The UserGUIModule is your best friend for screen work. It lets you change what is displayed, switch pages, set labels, and adjust properties programmatically, which means you can often get what you want without editing the screen file at all. For example, forcing a specific page on a button press:
function UserGUIModule.MyCustomPage()
scr.ShowPage("CustomPageName")
end
--If you want the button in your dashboard.
function UserGUIModule.CreateUserCommands()
w.CreateCommandActionOption("ShowMyCustomPage", function(from_opr_panel) UserGUIModule.MyCustomPage() end)
w.CreateSimpleCommand("ShowMyCustomPage")
end
Alternatively, if you wanted to call anything from the UserGUIModule inside the screen with, lets say a custom button:
--Button Script
h.ScreenButtonCall(User.MyCustomPage(), ...)
Anything you can do with scr.SetProperty or scr.ShowPage in a module survives updates cleanly, because modules are user code and are never overwritten. A good strategy is: use Save As for layout changes you can't achieve any other way (button placement for muscle memory), and use the UserGUIModule for everything else (showing pages, relabeling, hiding and showing elements). That minimizes the diff in your forked screen and makes any future merge almost painless.
1.4 Purpose of self-contained controls
The hard-coded composite controls exist for several reasons:
- They can't be broken by screen edits. The DRO block contains logic that must always work: position display, units handling, work/machine coordinate switching. If its internals were exposed to the editor, one accidental drag in edit mode could silently break a safety-critical readout. You'd never know until you scrapped a part.
- They come pre-automated. This is the big one, and it's easy to overlook if you're coming from Mach4. Much of what a Mach4 user had to hand-build, such as wiring buttons to function codes, scripting enable/disable behavior, linking LEDs to signals, and making DROs respond to units and coordinate modes, is already built into these controls. Drop a Cycle Start button on the screen and it works: no script, no signal plumbing, no PLC ladder. Anyone who set up a Mach4 screen from scratch knows how many evenings that used to take. MachPro's controls remove that startup burden. The screen is functional out of the box, and your time goes into the custom things your machine needs, not into re-implementing the basics.
- They survive updates. Because the control owns its own layout and behavior, a MachPro update can improve or fix the DRO block and your forked screen picks up the improvement automatically. The .set file just references the control; it doesn't freeze its internals.
- Your code still attaches to them. You can read their properties, react to their state, and build your own controls around them. They're sealed, not isolated.
In other words: the areas you can't edit are precisely the areas that either (a) would cost you parts if accidentally broken, or (b) would cost you evenings to rebuild if they weren't automated. The areas you can edit (pages, groups, dashboards, and your own buttons with your own code) are where all the productivity wins live anyway, and there the field is wide open.
1.5 Use h.ScreenButtonCall() for custom buttons
When you add your own button to the screen, its script property (for example, the Left Up Script) should call h.ScreenButtonCall(...) rather than calling your function directly. It is the standard entry point the shipped buttons use, and it gives your custom button the same protection and polish for free:
- Password protection is enforced automatically. If you set a password level on the button in the screen editor,
ScreenButtonCallchecks it before your code runs. No extra code in your handler, and no chance of an unauthorized operator triggering your function. - Errors can't crash the screen. Your handler runs inside a protected wrapper. If your Lua code throws an error, you get a warning message instead of a dead UI.
- Every press is logged. The button's label and press are written to the log, which makes diagnosing "what did the operator do just before this happened" much easier.
- Standard argument handling. It passes your custom arguments first and the standard screen-event parameters (control name, event source, control type) last, so your handler signature stays consistent with every other button in the system.
The pattern looks like this in the button's script:
h.ScreenButtonCall(m.screen.MyCustomFunction, ...)
1.6 Multiple pages and tabs
Tabs are a first-class feature of the screen system. A dedicated Machine Diagnostics tab holding your live offsets block (and as many or as few I/O LEDs as you feel are worth the effort) is a completely normal thing to build. It costs zero space on the main page until you click over to it, which is exactly the behavior you described wanting. And with a scr.ShowPage("Diagnostics(1)") style call in the UserGUIModule, you can even jump to it from a button or a key rather than hunting for the tab.
One note before you build it: check the Fixtures tab first. Between its offsets display and Edit Offsets mode, it may already cover much of what your diagnostics page was doing, which would save you the rebuild.
2. Signal-library architecture
2.1 Signal-handler layers and scope
The main implementation is in CommonGUIModule:
| Table | Scope | Purpose |
|---|---|---|
SIG_LIBRARY_INST |
Per Mach instance | Common machine behavior |
MACHINE_TYPE_SIG_LIBRARY_INST |
Per Mach instance | Machine-specific behavior, such as mill, plasma, grinder, or height control |
SCREEN_SIG_LIBRARY_INST |
Per Mach instance | Screen-specific behavior for multi-instance screens |
SCREEN_SIG_LIBRARY |
Global / legacy | Older single-instance screen handlers |
Each table is keyed by a Mach signal ID. The value is a Lua callback:
SIG_LIBRARY_INST[_inst][signalId] = function(sig_state)
-- Handle the signal transition
end
Each table uses a signal ID as its key and a Lua function as its value:
SIG_LIBRARY_INST[instanceIndex][signalId] = function(signalState)
-- React to the signal transition
end
signalState is normalized to a Boolean before dispatch.
2.2 Signal dispatch sequence
When Mach reports a signal transition, CommonSignalScript() converts the state to a Boolean and calls _CommonSignalScript(). That function dispatches the event through the layers above, then runs the generic SignalScript hooks:
When Mach detects a signal transition, the following path is used:
CommonSignalScript()_CommonSignalScript()- module
SignalScript() SIG_LIBRARY_INST[instance][signal]MACHINE_TYPE_SIG_LIBRARY_INST[instance][signal]SCREEN_SIG_LIBRARY_INST[instance][signal]SCREEN_SIG_LIBRARY[signal]SignalScripthooks
The dispatcher does not stop after the first matching table. A signal can therefore have common, machine-specific, and screen-specific behavior.
2.3 Build and update the signal libraries
The common table is rebuilt by UpdateSignalLibraryArray() in CommonGUIModule. It registers handlers for:
- Axis positive and negative limit status signals
- Axis homed signals
- Toolpath mouse down/up
- Motion-inhibit interlock input
- Machine enabled/disabled state
- Spindle-brake output lookup
It also stores the resolved spindle-brake signal ID in SIG_LIBRARY_INST[_inst].SpindleBrakeOutput and SIGNAL_LIB_ARRAY.SpindleBrakeOutput.
The rebuild entry point is CommonUpdateSignalLibraryArray() in CommonGUIModule. It:
- Calls the active screen/module’s
UpdateSignalLibraryArray(), if present. - Reads auxiliary-output button mappings from the profile.
- Enables or disables the corresponding auxiliary screen buttons.
- Runs the UpdateSignalLibrary hooks, allowing machine-specific modules to register more callbacks.
The screen-specific tables are populated separately. For example:
- V2 screens build
SCREEN_SIG_LIBRARY_INST[_inst] inCommonScreenV02. - Mill and plasma modules populate
MACHINE_TYPE_SIG_LIBRARY_INSTfor tool-changer signals inMillGUIModule.luaandPlasmaGUIModule. - Height controllers add their own signal handlers in
HeightControllers
CommonGUIModule.UpdateSignalLibraryArray() registers the standard handlers, including:
The same standard handlers include positive and negative axis-limit status, axis-homed signals, toolpath mouse events, the motion-inhibit interlock input, machine enable and disable signals, and spindle-brake output lookup.
The wrapper, CommonGUIModule.CommonUpdateSignalLibraryArray(), also:
- Calls a machine module’s
UpdateSignalLibraryArray(), if implemented - Reads auxiliary output-button assignments from the Mach profile
- Updates auxiliary-button enabled states
- Runs the UpdateSignalLibrary hooks
2.4 Instance indexing
The tables are indexed by the untagged instance index:
local instanceIndex = w.GetUTI(inst)
Handlers registered for one instance should remain associated with that instance. This matters in multi-instance configurations because the same signal number may have different mappings or machine behavior in different instances.
2.5 Register an OEM-mapped signal handler
A machine module can register a handler for an OEM-mapped signal like this:
local signalInfo = w.GetOEMParamIOSigObject(
"TC_ToolReleaseButtonInput",
instanceIndex
)
if signalInfo ~= nil and signalInfo["type"] == "Input Signal" then
MACHINE_TYPE_SIG_LIBRARY_INST[instanceIndex][signalInfo["sigid"]] =
function(signalState)
m.ToolReleaseButtonInputChanged(signalState, 1)
end
end
The mill and plasma implementations follow this pattern in MillGUIModule and PlasmaGUIModule.
2.6 Use case: tool-release button
A useful example is a tool changer with a physical tool-release button.
Requirement
When the operator presses or releases the tool-release input:
- The machine-specific tool-changer logic should run.
- The handler should use the signal mapping configured for the current instance.
- Multiple spindles or indexed tool-release inputs should be supported.
Registration
During signal-library setup, the machine module resolves the OEM parameter to its actual signal ID and registers a callback:
Use the registration example in section 2.5. It resolves TC_ToolReleaseButtonInput for the current instance and passes spindle index 1 to the callback.
For a second spindle, the module can resolve a parameter such as:
TC_ToolReleaseButtonInput2
and register a callback that passes spindle index 2.
Runtime behavior
When the physical input changes:
- Mach updates the underlying signal.
- Mach invokes
CommonSignalScript. - The signal ID is used to look up the callback in
MACHINE_TYPE_SIG_LIBRARY_INST. - The callback invokes:
m.ToolReleaseButtonInputChanged(signalState, spindleIndex)
The tool-changer module performs the configured action.
This keeps the machine module independent of the physical I/O device. The module only needs to know the logical OEM parameter and the resolved Mach signal ID.
2.7 Use case: machine-enable state
The common library registers mc.OSIG_MACHINE_ENABLED for every instance. Its callback updates the cached enable state and starts the appropriate enable or disable sequence.
The handler also performs special startup protection because Mach can emit an initial disabled state while signal synchronization is taking place. That logic is in CommonGUIModule.lua.
This is a good example of behavior that belongs in SIG_LIBRARY_INST rather than a machine-specific table because every screen and machine type needs consistent machine-enable behavior.
2.8 Choose the appropriate signal-library layer
Use SIG_LIBRARY_INST when:
- The behavior is common to all machine types.
- It is instance-specific.
- It concerns limits, homing, machine enable, or core GUI behavior.
Use MACHINE_TYPE_SIG_LIBRARY_INST when:
- The behavior belongs to a machine family.
- The signal is related to a tool changer, plasma THC, grinder, or similar subsystem.
- The same machine module may register different signals per instance.
Use SCREEN_SIG_LIBRARY_INST when:
- The callback updates controls belonging to a particular screen.
- The screen supports multiple Mach instances.
Use SCREEN_SIG_LIBRARY when:
- The behavior is legacy/global screen behavior.
- The callback is intentionally not instance-specific.
2.9 Relationship to logical signals and physical I/O
This Lua “library” sits above the actual Mach signal subsystem. The actual signal objects are handled by the C++/Mach API layer using functions such as mcSignalGetHandle, mcSignalGetNextHandle, mcSignalGetInfo, mcSignalGetState, and mcSignalMap. The API documentation explicitly distinguishes signals from physical I/O: signals can be mapped to I/O, but the signal abstraction does not depend on a particular I/O device.
The Lua signal library described earlier operates above this lower-level signal/I/O layer:
- Physical hardware or simulated device
HMCIOI/O handlemcSignalMap()HMCSIGlogical signalCommonSignalScript()- Lua signal-library callback
For example, a tool-release button may be physically connected to an input device. The device is mapped to a logical signal. The GUI layer then registers a Lua callback keyed by that signal ID:
MACHINE_TYPE_SIG_LIBRARY_INST[instanceIndex][signalId] =
function(signalState)
m.ToolReleaseButtonInputChanged(signalState, spindleIndex)
end
The callback does not need to know whether the signal came from:
- A simulator
- A motion controller
- An Ethernet I/O device
- A PCI or USB device
- Another plugin
2.10 Code documentation and wrapper functions
Additional code documentation is available for custom MachPro development:
https://support.machmotion.com/docs/current/dir_f2541a3b18981391fa76fac5599e978a.html
This is some of our code documentation.
I would highly recommend using the wrapper modules functions instead of the core mach API's as we have built in error handling for most of these functions. You should be able to invoke the wrapper module from anywhere with "w."
3. User signal handling and Mach4 migration
3.1 The signal-library concept is retained
That concept still exists.. MachPro still works exactly the way you described: whenWhen Mach detects a signal change, a Signal Script runs, it consults a table keyed by signal ID, and if an entry exists for that signal, its function executes. Your understanding of the mechanism is spot on.
What changed from Mach4 is where your custom entries live, and that change was made to protect people doing exactly what you're doing.
Why the organization changed
In Mach4, your SigLib lived inside the screen set's Screen Load script, so the only way to customize it was to copy wx4.set or wx6.set and maintain a fork and your copy drifted further from the shipped screens with every update.
In MachPro, the signal handling is split into layers:
- Core layer- the shipped screen code handles the things every machine needs (limit switches, homing status, machine enable/disable, emergency stop).
- Machine-type layer- mill, plasma, lathe-specific behavior (tool changers, torch height control, etc.).
- Screen layer- screen cosmetics like your jog-mode labels.
- User layer- this is yours. It runs first, before the built-in layers, and it lives in a file that belongs to your profile, not to the shipped screen set.
That user layer is the direct equivalent of your SigLib, with one improvement: because it's a dedicated user file rather than a copy of our screen code, a MachPro update can improve the shipped screens without touching your custom signal handlers at all. No more maintaining a fork.
3.2 Add signal handlers to UserGUIModule.mcs
Create (or edit) this file in your MachPro installation:
Profiles\<YourProfileName>\Macros\Scripts\UserGUIModule.mcs
That file sits inside your machine profile, so it is custom to your machine and survives updates, same as your personalized screen set, but cleaner.
Inside it you can define a signal library table almost identically to youra Mach4 one:
local UserGUIModule = {}
USER_SIG_LIBRARY = {
[mc.ISIG_INPUT20] = function(state) --EXAMPLE X servo alarm
local inst = mc.mcGetInstance("UserGUIModule, USER_SIG_LIBRARY, X servo alarm")
if state then
mc.mcCntlEStop(inst)
local handle = mc.mcRegGetHandle(inst, "iRegs0/XAlarm")
mc.mcRegSetValue(handle, 1)
else
local handle = mc.mcRegGetHandle(inst, "iRegs0/XAlarm")
mc.mcRegSetValue(handle, 0)
end
end,
-- ... add your other signals here, same pattern ...
}
function UserGUIModule.SignalScript(sig, state)
if USER_SIG_LIBRARY ~= nil and USER_SIG_LIBRARY[sig] ~= nil then
USER_SIG_LIBRARY[sig](state)
end
return true, true, "User Signal Script Complete"
end
return UserGUIModule
3.3 Differences from Mach4 signal code
- "state" is a true/false value, not 1/0. Your Mach4 code tests state == 1; in MachPro write if state then ... else ... end instead. (If you want, if tonumber(state) == 1 also works and is easier to port line-for-line.)
- Get the instance inside the handler with
mc.mcGetInstance("Insert Caller Name")rather than relying on a global inst from screen load.
Everything else, latching registers, calling mc.mcCntlEStop, triggering pendant functions, ports straight across.
User handlers and built-in handlers coexist:
MachPro runs the user signal handler first, then its own built-in layers. So your servo-alarm handler and the built-in limit/homing handlers can both respond to signals without interfering with each other. In Mach4, a single SigLib table meant your entry replaced (or was replaced by) whatever the donor screen set had in MachPro they coexist.
3.4 Screen-load logic and user modules
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.
The following sections describe the basics of custom M-code development and existing M-code hooks.
4. Create a custom M-code
4.1 What an M-code does
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).
4.2 Required files and their roles
A custom MCode needs two files. Keeping them straight is the whole trick:
| File | Folder path | Role |
|---|---|---|
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).
4.3 Step 1: create m500.mcs
- Open the folder Mach\Profiles\<your profile>\Macros\.
- Create a new plain-text file named exactly
m500.mcs(all lowercase; notM500.txt, notm500.mcs.txt- Windows sometimes hides the extension, so double-check). - Paste this in, exactly as shown, and save:
function m500(hVars, GcodeComment)
return m.InvokeMacro('m500', hVars, GcodeComment)
end
if (mc.mcInEditor() == 1) then
m500()
end
You 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', ...)).
4.4 Step 2: create or extend 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:
local UserMCodeModule = {}
-- Custom MCode functions go here (Step 4).
return UserMCodeModule
Now write what M500 should do. Add this function inside the file (between the two lines of the starter template):
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()
end
What the parts mean:
- function
UserMCodeModule.m500(hVars); the name must match the MCode: m500 forM500, m501 forM501, 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:
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.
4.5 Step 3: test the new M-code
- 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.
4.6 Troubleshoot a custom M-code
Check these in order:
m500.mcsexists 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
UserMCodeModuleis named m500 exactly; lowercase m, matching the number. UserMCodeModule.mcsstarts with localUserMCodeModule= {} and ends with returnUserMCodeModule.- 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.
4.7 Pass a value to an M-code
Passing a value to your MCode. You can write M500 P1 in a program and read the P word 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()
end
4.8 Safety lockout and separation from GUI logic
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.
5. Modify an existing M-code
5.1 Do not edit stock macro files
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.mcsin Mach\Profiles\<your profile>\Macros\Scripts\. That file is yours; updates never touch it.
5.2 How M-code hooks are called
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:
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.
5.3 Hook return values
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:
| Return values | Result | Use |
|---|---|---|
true, true, ... |
Built-in job runs normally | Add behavior before/after the stock job: log it, turn on an extra output, or check a condition. |
false, true, ... |
Built-in job is skipped | Replace the stock behavior entirely with your own. |
nil, false, "message" |
Everything stops; the operator sees your message as an alarm | Block the MCode when a safety condition fails. |
5.4 Recipe 1: add behavior before the stock job
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:
local UserMCodeModule = {}
return UserMCodeModule
Add a function named after the MCode; this runs before the spindle starts:
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
end
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.
5.5 Recipe 2: add behavior after the stock job
Use the …End hook instead. Example: after M5 (spindle stop) finishes, turn the lamp off:
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()
end
5.6 Recipe 3: block an M-code when a condition is not met
Example: don't allow M3 unless the guard door is closed (input #2).
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()
end
Returning false as the second value aborts the whole MCode and shows your message to the operator. Nothing else runs; the spindle never starts.
5.7 Recipe 4: replace the stock behavior
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:
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
end
Be 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.
5.8 Find the available hook names
- 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
5.9 Test and troubleshoot M-code hooks
- 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 returnUserMCodeModule. - The function name matches the hook exactly:
UserMCodeModule.m3(lowercase "m", correct number) orUserMCodeModule.m3End. - You restarted Mach or compiled after saving.
5.10 M-code hook quick reference
| Purpose | Hook name | Return values |
|---|---|---|
| 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() |
6. Mach4-to-MachPro setup questions
The following questions and answers retain the scenarios and responses from the supplied discussion. Hardware- and version-dependent statements remain tied to that discussion.
6.1 Develop without a connected ESS motion controller
Is a physical motion controller required to open and configure MachPro, or can it be run in offline/simulation mode without hardware connected?
This is dependent on the motion controller. For testing and configuration, I would recommend the SIM motion controller until you're ready to connect your hardware.
6.2 Display additional axes
How can I customize MachPro like the
wx6.setin Mach4 to support my specific machine configuration while retaining compatibility for 3, 4, and 5-axis toolpaths?
The visible axis are purely dependent on the axis you have enabled. The more you have enabled, the more DRO's will populate on the GUI. No dev time should be required to show additional axis on your main page.
6.3 Import an existing Mach4 screen set
Why does importing a Mach4Hobby screen set into MachPro result in missing Lua scripts and greyed-out, unusable buttons?
The Mach4 screens have a lot of dependencies that have been removed for MachPro. If you were really wantingwant to go back to the Mach4 screenset, you will need to make sure you have the correct dependencies in your Mach folder to make the screen work properly. These dependencies are typically "Modules".
6.4 Package the current profile
How do I export a screen set along with its associated Lua code in Mach4?
This would be under Help→Support→Help → Support → Package current profile. This howeverhowever, wouldwill not include all of the required dependencies of your screen, This will only move over your PMC, and everything directly in side of your profile folder.
6.5 Soft-limit errors during testing
Does SoftLimits need to be enabled and configured in MachPro to run G-code, or can it be turned off to clear a soft limits error?
We do push soft limits for your safety and your machines safety. If you're running into troubles with soft limits, I would recommend going into your configure → Control page and set your soft limits to -9999 Min - 9999. 9999 Max. Especially for testing.
6.6 Add and configure onscreen jogging
How do I manually jog axes via the screen or keyboard in the standard 3-axis screen set of MachPro?
Current versions have the onscreen keyboard selected by default. If it is not there, you can right click on the right side of the screen, click select configure Dashboards, make sure your Rows is configured to be greater than one, and in the widgets dropdown, select "Jogging". This should give you onscreen jogging. When it is up, you can configure it by right clicking on the widget itself, selecting Configure Widget, and find the best configuration that suits you.
Here is some documentation on our dashboards:
6.7 Locate MachPro-specific documentation
The included manual describes Mach4 and does not explain the new MachPro screen set. Where is the MachPro documentation?
A lot of the core of Mach is the same as it was built off of the bones of Mach4, however all of our MachPro documentation is located here:
https://support.machmotion.com/books/cnc-software/chapter/machpro-26-cnc-software-stack
6.8 Migrate a wired jog pendant and ScreenLoad logic
How do I configure input signals and Lua scripts for a custom pendant in MachPro?
We have a preferred method of handling custom scripts, as of right now, if you were to edit the screen load script, if you were to update mach in the future, your script will be blown out of the water.
Recommended custom development path:
- Go to \Profiles\{Your Profile Name}\Macros\Scripts
- Find
UserGUIModuleDefault.mcs - Make a copy of
UserGUIModuleDefault.mcs - Remove "Default - Copy" from the name of the file. This should leave you with
UserGUIModule.mcs - Open this file up in your code editor of choice - Could be ZeroBrane if you desire.
- For the screen load script, uncomment function
UserGUIModule.UserScreenLoadScript()and add your custom functionality here.
7. Create dashboard user commands
Other helpful tools will be user commands as well. Lets say you have a button you want to add to the screen, in the same file, you can write your function with the custom logic, then you can use the following functions to make your buttons:
function UserGUIModule.CreateUserCommands()
-- w.CreateCommandActionOption("Example", function(from_opr_panel) w.SetSignalState(mc.OSIG_OUTPUT0, true) end)
-- w.CreateCommandFeedbackOption("Example", function() return w.GetSignalState(mc.OSIG_OUTPUT0) end)
-- w.CreateSimpleCommand("Example")
end
7.1 CreateSimpleCommand
CreateSimpleCommand will add the button as a selectable option on your dashboard, you just give it a string as a recognizable name you can select, this will also tie in your logic to your button name.
7.2 CreateCommandActionOption
CreateCommandActionOption will apply the "Action" to your button. The first Arguement is the button string you put in "CreateSimpleCommand". The second arguement is a list of your logic or functions. I recommend leaving function(from_opr_panel) but any function listed out after is the list of functions that will be ran after the button is selected. Lastly, you will need to put "end" at the end since this second arguement is like stating
function UserGUIModule.CreateUserCommand(
"Your Custom Function",
function(from_opr_panel)
YourCustomFunctionHere()
end
)
7.3 CreateCommandFeedbackOption
Lastly we have CreateCommandFeedbackOption. This is looking to return a Boolean value, usually used for more advanced logic. Once again it will be feedback or button name in a string format for the first argument, and the function calls for the second argument.
function UserGUIModule.CreateCommandFeedbackOption(
"Your Custom Function",
function()
return YourCustomFunctionHere()
end
)