kopia lustrzana https://github.com/adliechty/realWorldGcodeSender
1161 wiersze
43 KiB
Python
1161 wiersze
43 KiB
Python
"""
|
|
Gerbil - Copyright (c) 2015 Michael Franzl
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a
|
|
copy of this software and associated documentation files (the "Software"),
|
|
to deal in the Software without restriction, including without limitation
|
|
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
and/or sell copies of the Software, and to permit persons to whom the
|
|
Software is furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included
|
|
in all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
OTHER DEALINGS IN THE SOFTWARE.
|
|
"""
|
|
|
|
import logging
|
|
import time
|
|
import re
|
|
import threading
|
|
import atexit
|
|
import os
|
|
import collections
|
|
|
|
from queue import Queue
|
|
|
|
from interface import Interface
|
|
from callbackloghandler import CallbackLogHandler
|
|
|
|
from gcode_machine import GcodeMachine
|
|
|
|
class Gerbil:
|
|
""" A universal Grbl CNC firmware interface module for Python3
|
|
providing a convenient high-level API for scripting or integration
|
|
into parent applications like GUI's.
|
|
|
|
There are a number of streaming applications available for the Grbl
|
|
CNC controller, but none of them seem to be an universal, re-usable
|
|
standard Python module. Gerbil attempts to fill that gap.
|
|
|
|
See README for usage examples.
|
|
|
|
Gerbil is a name of a cute desert rodent. We chose the name due to
|
|
its similarity to the name "Grbl".
|
|
|
|
Features:
|
|
|
|
* Re-usable across projects
|
|
* Non-blocking
|
|
* Asynchronous (event-based) callbacks for the parent application
|
|
* Two streaming modes: Incremental or fast ("counting characters")
|
|
* Defined shutdown
|
|
* G-Code cleanup
|
|
* G-Code variable expansion
|
|
* Dynamic feed override
|
|
* Buffer stashing
|
|
* Job halt and resume
|
|
|
|
Callbacks:
|
|
|
|
After assigning your own callback function (callback = ...) you will receive the following signals:
|
|
|
|
on_boot
|
|
: Emitted whenever Grbl boots (e.g. after a soft reset).
|
|
: No arguments.
|
|
|
|
on_disconnected
|
|
: Emitted whenever the serial port has been closed.
|
|
: No arguments
|
|
|
|
on_log
|
|
: Emitted for informal logging or debugging messages.
|
|
: 1 argument: LogRecord instance
|
|
|
|
on_line_sent
|
|
: Emitted whenever a line is actually sent to Grbl.
|
|
: 2 arguments: job_line_number, line
|
|
|
|
on_bufsize_change
|
|
: Emitted whenever lines have been appended to the buffer
|
|
: 1 argument: linecount
|
|
|
|
on_line_number_change
|
|
: Emitted whenever the current buffer position has been changed
|
|
: 1 argument: line_number
|
|
|
|
on_processed_command
|
|
: Emitted whenever Grbl confirms a command with "ok" and is now being executed physically
|
|
: 2 arguments: processed line number, processed line
|
|
|
|
on_alarm
|
|
: Emitted whenever Grbl sends an "ALARM" line
|
|
: 1 argument: the full line Grbl sent
|
|
|
|
on_error
|
|
: Emitted whenever Grbl sends an "ERROR" line
|
|
: 3 arguments: the full line Grbl sent, the line that caused the error, the line number in the buffer that caused the error
|
|
|
|
on_rx_buffer_percent
|
|
: Reports Grbl's serial receive buffer fill in percent. Emitted frequently while streaming.
|
|
: 1 argument: percentage integer from 0 to 100
|
|
|
|
on_progress_percent
|
|
: Reports the completion of the current job/buffer in percent. Emitted frequently while streaming.
|
|
: 1 argument: percentage integer from 0 to 100
|
|
|
|
on_job_completed
|
|
: Emitted when the current job/buffer has been streamed and physically executed entirely
|
|
|
|
on_stateupdate
|
|
: Emitted whenever Grbl's state has changed
|
|
: 3 arguments: Grbl's mode ('Idle', 'Run' etc.), machine position tuple, working position tupe
|
|
|
|
on_hash_stateupdate
|
|
: Emitted after Grbl's 'hash' EEPROM settings (`$#`) have been received
|
|
: 1 argument: dict of the settings
|
|
|
|
on_settings_downloaded
|
|
: Emitted after Grbl's EEPROM settings (`$$`) have been received
|
|
: 1 argument: dict of the settings
|
|
|
|
on_gcode_parser_stateupdate
|
|
: Emitted after Grbl's G-Code parser state has been received
|
|
: 1 argument: list of the state variables
|
|
|
|
on_simulation_finished
|
|
: Emitted when Gerbil's target is set to "simulator" and the job is executed.
|
|
: 1 argument: list of all G-Code commands that would have been sent to Grbl
|
|
|
|
on_vars_change
|
|
: Emitted after G-Code is loaded into the buffer and variables have been detected
|
|
: 1 argument: a dict of the detected variables
|
|
|
|
on_preprocessor_feed_change
|
|
: Emitted when a F keyword is parsed from the G-Code.
|
|
: 1 argument: the feed rate in mm/min
|
|
"""
|
|
|
|
__version__ = "0.5.0"
|
|
|
|
def __init__(self, callback, name="mygrbl"):
|
|
"""Straightforward initialization tasks.
|
|
|
|
@param callback
|
|
Set your own function that will be called when a number of
|
|
asynchronous events happen. Useful for UI's. The
|
|
default function will just log to stdout.
|
|
|
|
This callback function will receive two arguments. The first
|
|
is a string giving a label of the event, and the second is a variable
|
|
argument list `*args` containing data pertaining to the event.
|
|
|
|
Note that this function may be called from a Thread.
|
|
|
|
@param name
|
|
An informal name of the instance. Useful if you are running
|
|
several instances to control several CNC machines at once.
|
|
It is only used for logging output and UI messages.
|
|
"""
|
|
|
|
## @var name
|
|
# Set an informal name of the instance. Useful if you are
|
|
# running several instances to control several CNC machines at
|
|
# once. It is only used for logging output and UI messages.
|
|
self.name = name
|
|
|
|
## @var cmode
|
|
# Get Grbl's current mode.
|
|
# Will be strings 'Idle', 'Check', 'Run'
|
|
self.cmode = None
|
|
|
|
## @var cmpos
|
|
# Get a 3-tuple containing the current coordinates relative
|
|
# to the machine origin.
|
|
self.cmpos = (0, 0, 0)
|
|
|
|
## @var cwpos
|
|
# Get a 3-tuple containing the current working coordinates.
|
|
# Working coordinates are relative to the currently selected
|
|
# coordinate system.
|
|
self.cwpos = (0, 0, 0)
|
|
|
|
## @var gps
|
|
# Get list of 12 elements containing the 12 Gcode Parser State
|
|
# variables of Grbl which are obtained by sending the raw
|
|
# command `$G`. Will be available after setting
|
|
# `hash_state_requested` to True.
|
|
self.gps = [
|
|
"0", # motion mode
|
|
"54", # current coordinate system
|
|
"17", # current plane mode
|
|
"21", # units
|
|
"90", # current distance mode
|
|
"94", # feed rate mode
|
|
"0", # program mode
|
|
"0", # spindle state
|
|
"5", # coolant state
|
|
"0", # tool number
|
|
"99", # current feed
|
|
"0", # spindle speed
|
|
]
|
|
|
|
## @var poll_interval
|
|
# Set an interval in seconds for polling Grbl's state via
|
|
# the `?` command. The Grbl Wiki recommends to set this no lower
|
|
# than 0.2 (5 per second).
|
|
self.poll_interval = 0.2
|
|
|
|
## @var settings
|
|
# Get a dictionary of Grbl's EEPROM settings which can be read
|
|
# after sending the `$$` command, or more conveniently after
|
|
# calling the method `request_settings()` of this class.
|
|
self.settings = {
|
|
130: { "val": "1000", "cmt": "width" },
|
|
131: { "val": "1000", "cmt": "height" }
|
|
}
|
|
|
|
## @var settings_hash
|
|
# Get a dictionary of Grbl's 'hash' settings (also stored in the
|
|
# EEPROM) which can be read after sending the `$#` command. It
|
|
# contains things like coordinate system offsets. See Grbl
|
|
# documentation for more info. Will be available shortly after
|
|
# setting `hash_state_requested` to `True`.
|
|
self.settings_hash = {
|
|
"G54": (-600, -300, 0),
|
|
"G55": (-400, -300, 0),
|
|
"G56": (-200, -300, 0),
|
|
"G57": (-600, -600, 0),
|
|
"G58": (-400, -600, 0),
|
|
"G59": (-200, -600, 0),
|
|
"G28": (0, 0, 0),
|
|
"G30": (0, 0, 0),
|
|
"G92": (0, 0, 0),
|
|
"TLO": 0,
|
|
"PRB": (0, 0, 0),
|
|
}
|
|
|
|
## @var gcode_parser_state_requested
|
|
# Set this variable to `True` to receive a callback with the
|
|
# event string "on_gcode_parser_stateupdate" containing
|
|
# data that Grbl sends after receiving the `$G` command.
|
|
# After the callback, this variable reverts to `False`.
|
|
self.gcode_parser_state_requested = False
|
|
|
|
## @var hash_state_requested
|
|
# Set this variable to `True` to receive a callback with the
|
|
# event string "on_hash_stateupdate" containing
|
|
# the requested data. After the callback, this variable reverts
|
|
# to `False`.
|
|
self.hash_state_requested = False
|
|
|
|
|
|
|
|
## @var logger
|
|
# The logger used by this class. The default is Python's own
|
|
# logger module. Use `setup_logging()` to attach custom
|
|
# log handlers.
|
|
self.logger = logging.getLogger("gerbil")
|
|
self.logger.setLevel(5)
|
|
self.logger.propagate = False
|
|
|
|
## @var target
|
|
# Set this to change the output target. Default is "firmware"
|
|
# which means the serial port. Another target is "simulator",
|
|
# you will receive a callback with even string
|
|
# "on_simulation_finished" and a buffer of the G-Code commands
|
|
# that would have been sent out to Grbl.
|
|
# TODO: Add "file" target.
|
|
self.target = "firmware"
|
|
|
|
## @var connected
|
|
# `True` when connected to Grbl (after boot), otherwise `False`
|
|
self.connected = False
|
|
|
|
## @var preprocessor
|
|
# All G-code commands will go through the preprocessor
|
|
# before they are sent out via the serial port. The preprocessor
|
|
# keeps track of, and can dynamically change, feed rates, as well
|
|
# as substitute variables. It has its own state and callback
|
|
# functions.
|
|
self.preprocessor = GcodeMachine()
|
|
self.preprocessor.callback = self._preprocessor_callback
|
|
|
|
## @var travel_dist_buffer
|
|
# The total distance of all G-Codes in the buffer.
|
|
self.travel_dist_buffer = {}
|
|
|
|
## @var travel_dist_current
|
|
# The currently travelled distance. Can be used to calculate ETA.
|
|
self.travel_dist_current = {}
|
|
|
|
## @var is_standstill
|
|
# If the machine is currently not moving
|
|
self.is_standstill = False
|
|
|
|
self._ifacepath = None
|
|
self._last_setting_number = 132
|
|
|
|
self._last_cmode = None
|
|
self._last_cmpos = (0, 0, 0)
|
|
self._last_cwpos = (0, 0, 0)
|
|
|
|
self._standstill_watchdog_increment = 0
|
|
|
|
self._rx_buffer_size = 128
|
|
self._rx_buffer_fill = []
|
|
self._rx_buffer_backlog = []
|
|
self._rx_buffer_backlog_line_number = []
|
|
self._rx_buffer_fill_percent = 0
|
|
|
|
self._current_line = ""
|
|
self._current_line_sent = True
|
|
self._streaming_mode = None
|
|
self._wait_empty_buffer = False
|
|
self.streaming_complete = True
|
|
self.job_finished = True
|
|
self._streaming_src_end_reached = True
|
|
self._streaming_enabled = True
|
|
self._error = False
|
|
self._incremental_streaming = False
|
|
self._hash_state_sent = False
|
|
|
|
self.buffer = []
|
|
self.buffer_size = 0
|
|
self._current_line_nr = 0
|
|
|
|
self.buffer_stash = []
|
|
self.buffer_size_stash = 0
|
|
self._current_line_nr_stash = 0
|
|
|
|
self._poll_keep_alive = False
|
|
self._iface_read_do = False
|
|
|
|
self._thread_polling = None
|
|
self._thread_read_iface = None
|
|
|
|
self._iface = None
|
|
self._queue = Queue()
|
|
|
|
self._loghandler = None
|
|
|
|
self._counter = 0 # general-purpose counter for timing tasks inside of _poll_state
|
|
|
|
self._callback = callback
|
|
|
|
atexit.register(self.disconnect)
|
|
|
|
# supply defaults to GUI to make it operational
|
|
self._callback("on_settings_downloaded", self.settings)
|
|
self._callback("on_hash_stateupdate", self.settings_hash)
|
|
self.preprocessor.cs_offsets = self.settings_hash
|
|
self._callback("on_gcode_parser_stateupdate", self.gps)
|
|
|
|
|
|
def setup_logging(self, handler=None):
|
|
"""Assign a custom log handler.
|
|
|
|
Gerbil can be used in both console applications as well as
|
|
integrated in other projects like GUI's. Therefore, logging to
|
|
stdout is not always useful. You can pass a custom log message
|
|
handler to this method. If no handler is passed in, the default
|
|
handler is an instance of class `CallbackLogHandler`
|
|
(see file `callback_loghandler.py` included in this module).
|
|
CallbackLogHandler will deliver logged strings as callbacks to
|
|
the parent application, the event string will be "on_log".
|
|
|
|
@param handler=None
|
|
An instance of a subclass inheriting from `logging.StreamHandler`
|
|
"""
|
|
|
|
if handler:
|
|
self._loghandler = handler
|
|
else:
|
|
# The default log handler shipped with this module will call
|
|
# self._callback() with first parameter "on_log" and second
|
|
# parameter with the logged string.
|
|
lh = CallbackLogHandler()
|
|
self._loghandler = lh
|
|
|
|
# attach the selected log handler
|
|
self.logger.addHandler(self._loghandler)
|
|
self._loghandler.callback = self._callback
|
|
|
|
|
|
def cnect(self, path=None, baudrate=115200):
|
|
"""
|
|
Connect to the RS232 port of the Grbl controller.
|
|
|
|
@param path=None Path to the device node
|
|
|
|
This is done by instantiating a RS232 class, included in this
|
|
module, which by itself block-listens (in a thread) to
|
|
asynchronous data sent by the Grbl controller.
|
|
"""
|
|
if path == None or path.strip() == "":
|
|
return
|
|
else:
|
|
self._ifacepath = path
|
|
|
|
if self._iface == None:
|
|
self.logger.debug("{}: Setting up interface on {}".format(self.name, self._ifacepath))
|
|
self._iface = Interface("iface_" + self.name, self._ifacepath, baudrate)
|
|
self._iface.start(self._queue)
|
|
else:
|
|
self.logger.info("{}: Cannot start another interface. There is already an interface {}.".format(self.name, self._iface))
|
|
|
|
self._iface_read_do = True
|
|
self._thread_read_iface = threading.Thread(target=self._onread)
|
|
self._thread_read_iface.start()
|
|
|
|
self.softreset()
|
|
|
|
|
|
def disconnect(self):
|
|
"""
|
|
This method provides a controlled shutdown and cleanup of this
|
|
module.
|
|
|
|
It stops all threads, joins them, then closes the serial
|
|
connection. For a safe shutdown of Grbl you may also want to
|
|
call `softreset()` before you call this method.
|
|
"""
|
|
if self.is_connected() == False: return
|
|
|
|
self.poll_stop()
|
|
|
|
self._iface.stop()
|
|
self._iface = None
|
|
|
|
self.logger.debug("{}: Please wait until reading thread has joined...".format(self.name))
|
|
self._iface_read_do = False
|
|
self._queue.put("dummy_msg_for_joining_thread")
|
|
self._thread_read_iface.join()
|
|
self.logger.debug("{}: Reading thread successfully joined.".format(self.name))
|
|
|
|
self.connected = False
|
|
|
|
self._callback("on_disconnected")
|
|
|
|
def softreset(self):
|
|
"""
|
|
Immediately sends `Ctrl-X` to Grbl.
|
|
"""
|
|
self._iface.write("\x18") # Ctrl-X
|
|
self.update_preprocessor_position()
|
|
|
|
|
|
def abort(self):
|
|
"""
|
|
An alias for `softreset()`.
|
|
"""
|
|
if self.is_connected() == False: return
|
|
self.softreset()
|
|
|
|
|
|
def hold(self):
|
|
"""
|
|
Immediately sends the feed hold command (exclamation mark)
|
|
to Grbl.
|
|
"""
|
|
if self.is_connected() == False: return
|
|
self._iface_write("!")
|
|
|
|
|
|
def resume(self):
|
|
"""
|
|
Immediately send the resume command (tilde) to Grbl.
|
|
"""
|
|
if self.is_connected() == False: return
|
|
self._iface_write("~")
|
|
|
|
|
|
def killalarm(self):
|
|
"""
|
|
Immediately send the kill alarm command ($X) to Grbl.
|
|
"""
|
|
self._iface_write("$X\n")
|
|
|
|
|
|
def homing(self):
|
|
"""
|
|
Immediately send the homing command ($H) to Grbl.
|
|
"""
|
|
self._iface_write("$H\n")
|
|
|
|
|
|
def poll_start(self):
|
|
"""
|
|
Starts forever polling Grbl's status with the `?` command. The
|
|
polling interval is controlled by setting `self.poll_interval`.
|
|
You will receive callbacks with the "on_stateupdate" event
|
|
string containing 3 data parameters self.cmode, self.cmpos,
|
|
self.cwpos, but only when Grbl's state CHANGES.
|
|
"""
|
|
if self.is_connected() == False: return
|
|
self._poll_keep_alive = True
|
|
self._last_cmode = None
|
|
if self._thread_polling == None:
|
|
self._thread_polling = threading.Thread(target=self._poll_state)
|
|
self._thread_polling.start()
|
|
self.logger.debug("{}: Polling thread started".format(self.name))
|
|
else:
|
|
self.logger.debug("{}: Polling thread already running...".format(self.name))
|
|
|
|
|
|
def poll_stop(self):
|
|
"""
|
|
Stops polling that has been started with `poll_start()`
|
|
"""
|
|
if self.is_connected() == False: return
|
|
if self._thread_polling != None:
|
|
self._poll_keep_alive = False
|
|
self.logger.debug("{}: Please wait until polling thread has joined...".format(self.name))
|
|
self._thread_polling.join()
|
|
self.logger.debug("{}: Polling thread has successfully joined...".format(self.name))
|
|
else:
|
|
self.logger.debug("{}: Cannot start a polling thread. Another one is already running.".format(self.name))
|
|
|
|
self._thread_polling = None
|
|
|
|
|
|
def set_feed_override(self, val):
|
|
"""
|
|
Enable or disable the feed override feature.
|
|
|
|
@param val
|
|
Pass `True` or `False` as argument to enable or disable dynamic
|
|
feed override. After passing `True`, you may set the
|
|
requested feed by calling `self.request_feed()` one or many
|
|
times.
|
|
"""
|
|
self.preprocessor.do_feed_override = val
|
|
|
|
|
|
def request_feed(self, requested_feed):
|
|
"""
|
|
Override the feed speed. Effecive only when you set `set_feed_override(True)`.
|
|
|
|
@param requested_feed
|
|
The feed speed in mm/min.
|
|
"""
|
|
self.preprocessor.request_feed = float(requested_feed)
|
|
|
|
|
|
@property
|
|
def incremental_streaming(self):
|
|
return self._incremental_streaming
|
|
|
|
@incremental_streaming.setter
|
|
def incremental_streaming(self, onoff):
|
|
"""
|
|
Incremental streaming means that a new command is sent to Grbl
|
|
only after Grbl has responded with 'ok' to the last sent
|
|
command. This is necessary to flash $ settings to the EEPROM.
|
|
|
|
Non-incremental streaming means that Grbl's 100-some-byte
|
|
receive buffer will be kept as full as possible at all times,
|
|
to give its motion planner system enough data to work with.
|
|
This results in smoother and faster axis motion. This is also
|
|
called 'advanced streaming protocol based on counting
|
|
characters' -- see Grbl Wiki.
|
|
|
|
You can dynamically change the streaming method even
|
|
during streaming, while running a job. The buffer fill
|
|
percentage will reflect the change even during streaming.
|
|
|
|
@param onoff
|
|
Set to `True` to use incremental streaming. Set to `False` to
|
|
use non-incremental streaming. The default on module startup
|
|
is `False`.
|
|
"""
|
|
self._incremental_streaming = onoff
|
|
if self._incremental_streaming == True:
|
|
self._wait_empty_buffer = True
|
|
self.logger.debug("{}: Incremental streaming set to {}".format(self.name, self._incremental_streaming))
|
|
|
|
|
|
def send_immediately(self, line):
|
|
"""
|
|
G-Code command strings passed to this function will bypass
|
|
buffer management and will be sent to Grbl immediately.
|
|
Use this function with caution: Only send when you
|
|
are sure Grbl's receive buffer can handle the data volume and
|
|
when it doesn't interfere with currently running streams.
|
|
Only send single commands at a time.
|
|
|
|
Applications of this method: manual jogging, coordinate settings
|
|
etc.
|
|
|
|
@param line
|
|
A string of a single G-Code command to be sent. Doesn't have to
|
|
be \n terminated.
|
|
"""
|
|
bytes_in_firmware_buffer = sum(self._rx_buffer_fill)
|
|
if bytes_in_firmware_buffer > 0:
|
|
self.logger.error("Firmware buffer has {:d} unprocessed bytes in it. Will not send {}".format(bytes_in_firmware_buffer, line))
|
|
return
|
|
|
|
if self.cmode == "Alarm":
|
|
self.logger.error("Grbl is in ALARM state. Will not send {}.".format(line))
|
|
return
|
|
|
|
if self.cmode == "Hold":
|
|
self.logger.error("Grbl is in HOLD state. Will not send {}.".format(line))
|
|
return
|
|
|
|
if "$#" in line:
|
|
# The PRB response is sent for $# as well as when probing.
|
|
# Regular querying of the hash state needs to be done like this,
|
|
# otherwise the PRB response would be interpreted as a probe answer.
|
|
self.hash_state_requested = True
|
|
return
|
|
|
|
self.preprocessor.set_line(line)
|
|
self.preprocessor.strip()
|
|
self.preprocessor.tidy()
|
|
self.preprocessor.parse_state()
|
|
self.preprocessor.override_feed()
|
|
|
|
self._iface_write(self.preprocessor.line + "\n")
|
|
|
|
|
|
def stream(self, lines):
|
|
"""
|
|
A more convenient alias for `write(lines)` and `job_run()`
|
|
|
|
@param lines
|
|
A string of G-Code commands. Each command is \n separated.
|
|
"""
|
|
self._load_lines_into_buffer(lines)
|
|
self.job_run()
|
|
|
|
|
|
def write(self, lines):
|
|
"""
|
|
G-Code command strings passed to this function will be appended
|
|
to the current queue buffer, however a job is not started
|
|
automatically. You have to call `job_run()` to start streaming.
|
|
|
|
You can call this method repeatedly, e.g. for submitting chunks
|
|
of G-Code, even while a job is running.
|
|
|
|
@param lines
|
|
A string of G-Code commands. Each command is \n separated.
|
|
"""
|
|
if type(lines) is list:
|
|
lines = "\n".join(lines)
|
|
|
|
self._load_lines_into_buffer(lines)
|
|
|
|
|
|
def load_file(self, filename):
|
|
"""
|
|
Pass a filename to this function to load its contents into the
|
|
buffer. This only works when Grbl is Idle and the previous job
|
|
has completed. The previous buffer will be cleared. After this
|
|
function has completed, the buffer's contents will be identical
|
|
to the file content. Job is not started automatically.
|
|
Call `job_run` to start the job.
|
|
|
|
@param filename
|
|
A string giving the relative or absolute file path
|
|
"""
|
|
if self.job_finished == False:
|
|
self.logger.warning("{}: Job must be finished before you can load a file".format(self.name))
|
|
return
|
|
|
|
self.job_new()
|
|
|
|
with open(filename) as f:
|
|
self._load_lines_into_buffer(f.read())
|
|
|
|
|
|
def job_run(self, linenr=None):
|
|
"""
|
|
Run the current job, i.e. start streaming the current buffer
|
|
from a specific line number.
|
|
|
|
@param linenr
|
|
If `linenr` is not specified, start streaming from the current
|
|
buffer position (`self.current_line_number`). If `linenr` is specified, start streaming from this line.
|
|
"""
|
|
if self.buffer_size == 0:
|
|
self.logger.warning("{}: Cannot run job. Nothing in the buffer!".format(self.name))
|
|
return
|
|
|
|
if linenr:
|
|
self.current_line_number = linenr
|
|
|
|
self.travel_dist_current = {}
|
|
|
|
#self.preprocessor.current_feed = None
|
|
|
|
self._set_streaming_src_end_reached(False)
|
|
self._set_streaming_complete(False)
|
|
self._streaming_enabled = True
|
|
self._current_line_sent = True
|
|
self._set_job_finished(False)
|
|
self._stream()
|
|
|
|
|
|
def job_halt(self):
|
|
"""
|
|
Stop streaming. Grbl still will continue processing
|
|
all G-Code in its internal serial receive buffer.
|
|
"""
|
|
self._streaming_enabled = False
|
|
|
|
|
|
def job_new(self):
|
|
"""
|
|
Start a new job. A "job" in our terminology means the buffer's
|
|
contents. This function will empty the buffer, set the buffer
|
|
position to 0, and reset internal state.
|
|
"""
|
|
del self.buffer[:]
|
|
self.buffer_size = 0
|
|
self._current_line_nr = 0
|
|
self._callback("on_line_number_change", 0)
|
|
self._callback("on_bufsize_change", 0)
|
|
self._set_streaming_complete(True)
|
|
self.job_finished = True
|
|
self._set_streaming_src_end_reached(True)
|
|
self._error = False
|
|
self._current_line = ""
|
|
self._current_line_sent = True
|
|
self.travel_dist_buffer = {}
|
|
self.travel_dist_current = {}
|
|
|
|
self._callback("on_vars_change", self.preprocessor.vars)
|
|
|
|
@property
|
|
def current_line_number(self):
|
|
return self._current_line_nr
|
|
|
|
@current_line_number.setter
|
|
def current_line_number(self, linenr):
|
|
if linenr < self.buffer_size:
|
|
self._current_line_nr = linenr
|
|
self._callback("on_line_number_change", self._current_line_nr)
|
|
|
|
|
|
def request_settings(self):
|
|
"""
|
|
This will send `$$` to Grbl and you will receive a callback with
|
|
the argument 1 "on_settings_downloaded", and argument 2 a dict
|
|
of the settings.
|
|
"""
|
|
self._iface_write("$$\n")
|
|
|
|
|
|
def do_buffer_stash(self):
|
|
"""
|
|
Stash the current buffer and position away and initialize a
|
|
new job. This is useful if you want to stop the current job,
|
|
stream changed $ settings to Grbl, and then resume the job
|
|
where you left off. See also `self.buffer_unstash()`.
|
|
"""
|
|
self.buffer_stash = list(self.buffer)
|
|
self.buffer_size_stash = self.buffer_size
|
|
self._current_line_nr_stash = self._current_line_nr
|
|
self.job_new()
|
|
|
|
def do_buffer_unstash(self):
|
|
"""
|
|
Restores the previous stashed buffer and position.
|
|
"""
|
|
self.buffer = list(self.buffer_stash)
|
|
self.buffer_size = self.buffer_size_stash
|
|
self.current_line_number = self._current_line_nr_stash
|
|
self._callback("on_bufsize_change", self.buffer_size)
|
|
|
|
|
|
def update_preprocessor_position(self):
|
|
# keep preprocessor informed about current working pos
|
|
self.preprocessor.position_m = list(self.cmpos)
|
|
#self.preprocessor.target = list(self.cmpos)
|
|
|
|
def _preprocessor_callback(self, event, *data):
|
|
if event == "on_preprocessor_var_undefined":
|
|
self.logger.critical("HALTED JOB BECAUSE UNDEFINED VAR {}".format(data[0]))
|
|
self._set_streaming_src_end_reached(True)
|
|
self.job_halt()
|
|
else:
|
|
self._callback(event, *data)
|
|
|
|
def _stream(self):
|
|
if self._streaming_src_end_reached:
|
|
return
|
|
|
|
if self._streaming_enabled == False:
|
|
return
|
|
|
|
if self.target == "firmware":
|
|
if self._incremental_streaming:
|
|
self._set_next_line()
|
|
if self._streaming_src_end_reached == False:
|
|
self._send_current_line()
|
|
else:
|
|
self._set_job_finished(True)
|
|
else:
|
|
self._fill_rx_buffer_until_full()
|
|
|
|
elif self.target == "simulator":
|
|
buf = []
|
|
while self._streaming_src_end_reached == False:
|
|
self._set_next_line(True)
|
|
if self._current_line_nr < self.buffer_size:
|
|
buf.append(self._current_line)
|
|
|
|
# one line still to go
|
|
self._set_next_line(True)
|
|
buf.append(self._current_line)
|
|
|
|
self._set_job_finished(True)
|
|
self._callback("on_simulation_finished", buf)
|
|
|
|
def _fill_rx_buffer_until_full(self):
|
|
while True:
|
|
if self._current_line_sent == True:
|
|
self._set_next_line()
|
|
|
|
if self._streaming_src_end_reached == False and self._rx_buf_can_receive_current_line():
|
|
self._send_current_line()
|
|
else:
|
|
break
|
|
|
|
|
|
def _set_next_line(self, send_comments=False):
|
|
progress_percent = int(100 * self._current_line_nr / self.buffer_size)
|
|
self._callback("on_progress_percent", progress_percent)
|
|
|
|
if self._current_line_nr < self.buffer_size:
|
|
# still something in _buffer, pop it
|
|
line = self.buffer[self._current_line_nr].strip()
|
|
self.preprocessor.set_line(line)
|
|
self.preprocessor.substitute_vars()
|
|
self.preprocessor.parse_state()
|
|
self.preprocessor.override_feed()
|
|
self.preprocessor.scale_spindle()
|
|
|
|
if send_comments == True:
|
|
self._current_line = self.preprocessor.line + self.preprocessor.comment
|
|
else:
|
|
self._current_line = self.preprocessor.line
|
|
|
|
self._current_line_sent = False
|
|
self._current_line_nr += 1
|
|
|
|
self.preprocessor.done()
|
|
|
|
else:
|
|
# the buffer is empty, nothing more to read
|
|
self._set_streaming_src_end_reached(True)
|
|
|
|
def _send_current_line(self):
|
|
if self._error:
|
|
self.logger.error("Firmware reported error. Halting.")
|
|
self._set_streaming_src_end_reached(True)
|
|
self._set_streaming_complete(True)
|
|
return
|
|
|
|
self._set_streaming_complete(False)
|
|
|
|
line_length = len(self._current_line) + 1 # +1 for \n which we will append below
|
|
self._rx_buffer_fill.append(line_length)
|
|
self._rx_buffer_backlog.append(self._current_line)
|
|
self._rx_buffer_backlog_line_number.append(self._current_line_nr)
|
|
self._iface_write(self._current_line + "\n")
|
|
|
|
self._current_line_sent = True
|
|
self._callback("on_line_sent", self._current_line_nr, self._current_line)
|
|
|
|
def _rx_buf_can_receive_current_line(self):
|
|
rx_free_bytes = self._rx_buffer_size - sum(self._rx_buffer_fill)
|
|
required_bytes = len(self._current_line) + 1 # +1 because \n
|
|
return rx_free_bytes >= required_bytes
|
|
|
|
def _rx_buffer_fill_pop(self):
|
|
if len(self._rx_buffer_fill) > 0:
|
|
self._rx_buffer_fill.pop(0)
|
|
processed_command = self._rx_buffer_backlog.pop(0)
|
|
ln = self._rx_buffer_backlog_line_number.pop(0) - 1
|
|
self._callback("on_processed_command", ln, processed_command)
|
|
|
|
if self._streaming_src_end_reached == True and len(self._rx_buffer_fill) == 0:
|
|
self._set_job_finished(True)
|
|
self._set_streaming_complete(True)
|
|
|
|
def _iface_write(self, line):
|
|
self._callback("on_write", line)
|
|
if self._iface:
|
|
num_written = self._iface.write(line)
|
|
|
|
def _onread(self):
|
|
while self._iface_read_do == True:
|
|
line = self._queue.get()
|
|
|
|
if len(line) > 0:
|
|
if line[0] == "<":
|
|
self._update_state(line)
|
|
|
|
elif line == "ok":
|
|
self._handle_ok()
|
|
|
|
elif re.match("^\[G[0123] .*", line):
|
|
self._update_gcode_parser_state(line)
|
|
self._callback("on_read", line)
|
|
|
|
elif re.match("^\[...:.*", line):
|
|
self._update_hash_state(line)
|
|
self._callback("on_read", line)
|
|
|
|
if "PRB" in line:
|
|
# last line
|
|
if self.hash_state_requested == True:
|
|
self._hash_state_sent = False
|
|
self.hash_state_requested = False
|
|
self._callback("on_hash_stateupdate", self.settings_hash)
|
|
self.preprocessor.cs_offsets = self.settings_hash
|
|
else:
|
|
self._callback("on_probe", self.settings_hash["PRB"])
|
|
|
|
|
|
|
|
elif "ALARM" in line:
|
|
self.cmode = "Alarm" # grbl for some reason doesn't respond to ? polling when alarm due to soft limits
|
|
self._callback("on_stateupdate", self.cmode, self.cmpos, self.cwpos)
|
|
self._callback("on_read", line)
|
|
self._callback("on_alarm", line)
|
|
|
|
elif "error" in line:
|
|
#self.logger.debug("ERROR")
|
|
self._error = True
|
|
#self.logger.debug("%s: _rx_buffer_backlog at time of error: %s", self.name, self._rx_buffer_backlog)
|
|
if len(self._rx_buffer_backlog) > 0:
|
|
problem_command = self._rx_buffer_backlog[0]
|
|
problem_line = self._rx_buffer_backlog_line_number[0]
|
|
else:
|
|
problem_command = "unknown"
|
|
problem_line = -1
|
|
self._callback("on_error", line, problem_command, problem_line)
|
|
self._set_streaming_complete(True)
|
|
self._set_streaming_src_end_reached(True)
|
|
|
|
elif "Grbl " in line:
|
|
self._callback("on_read", line)
|
|
self._on_bootup()
|
|
self.hash_state_requested = True
|
|
self.request_settings()
|
|
self.gcode_parser_state_requested = True
|
|
|
|
else:
|
|
m = re.match("\$(.*)=(.*) \((.*)\)", line)
|
|
if m:
|
|
key = int(m.group(1))
|
|
val = m.group(2)
|
|
comment = m.group(3)
|
|
self.settings[key] = {
|
|
"val" : val,
|
|
"cmt" : comment
|
|
}
|
|
self._callback("on_read", line)
|
|
if key == self._last_setting_number:
|
|
self._callback("on_settings_downloaded", self.settings)
|
|
else:
|
|
self._callback("on_read", line)
|
|
#self.logger.info("{}: Could not parse settings: {}".format(self.name, line))
|
|
|
|
def _handle_ok(self):
|
|
if self.streaming_complete == False:
|
|
self._rx_buffer_fill_pop()
|
|
if not (self._wait_empty_buffer and len(self._rx_buffer_fill) > 0):
|
|
self._wait_empty_buffer = False
|
|
self._stream()
|
|
|
|
self._rx_buffer_fill_percent = int(100 - 100 * (self._rx_buffer_size - sum(self._rx_buffer_fill)) / self._rx_buffer_size)
|
|
self._callback("on_rx_buffer_percent", self._rx_buffer_fill_percent)
|
|
|
|
def _on_bootup(self):
|
|
self._onboot_init()
|
|
self.connected = True
|
|
self.logger.debug("{}: Grbl has booted!".format(self.name))
|
|
self._callback("on_boot")
|
|
|
|
def _update_hash_state(self, line):
|
|
line = line.replace("]", "").replace("[", "")
|
|
parts = line.split(":")
|
|
key = parts[0]
|
|
tpl_str = parts[1].split(",")
|
|
tpl = tuple([float(x) for x in tpl_str])
|
|
self.settings_hash[key] = tpl
|
|
|
|
def _update_gcode_parser_state(self, line):
|
|
m = re.match("\[G(\d) G(\d\d) G(\d\d) G(\d\d) G(\d\d) G(\d\d) M(\d) M(\d) M(\d) T(\d) F([\d.-]*?) S([\d.-]*?)\]", line)
|
|
if m:
|
|
self.gps[0] = m.group(1) # motionmode
|
|
self.gps[1] = m.group(2) # current coordinate system
|
|
self.gps[2] = m.group(3) # plane
|
|
self.gps[3] = m.group(4) # units
|
|
self.gps[4] = m.group(5) # dist
|
|
self.gps[5] = m.group(6) # feed rate mode
|
|
self.gps[6] = m.group(7) # program mode
|
|
self.gps[7] = m.group(8) # spindle state
|
|
self.gps[8] = m.group(9) # coolant state
|
|
self.gps[9] = m.group(10) # tool number
|
|
self.gps[10] = m.group(11) # current feed
|
|
self.gps[11] = m.group(12) # current rpm
|
|
self._callback("on_gcode_parser_stateupdate", self.gps)
|
|
|
|
self.update_preprocessor_position()
|
|
else:
|
|
self.logger.error("{}: Could not parse gcode parser report: '{}'".format(self.name, line))
|
|
|
|
def _update_state(self, line):
|
|
m = re.match("<(.*?),MPos:(.*?),WPos:(.*?)>", line)
|
|
self.cmode = m.group(1)
|
|
mpos_parts = m.group(2).split(",")
|
|
wpos_parts = m.group(3).split(",")
|
|
self.cmpos = (float(mpos_parts[0]), float(mpos_parts[1]), float(mpos_parts[2]))
|
|
self.cwpos = (float(wpos_parts[0]), float(wpos_parts[1]), float(wpos_parts[2]))
|
|
|
|
if (self.cmode != self._last_cmode or
|
|
self.cmpos != self._last_cmpos or
|
|
self.cwpos != self._last_cwpos):
|
|
self._callback("on_stateupdate", self.cmode, self.cmpos, self.cwpos)
|
|
if self.streaming_complete == True and self.cmode == "Idle":
|
|
self.update_preprocessor_position()
|
|
self.gcode_parser_state_requested = True
|
|
|
|
|
|
if (self.cmpos != self._last_cmpos):
|
|
if self.is_standstill == True:
|
|
self._standstill_watchdog_increment = 0
|
|
self.is_standstill = False
|
|
self._callback("on_movement")
|
|
else:
|
|
# no change in positions
|
|
self._standstill_watchdog_increment += 1
|
|
|
|
|
|
if self.is_standstill == False and self._standstill_watchdog_increment > 10:
|
|
# machine is not moving
|
|
self.is_standstill = True
|
|
self._callback("on_standstill")
|
|
|
|
self._last_cmode = self.cmode
|
|
self._last_cmpos = self.cmpos
|
|
self._last_cwpos = self.cwpos
|
|
|
|
|
|
def _load_line_into_buffer(self, line):
|
|
self.preprocessor.set_line(line)
|
|
split_lines = self.preprocessor.split_lines()
|
|
|
|
for l1 in split_lines:
|
|
self.preprocessor.set_line(l1)
|
|
self.preprocessor.strip()
|
|
self.preprocessor.tidy()
|
|
self.preprocessor.parse_state()
|
|
self.preprocessor.find_vars()
|
|
fractionized_lines = self.preprocessor.fractionize()
|
|
|
|
for l2 in fractionized_lines:
|
|
self.buffer.append(l2)
|
|
self.buffer_size += 1
|
|
|
|
self.preprocessor.done()
|
|
|
|
def _load_lines_into_buffer(self, string):
|
|
lines = string.split("\n")
|
|
for line in lines:
|
|
self._load_line_into_buffer(line)
|
|
self._callback("on_bufsize_change", self.buffer_size)
|
|
self._callback("on_vars_change", self.preprocessor.vars)
|
|
|
|
def is_connected(self):
|
|
if self.connected != True:
|
|
#self.logger.info("{}: Not yet connected".format(self.name))
|
|
pass
|
|
return self.connected
|
|
|
|
def _onboot_init(self):
|
|
# called after boot. Mimics Grbl's initial state after boot.
|
|
del self._rx_buffer_fill[:]
|
|
del self._rx_buffer_backlog[:]
|
|
del self._rx_buffer_backlog_line_number[:]
|
|
self._set_streaming_complete(True)
|
|
self._set_job_finished(True)
|
|
self._set_streaming_src_end_reached(True)
|
|
self._error = False
|
|
self._current_line = ""
|
|
self._current_line_sent = True
|
|
self._clear_queue()
|
|
self.is_standstill = False
|
|
self.preprocessor.reset()
|
|
self._callback("on_progress_percent", 0)
|
|
self._callback("on_rx_buffer_percent", 0)
|
|
|
|
def _clear_queue(self):
|
|
try:
|
|
junk = self._queue.get_nowait()
|
|
self.logger.debug("Discarding junk %s", junk)
|
|
except:
|
|
#self.logger.debug("Queue was empty")
|
|
pass
|
|
|
|
def _poll_state(self):
|
|
while self._poll_keep_alive:
|
|
self._counter += 1
|
|
|
|
if self.hash_state_requested:
|
|
self.get_hash_state()
|
|
|
|
elif self.gcode_parser_state_requested:
|
|
self.get_gcode_parser_state()
|
|
self.gcode_parser_state_requested = False
|
|
|
|
else:
|
|
self._get_state()
|
|
|
|
time.sleep(self.poll_interval)
|
|
|
|
self.logger.debug("{}: Polling has been stopped".format(self.name))
|
|
|
|
def _get_state(self):
|
|
self._iface.write("?")
|
|
|
|
def get_gcode_parser_state(self):
|
|
self._iface_write("$G\n")
|
|
|
|
def get_hash_state(self):
|
|
if self.cmode == "Hold":
|
|
self.hash_state_requested = False
|
|
self.logger.info("{}: $# command not supported in Hold mode.".format(self.name))
|
|
return
|
|
|
|
if self._hash_state_sent == False:
|
|
self._iface_write("$#\n")
|
|
self._hash_state_sent = True
|
|
|
|
def _set_streaming_src_end_reached(self, a):
|
|
self._streaming_src_end_reached = a
|
|
|
|
def _set_streaming_complete(self, a):
|
|
self.streaming_complete = a
|
|
|
|
def _set_job_finished(self, a):
|
|
self.job_finished = a
|
|
if a == True:
|
|
self._callback("on_job_completed")
|
|
|
|
def _default_callback(self, status, *args):
|
|
print("GERBIL DEFAULT CALLBACK", status, args)
|