Duet3D / RepRapFirmware

OO C++ RepRap Firmware
GNU General Public License v3.0
947 stars 538 forks source link

[FeatureRequest]: Return Values from Macros #1038

Open benagricola opened 2 months ago

benagricola commented 2 months ago

Is your feature request related to a problem? Please describe.

It would be helpful if macros had a well-defined API to return data to their calling macro or file, so that there is an obvious single way of passing data. This may open up more advanced custom macro writing where specific behaviour can be more easily broken down into files to e.g. perform calculations.

It would allow for more advanced gcode to be written by multiple people while allowing code to interact in a standard manner (which we already have for getting data into macros, in parameters).

Describe the solution you propose.

Add an R parameter to M99 (return from macro), that takes an RRF Expression.

When M99 is executed in the called macro with this parameter, set a NamedConstant that is available from the calling macro, for example retval. This is almost exactly how the input variable works when using M291 to select an input value.

Ideally, this retval constant would only exist in the local scope of the calling macro, and if a macro were to call M99 without the R parameter this value would be set to null.

Describe alternatives you've considered

When writing complex macros or sets of macros (in my case, probing for CNC machines), returning values from a macro to wherever it was called can be performed by setting a global variable in the called macro and reading it in the calling macro.

However, this requires boilerplate code in each calling / called macro to manage the global variable, and once created, that variable will always exist (taking up space in the object model to store the variable name even if the value is null).

It is up to macro writers to decide what their global variable will be called, and with macros written by multiple people (for example with macro libraries) it is very likely that the global namespace will be polluted by return variables.

The other side of this is if there is any mismanagement of the global variable, a calling macro might receive unexpected data and cause errors.

Another alternative I have considered is to allow functions to be executed from a file as part of a meta gcode expression, but this looks to be much more of a task to implement than using the existing macro functionality.

Provide any additional context or information.

Discussion here

As an example, I need absolute co-ordinates all the time when calculating probing moves and positions. Originally I started with using machinePosition, but this is problematic because it is affected by backlash compensation.

To get around this, I use userPosition but add it to the workplace and tool offsets to find the absolute co-ordinates, something like this (untested code, going from memory):

set var.cP[0] = { move.axes[0].workplaceOffsets[move.workplaceNumber] + tools[state.currentTool].offsets[0] + move.axes[0].userPosition }
set var.cP[1] = { move.axes[1].workplaceOffsets[move.workplaceNumber] + tools[state.currentTool].offsets[1] + move.axes[1].userPosition }
set var.cP[2] = { move.axes[2].workplaceOffsets[move.workplaceNumber] + tools[state.currentTool].offsets[2] + move.axes[2].userPosition }

This code has to exist everywhere I need the absolute machine position, which means repeating myself often and also means I have to split it into multiple lines due to line length limits.

With an obvious return feature in M99, I could rewrite this as a "function macro" that simply returns the right information.

M99 R { move.axes[0].workplaceOffsets[move.workplaceNumber] + tools[state.currentTool].offsets[0] + move.axes[0].userPosition, move.axes[1].workplaceOffsets[move.workplaceNumber] + tools[state.currentTool].offsets[1] + move.axes[1].userPosition, move.axes[2].workplaceOffsets[move.workplaceNumber] + tools[state.currentTool].offsets[2] + move.axes[2].userPosition }
benagricola commented 2 months ago

I have implementations of this functionality that might be useful:

1: Basic implementation that works exactly the same way as M291 responses - stored on GCodeBuffer - essentially equivalent to storing return values in a global variable.

2: Slightly more advanced implementation, grabs the previous machine state and writes the return value as a local variable in the parent, so it is cleared up when the parent macro (or file, I guess) is finished. In this case, the retval named constant simply acts as a standard way of reading the local variable (currently named var._mrv).

If either of these approaches are acceptable, I'm happy to submit a PR.