diff --git a/inkscape/linux/AxiDraw_395_LinX86.zip b/inkscape/linux/AxiDraw_395_LinX86.zip new file mode 100644 index 0000000..5eb9cba Binary files /dev/null and b/inkscape/linux/AxiDraw_395_LinX86.zip differ diff --git a/inkscape/linux/patch/ebb_serial.py b/inkscape/linux/patch/ebb_serial.py new file mode 100644 index 0000000..71fdb2d --- /dev/null +++ b/inkscape/linux/patch/ebb_serial.py @@ -0,0 +1,453 @@ +# coding=utf-8 +''' +ebb_serial.py +Serial connection utilities for EiBotBoard +https://github.com/evil-mad/plotink + +Intended to provide some common interfaces that can be used by +EggBot, WaterColorBot, AxiDraw, and similar machines. + +See below for version information + +Thanks to Shel Michaels for bug fixes and helpful suggestions. + + +The MIT License (MIT) + +Copyright (c) 2022 Windell H. Oskay, Evil Mad Scientist Laboratories + +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 +from packaging.version import parse + +from .plot_utils_import import from_dependency_import +inkex = from_dependency_import('ink_extensions.inkex') +serial = from_dependency_import('serial') +from serial.tools.list_ports import comports \ + #pylint: disable=wrong-import-position, wrong-import-order + +logger = logging.getLogger(__name__) + +def version(): + '''Version number for this document''' + return "0.19" # Dated 2022-10-05 + + +def findPort(): + ''' + Find first available EiBotBoard by searching USB ports. Return serial port object. + ''' + try: + com_ports_list = list(comports()) + except TypeError: + return None + ebb_port = None + for port in com_ports_list: + if port[0].startswith("/dev/ttyUSB"): + ebb_port = port[0] # Success; EBB found by name match. + #raise Exception(ebb_port) + break # stop searching-- we are done. + for port in com_ports_list: + if port[1].startswith("EiBotBoard"): + ebb_port = port[0] # Success; EBB found by name match. + break # stop searching-- we are done. + if ebb_port is None: + for port in com_ports_list: + if port[2].startswith("USB VID:PID=04D8:FD92"): + ebb_port = port[0] # Success; EBB found by VID/PID match. + break # stop searching-- we are done. + return ebb_port + + +def find_named_ebb(port_name): + ''' + Find a specific EiBotBoard identified by a string giving either: + The enumerated serial port, or + An EBB "Name tag" + Names should be 3-16 characters long. Comparisons are not case sensitive. + (Name tags may assigned with the ST command on firmware 2.5.5 and later.) + If found: Return serial port name (enumeration) + If not found, Return None + ''' + if port_name is not None: + needle = 'SER=' + port_name # pyserial 3 + needle2 = 'SNR=' + port_name # pyserial 2.7 + needle3 = '(' + port_name + ')' # e.g., "(COM4)" + + needle = needle.lower() + needle2 = needle2.lower() + needle3 = needle3.lower() + plower = port_name.lower() + + try: + com_ports_list = list(comports()) + except TypeError: + return None + + for port in com_ports_list: + p_0 = port[0].lower() + p_1 = port[1].lower() + p_2 = port[2].lower() + + if needle in p_2: + return port[0] # Success; EBB found by name match. + if needle2 in p_2: + return port[0] # Success; EBB found by name match. + if needle3 in p_1: + return port[0] # Success; EBB found by port match. + + p_1 = p_1[11:] + if p_1.startswith(plower): + return port[0] # Success; EBB found by name match. + if p_0.startswith(plower): + return port[0] # Success; EBB found by port match. + + needle.replace(" ", "_") # SN on Windows has underscores, not spaces. + if needle in p_2: + return port[0] # Success; EBB found by port match. + + needle2.replace(" ", "_") # SN on Windows has underscores, not spaces. + if needle2 in p_2: + return port[0] # Success; EBB found by port match. + return None + + +def query_nickname(port_name, verbose=True): + ''' + Query the EBB nickname and report it. + If verbose is True or omitted, the result will be human readable. + A short version is returned if verbose is False. + Requires firmware version 2.5.5 or newer. http://evil-mad.github.io/EggBot/ebb.html#QT + ''' + if port_name is not None: + version_status = min_version(port_name, "2.5.5") + + if version_status: + raw_string = (query(port_name, 'QT\r')) + if raw_string.isspace(): + if verbose: + return "This AxiDraw does not have a nickname assigned." + return None + if verbose: + return "AxiDraw nickname: " + raw_string + return str(raw_string).strip() + if version_status is False: + if verbose: + return "AxiDraw naming requires firmware version 2.5.5 or higher." + return None + + +def write_nickname(port_name, nickname): + ''' + Write the EBB nickname. + Requires firmware version 2.5.5 or newer. http://evil-mad.github.io/EggBot/ebb.html#ST + ''' + if port_name is not None: + version_status = min_version(port_name, "2.5.5") + + if version_status: + try: + cmd = 'ST,' + nickname + '\r' + command(port_name,cmd) + return True + except: + return False + return None + + +def reboot(port_name): + ''' + Reboot the EBB, as though it were just powered on. + Requires firmware version 2.5.5 or newer. http://evil-mad.github.io/EggBot/ebb.html#RB + ''' + if port_name is not None: + version_status = min_version(port_name, "2.5.5") + if version_status: + try: + command(port_name,'RB\r') + except: + pass + + +def list_port_info(): + '''Find and return a list of all USB devices and their information.''' + try: + com_ports_list = list(comports()) + except TypeError: + return None + + port_info_list = [] + for port in com_ports_list: + port_info_list.append(port[0]) # port name + port_info_list.append(port[1]) # Identifier + port_info_list.append(port[2]) # VID/PID + if port_info_list: + return port_info_list + return None + + +def listEBBports(): + '''Find and return a list of all EiBotBoard units connected via USB port.''' + + try: + com_ports_list = list(comports()) + except TypeError: + return None + ebb_ports_list = [] + for port in com_ports_list: + port_has_ebb = False + if port[0].startswith("/dev/ttyUSB"): + port_has_ebb = True + elif port[1].startswith("EiBotBoard"): + port_has_ebb = True + elif port[2].startswith("USB VID:PID=04D8:FD92"): + port_has_ebb = True + if port_has_ebb: + ebb_ports_list.append(port) + if ebb_ports_list: + return ebb_ports_list + return None + + +def list_named_ebbs(): + '''Return descriptive list of all EiBotBoard units''' + ebb_ports_list = listEBBports() + if not ebb_ports_list: + return None + ebb_names_list = [] + for port in ebb_ports_list: + name_found = False + p_0 = port[0] + p_1 = port[1] + p_2 = port[2] + if p_1.startswith("EiBotBoard"): + temp_string = p_1[11:] + if temp_string: + if temp_string is not None: + ebb_names_list.append(temp_string) + name_found = True + if not name_found: + # Look for "SER=XXXX LOCAT" pattern, + # typical of Pyserial 3 on Windows. + if 'SER=' in p_2 and ' LOCAT' in p_2: + index1 = p_2.find('SER=') + len('SER=') + index2 = p_2.find(' LOCAT', index1) + temp_string = p_2[index1:index2] + if len(temp_string) < 3: + temp_string = None + if temp_string is not None: + ebb_names_list.append(temp_string) + name_found = True + if not name_found: + # Look for "...SNR=XXXX" pattern, + # typical of Pyserial 2.7 on Windows + if 'SNR=' in p_2: + index1 = p_2.find('SNR=') + len('SNR=') + index2 = len(p_2) + temp_string = p_2[index1:index2] + if len(temp_string) < 3: + temp_string = None + if temp_string is not None: + ebb_names_list.append(temp_string) + name_found = True + if not name_found: + ebb_names_list.append(p_0) + return ebb_names_list + + +def testPort(port_name): + """ + Open a given serial port, verify that it is an EiBotBoard, + and return a SerialPort object that we can reference later. + + This routine only opens the port; it will need to be closed as well, + for example with closePort( port_name ). + You, who open the port, are responsible for closing it as well. + """ + if port_name is not None: + try: + serial_port = serial.Serial(port_name, timeout=1.0) # 1 second timeout! + + serial_port.flushInput() # deprecated function name; + # use serial_port.reset_input_buffer() + # if we can be sure that we have pySerial 3+. + + serial_port.write('v\r'.encode('ascii')) + str_version = serial_port.readline() + if str_version and str_version.startswith("EBB".encode('ascii')): + return serial_port + + serial_port.write('v\r'.encode('ascii')) + str_version = serial_port.readline() + if str_version and str_version.startswith("EBB".encode('ascii')): + return serial_port + serial_port.close() + except serial.SerialException as err: + logger.error("Error testing serial port `{}` connection".format(port_name)) + logger.info("Error context:", exc_info=err) + return None + + +def openPort(): + ''' + Find and open a port to a single attached EiBotBoard. + The first port located will be used. + ''' + found_port = findPort() + serial_port = testPort(found_port) + if serial_port: + return serial_port + return None + + +def open_named_port(port_name): + ''' + Find and open a port to a single attached EiBotBoard, indicated by name. + The first port located will be used. + ''' + found_port = find_named_ebb(port_name) + serial_port = testPort(found_port) + if serial_port: + return serial_port + return None + + +def closePort(port_name): + '''Close the given serial port.''' + if port_name is not None: + try: + port_name.close() + except serial.SerialException: + pass + + +def query(port_name, cmd, verbose=True): + '''General command to send a query to the EiBotBoard''' + if port_name is not None and cmd is not None: + response = '' + try: + port_name.write(cmd.encode('ascii')) + response = port_name.readline().decode('ascii') + n_retry_count = 0 + while len(response) == 0 and n_retry_count < 100: + # get new response to replace null response if necessary + response = port_name.readline() + n_retry_count += 1 + if cmd.split(",")[0].strip().lower() not in ["a", "i", "mr", "pi", "qm", "qg", "v"]: + # Most queries return an "OK" after the data requested. + # We skip this for those few queries that do not return an extra line. + unused_response = port_name.readline() # read in extra blank/OK line + n_retry_count = 0 + while len(unused_response) == 0 and n_retry_count < 100: + # get new response to replace null response if necessary + unused_response = port_name.readline() + n_retry_count += 1 + except (serial.SerialException, IOError, RuntimeError, OSError) as err: + if verbose: + logger.error("Error reading serial data") + else: + logger.info("Error reading serial data") + logger.info("Error context:", exc_info=err) + + if 'Err:' in response: + error_msg = '\n'.join(('Unexpected response from EBB.', + ' Command: {0}'.format(cmd.strip()), + ' Response: {0}'.format(response.strip()))) + if verbose: + logger.error(error_msg) + else: + logger.info(error_msg) + return response + return None + + +def command(port_name, cmd, verbose=True): + '''General command to send a command to the EiBotBoard''' + if port_name is not None and cmd is not None: + try: + port_name.write(cmd.encode('ascii')) + response = port_name.readline().decode('ascii') + n_retry_count = 0 + while len(response) == 0 and n_retry_count < 100: + # get new response to replace null response if necessary + response = port_name.readline().decode('ascii') + n_retry_count += 1 + if response.strip().startswith("OK"): + # Debug option: indicate which command: + # inkex.errormsg( 'OK after command: ' + cmd ) + pass + else: + if response: + error_msg = '\n'.join(('Unexpected response from EBB.', + ' Command: {0}'.format(cmd.strip()), + ' Response: {0}'.format(response.strip()))) + else: + error_msg = 'EBB Serial Timeout after command: {0}'.format(cmd) + if verbose: + logger.error(error_msg) + else: + logger.info(error_msg) + except (serial.SerialException, IOError, RuntimeError, OSError) as err: + if cmd.strip().lower() not in ["rb"]: # Ignore error on reboot (RB) command + if verbose: + logger.error('Failed after command: {0}'.format(cmd)) + else: + logger.info('Failed after command: {0}'.format(cmd)) + logger.info("Error context:", exc_info=err) + + +def bootload(port_name): + '''Enter bootloader mode. Do not try to read back data.''' + if port_name is not None: + try: + port_name.write('BL\r'.encode('ascii')) + return True + except: + return False + return None + + +def min_version(port_name, version_string): + ''' + Query the EBB firmware version for the EBB located at port_name. + Return True if the EBB firmware version is at least version_string. + Return False if the EBB firmware version is below version_string. + Return None if we are unable to determine True or False. + ''' + if port_name is not None: + ebb_version_string = queryVersion(port_name) # Full string, human readable + ebb_version_string = ebb_version_string.split("Firmware Version ", 1) + + if len(ebb_version_string) > 1: + ebb_version_string = ebb_version_string[1] + else: + return None # We haven't received a reasonable version number response. + + ebb_version_string = ebb_version_string.strip() # Stripped copy, for number comparisons + if parse(ebb_version_string) >= parse(version_string): + return True + return False + return None + + +def queryVersion(port_name): + '''Query EBB Version String''' + return query(port_name, 'V\r', True) diff --git a/inkscape/linux/readme.md b/inkscape/linux/readme.md new file mode 100644 index 0000000..b196f89 --- /dev/null +++ b/inkscape/linux/readme.md @@ -0,0 +1,6 @@ +# Inkscape Plugin +This directory contains the plugin that fits to the EggBot firmware. +Use the ebb_serial.py file to patch the plugin so that all /dev/ttyUSBx devices can be used. +Currently only the first found tty USB device will be used. + +If plugin doesn't work because of missing python dependencies, just install them with apt or similar system tool. \ No newline at end of file