Skip to content

grbl

plantimager.grbl Link

Implementation of a CNC module adapted to Grbl motherboard.

The CNC is used to move a multi-purpose arm. It offers 3-axis of movements.

CNC Link

CNC(port='/dev/ttyUSB0', baudrate=115200, homing=True, safe_start=True, x_lims=None, y_lims=None, z_lims=None, invert_x=True, invert_y=True, invert_z=True)

Bases: AbstractCNC

CNC functionalities.

Attributes:

Name Type Description
port str

Serial port to use for communication with the CNC controller (Arduino UNO).

baudrate int

Communication baud rate, 115200 whould work with the Arduino UNO.

homing bool

If True, axes homing will be performed upon CNC object instantiation [RECOMMENDED].

x_lims (int, int)

The allowed range of X-axis positions.

y_lims (int, int)

The allowed range of Y-axis positions.

z_lims (int, int)

The allowed range of Z-axis positions.

serial_port Serial

The Serial instance used to send commands to the Grbl.

x int

The current position, in millimeter, of the CNC arm on the X-axis.

y int

The current position, in millimeter, of the CNC arm on the Y-axis.

z int

The current position, in millimeter, of the CNC arm on the Z-axis.

invert_x bool

If True, "mirror" the coordinates direction respectively to 0.

invert_y bool

If True, "mirror" the coordinates direction respectively to 0.

invert_z bool

If True, "mirror" the coordinates direction respectively to 0.

References

http://linuxcnc.org/docs/html/gcode/g-code.html

See Also

plantimager.hal.AbstractCNC

Examples:

>>> from plantimager.grbl import CNC
>>> cnc = CNC("/dev/ttyACM0",x_lims=[0, 780],y_lims=[0, 780],z_lims=[0, 90])
>>> cnc.moveto(200, 200, 50)  # move the CNC to this XYZ coordinate (in mm)

Constructor.

Parameters:

Name Type Description Default
port str

Serial port to use for communication with the CNC controller. This can also be a regular expression to identify in a unique way the corresponding port. Defaults to "/dev/ttyUSB0".

'/dev/ttyUSB0'
baudrate int

Communication baud rate, 115200 should work with the Arduino UNO.

115200
homing bool

If True (default), axes homing will be performed upon CNC object instantiation [RECOMMENDED].

True
safe_start bool

If True, check the object have been initialized with proper axes limits.

True
x_lims (int, int)

The allowed range of X-axis positions, if None (default) use the settings from Grbl ("$130", see GRBL_SETTINGS).

None
y_lims (int, int)

The allowed range of Y-axis positions, if None (default) use the settings from Grbl ("$131", see GRBL_SETTINGS).

None
z_lims (int, int)

The allowed range of Z-axis positions, if None (default) use the settings from Grbl ("$132", see GRBL_SETTINGS).

None
invert_x bool

If True (default), "mirror" the coordinates direction respectively to 0.

True
invert_y bool

If True (default), "mirror" the coordinates direction respectively to 0.

True
invert_z bool

If True (default), "mirror" the coordinates direction respectively to 0.

True

Examples:

>>> from plantimager.grbl import CNC
>>> cnc = CNC("Arduino",x_lims=[0, 780],y_lims=[0, 780],z_lims=[0, 90])
>>> cnc.moveto(200, 200, 50)  # move the CNC to this XYZ coordinate (in mm)
>>> cnc.home()  # homing command (automatically called on startup)
>>> cnc.moveto_async(200, 200, 50)
>>> cnc.send_cmd("$$")  # send a Grbl command, here "$$"
>>> cnc.print_grbl_settings()  # Get Grbl settings from the firmware
>>> cnc.stop()  # close the serial connection
Source code in plantimager/grbl.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
def __init__(self, port="/dev/ttyUSB0", baudrate=115200, homing=True, safe_start=True,
             x_lims=None, y_lims=None, z_lims=None, invert_x=True, invert_y=True, invert_z=True):
    """Constructor.

    Parameters
    ----------
    port : str, optional
        Serial port to use for communication with the CNC controller.
        This can also be a regular expression to identify in a unique way the corresponding port.
        Defaults to `"/dev/ttyUSB0"`.
    baudrate : int, optional
        Communication baud rate, `115200` should work with the Arduino UNO.
    homing : bool, optional
        If `True` (default), axes homing will be performed upon CNC object instantiation [RECOMMENDED].
    safe_start : bool, optional
        If ``True``, check the object have been initialized with proper axes limits.
    x_lims : (int, int), optional
        The allowed range of X-axis positions, if `None` (default) use the settings from Grbl ("$130", see GRBL_SETTINGS).
    y_lims : (int, int), optional
        The allowed range of Y-axis positions, if `None` (default) use the settings from Grbl ("$131", see GRBL_SETTINGS).
    z_lims : (int, int), optional
        The allowed range of Z-axis positions, if `None` (default) use the settings from Grbl ("$132", see GRBL_SETTINGS).
    invert_x : bool, optional
        If `True` (default), "mirror" the coordinates direction respectively to 0.
    invert_y : bool, optional
        If `True` (default), "mirror" the coordinates direction respectively to 0.
    invert_z : bool, optional
        If `True` (default), "mirror" the coordinates direction respectively to 0.

    Examples
    --------
    >>> from plantimager.grbl import CNC
    >>> cnc = CNC("Arduino",x_lims=[0, 780],y_lims=[0, 780],z_lims=[0, 90])
    >>> cnc.moveto(200, 200, 50)  # move the CNC to this XYZ coordinate (in mm)
    >>> cnc.home()  # homing command (automatically called on startup)
    >>> cnc.moveto_async(200, 200, 50)
    >>> cnc.send_cmd("$$")  # send a Grbl command, here "$$"
    >>> cnc.print_grbl_settings()  # Get Grbl settings from the firmware
    >>> cnc.stop()  # close the serial connection

    """
    super().__init__()
    self.port = port if port.startswith('/dev') else guess_port(port)
    self.baudrate = baudrate
    self.x_lims = x_lims
    self.y_lims = y_lims
    self.z_lims = z_lims
    self.invert_x = invert_x
    self.invert_y = invert_y
    self.invert_z = invert_z
    self.serial_port = None
    self.x = 0
    self.y = 0
    self.z = 0
    self.grbl_settings = None
    self._start(homing, safe_start)
    atexit.register(self.stop)

_check_axes_limits Link

_check_axes_limits(axis_limits, grbl_limits, axis_name)

Make sure given axe_limits are within grbl_limits (firmware limits).

Parameters:

Name Type Description Default
axis_limits [float, float]

Axis limits to use.

required
grbl_limits [float, float]

Limits knwon to Grbl firmaware ("$130", "$131" & "$132" in GRBL_SETTINGS)

required
axis_name str

Name of the axis currently checked.

required
See Also

GRBL_SETTINGS

Raises:

Type Description
ValueError

If given axe_limits do not respect grbl_limits.

Examples:

>>> from plantimager.grbl import CNC
>>> cnc = CNC("/dev/ttyACM0")
>>> grbl = cnc.get_grbl_settings()
>>> print(f"Grbl axes limits are: X=[0, {grbl['$130']}], Y=[0, {grbl['$131']}], Z=[0, {grbl['$132']}]")
>>> wrong_cnc = CNC("/dev/ttyACM0",x_lims=[-1, 780],y_lims=[0, 780],z_lims=[0, 90])
Source code in plantimager/grbl.py
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
def _check_axes_limits(self, axis_limits, grbl_limits, axis_name):
    """Make sure given `axe_limits` are within `grbl_limits` (firmware limits).

    Parameters
    ----------
    axis_limits : [float, float]
        Axis limits to use.
    grbl_limits : [float, float]
        Limits knwon to Grbl firmaware ("$130", "$131" & "$132" in ``GRBL_SETTINGS``)
    axis_name : str
        Name of the axis currently checked.

    See Also
    --------
    GRBL_SETTINGS

    Raises
    ------
    ValueError
        If given `axe_limits` do not respect `grbl_limits`.

    Examples
    --------
    >>> from plantimager.grbl import CNC
    >>> cnc = CNC("/dev/ttyACM0")
    >>> grbl = cnc.get_grbl_settings()
    >>> print(f"Grbl axes limits are: X=[0, {grbl['$130']}], Y=[0, {grbl['$131']}], Z=[0, {grbl['$132']}]")
    >>> wrong_cnc = CNC("/dev/ttyACM0",x_lims=[-1, 780],y_lims=[0, 780],z_lims=[0, 90])

    """
    try:
        assert axis_limits[0] >= grbl_limits[0] and axis_limits[1] <= grbl_limits[1]
    except AssertionError:
        msg = f"Given {axis_name}-axis limits are WRONG!\n"
        msg += f"Should be in '{grbl_limits[0]}:{grbl_limits[1]}', but got '{axis_limits[0]}:{axis_limits[1]}'!"
        raise ValueError(msg)
    return None

_check_move Link

_check_move(x, y, z)

Make sure the moveto coordinates are within the axes limits.

Source code in plantimager/grbl.py
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
def _check_move(self, x, y, z):
    """ Make sure the `moveto` coordinates are within the axes limits."""
    try:
        assert self.x_lims[0] <= x <= self.x_lims[1]
    except AssertionError:
        raise ValueError("Move command coordinates is outside the x-limits!")
    try:
        assert self.y_lims[0] <= y <= self.y_lims[1]
    except AssertionError:
        raise ValueError("Move command coordinates is outside the y-limits!")
    try:
        assert self.z_lims[0] <= z <= self.z_lims[1]
    except AssertionError:
        raise ValueError("Move command coordinates is outside the z-limits!")
    return None

_start Link

_start(homing=True, safe_start=True)

Start the serial connection with the Arduino & initialize the CNC (hardware).

Parameters:

Name Type Description Default
homing bool

If True, performs homing procedure.

True
safe_start bool

If True, check the object have been initialized with proper axes limits.

True
References

http://linuxcnc.org/docs/html/gcode/g-code.html#gcode:g90-g91 http://linuxcnc.org/docs/html/gcode/g-code.html#gcode:g20-g21

Source code in plantimager/grbl.py
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def _start(self, homing=True, safe_start=True):
    """Start the serial connection with the Arduino & initialize the CNC (hardware).

    Parameters
    ----------
    homing : bool
        If ``True``, performs homing procedure.
    safe_start : bool
        If ``True``, check the object have been initialized with proper axes limits.

    References
    ----------
    http://linuxcnc.org/docs/html/gcode/g-code.html#gcode:g90-g91
    http://linuxcnc.org/docs/html/gcode/g-code.html#gcode:g20-g21

    """
    self.serial_port = serial.Serial(self.port, self.baudrate, timeout=10)
    self.has_started = True
    self.serial_port.write("\r\n\r\n".encode())
    time.sleep(2)
    self.serial_port.flushInput()
    # Performs homing procedure if required:
    if homing:
        self.home()
    # Set to "absolute distance mode":
    self.send_cmd("g90")
    # Use millimeters for length units:
    self.send_cmd("g21")

    if safe_start:
        # Initialize axes limits with Grbl settings if not set, else check given settings:
        self.grbl_settings = self.get_grbl_settings()
        if self.x_lims is None:
            self.x_lims = [0, self.grbl_settings["$130"]]
        else:
            self._check_axes_limits(self.x_lims, [0, self.grbl_settings["$130"]], 'X')
        if self.y_lims is None:
            self.y_lims = [0, self.grbl_settings["$131"]]
        else:
            self._check_axes_limits(self.y_lims, [0, self.grbl_settings["$131"]], 'Y')
        if self.z_lims is None:
            self.z_lims = [0, self.grbl_settings["$132"]]
        else:
            self._check_axes_limits(self.z_lims, [0, self.grbl_settings["$132"]], 'Z')
    return None

get_grbl_settings Link

get_grbl_settings()

Returns the Grbl settings as a dictionary {'param': value}.

Source code in plantimager/grbl.py
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
def get_grbl_settings(self) -> dict:
    """ Returns the Grbl settings as a dictionary {'param': value}."""
    self.serial_port.reset_input_buffer()
    self.serial_port.write(("$$" + "\n").encode())
    str_settings = self.serial_port.readlines()
    settings = {}
    for line in str_settings:
        line = line.strip()  # remove potential leading and trailing whitespace & eol
        line = line.decode()
        if not line.startswith('$'):
            # All params are prefixed with a dollar sign '$'
            continue
        param, value = line.split("=")
        try:
            settings[param] = int(value)
        except ValueError:
            settings[param] = float(value)

    logger.info("Grbl settings loaded from firmware!")
    return settings

get_position Link

get_position()

Returns the x, y & z positions of the CNC.

Source code in plantimager/grbl.py
277
278
279
def get_position(self):
    """Returns the x, y & z positions of the CNC."""
    return self.x, self.y, self.z

get_status Link

get_status()

Returns Grbl status.

Source code in plantimager/grbl.py
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
def get_status(self) -> dict:
    """ Returns Grbl status."""
    self.serial_port.write("?".encode("utf-8"))
    try:
        res = self.serial_port.readline()
        res = res.decode("utf-8")
        res = res[1:-1]
        res = res.split('|')
        print(res)
        res_fmt = {}
        res_fmt['status'] = res[0]
        pos = res[1].split(':')[-1].split(',')
        pos = [-float(p) for p in pos]  # why - ?
        res_fmt['position'] = pos
    except:
        return None
    return res_fmt

home Link

home()

Performs axes homing procedure.

References

https://github.com/gnea/grbl/wiki/Grbl-v1.1-Commands#h---run-homing-cycle http://linuxcnc.org/docs/html/gcode/g-code.html#gcode:g92

Source code in plantimager/grbl.py
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
def home(self):
    """Performs axes homing procedure.

    References
    ----------
    https://github.com/gnea/grbl/wiki/Grbl-v1.1-Commands#h---run-homing-cycle
    http://linuxcnc.org/docs/html/gcode/g-code.html#gcode:g92

    """
    # Send Grbl homing command:
    self.send_cmd("$H")
    # self.send_cmd("g28") #reaching workspace origin
    # Set current position to [0, 0, 0] (origin)
    # Note that there is a 'homing pull-off' value ($27)!
    self.send_cmd("g92 x0 y0 z0")
    return None

moveto Link

moveto(x, y, z)

Send a 'G0' move command and wait until reaching target XYZ position.

Parameters:

Name Type Description Default
x int

The target position, in millimeters, along the X-axis.

required
y int

The target position, in millimeters, along the Y-axis.

required
z int

The target position, in millimeters, along the Z-axis.

required
Source code in plantimager/grbl.py
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
def moveto(self, x, y, z):
    """Send a 'G0' move command and wait until reaching target XYZ position.

    Parameters
    ----------
    x : int
        The target position, in millimeters, along the X-axis.
    y : int
        The target position, in millimeters, along the Y-axis.
    z : int
        The target position, in millimeters, along the Z-axis.

    """
    self._check_move(x, y, z)
    self.moveto_async(x, y, z)
    self.wait()
    return None

moveto_async Link

moveto_async(x, y, z)

Send a non-blocking 'G0' move command to target XYZ position.

Parameters:

Name Type Description Default
x int

The target position, in millimeters, along the X-axis.

required
y int

The target position, in millimeters, along the Y-axis.

required
z int

The target position, in millimeters, along the Z-axis.

required
References

http://linuxcnc.org/docs/html/gcode/g-code.html#gcode:g0

Source code in plantimager/grbl.py
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
def moveto_async(self, x, y, z):
    """Send a non-blocking 'G0' move command to target XYZ position.

    Parameters
    ----------
    x : int
        The target position, in millimeters, along the X-axis.
    y : int
        The target position, in millimeters, along the Y-axis.
    z : int
        The target position, in millimeters, along the Z-axis.

    References
    ----------
    http://linuxcnc.org/docs/html/gcode/g-code.html#gcode:g0

    """
    x = int(-x) if self.invert_x else int(x)
    y = int(-y) if self.invert_y else int(y)
    z = int(-z) if self.invert_z else int(z)
    self.send_cmd(f"g0 x{x} y{y} z{z}")
    self.x, self.y, self.z = x, y, z
    time.sleep(0.1)  # Add a little sleep between calls
    return None

print_grbl_settings Link

print_grbl_settings()

Print the Grbl settings.

See Also

GRBL_SETTINGS

References

https://github.com/gnea/grbl/wiki/Grbl-v1.1-Configuration#grbl-settings

Source code in plantimager/grbl.py
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
def print_grbl_settings(self):
    """ Print the Grbl settings.

    See Also
    --------
    GRBL_SETTINGS

    References
    ----------
    https://github.com/gnea/grbl/wiki/Grbl-v1.1-Configuration#grbl-settings

    """
    settings = self.get_grbl_settings()
    print("Obtained Grbl settings:")
    for param, value in settings.items():
        param_name, param_unit = GRBL_SETTINGS[param]
        if param_unit in ['boolean', 'mask']:
            param_unit = f"({param_unit})"
        print(f" - ({param}) {param_name}: {value} {param_unit}")
    return None

send_cmd Link

send_cmd(cmd)

Send given command to Grbl.

Parameters:

Name Type Description Default
cmd str

A Grbl compatible command.

required
References

https://github.com/gnea/grbl/wiki/Grbl-v1.1-Commands

Source code in plantimager/grbl.py
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
def send_cmd(self, cmd):
    """ Send given command to Grbl.

    Parameters
    ----------
    cmd : str
        A Grbl compatible command.

    References
    ----------
    https://github.com/gnea/grbl/wiki/Grbl-v1.1-Commands

    """
    self.serial_port.reset_input_buffer()
    logger.debug(f"{cmd} -> cnc")
    self.serial_port.write((cmd + "\n").encode())
    grbl_out = self.serial_port.readline()
    logger.debug(f"cnc -> {grbl_out.strip()}")
    time.sleep(0.1)
    return grbl_out

stop Link

stop()

Close the serial connection.

Source code in plantimager/grbl.py
271
272
273
274
275
def stop(self):
    """Close the serial connection."""
    if (self.has_started):
        self.serial_port.close()
    return None

wait Link

wait()

Send a 1-second wait (dwell) command to Grbl.

References

http://linuxcnc.org/docs/html/gcode/g-code.html#gcode:g4

Source code in plantimager/grbl.py
360
361
362
363
364
365
366
367
368
369
def wait(self):
    """ Send a 1-second wait (dwell) command to Grbl.

    References
    ----------
    http://linuxcnc.org/docs/html/gcode/g-code.html#gcode:g4

    """
    self.send_cmd("g4 p1")
    return None