Hello Guest it is October 18, 2026, 08:51:35 AM

Author Topic: MachPro upgrade questions  (Read 487108 times)

0 Members and 5 Guests are viewing this topic.

Re: MachPro upgrade questions
« Reply #10 on: September 15, 2026, 12:08:41 PM »
Hi,

I am not 100% confident in what you mean. We do have a signal library, but I don't believe this implementation was in Mach4. It was added with the CommonGUIModule inside of MachPro.

The main implementation is in CommonGUIModule:

SIG_LIBRARY_INST[_inst]: common, per-Mach-instance handlers.
MACHINE_TYPE_SIG_LIBRARY_INST[_inst]: machine-specific handlers, such as mill, plasma, grinder, or height-control behavior.
SCREEN_SIG_LIBRARY_INST[_inst]: screen-specific handlers for multi-instance screens.
SCREEN_SIG_LIBRARY: legacy/global screen handlers used by older screen versions.
Each table is keyed by a Mach signal ID. The value is a Lua callback:

Code: [Select]
SIG_LIBRARY_INST[_inst][signalId] = function(sig_state)
    -- Handle the signal transition
end

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:

CommonSignalScript
  • SignalScript callback
  • SIG_LIBRARY_INST[instance][signal]
  • MACHINE_TYPE_SIG_LIBRARY_INST[instance][signal]
  • SCREEN_SIG_LIBRARY_INST[instance][signal]
  • SCREEN_SIG_LIBRARY[signal]
  • SignalScript hooks

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] in CommonScreenV02.
  • Mill and plasma modules populate MACHINE_TYPE_SIG_LIBRARY_INST for tool-changer signals in MillGUIModule.lua and PlasmaGUIModule.
  • Height controllers add their own signal handlers in HeightControllers

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.


TableScopePurpose
SIG_LIBRARY_INSTPer Mach instanceCommon machine behavior
MACHINE_TYPE_SIG_LIBRARY_INST  Per Mach instanceMachine-specific behavior, such as mill, plasma, grinder, or height control
SCREEN_SIG_LIBRARY_INSTPer Mach instanceScreen-specific behavior
SCREEN_SIG_LIBRARYGlobal/legacyOlder single-instance screen handlers

Each table uses a signal ID as its key and a Lua function as its value:

Code: [Select]
SIG_LIBRARY_INST[instanceIndex][signalId] = function(signalState)
    -- React to the signal transition
end
signalState is normalized to a Boolean before dispatch.

Dispatch sequence
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]
      • SignalScript hooks

The dispatcher does not stop after the first matching table. A signal can therefore have common, machine-specific, and screen-specific behavior.

Building the common library
CommonGUIModule.UpdateSignalLibraryArray() registers the standard handlers, including:

  • Positive and negative axis-limit status signals
  • Axis-homed signals
  • Toolpath mouse events
  • Motion-inhibit interlock input
  • Machine enable and disable signals
  • 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

nstance indexing
The tables are indexed by the untagged instance index:
Code: [Select]
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.

Registration example
A machine module can register a handler for an OEM-mapped signal like this:

Code: [Select]
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.

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:

Code: [Select]
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

For a second spindle, the module can resolve a parameter such as:

Code: [Select]
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:
  • Mach updates the underlying signal.
  • Mach invokes CommonSignalScript.
  • The signal ID is used to look up the callback in MACHINE_TYPE_SIG_LIBRARY_INST.
[li]The callback invokes:
[/li][/list]
Code: [Select]
[code]m.ToolReleaseButtonInputChanged(signalState, spindleIndex)[/code]
    [/li]
  • 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.

Another 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.

When to use each 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.

Use SCREEN_SIG_LIBRARY only for legacy/global screen behavior where the callback is intentionally not instance-specific.

Relationship to the Lua Signal Library
The Lua signal library described earlier operates above this lower-level signal/I/O layer:
Physical hardware or simulated device
  • HMCIO I/O handle
  • mcSignalMap()
  • HMCSIG logical signal
  • CommonSignalScript()
  • 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:
Code: [Select]
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

I hope this info dump can help solve your questions?

Something else that will be beneficial if you wish to do any custom development for MachPro:
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."
Thanks,

Paul
Re: MachPro upgrade questions
« Reply #11 on: September 15, 2026, 03:11:38 PM »
Hi Paul,
thanks for your extensive reply.

I get a general sense as to how MachPro handles signals, but my programming skills are only fair and my understanding of your terminology is poor.
I do rather believe however that MachPro and Mach4 have a similarity.

In Mach4, for which I am familiar each screen set has a ScreenLoad script, and in that script there is a Lua table called SigLib{...........}. Given that the ScreenLoad script
is part of the screen set, and most operators choose to use say wx4.set or wx6.set as a basis for their own personalised copy, and thus survive Mach build updates.

This is the SigLib table in my current Mach4 installation:
Code: [Select]
---------------------------------------------------------------
-- Signal Library
---------------------------------------------------------------
SigLib = {
[mc.OSIG_MACHINE_ENABLED] = function (state)
    machEnabled = state;
    ButtonEnable()
end,

[mc.ISIG_INPUT0] = function (state)
   
end,

[mc.ISIG_INPUT1] = function (state) -- this is an example for a condition in the signal table.
   -- if (state == 1) then   
--        CycleStart()
--    --else
--        --mc.mcCntlFeedHold (0)
--    end

end,

[mc.OSIG_JOG_CONT] = function (state)
    if( state == 1) then
       scr.SetProperty('labJogMode', 'Label', 'Continuous');
       scr.SetProperty('txtJogInc', 'Bg Color', '#C0C0C0');--Light Grey
       scr.SetProperty('txtJogInc', 'Fg Color', '#808080');--Dark Grey
    end
end,

[mc.OSIG_JOG_INC] = function (state)
    if( state == 1) then
        scr.SetProperty('labJogMode', 'Label', 'Incremental');
        scr.SetProperty('txtJogInc', 'Bg Color', '#FFFFFF');--White   
        scr.SetProperty('txtJogInc', 'Fg Color', '#000000');--Black
   end
end,

[mc.OSIG_JOG_MPG] = function (state)
    if( state == 1) then
        scr.SetProperty('labJogMode', 'Label', '');
        scr.SetProperty('txtJogInc', 'Bg Color', '#C0C0C0');--Light Grey
        scr.SetProperty('txtJogInc', 'Fg Color', '#808080');--Dark Grey
        --add the bits to grey jog buttons becasue buttons can't be MPGs
    end
end,
[mc.ISIG_INPUT20] = function(state) --X servo alarm port1 pin10
if(state==1)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,
[mc.ISIG_INPUT21] = function(state) --Y servo alarm port1 pin11
if(state==1)then
mc.mcCntlEStop(inst)
local handle=mc.mcRegGetHandle(inst,"iRegs0/YAlarm")
mc.mcRegSetValue(handle,1)
else
local handle=mc.mcRegGetHandle(inst,"iRegs0/YAlarm")
mc.mcRegSetValue(handle,0)
end
end,
[mc.ISIG_INPUT22] = function(state) --Z servo alarm port1 pin12
if(state==1)then
mc.mcCntlEStop(inst)
local handle=mc.mcRegGetHandle(inst,"iRegs0/ZAlarm")
mc.mcRegSetValue(handle,1)
else
local handle=mc.mcRegGetHandle(inst,"iRegs0/ZAlarm")
mc.mcRegSetValue(handle,0)
end
end,
[mc.ISIG_INPUT23] = function(state) --A servo alarm port1 pin13
if(state==1)then
mc.mcCntlEStop(inst)
local handle=mc.mcRegGetHandle(inst,"iRegs0/AAlarm")
mc.mcRegSetValue(handle,1)
else
local handle=mc.mcRegGetHandle(inst,"iRegs0/AAlarm")
mc.mcRegSetValue(handle,0)
end
end,
[mc.ISIG_INPUT24] = function(state) --B servo alarm port1 pin15
if(state==1)then
mc.mcCntlEStop(inst)
local handle=mc.mcRegGetHandle(inst,"iRegs0/BAlarm")
mc.mcRegSetValue(handle,1)
else
local handle=mc.mcRegGetHandle(inst,"iRegs0/BAlarm")
mc.mcRegSetValue(handle,0)
end
end,
[mc.ISIG_INPUT29] = function(state) --C servo alarm port2 pin2
if(state==1)then
mc.mcCntlEStop(inst)
local handle=mc.mcRegGetHandle(inst,"iRegs0/CAlarm")
mc.mcRegSetValue(handle,1)
else
local handle=mc.mcRegGetHandle(inst,"iRegs0/CAlarm")
mc.mcRegSetValue(handle,0)
end
end,

[mc.ISIG_INPUT25] = function(state) --AXIS1 port3 pin6
if(state==1)then
local handle=mc.mcRegGetHandle(inst,"iRegs0/AXIS1")
mc.mcRegSetValue(handle,0)
else
local handle=mc.mcRegGetHandle(inst,"iRegs0/AXIS1")
mc.mcRegSetValue(handle,1)
end
ChangeJogAxis()
end,
[mc.ISIG_INPUT26] = function(state) --AXIS2 port3 pin7
if(state==1)then
local handle=mc.mcRegGetHandle(inst,"iRegs0/AXIS2")
mc.mcRegSetValue(handle,0)
else
local handle=mc.mcRegGetHandle(inst,"iRegs0/AXIS2")
mc.mcRegSetValue(handle,1)
end
ChangeJogAxis()
end,
[mc.ISIG_INPUT27] = function(state) --AXIS3  port3 pin8
if(state==1)then
local handle=mc.mcRegGetHandle(inst,"iRegs0/AXIS3")
mc.mcRegSetValue(handle,0)
else
local handle=mc.mcRegGetHandle(inst,"iRegs0/AXIS3")
mc.mcRegSetValue(handle,1)
end
ChangeJogAxis()
end,

[mc.ISIG_INPUT28] = function(state) --FEEDHOLD port3 pin9
mc.mcCntlFeedHold(inst)
end,
[mc.ISIG_INPUT30] = function(state) --AXIS3  port3 pin8
if(state==1)then
local handle=mc.mcRegGetHandle(inst,"iRegs0/Velocity1")
mc.mcRegSetValue(handle,1)
else
local handle=mc.mcRegGetHandle(inst,"iRegs0/Velocity1")
mc.mcRegSetValue(handle,0)
end
ChangeJogAxis()
end,
[mc.ISIG_INPUT31] = function(state) --AXIS3  port3 pin8
if(state==1)then
local handle=mc.mcRegGetHandle(inst,"iRegs0/Velocity2")
mc.mcRegSetValue(handle,0)
else
local handle=mc.mcRegGetHandle(inst,"iRegs0/Velocity2")
mc.mcRegSetValue(handle,1)
end
ChangeJogAxis()
end,
[mc.ISIG_INPUT1]=function(state)

if(state==1)then

local handle=mc.mcRegGetHandle(inst,"iRegs0/Trial")
local LEDState=mc.mcRegGetValue(inst,handle)
wx.wxMessageBox("LED State=="..tostring(LEDState))
if(LEDState==1)then
wx.wxMessageBox("here"..tostring(LEDState))
local handle=mc.mcRegGetHandle(inst,"iRegs0/Trial")
mc.mcRegSetValue(handle,0)
else
local handle=mc.mcRegGetHandle(inst,"iRegs0/Trial")
mc.mcRegSetValue(handle,1)
end
end
end,

}

Several of the signals are generic and came fro the donor screen set, wx6.set in this case, but quite a number more are signal entries specific to my machine and I coded them.
I think you are familiar with the general idea, whenever Mach detects a signal change, of any description , from any source,  the SignalScript runs, wherein it interrogates the
SigLib table, and if it encounters an entry for that signal identity it executes the function in that table entry. My understanding is that MachPro is similar.

Where MachPro and Mach4 differ is that the SigLib table is immediately readily available to the user to add signal entries or change functionalities. Because the SigLib table is in the
ScreenLoad script it is in effect custom to the users machine.

As you can see from the code I have entries for the servo axis alarms, with each latching data into a register, and enacting an Estop. Easy.
Other signals are from my wired pendant. The SigLib entries capture those signal events and invoke functions also within the ScreenLoad script. This is a method I am very familiar with,
it is both simple and yet immensly flexible and powerful.

Why did you deem in necessary to do away with it?

Craig
'I enjoy sex at 73.....I live at 71 so its not too far to walk.'
Re: MachPro upgrade questions
« Reply #12 on: September 16, 2026, 03:05:30 PM »
Hi,

That concept still exists.. MachPro still works exactly the way you described: when 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 is where your custom entries live, and that change was made to protect people doing exactly what you're doing.

Why it 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.

Where your code goes in MachPro:

Create (or edit) this file in your MachPro installation:
Code: [Select]
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 your Mach4 one:

Code: [Select]
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

Two small differences from your Mach4 code to watch for:

  • "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.

One more thing worth knowing:

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.


Thanks,

Paul
Re: MachPro upgrade questions
« Reply #13 on: September 16, 2026, 03:43:34 PM »
Hi Paul,
excellent description. I do absolutely now understand how and why you have proceeded in the manner you have, and that refinement in signal handling over
what I am used to in Mach4 is to be congratulated. It is also true that I'm going to have to learn a new procedure that reflects this newer architecture, but I readily
and easily understand why the effort is worth it.

Can I also assume that any functionality that I would normally include in the Mach4 ScreenLoad script I can include in UserGUIModule.mcs?

Craig
'I enjoy sex at 73.....I live at 71 so its not too far to walk.'
Re: MachPro upgrade questions
« Reply #14 on: September 17, 2026, 11:42:34 AM »
Hi,

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:

FileFolder PathJob / Description
m500.mcsMach\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()
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', ...)).

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 UserMCodeModule
Now 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()
end

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:
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()
end

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:
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 happensUse this to…
return true, true, ...Built-in job runs normallyAdd behavior before/after the stock job (log it, turn on an extra output, check a condition)
return false, true, ...Built-in job is skippedReplace the stock behavior entirely with your own
return nil, false, "message"Everything stops, operator sees your message     as an alarmBlock 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 UserMCodeModule

Add 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
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.

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()
end

Recipe 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()
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.

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
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.

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 nameReturn
Add before stock job     UserMCodeModule.m3     true, true, w.FunctionCompleted()
Add after stock jobUserMCodeModule.m3End     true, true, w.FunctionCompleted()
Block with alarmUserMCodeModule.m3nil, false, "message"
Replace stock jobUserMCodeModule.m3false, true, w.FunctionCompleted()
Thanks,

Paul
Re: MachPro upgrade questions
« Reply #15 on: September 17, 2026, 03:01:24 PM »
Hi Paul,
another excellent description.

I have several Mcodes on which my work depends.

In particular m40 and m41, which were back in Mach3 days, built-in functions to open a data file, and then close it. This file would be used for storing probing or point cloud data.

I use Autoleveller, a freeware utility to probe PCB blanks and relies on storing probing data. Mach4 had some facsimilies of m40, m41 but I had to extensively modify them to use Autoleveller.
The procedures you've outlined will allow me to import those macros into MachPro.

The same advice with respects to built-in Mach4 Mcodes also applied, you risked overwriting Mach's functionality with unpredictable results. You could however do so if you chose.
For instance I have a custom m8 macro to introduce a delay when turning on the coolant pump. When Mach4 encountered an Mcode it would search, in the first instance,
in the Profile/Macros folder, ie the user file,  and enact it if it did find it. If there was no such macro in that folder then Mach would 'search up the file tree' until it found it,
usually Mach4's built-in function.

MachPro's handling of the same process is very much more structured. Sure it adds some architectural complexity but much increases the reliability of the outcome.

Also it demystifies the use of 'hooks', a term I had not previously been familiar with in connection with Mach software.

I think it fair to say that in your last two posts you have very convincingly demonstrated that MachPro is in fact a more architecturally sophisticated evolution of Mach4. All the functionality
that I regarded as essential in Mach4 is available, in fact is a subset of what is available in MachPro. All to the good. There remains now another 'bone of contention' regarding the
editability or otherwise of the screen set. This I will address it in that thread, as you have comprehensively answered my upgrade concerns in this thread.

Craig
'I enjoy sex at 73.....I live at 71 so its not too far to walk.'