From a3ffa6ab7cf2383d741919f7f72a555ad86a4dee Mon Sep 17 00:00:00 2001 From: Windell Oskay Date: Thu, 6 Oct 2022 15:39:18 -0700 Subject: [PATCH 1/3] Add new example file --- cli/examples_python/low_level_usb.py | 122 +++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100755 cli/examples_python/low_level_usb.py diff --git a/cli/examples_python/low_level_usb.py b/cli/examples_python/low_level_usb.py new file mode 100755 index 0000000..e3aa6cd --- /dev/null +++ b/cli/examples_python/low_level_usb.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -#- + +''' +low_level_usb.py + +Demonstrate advanced features of axidraw python module in "interactive" mode. + + +This demo file shows off the "usb_command" and "usb_query" features of +the interactive API. Interaction through these two commands essentially bypass +all software counters, speed, position, and limit checks that otherwise +ensure safe operations. + +While these two "low-level USB" serial interface functions are very direct, they are also +powerful and potentially dangerous. They should be used with reluctance and caution, +since improper use is capable of causing damage of an unpredictable nature. + +The serial protocol is documented at: +http://evil-mad.github.io/EggBot/ebb.html + + +This particular example file demonstrates: +* Moving the carriage away from the home position via moveto +* Querying and printing the firmware version via usb_query +* Querying and printing the step position via usb_query +* Returning to the home position via usb_command + + +The functions demonstrated here require that the AxiDraw has at least +firmware version 2.6.2. Visit http://axidraw.com/fw for information +about firmware updates. + +Run this demo by calling: python low_level_usb.py + + +--------------------------------------------------------------------- + +About the interactive API: + +Interactive mode is a mode of use, designed for plotting individual motion +segments upon request, using direct XY control. It is a complement to the +usual plotting modes, which take an SVG document as input. + +So long as the AxiDraw is started in the home corner, moves are limit checked, +and constrained to be within the safe travel range of the AxiDraw. + + +AxiDraw python API documentation is hosted at: https://axidraw.com/doc/py_api/ + +--------------------------------------------------------------------- + +About this software: + +The AxiDraw writing and drawing machine is a product of Evil Mad Scientist +Laboratories. https://axidraw.com https://shop.evilmadscientist.com + +This open source software is written and maintained by Evil Mad Scientist +to support AxiDraw users across a wide range of applications. Please help +support Evil Mad Scientist and open source software development by purchasing +genuine AxiDraw hardware. + +AxiDraw software development is hosted at https://github.com/evil-mad/axidraw + +Additional AxiDraw documentation is available at http://axidraw.com/docs + +AxiDraw owners may request technical support for this software through our +github issues page, support forums, or by contacting us directly at: +https://shop.evilmadscientist.com/contact + + +--------------------------------------------------------------------- + +Copyright 2022 Windell H. Oskay, Evil Mad Scientist Laboratories + +The MIT License (MIT) + +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 sys + +from pyaxidraw import axidraw + +ad = axidraw.AxiDraw() # Initialize class + +ad.interactive() # Enter interactive mode +connected = ad.connect() # Open serial port to AxiDraw + +if not connected: + sys.exit() # end script + +ad.moveto(2,1) # Absolute pen-up move, to (2 inch, 1 inch) + +version = ad.usb_query("V\r") # Query firmware version +print("Firmware version data: " + version) + +step_pos = ad.usb_query("QS\r") # Query step position +print("Step pos: " + step_pos) + +ad.usb_command("HM,3200\r") # Return home at a rate of 3200 steps per second + +ad.disconnect() # Close serial port to AxiDraw From bf7d31364871c358eb8f436248154b3cb78e0d8c Mon Sep 17 00:00:00 2001 From: Windell Oskay Date: Thu, 6 Oct 2022 15:39:37 -0700 Subject: [PATCH 2/3] Clean up examples --- cli/examples_python/estimate_time.py | 63 ++++--- cli/examples_python/interactive_draw_path.py | 9 +- cli/examples_python/interactive_penheights.py | 40 +++-- cli/examples_python/interactive_usb_com.py | 75 ++++---- cli/examples_python/interactive_xy.py | 160 +++++------------- cli/examples_python/plot.py | 60 +++---- cli/examples_python/plot_inline.py | 44 +++-- cli/examples_python/report_pos_inch.py | 21 ++- cli/examples_python/report_pos_mm.py | 23 ++- cli/examples_python/toggle.py | 11 +- cli/examples_python/turtle_pos.py | 4 + 11 files changed, 233 insertions(+), 277 deletions(-) diff --git a/cli/examples_python/estimate_time.py b/cli/examples_python/estimate_time.py index 7f37847..f2e207b 100755 --- a/cli/examples_python/estimate_time.py +++ b/cli/examples_python/estimate_time.py @@ -9,11 +9,23 @@ Run this demo by calling: python estimate_time.py + +--------------------------------------------------------------------- + +About the interactive API: + +Interactive mode is a mode of use, designed for plotting individual motion +segments upon request, using direct XY control. It is a complement to the +usual plotting modes, which take an SVG document as input. + +So long as the AxiDraw is started in the home corner, moves are limit checked, +and constrained to be within the safe travel range of the AxiDraw. + + AxiDraw python API documentation is hosted at: https://axidraw.com/doc/py_api/ -''' +--------------------------------------------------------------------- -''' About this software: The AxiDraw writing and drawing machine is a product of Evil Mad Scientist @@ -28,13 +40,14 @@ Additional AxiDraw documentation is available at http://axidraw.com/docs -AxiDraw owners may request technical support for this software through our +AxiDraw owners may request technical support for this software through our github issues page, support forums, or by contacting us directly at: https://shop.evilmadscientist.com/contact +--------------------------------------------------------------------- -Copyright 2021 Windell H. Oskay, Evil Mad Scientist Laboratories +Copyright 2022 Windell H. Oskay, Evil Mad Scientist Laboratories The MIT License (MIT) @@ -58,7 +71,7 @@ ''' - +import sys import os.path from pyaxidraw import axidraw @@ -70,37 +83,33 @@ in the same directory with the test file. ''' -location1 = "test/assets/AxiDraw_trivial.svg" -location2 = "../test/assets/AxiDraw_trivial.svg" -location3 = "AxiDraw_trivial.svg" +LOCATION1 = "test/assets/AxiDraw_trivial.svg" +LOCATION2 = "../test/assets/AxiDraw_trivial.svg" +LOCATION3 = "AxiDraw_trivial.svg" -file = None +FILE = None -if os.path.exists(location1): - file = location1 -if os.path.exists(location2): - file = location2 -if os.path.exists(location3): - file = location3 +if os.path.exists(LOCATION1): + FILE = LOCATION1 +if os.path.exists(LOCATION2): + FILE = LOCATION2 +if os.path.exists(LOCATION3): + FILE = LOCATION3 -if file: - print("Example file located at: " + file) - ad.plot_setup(file) # Parse the input file +if FILE: + print("Example file located at: " + FILE) + ad.plot_setup(FILE) # Parse the input file else: print("Unable to locate example file; exiting.") - exit() - -''' -The above code, starting with "location1" can all be replaced by a single line -if you already know where the file is. This can be as simple as: + sys.exit() # end script -ad.plot_setup("AxiDraw_trivial.svg") -''' +# The above code, starting with "LOCATION1" can all be replaced by a single line +# if you already know where the file is. This can be as simple as: +# ad.plot_setup("AxiDraw_trivial.svg") ad.options.preview = True ad.options.report_time = True # Enable time estimates ad.plot_run() # plot the document print_time_seconds = ad.time_estimate -print("Estimated print time: {0} s".format(print_time_seconds)) - +print(f"Estimated print time: {print_time_seconds} s") diff --git a/cli/examples_python/interactive_draw_path.py b/cli/examples_python/interactive_draw_path.py index 70136d5..30c2abf 100755 --- a/cli/examples_python/interactive_draw_path.py +++ b/cli/examples_python/interactive_draw_path.py @@ -10,6 +10,9 @@ Run this demo by calling: python interactive_draw_path.py +--------------------------------------------------------------------- + +About the interactive API: Interactive mode is a mode of use, designed for plotting individual motion segments upon request, using direct XY control. It is a complement to the @@ -43,8 +46,7 @@ --------------------------------------------------------------------- - -Copyright 2020 Windell H. Oskay, Evil Mad Scientist Laboratories +Copyright 2022 Windell H. Oskay, Evil Mad Scientist Laboratories The MIT License (MIT) @@ -75,8 +77,6 @@ ad = axidraw.AxiDraw() # Initialize class - - def print_position(): ''' Query, report, and print position and pen state @@ -122,7 +122,6 @@ def print_position(): print_position() - ad.options.units = 0 # Switch to inch units ad.update() # Process changes to options diff --git a/cli/examples_python/interactive_penheights.py b/cli/examples_python/interactive_penheights.py index fd6d133..40c1e8d 100755 --- a/cli/examples_python/interactive_penheights.py +++ b/cli/examples_python/interactive_penheights.py @@ -5,15 +5,28 @@ interactive_penheights.py Demonstrate use of axidraw module in "interactive" mode. - Set pen to different heights. Run this demo by calling: python interactive_penheights.py -''' -''' +--------------------------------------------------------------------- + +About the interactive API: + +Interactive mode is a mode of use, designed for plotting individual motion +segments upon request, using direct XY control. It is a complement to the +usual plotting modes, which take an SVG document as input. + +So long as the AxiDraw is started in the home corner, moves are limit checked, +and constrained to be within the safe travel range of the AxiDraw. + + +AxiDraw python API documentation is hosted at: https://axidraw.com/doc/py_api/ + +--------------------------------------------------------------------- + About this software: The AxiDraw writing and drawing machine is a product of Evil Mad Scientist @@ -28,13 +41,14 @@ Additional AxiDraw documentation is available at http://axidraw.com/docs -AxiDraw owners may request technical support for this software through our +AxiDraw owners may request technical support for this software through our github issues page, support forums, or by contacting us directly at: https://shop.evilmadscientist.com/contact +--------------------------------------------------------------------- -Copyright 2021 Windell H. Oskay, Evil Mad Scientist Laboratories +Copyright 2022 Windell H. Oskay, Evil Mad Scientist Laboratories The MIT License (MIT) @@ -66,31 +80,31 @@ ad = axidraw.AxiDraw() # Initialize class ad.interactive() # Enter interactive mode -connected = ad.connect() # Open serial port to AxiDraw +connected = ad.connect() # Open serial port to AxiDraw if not connected: sys.exit() # end script - + ad.penup() # Change some options, just to show how we do so: ad.options.pen_pos_down = 40 ad.options.pen_pos_up = 60 -ad.update() # Process changes to options +ad.update() # Process changes to options ad.pendown() -time.sleep(1.0) +time.sleep(1.0) ad.penup() -time.sleep(1.0) +time.sleep(1.0) ad.options.pen_pos_down = 0 ad.options.pen_pos_up = 100 -ad.update() # Process changes to options +ad.update() # Process changes to options ad.pendown() -time.sleep(1.0) +time.sleep(1.0) ad.penup() -ad.disconnect() # Close serial port to AxiDraw \ No newline at end of file +ad.disconnect() # Close serial port to AxiDraw diff --git a/cli/examples_python/interactive_usb_com.py b/cli/examples_python/interactive_usb_com.py index cf5d360..6105dfe 100755 --- a/cli/examples_python/interactive_usb_com.py +++ b/cli/examples_python/interactive_usb_com.py @@ -4,7 +4,8 @@ ''' interactive_usb_com.py -This demonstration toggles a hobby servo motor, connected to I/O B3. +This demonstration toggles a hobby servo motor, connected to I/O B3, +using advanced features of the AxiDraw Python API. In doing so, it demonstrates the following "advanced" topics: * Issuing a "low level" direct EBB USB command @@ -22,17 +23,30 @@ The pen-lift servo on an AxiDraw V3 is normally connected to output B1, the *lowest* set of three pins on the AxiDraw's EBB control board, with the -black wire (ground) towards the back of the machine. +black wire (ground) towards the back of the machine. -To try this demo, connect a hobby servo motor to pins B3, which is the +To try this demo, connect a hobby servo motor to pins B3, which is the *highest* set of three pins on the AxiDraw's EBB control board, three positions above the standard servo motor position. You can disconnect the servo motor connection from the lowest three pins and moving it to the highest three pins, keeping the black wire towards the back of the machine. -''' +--------------------------------------------------------------------- + +About the interactive API: + +Interactive mode is a mode of use, designed for plotting individual motion +segments upon request, using direct XY control. It is a complement to the +usual plotting modes, which take an SVG document as input. + +So long as the AxiDraw is started in the home corner, moves are limit checked, +and constrained to be within the safe travel range of the AxiDraw. + + +AxiDraw python API documentation is hosted at: https://axidraw.com/doc/py_api/ + +--------------------------------------------------------------------- -''' About this software: The AxiDraw writing and drawing machine is a product of Evil Mad Scientist @@ -47,13 +61,14 @@ Additional AxiDraw documentation is available at http://axidraw.com/docs -AxiDraw owners may request technical support for this software through our +AxiDraw owners may request technical support for this software through our github issues page, support forums, or by contacting us directly at: https://shop.evilmadscientist.com/contact +--------------------------------------------------------------------- -Copyright 2021 Windell H. Oskay, Evil Mad Scientist Laboratories +Copyright 2022 Windell H. Oskay, Evil Mad Scientist Laboratories The MIT License (MIT) @@ -77,35 +92,34 @@ ''' + import sys import time from pyaxidraw import axidraw - -pen_up_percent = 75 # Percent height that we will use for pen up -pen_down_percent = 25 # Percent height that we will use for pen down -wait_time_s = 3 # Time, in seconds, before switching to next position -port_pin = 6 # Logical pin RP6 drives the output labeled "B3", from +PEN_UP_PERCENT = 75 # Percent height that we will use for pen up +PEN_DOWN_PERCENT = 25 # Percent height that we will use for pen down +WAIT_TIME_S = 3 # Time, in seconds, before switching to next position +PORT_PIN = 6 # Logical pin RP6 drives the output labeled "B3", from # docs at: http://evil-mad.github.io/EggBot/ebb.html#S2 - ad = axidraw.AxiDraw() # Initialize class ad.interactive() # Enter interactive mode -connected = ad.connect() # Open serial port to AxiDraw +connected = ad.connect() # Open serial port to AxiDraw if not connected: sys.exit() # end script -pen_is_up = False # Initial value of pen state +PEN_IS_UP = False # Initial value of pen state # Lowest allowed position; "0%" on the scale. Default value: 10800 units, or 0.818 ms. servo_min = ad.params.servo_min # Highest allowed position; "100%" on the scale. Default value: 25200 units, or 2.31 ms. -servo_max = ad.params.servo_max +servo_max = ad.params.servo_max # Optional debug statements: print("servo_min: " + str(servo_min)) @@ -114,33 +128,32 @@ servo_range = servo_max - servo_min -pen_up_pos = int (pen_up_percent * servo_range / 100 + servo_min) -pen_down_pos = int (pen_down_percent * servo_range / 100 + servo_min) +pen_up_pos = int (PEN_UP_PERCENT * servo_range / 100 + servo_min) +pen_down_pos = int (PEN_DOWN_PERCENT * servo_range / 100 + servo_min) try: while True: # Repeat until interrupted - if pen_is_up: + if PEN_IS_UP: position = pen_down_pos - pen_is_up = False + PEN_IS_UP = False else: position = pen_up_pos - pen_is_up = True - - command = "S2," + str(position) + ',' + str(port_pin) + "\r" - + PEN_IS_UP = True + + COMMAND = "S2," + str(position) + ',' + str(PORT_PIN) + "\r" + # Optional debug statements: - if pen_is_up: + if PEN_IS_UP: print("Raising pen") else: print("Lowering pen") print("New servo position: " + str(position)) - print("command: " + command) - - ad.usb_command(command + "\r") - - time.sleep(wait_time_s) + print("command: " + COMMAND) + + ad.usb_command(COMMAND + "\r") + + time.sleep(WAIT_TIME_S) except KeyboardInterrupt: ad.disconnect() # Close serial port to AxiDraw - diff --git a/cli/examples_python/interactive_xy.py b/cli/examples_python/interactive_xy.py index 9454e92..941397d 100755 --- a/cli/examples_python/interactive_xy.py +++ b/cli/examples_python/interactive_xy.py @@ -9,15 +9,22 @@ Run this demo by calling: python interactive_xy.py -(There is also a separate "plot" mode, which can be used for plotting an -SVG file, rather than moving to various points upon command.) +--------------------------------------------------------------------- -AxiDraw python API documentation is hosted at: https://axidraw.com/doc/py_api/ +About the interactive API: -''' +Interactive mode is a mode of use, designed for plotting individual motion +segments upon request, using direct XY control. It is a complement to the +usual plotting modes, which take an SVG document as input. +So long as the AxiDraw is started in the home corner, moves are limit checked, +and constrained to be within the safe travel range of the AxiDraw. + + +AxiDraw python API documentation is hosted at: https://axidraw.com/doc/py_api/ + +--------------------------------------------------------------------- -''' About this software: The AxiDraw writing and drawing machine is a product of Evil Mad Scientist @@ -32,13 +39,15 @@ Additional AxiDraw documentation is available at http://axidraw.com/docs -AxiDraw owners may request technical support for this software through our +AxiDraw owners may request technical support for this software through our github issues page, support forums, or by contacting us directly at: https://shop.evilmadscientist.com/contact +--------------------------------------------------------------------- + -Copyright 2020 Windell H. Oskay, Evil Mad Scientist Laboratories +Copyright 2022 Windell H. Oskay, Evil Mad Scientist Laboratories The MIT License (MIT) @@ -62,96 +71,6 @@ ''' - - - - - -''' - -Interactive mode is a mode of use, designed for plotting individual motion -segments upon request. It is a complement to the usual plotting modes, which -take an SVG document as input. - -So long as the AxiDraw is started in the home corner, moves are limit checked, -and constrained to be within the safe travel range of the AxiDraw. - - - -Recommended usage: - -ad = axidraw.AxiDraw() # Initialize class -ad.interactive() # Enter interactive mode - -[Optional: Apply custom settings] - -ad.connect() # Open serial port to AxiDraw - -[One or more motion commands] -[Optional: Update settings, followed by calling update().] - -ad.disconnect() # Close connection to AxiDraw - - -The motion commands are as follows: - -goto(x,y) # Absolute XY move to new location -moveto(x,y) # Absolute XY pen-up move. Lift pen before moving, if it is down. -lineto(x,y) # Absolute XY pen-down move. Lower pen before moving, if it is up. - -go(x,y) # XY relative move. -move(x,y) # XY relative pen-up move. Lift pen before moving, if it is down. -line(x,y) # XY relative pen-down move. Lower pen before moving, if it is up. - -penup() # lift pen -pendown() # lower pen - - -Utility commands: - -interactive() # Enter interactive mode -connect() # Open serial connection to AxiDraw. Returns True if connected successfully. -update() # Apply changes to options -disable() # Disable XY motors, for example to manually move carriage to home position. -disconnect() # Terminate serial session to AxiDraw. (Required.) - - - - -The available options are as follows: - -options.speed_pendown # Range: 1-110 (percent). -options.speed_penup # Range: 1-110 (percent). -options.accel # Range: 1-100 (percent). -options.pen_pos_down # Range: 0-100 (percent). -options.pen_pos_up # Range: 0-100 (percent). -options.pen_rate_lower # Range: 1-100 (percent). -options.pen_rate_raise # Range: 1-100 (percent). -options.pen_delay_down # Range: -500 - 500 (ms). -options.pen_delay_up # Range: -500 - 500 (ms). -options.const_speed # True or False. Default: False -options.units # Range: 0-1. 0: Inches (default), 1: cm -options.model # Range: 1-3. 1: AxiDraw V2 or V3 ( Default) - # 2: AxiDraw V3/A3 - # 3: AxiDraw V3 XLX -options.port # String: Port name or USB nickname -options.port_config # Range: 0-1. 0: Plot to first unit found, unless port specified. (Default) - # 1: Plot to first unit found - -One or more options can be set after the interactive() call, and before connect() -for example as: - -ad.options.speed_pendown = 75 - - - -All options except port and port_config can be changed after connect(). However, -you must call update() after changing the options and before calling any -additional motion commands. - - -''' - import sys from pyaxidraw import axidraw @@ -159,59 +78,60 @@ ad = axidraw.AxiDraw() # Initialize class ad.interactive() # Enter interactive mode -connected = ad.connect() # Open serial port to AxiDraw +connected = ad.connect() # Open serial port to AxiDraw if not connected: sys.exit() # end script - - + # Draw square, using "moveto/lineto" (absolute move) syntax: -ad.moveto(1,1) # Absolute pen-up move, to (1 inch, 1 inch) -ad.lineto(2,1) # Absolute pen-down move, to (2 inches, 1 inch) -ad.lineto(2,2) -ad.lineto(1,2) -ad.lineto(1,1) # Finish drawing square -ad.moveto(0,0) # Absolute pen-up move, back to origin. +ad.moveto(1, 1) # Absolute pen-up move, to (1 inch, 1 inch) +ad.lineto(2, 1) # Absolute pen-down move, to (2 inches, 1 inch) +ad.lineto(2, 2) +ad.lineto(1, 2) +ad.lineto(1, 1) # Finish drawing square +ad.moveto(0, 0) # Absolute pen-up move, back to origin. +ad.delay(2000) # Delay 2 seconds # Change some options: -ad.options.units = 1 # set working units to cm. +ad.options.units = 1 # set working units to cm. ad.options.speed_pendown = 10 # set pen-down speed to slow -ad.update() # Process changes to options +ad.options.pen_pos_up = 90 # select a large range for the pen up/down swing +ad.options.pen_pos_down = 10 +ad.update() # Process changes to options # Draw an "X" through the square, using "move/line" (relative move) syntax: -# Note that we have just changed the units to be in cm. +# Note that we have just changed the units to be in cm. -ad.move(5.08,5.08) # Relative move to (2 inches,2 inches), in cm -ad.line(-2.54,-2.54) # Relative move 2.54 cm in X and Y -ad.move(0,2.54) -ad.line(2.54,-2.54) # Relative move 2.54 cm in X and Y +ad.move(5.08, 5.08) # Relative move to (2 inches,2 inches), in cm +ad.line(-2.54, -2.54) # Relative move 2.54 cm in X and Y +ad.move(0, 2.54) +ad.line(2.54, -2.54) # Relative move 2.54 cm in X and Y -ad.moveto(0,0) # Return home +ad.moveto(0, 0) # Return home # Change some options, just to show how we do so: ad.options.units = 0 # set working units back to inches. ad.options.speed_pendown = 75 # set pen-down speed to fast ad.options.pen_rate_lower = 10 # Set pen down very slowly -ad.update() # Process changes to options +ad.update() # Process changes to options # Draw a "+" through the square, using "go/goto" commands, # which do not automatically set the pen up or down: -ad.goto(1.5,1.0) +ad.goto(1.5, 1.0) ad.pendown() ad.go(0,1) ad.penup() -ad.goto(1.0,1.5) +ad.goto(1.0, 1.5) ad.pendown() ad.go(1,0) ad.penup() -ad.goto(0,0) # Return home - +ad.goto(0, 0) # Return home -ad.disconnect() # Close serial port to AxiDraw \ No newline at end of file +ad.disconnect() # Close serial port to AxiDraw diff --git a/cli/examples_python/plot.py b/cli/examples_python/plot.py index 07bf4a6..f4929cb 100755 --- a/cli/examples_python/plot.py +++ b/cli/examples_python/plot.py @@ -15,18 +15,12 @@ (There is also a separate "interactive" mode, which can be used for moving the AxiDraw to various points upon command, rather than plotting an SVG file.) +--------------------------------------------------------------------- AxiDraw python API documentation is hosted at: https://axidraw.com/doc/py_api/ -''' - - - - +--------------------------------------------------------------------- - - -''' About this software: The AxiDraw writing and drawing machine is a product of Evil Mad Scientist @@ -41,13 +35,14 @@ Additional AxiDraw documentation is available at http://axidraw.com/docs -AxiDraw owners may request technical support for this software through our +AxiDraw owners may request technical support for this software through our github issues page, support forums, or by contacting us directly at: https://shop.evilmadscientist.com/contact +--------------------------------------------------------------------- -Copyright 2021 Windell H. Oskay, Evil Mad Scientist Laboratories +Copyright 2022 Windell H. Oskay, Evil Mad Scientist Laboratories The MIT License (MIT) @@ -71,56 +66,47 @@ ''' - - +import sys import os.path from pyaxidraw import axidraw ad = axidraw.AxiDraw() # Create class instance - - ''' Try a few different possible locations for our file, so that this can be called from either the root or examples_python directory, or if you're in the same directory with the file. ''' -location1 = "test/assets/AxiDraw_trivial.svg" -location2 = "../test/assets/AxiDraw_trivial.svg" -location3 = "AxiDraw_trivial.svg" +LOCATION1 = "test/assets/AxiDraw_trivial.svg" +LOCATION2 = "../test/assets/AxiDraw_trivial.svg" +LOCATION3 = "AxiDraw_trivial.svg" -file = None +FILE = None -if os.path.exists(location1): - file = location1 -if os.path.exists(location2): - file = location2 -if os.path.exists(location3): - file = location3 +if os.path.exists(LOCATION1): + FILE = LOCATION1 +if os.path.exists(LOCATION2): + FILE = LOCATION2 +if os.path.exists(LOCATION3): + FILE = LOCATION3 -if file: - print("Example file located at: " + file) - ad.plot_setup(file) # Parse the input file +if FILE: + print("Example file located at: " + FILE) + ad.plot_setup(FILE) # Parse the input file else: print("Unable to locate example file; exiting.") - exit() - -''' -The above code, starting with "location1" can all be replaced by a single line -if you already know where the file is. This can be as simple as: - -ad.plot_setup("AxiDraw_trivial.svg") -''' + sys.exit() # end script +# The above code, starting with "LOCATION1" can all be replaced by a single line +# if you already know where the file is. This can be as simple as: +# ad.plot_setup("AxiDraw_trivial.svg") ad.options.speed_pendown = 50 # Set maximum pen-down speed to 50% - ''' See documentation for a description of additional options and their allowed values: https://axidraw.com/doc/py_api/ - ''' ad.plot_run() # plot the document diff --git a/cli/examples_python/plot_inline.py b/cli/examples_python/plot_inline.py index e1ed3ac..50718b6 100755 --- a/cli/examples_python/plot_inline.py +++ b/cli/examples_python/plot_inline.py @@ -15,18 +15,12 @@ * Compose a string containing SVG -- what would be an SVG file *if* we saved it. * Plot that SVG string with the AxiDraw. +--------------------------------------------------------------------- AxiDraw python API documentation is hosted at: https://axidraw.com/doc/py_api/ -''' - - - - - +--------------------------------------------------------------------- - -''' About this software: The AxiDraw writing and drawing machine is a product of Evil Mad Scientist @@ -41,13 +35,14 @@ Additional AxiDraw documentation is available at http://axidraw.com/docs -AxiDraw owners may request technical support for this software through our +AxiDraw owners may request technical support for this software through our github issues page, support forums, or by contacting us directly at: https://shop.evilmadscientist.com/contact +--------------------------------------------------------------------- -Copyright 2021 Windell H. Oskay, Evil Mad Scientist Laboratories +Copyright 2022 Windell H. Oskay, Evil Mad Scientist Laboratories The MIT License (MIT) @@ -90,7 +85,7 @@ """ -svg_head = """ +SVG_HEAD = """ """ -svg_tail = "" +SVG_TAIL = "" # Add a circle at X = 40 mm, Y = 30 mm, r = 10 mm -svg_body = """ - """ + """ # Add a path starting at 70,40, using absolute Moveto (M) and absolute Lineto (L) commands. # Additional coordinate pairs following L X,Y are additional lineto coordinates. -svg_body += """ - """ + """ # Add a similar path starting at 70,70, using relative moveto (m) and relative lineto (l) commands. # (except for the first absolute move) -svg_body += """ - """ + """ # Add a programatic ellipse, with randomly generated size and position. # This will be different every time that you run this script. @@ -145,15 +140,15 @@ rx = 20 * random.random() ry = 20 * random.random() -random_ellipse = '' -random_ellipse = random_ellipse.format( rx, ry, cx, cy) +RANDOM_ELLIPSE = '' +RANDOM_ELLIPSE = RANDOM_ELLIPSE.format( rx, ry, cx, cy) -svg_body += random_ellipse +SVG_BODY += RANDOM_ELLIPSE -svg = svg_head + svg_body + svg_tail +SVG = SVG_HEAD + SVG_BODY + SVG_TAIL -ad.plot_setup(svg) # Parse the SVG +ad.plot_setup(SVG) # Parse the SVG ad.options.speed_pendown = 50 # Set maximum pen-down speed to 50% @@ -165,4 +160,3 @@ ''' ad.plot_run() # plot the document - diff --git a/cli/examples_python/report_pos_inch.py b/cli/examples_python/report_pos_inch.py index 7150467..6c65774 100755 --- a/cli/examples_python/report_pos_inch.py +++ b/cli/examples_python/report_pos_inch.py @@ -10,12 +10,21 @@ X.XXX is the current AxiDraw X position and Y.YYY is the current AxiDraw Y position, in inch units. -Requires Python 3.6 or newer and the AxiDraw python API, version 3.2 or newer. +--------------------------------------------------------------------- -AxiDraw python API documentation is hosted at: https://axidraw.com/doc/py_api/ +About the interactive API: + +Interactive mode is a mode of use, designed for plotting individual motion +segments upon request, using direct XY control. It is a complement to the +usual plotting modes, which take an SVG document as input. + +So long as the AxiDraw is started in the home corner, moves are limit checked, +and constrained to be within the safe travel range of the AxiDraw. +AxiDraw python API documentation is hosted at: https://axidraw.com/doc/py_api/ +--------------------------------------------------------------------- About this software: @@ -36,8 +45,7 @@ https://shop.evilmadscientist.com/contact - - +--------------------------------------------------------------------- Copyright 2022 Windell H. Oskay, Evil Mad Scientist Laboratories @@ -63,13 +71,14 @@ ''' +import sys from pyaxidraw import axidraw ad = axidraw.AxiDraw() # Initialize class ad.interactive() ad.connect() # Open USB serial session if not ad.connected: - exit() + sys.exit() # end script result = ad.usb_query('QS\r') # Query global step position result_list = result.strip().split(",") @@ -81,6 +90,6 @@ x_pos_inch *= 2 y_pos_inch *= 2 -print("{0:0.3f}, {1:0.3f}".format(x_pos_inch, y_pos_inch)) +print(f"{x_pos_inch:0.3f}, {y_pos_inch:0.3f}") ad.disconnect() # Close serial port to AxiDraw diff --git a/cli/examples_python/report_pos_mm.py b/cli/examples_python/report_pos_mm.py index 53c5749..7c10ae3 100755 --- a/cli/examples_python/report_pos_mm.py +++ b/cli/examples_python/report_pos_mm.py @@ -10,12 +10,21 @@ X.XXX is the current AxiDraw X position and Y.YYY is the current AxiDraw Y position, in mm units. -Requires Python 3.6 or newer and the AxiDraw python API, version 3.2 or newer. +--------------------------------------------------------------------- -AxiDraw python API documentation is hosted at: https://axidraw.com/doc/py_api/ +About the interactive API: + +Interactive mode is a mode of use, designed for plotting individual motion +segments upon request, using direct XY control. It is a complement to the +usual plotting modes, which take an SVG document as input. +So long as the AxiDraw is started in the home corner, moves are limit checked, +and constrained to be within the safe travel range of the AxiDraw. +AxiDraw python API documentation is hosted at: https://axidraw.com/doc/py_api/ + +--------------------------------------------------------------------- About this software: @@ -36,8 +45,7 @@ https://shop.evilmadscientist.com/contact - - +--------------------------------------------------------------------- Copyright 2022 Windell H. Oskay, Evil Mad Scientist Laboratories @@ -63,15 +71,14 @@ ''' - - +import sys from pyaxidraw import axidraw ad = axidraw.AxiDraw() # Initialize class ad.interactive() ad.connect() # Open USB serial session if not ad.connected: - exit() + sys.exit() # end script result = ad.usb_query('QS\r') # Query global step position result_list = result.strip().split(",") @@ -86,6 +93,6 @@ x_pos_mm = x_pos_inch * 25.4 y_pos_mm = y_pos_inch * 25.4 -print("{0:0.3f}, {1:0.3f}".format(x_pos_mm, y_pos_mm)) +print(f"{x_pos_mm:0.3f}, {y_pos_mm:0.3f}") ad.disconnect() # Close serial port to AxiDraw diff --git a/cli/examples_python/toggle.py b/cli/examples_python/toggle.py index 39e9a46..a8a741c 100755 --- a/cli/examples_python/toggle.py +++ b/cli/examples_python/toggle.py @@ -13,11 +13,12 @@ and use it to call a utility command for the AxiDraw. +--------------------------------------------------------------------- + AxiDraw python API documentation is hosted at: https://axidraw.com/doc/py_api/ -''' +--------------------------------------------------------------------- -''' About this software: The AxiDraw writing and drawing machine is a product of Evil Mad Scientist @@ -32,13 +33,14 @@ Additional AxiDraw documentation is available at http://axidraw.com/docs -AxiDraw owners may request technical support for this software through our +AxiDraw owners may request technical support for this software through our github issues page, support forums, or by contacting us directly at: https://shop.evilmadscientist.com/contact +--------------------------------------------------------------------- -Copyright 2021 Windell H. Oskay, Evil Mad Scientist Laboratories +Copyright 2022 Windell H. Oskay, Evil Mad Scientist Laboratories The MIT License (MIT) @@ -64,7 +66,6 @@ - from pyaxidraw import axidraw ad = axidraw.AxiDraw() # Create class instance diff --git a/cli/examples_python/turtle_pos.py b/cli/examples_python/turtle_pos.py index 6066a1b..a7e411e 100755 --- a/cli/examples_python/turtle_pos.py +++ b/cli/examples_python/turtle_pos.py @@ -11,6 +11,10 @@ Run this demo by calling: python turtle_pos.py +--------------------------------------------------------------------- + +About the interactive API: + Interactive mode is a mode of use, designed for plotting individual motion segments upon request, using direct XY control. It is a complement to the usual plotting modes, which take an SVG document as input. From 6a03ba7bac721ec955f89f559cf7e1af7efc3ca7 Mon Sep 17 00:00:00 2001 From: Windell Oskay Date: Thu, 6 Oct 2022 15:39:56 -0700 Subject: [PATCH 3/3] Updates for v3.6 --- cli/axicli/axidraw_cli.py | 7 +- inkscape driver/__init__.py | 5 + inkscape driver/axidraw.inx | 2 +- inkscape driver/axidraw.py | 250 +++++++----------- inkscape driver/axidraw_conf.py | 73 ++--- inkscape driver/axidraw_control.py | 3 +- .../axidraw_options/common_options.py | 18 +- inkscape driver/axidraw_options/versions.py | 14 +- inkscape driver/digest_svg.py | 142 +++++----- inkscape driver/pen_handling.py | 81 +++--- inkscape driver/plot_status.py | 76 ++++-- inkscape driver/plot_warnings.py | 28 +- inkscape driver/serial_utils.py | 75 ++++++ 13 files changed, 423 insertions(+), 351 deletions(-) create mode 100644 inkscape driver/__init__.py create mode 100644 inkscape driver/serial_utils.py diff --git a/cli/axicli/axidraw_cli.py b/cli/axicli/axidraw_cli.py index 00ad68f..8dab141 100644 --- a/cli/axicli/axidraw_cli.py +++ b/cli/axicli/axidraw_cli.py @@ -73,7 +73,7 @@ from plotink.plot_utils_import import from_dependency_import # plotink exit_status = from_dependency_import("ink_extensions_utils.exit_status") -cli_version = "AxiDraw Command Line Interface 3.5.0" +cli_version = "AxiDraw Command Line Interface 3.6.0" quick_help = ''' Basic syntax to plot a file: axicli svg_in [OPTIONS] @@ -162,7 +162,7 @@ def axidraw_CLI(dev = False): parser.add_argument("-w","--walk_dist", \ metavar='DISTANCE', type=float, \ - help="Distance for manual walk (inches)") + help="Distance for manual walk") parser.add_argument("-l","--layer", \ type=int, \ @@ -345,4 +345,7 @@ def axidraw_CLI(dev = False): if utils.has_output(adc) and not use_trivial_file: utils.output_result(args.output_file, adc.outdoc) + if adc.status_code >= 100: # Give non-zero exit code. + sys.exit(1) # No need to be more verbose; we have already printed error messages. + return adc if dev else None # returning adc is useful for tests diff --git a/inkscape driver/__init__.py b/inkscape driver/__init__.py new file mode 100644 index 0000000..104dd37 --- /dev/null +++ b/inkscape driver/__init__.py @@ -0,0 +1,5 @@ +# Bail out if python is less than 3.7 +import sys +MIN_VERSION = (3, 7) +if sys.version_info < MIN_VERSION: + sys.exit("AxiDraw software must be run with python 3.7 or greater.") diff --git a/inkscape driver/axidraw.inx b/inkscape driver/axidraw.inx index 462c003..fe7176e 100755 --- a/inkscape driver/axidraw.inx +++ b/inkscape driver/axidraw.inx @@ -258,7 +258,7 @@ the Return to Home Corner command. <_param name="copyright" type="description" indent="5" xml:space="preserve" ->Version 3.5.0 — Copyright 2022 Evil Mad Scientist +>Version 3.6.0 — Copyright 2022 Evil Mad Scientist diff --git a/inkscape driver/axidraw.py b/inkscape driver/axidraw.py index 1309d78..2110558 100644 --- a/inkscape driver/axidraw.py +++ b/inkscape driver/axidraw.py @@ -61,6 +61,7 @@ from axidrawinternal import plot_status from axidrawinternal import pen_handling from axidrawinternal import plot_warnings +from axidrawinternal import serial_utils # from axidrawinternal import preview logger = logging.getLogger(__name__) @@ -82,7 +83,7 @@ def __init__(self, default_logging=True, user_message_fun=message.emit, params=N self.OptionParser.add_option_group( common_options.core_mode_options(self.OptionParser, params.__dict__)) - self.version_string = "3.5.0" # Dated 2022-07-31 + self.version_string = "3.6.0" # Dated 2022-10-01 self.plot_status = plot_status.PlotStatus() self.pen = pen_handling.PenHandler() @@ -235,7 +236,7 @@ def effect(self): self.text_out = '' # Text log for basic communication messages self.error_out = '' # Text log for significant errors - self.plot_status.stats.pt_estimate = 0.0 # plot time estimate, milliseconds + self.plot_status.stats.reset() # Reset plot duration and distance statistics self.time_estimate = 0.0 # plot time estimate, s. Available to Python API self.doc_units = "in" @@ -274,10 +275,6 @@ def effect(self): self.update_options() - self.warn_out_of_bounds = False - - self.pen_up_travel_inches = 0.0 - self.pen_down_travel_inches = 0.0 self.path_data_pu = [] # pen-up path data for preview layers self.path_data_pd = [] # pen-down path data for preview layers @@ -383,6 +380,7 @@ def effect(self): # # New random seed for new plot; Changes every 10 ms: self.svg_rand_seed = int(time.time()*100) + self.pathcount = 0 self.svg_node_count = 0 self.svg_last_path = 0 self.svg_layer = -1 # indicate (to resume routine) that we are plotting all layers @@ -392,7 +390,7 @@ def effect(self): self.plot_document() self.plot_status.delay_between_copies = True # Currently delaying between copies - if self.plot_status.copies_to_plot == 0 or self.plot_status.b_stopped: + if self.plot_status.copies_to_plot == 0 or self.plot_status.stopped: continue # No delay after last copy, or if stopped. self.plot_status.progress.launch(self.plot_status, self.options, True, @@ -400,15 +398,16 @@ def effect(self): time_counter = 10 * self.options.page_delay while time_counter > 0: time_counter -= 1 - if self.plot_status.copies_to_plot != 0 and not self.plot_status.b_stopped: + if self.plot_status.copies_to_plot != 0 and not self.plot_status.stopped: # Delay if we're between copies, not after the last or paused. + self.plot_status.stats.page_delays += 100 if self.options.preview: self.plot_status.stats.pt_estimate += 100 else: time.sleep(0.100) # Use short intervals to improve responsiveness self.plot_status.progress.update_rel(100) self.pause_res_check() # Detect button press while between plots - if self.plot_status.b_stopped: + if self.plot_status.stopped: self.plot_status.copies_to_plot = 0 self.plot_status.progress.close() elif self.options.mode in ["res_home", "res_plot"]: @@ -453,6 +452,7 @@ def effect(self): self.svg_rand_seed = int(time.time() * 100) # New random seed for new plot self.svg_last_path = 0 self.svg_node_count = 0 + self.pathcount = 0 self.svg_layer = self.options.layer self.plot_status.delay_between_copies = False self.plot_status.copies_to_plot -= 1 @@ -463,15 +463,17 @@ def effect(self): time_counter = 10 * self.options.page_delay while time_counter > 0: time_counter -= 1 - if self.plot_status.copies_to_plot != 0 and not self.plot_status.b_stopped: + if self.plot_status.copies_to_plot != 0 and not self.plot_status.stopped: # Delay if we're between copies, not after the last or paused. if self.options.preview: self.plot_status.stats.pt_estimate += 100 + self.plot_status.stats.page_delays += 100 else: time.sleep(0.100) # Use short intervals to improve responsiveness + self.plot_status.stats.page_delays += 100 self.plot_status.progress.update_rel(100) self.pause_res_check() # Detect button press while between plots - if self.plot_status.b_stopped: + if self.plot_status.stopped: self.plot_status.copies_to_plot = 0 self.plot_status.progress.close() elif self.options.mode in ('align', 'toggle', 'cycle'): @@ -483,12 +485,13 @@ def effect(self): if self.resume_data_needs_updating: self.update_plotdata() if self.plot_status.port is not None: - ebb_motion.doTimedPause(self.plot_status.port, 10) # Add a last, timed motion command. + ebb_motion.doTimedPause(self.plot_status.port, 10, False) # Final timed motion command if self.options.port is None: # Do not close serial port if it was opened externally. self.disconnect() - for warning_message in self.warnings.return_text_list(): - self.user_message_fun(warning_message) + if not self.called_externally: # Print optional time reports + for warning_message in self.warnings.return_text_list(): + self.user_message_fun(warning_message) def resume_plot_setup(self): @@ -593,7 +596,7 @@ def setup_command(self): if self.options.mode == "align": self.pen.pen_raise(self.options, self.params, self.plot_status) - ebb_motion.sendDisableMotors(self.plot_status.port) + ebb_motion.sendDisableMotors(self.plot_status.port, False) elif self.options.mode == "toggle": self.pen.toggle(self.options, self.params, self.plot_status) elif self.options.mode == "cycle": @@ -642,8 +645,7 @@ def manual_command(self): temp_string = temp_string[:16] # Only use first 16 characters in name if not temp_string: temp_string = "" # Use empty string to clear nickname. - version_status = ebb_serial.min_version(self.plot_status.port, "2.5.5") - if version_status: + if ebb_serial.min_version(self.plot_status.port, "2.5.5"): renamed = ebb_serial.write_nickname(self.plot_status.port, temp_string) if renamed is True: self.user_message_fun('Nickname written. Rebooting EBB.') @@ -658,7 +660,7 @@ def manual_command(self): # Next: Commands that require both power and serial connectivity: self.query_ebb_voltage() # Query if button pressed, to clear the result: - ebb_motion.QueryPRGButton(self.plot_status.port) + ebb_motion.QueryPRGButton(self.plot_status.port, False) if self.options.manual_cmd == "raise_pen": self.pen.servo_setup_wrapper(self.options, self.params, self.plot_status) self.pen.pen_raise(self.options, self.params, self.plot_status) @@ -668,13 +670,13 @@ def manual_command(self): elif self.options.manual_cmd == "enable_xy": self.enable_motors() elif self.options.manual_cmd == "disable_xy": - ebb_motion.sendDisableMotors(self.plot_status.port) + ebb_motion.sendDisableMotors(self.plot_status.port, False) else: # walk motors or move home cases: self.pen.servo_setup_wrapper(self.options, self.params, self.plot_status) self.enable_motors() # Set plotting resolution if self.options.manual_cmd == "walk_home": if ebb_serial.min_version(self.plot_status.port, "2.6.2"): - a_pos, b_pos = ebb_motion.query_steps(self.plot_status.port) + a_pos, b_pos = ebb_motion.query_steps(self.plot_status.port, False) n_delta_x = -(a_pos + b_pos) / (4 * self.params.native_res_factor) n_delta_y = -(a_pos - b_pos) / (4 * self.params.native_res_factor) if self.options.resolution == 2: # Low-resolution mode @@ -750,7 +752,7 @@ def plot_document(self): if self.plot_status.port is None: return self.query_ebb_voltage() - _unused = ebb_motion.QueryPRGButton(self.plot_status.port) # Initialize button detection + _unused = ebb_motion.QueryPRGButton(self.plot_status.port, False) # Initialize button self.plot_status.progress.launch(self.plot_status, self.options) @@ -853,6 +855,7 @@ def plot_document(self): self.plot_seg_with_v(f_x, f_y, 0, 0) # pen-up move to starting point self.plot_status.resume.resume_mode = True self.node_count = 0 # Clear this _after_ move to first point. + self.pathcount = 0 elif self.options.mode == "res_home": f_x = self.pt_first[0] f_y = self.pt_first[1] @@ -867,7 +870,7 @@ def plot_document(self): v_time = self.pen.pen_raise(self.options, self.params, self.plot_status) self.v_chart_rest(v_time) - if not self.plot_status.b_stopped and self.pt_first: # Return Home after normal plot + if self.plot_status.stopped == 0 and self.pt_first: # Return Home after normal plot self.x_bounds_min = 0 self.y_bounds_min = 0 if self.end_x is not None: # Option for different final XY position: @@ -902,10 +905,9 @@ def plot_document(self): self.document = copy.deepcopy(self.original_document) self.svg = self.document.getroot() - if not self.plot_status.b_stopped: + if self.plot_status.stopped == 0: # If the plot has ended normally... if self.options.mode in ["plot", "layers", "res_home", "res_plot"]: # Clear saved plot data from the SVG file, - # IF we have _successfully completed_ a plot in plot, layer, or resume mode. self.svg_layer = -2 self.svg_node_count = 0 self.svg_last_path = 0 @@ -1026,38 +1028,15 @@ def plot_document(self): if self.plot_status.copies_to_plot == 0: # Only calculate after plotting last copy - if self.plot_status.progress.enable: - self.user_message_fun("\naxicli plot complete.\n") + if self.plot_status.progress.enable and self.plot_status.stopped == 0: + self.user_message_fun("\nAxiCLI plot complete.\n") # If sequence ended normally. elapsed_time = time.time() - self.start_time self.time_elapsed = elapsed_time # Available for use by python API - if self.options.report_time: - if self.options.preview: # Variable for public python API - self.time_estimate = self.plot_status.stats.pt_estimate / 1000.0 - else: - self.time_estimate = elapsed_time # Available for use by python API - d_dist = 0.0254 * self.pen_down_travel_inches - u_dist = 0.0254 * self.pen_up_travel_inches - t_dist = d_dist + u_dist # Total distance - self.distance_pendown = d_dist # Available for use by python API - self.distance_total = t_dist # Available for use by python API - - if not self.called_externally: # Verbose; report data to user - if self.options.preview: - self.user_message_fun("Estimated print time: " +\ - text_utils.format_hms(self.plot_status.stats.pt_estimate, True)) - elapsed_text = text_utils.format_hms(elapsed_time) - if self.options.preview: - self.user_message_fun(f"Length of path to draw: {d_dist:1.2f} m") - self.user_message_fun(f"Pen-up travel distance: {u_dist:1.2f} m") - self.user_message_fun(f"Total movement distance: {t_dist:1.2f} m") - self.user_message_fun("This estimate took " + elapsed_text + "\n") - else: - self.user_message_fun("Elapsed time: " + elapsed_text) - self.user_message_fun(f"Length of path drawn: {d_dist:1.2f} m") - self.user_message_fun(f"Total distance moved: {t_dist:1.2f} m\n") - if self.params.report_lifts: - self.user_message_fun(f"Number of pen lifts: {self.pen.status.lifts}\n") + if not self.called_externally: # Print optional time reports + self.plot_status.stats.report(self.options, self.user_message_fun, elapsed_time) + self.pen.status.report(self.params, self.user_message_fun) + if self.options.webhook and not self.options.preview: if self.options.webhook_url is not None: payload = {'value1': str(digest.name), @@ -1094,7 +1073,7 @@ def plot_doc_digest(self, digest): self.eval_layer_properties(layer.name) # Raise pen; compute with pen up. for path_item in layer.paths: - if self.plot_status.b_stopped: + if self.plot_status.stopped: return # if we're in resume mode AND self.pathcount < self.svg_last_path, skip. @@ -1139,27 +1118,25 @@ def eval_layer_properties(self, str_layer_name): max_length = len(current_layer_name) if max_length > 0: - if current_layer_name[0] == '!': # First character is "!"; insert a pause + if current_layer_name[0] == '!': # First character is "!"; insert programmatic pause # If in resume mode AND self.pathcount < self.svg_last_path, skip over this path. # If two or more forced pauses occur without any plotting between them, they # may be treated as a _single_ pause when resuming. - do_we_pause_now = False if self.plot_status.resume.resume_mode: if self.pathcount < self.svg_last_path_old: # Fully plotted; skip. # This pause was *already executed*, and we are resuming past it. Skip. self.pathcount += 1 else: - do_we_pause_now = True - if do_we_pause_now: self.pathcount += 1 # Pause counts as a "path node" for pause/resume # Record this as though it were a completed path: self.svg_last_path = self.pathcount # The number of the last path completed self.svg_last_path_nc = self.node_count # Node count after last path completed - self.plot_status.force_pause = True # set flag to pause the plot + if self.plot_status.stopped == 0: # If not already stopped + self.plot_status.stopped = -1 # Set flag for programmatic pause self.pause_res_check() # Carry out the pause, or resume if required. current_layer_name = current_layer_name[1:] # Remove leading '!' @@ -1236,8 +1213,8 @@ def plot_polyline(self, vertex_list): """ # logger.debug('plot_polyline()\nPolyline vertex_list: ' + str(vertex_list)) - if self.plot_status.b_stopped: - logger.debug('Returning: self.plot_status.b_stopped.') + if self.plot_status.stopped: + logger.debug('Polyline: self.plot_status.stopped.') return if not vertex_list: logger.debug('No vertex list to plot. Returning.') @@ -1265,9 +1242,9 @@ def plot_polyline(self, vertex_list): self.plan_trajectory(vertex_list) - if not self.plot_status.b_stopped: # Populate our "index" for resuming plots quickly: + if self.plot_status.stopped == 0: # Populate our "index" for resuming plots quickly: self.svg_last_path = self.pathcount # The number of the last path completed - self.svg_last_path_nc = self.node_count # Node count after the last path was completed. + self.svg_last_path_nc = self.node_count # Node count after the last path completed. def plan_trajectory(self, input_path): @@ -1289,7 +1266,7 @@ def plan_trajectory(self, input_path): traj_logger.debug('\nplan_trajectory()\n') - if self.plot_status.b_stopped: + if self.plot_status.stopped: return if self.f_curr_x is None: return @@ -1631,7 +1608,7 @@ def plot_seg_with_v(self, x_dest, y_dest, v_i, v_f, ignore_limits=False): seg_logger.setLevel(logging.DEBUG) # by default level is INFO seg_logger.debug('\nplot_seg_with_v({0}, {1}, {2}, {3})'.format(x_dest, y_dest, v_i, v_f)) - if self.plot_status.resume.resume_mode or self.plot_status.b_stopped: + if self.plot_status.resume.resume_mode or self.plot_status.stopped: spew_text = '\nSkipping ' else: spew_text = '\nExecuting ' @@ -1646,14 +1623,14 @@ def plot_seg_with_v(self, x_dest, y_dest, v_i, v_f, ignore_limits=False): seg_logger.debug(spew_text) if self.plot_status.resume.resume_mode: seg_logger.debug(' -> NOTE: ResumeMode is active') - if self.plot_status.b_stopped: - seg_logger.debug(' -> NOTE: Stopped by button press.') + if self.plot_status.stopped: + seg_logger.debug(' -> NOTE: Plot is in a Stopped state.') constant_vel_mode = False if self.options.const_speed and not self.pen.status.pen_up: constant_vel_mode = True - if self.plot_status.b_stopped: + if self.plot_status.stopped: self.plot_status.copies_to_plot = 0 return if self.f_curr_x is None: @@ -1710,16 +1687,10 @@ def plot_seg_with_v(self, x_dest, y_dest, v_i, v_f, ignore_limits=False): seg_logger.debug('\nBefore speedlimit check::') seg_logger.debug('vi_inch_per_s: {0}'.format(vi_inch_per_s)) seg_logger.debug('vf_inch_per_s: {0}\n'.format(vf_inch_per_s)) - - if self.options.report_time: # Also keep track of distance: - if self.pen.status.pen_up: - self.pen_up_travel_inches = self.pen_up_travel_inches + segment_length_inches - else: - self.pen_down_travel_inches = self.pen_down_travel_inches + segment_length_inches + self.plot_status.stats.add_dist(self.pen.status.pen_up, segment_length_inches) # Maximum travel speeds: # & acceleration/deceleration rate: (Maximum speed) / (time to reach that speed) - if self.pen.status.pen_up: speed_limit = self.speed_penup else: @@ -2150,7 +2121,7 @@ def plot_seg_with_v(self, x_dest, y_dest, v_i, v_f, ignore_limits=False): x_delta = (motor_dist1_temp + motor_dist2_temp) y_delta = (motor_dist1_temp - motor_dist2_temp) - if not self.plot_status.resume.resume_mode and not self.plot_status.b_stopped: + if not self.plot_status.resume.resume_mode and self.plot_status.stopped == 0: f_new_x = self.f_curr_x + x_delta f_new_y = self.f_curr_y + y_delta @@ -2200,7 +2171,7 @@ def plot_seg_with_v(self, x_dest, y_dest, v_i, v_f, ignore_limits=False): x_new_t, y_new_t)) else: ebb_motion.doXYMove(self.plot_status.port, move_steps2, move_steps1,\ - move_time) + move_time, False) self.plot_status.progress.update(self.node_count) if move_time > 50: # Sleep before issuing next command if self.options.mode != "manual": @@ -2218,61 +2189,64 @@ def plot_seg_with_v(self, x_dest, y_dest, v_i, v_f, ignore_limits=False): self.svg_last_known_pos_x = self.f_curr_x - self.pt_first[0] self.svg_last_known_pos_y = self.f_curr_y - self.pt_first[1] - def pause_res_check(self): """ Manage Pause & Resume functionality """ # First check to see if the pause button has been pressed. Increment the node counter. # Also, resume drawing if we _were_ in resume mode and need to resume at this node. - pause_state = 0 - - if self.plot_status.b_stopped: - return # We have _already_ halted the plot due to a button press. No need to proceed. + if self.plot_status.stopped > 0: + return # Plot is already stopped. No need to proceed. if self.options.preview: str_button = 0 - else: - str_button = ebb_motion.QueryPRGButton(self.plot_status.port) # Query if button pressed + else: # Query button press + str_button = ebb_motion.QueryPRGButton(self.plot_status.port, False) # To test corner cases of pause and resume cycles, one may manually force a pause: # if (self.options.mode == "plot") and (self.node_count == 24): - # self.plot_status.force_pause = True + # self.plot_status.stopped = -1 # Flag to request programmatic pause + + if self.receive_pause_request(): # Keyboard interrupt detected! + self.plot_status.stopped = -103 # Code 104: "Keyboard interrupt" + if self.plot_status.delay_between_copies: # However... it could have been... + self.plot_status.stopped = -2 # Paused between copies (OK). - self.plot_status.force_pause |= self.receive_pause_request() + if self.plot_status.stopped == -1: + self.user_message_fun('Plot paused programmatically.\n') + if self.plot_status.stopped == -103: + self.user_message_fun('\nPlot paused by keyboard interrupt.\n') - if self.plot_status.force_pause: - pause_state = 1 - elif self.plot_status.port is not None: + pause_button_pressed = 0 + if self.plot_status.stopped == 0 and self.plot_status.port is not None: try: - pause_state = int(str_button[0]) - except: - logger.error('\nUSB connection to AxiDraw lost.') + pause_button_pressed = int(str_button[0]) + except (IndexError, ValueError): + self.user_message_fun(\ + f'\nError: USB connection to AxiDraw lost. [Node {self.node_count}]\n') self.connected = False # Python interactive API variable - pause_state = 2 # Pause the plot; we appear to have lost connectivity. - logger.debug('\n (Node # : ' + str(self.node_count) + ')') - - if pause_state == 1 and not self.plot_status.delay_between_copies: - if self.plot_status.force_pause: - self.user_message_fun('Plot paused programmatically.\n') + self.plot_status.stopped = -104 # Code 104: "Lost connectivity" + + if pause_button_pressed == 1: + if self.plot_status.delay_between_copies: + self.plot_status.stopped = -2 # Paused between copies. + elif self.options.mode == "interactive": + logger.warning('Plot halted by button press during interactive session.') + logger.warning('Manually home this AxiDraw before plotting next item.\n') + self.plot_status.stopped = -102 # Code 102: "Paused by button press" else: - if self.Secondary or self.options.mode == "interactive": - logger.warning('Plot halted by button press during interactive session.') - logger.warning('Manually home this AxiDraw before plotting next item.\n') - else: - self.user_message_fun('Plot paused by button press.\n') + self.user_message_fun('Plot paused by button press.\n') + self.plot_status.stopped = -102 # Code 102: "Paused by button press" - if self.options.mode == "res_plot": - if self.node_count < self.node_target: - self.node_count = self.node_target # Special case: Paused again before resuming + if self.plot_status.stopped == -2: + self.user_message_fun('Plot sequence ended between copies.\n') + if self.plot_status.stopped: logger.debug('\n (Paused after node number : ' + str(self.node_count) + ')') + if self.options.mode == "res_plot": #Check for a special case: + if self.node_count < self.node_target: # If Paused again before resuming, + self.node_count = self.node_target # Skip to end of resume mode. - self.plot_status.force_pause = False # Clear the flag, if set - - if pause_state == 1 and self.plot_status.delay_between_copies: - self.user_message_fun('Plot sequence ended between copies.\n') - - if pause_state in (1, 2): # Stop plot + if self.plot_status.stopped < 0: # Stop plot self.svg_node_count = self.node_count self.svg_paused_x = self.f_curr_x - self.pt_first[0] self.svg_paused_y = self.f_curr_y - self.pt_first[1] @@ -2280,9 +2254,10 @@ def pause_res_check(self): self.v_chart_rest(v_time) if not self.plot_status.delay_between_copies and \ not self.Secondary and self.options.mode != "interactive": - # Only say this if we're not in the delay between copies, nor a "second" unit. - self.user_message_fun('Use the resume feature to continue.\n') - self.plot_status.b_stopped = True + # Only print if we're not in the delay between copies, nor a "second" unit. + if self.plot_status.stopped != -104: # Do not display after loss of USB. + self.user_message_fun('Use the resume feature to continue.\n') + self.plot_status.stopped = - self.plot_status.stopped return # Note: This segment is not plotted. self.node_count += 1 # This whole segment move counts as ONE pause/resume node in our plot @@ -2305,38 +2280,12 @@ def pause_res_check(self): v_time = self.pen.pen_lower(self.options, self.params, self.plot_status) self.v_chart_rest(v_time) - def serial_connect(self): """ Connect to AxiDraw over USB """ - port_name = None - if self.options.port_config == 1: # port_config value "1": Use first available AxiDraw. - self.options.port = None - if not self.options.port: # Try to connect to first available AxiDraw. - self.plot_status.port = ebb_serial.openPort() - elif str(type(self.options.port)) in ( - "", "", ""): - # This function may be passed a port name to open (and later close). - self.options.port = str(self.options.port).strip('\"') - port_name = self.options.port - the_port = ebb_serial.find_named_ebb(self.options.port) - self.plot_status.port = ebb_serial.testPort(the_port) - self.options.port = None # Clear this input, to ensure that we close the port later. - else: - # self.options.port may be a serial port object of type serial.serialposix.Serial. - # In that case, interact with that given port object, and leave it open at the end. - self.plot_status.port = self.options.port - if self.plot_status.port is None: - if port_name: - self.user_message_fun(gettext.gettext('Failed to connect to AxiDraw ') +\ - str(port_name)) - else: - self.user_message_fun(gettext.gettext("Failed to connect to AxiDraw.")) - return - self.connected = True # Python interactive API variable - if port_name: - logger.debug(gettext.gettext('Connected successfully to port: ' + str(port_name))) + if serial_utils.connect(self.options, self.plot_status, self.user_message_fun, logger): + self.connected = True # Variable available in the Python interactive API. else: - logger.debug(" Connected successfully") + self.plot_status.stopped = 101 # Will become exit code 101; failed to connect def enable_motors(self): """ @@ -2352,7 +2301,7 @@ def enable_motors(self): if self.options.resolution == 1: # High-resolution ("Super") mode if not self.options.preview: - res_1, res_2 = ebb_motion.query_enable_motors(self.plot_status.port) + res_1, res_2 = ebb_motion.query_enable_motors(self.plot_status.port, False) if not (res_1 == 1 and res_2 == 1): # Do not re-enable if already enabled ebb_motion.sendEnableMotors(self.plot_status.port, 1) # 16X microstepping self.step_scale = 2.0 * self.params.native_res_factor @@ -2362,7 +2311,7 @@ def enable_motors(self): self.speed_pendown = self.speed_pendown * self.params.const_speed_factor_hr else: # i.e., self.options.resolution == 2; Low-resolution ("Normal") mode if not self.options.preview: - res_1, res_2 = ebb_motion.query_enable_motors(self.plot_status.port) + res_1, res_2 = ebb_motion.query_enable_motors(self.plot_status.port, False) if not (res_1 == 2 and res_2 == 2): # Do not re-enable if already enabled ebb_motion.sendEnableMotors(self.plot_status.port, 2) # 8X microstepping self.step_scale = self.params.native_res_factor @@ -2372,16 +2321,11 @@ def enable_motors(self): if self.options.const_speed: self.speed_pendown = self.speed_pendown * self.params.const_speed_factor_lr if self.params.use_b3_out: - ebb_motion.PBOutConfig(self.plot_status.port, 3, 0) # Set I/O Pin B3 as an output, low + ebb_motion.PBOutConfig(self.plot_status.port, 3, 0, False) # I/O Pin B3 -> output, low def query_ebb_voltage(self): """ Check that power supply is detected. """ - if self.params.skip_voltage_check: - return - if self.plot_status.port is not None and not self.options.preview: - voltage_o_k = ebb_motion.queryVoltage(self.plot_status.port) - if not voltage_o_k: - self.warnings.add_new('voltage') + serial_utils.query_voltage(self.options, self.params, self.plot_status, self.warnings) def get_doc_props(self): """ diff --git a/inkscape driver/axidraw_conf.py b/inkscape driver/axidraw_conf.py index 7d287fe..d84702b 100644 --- a/inkscape driver/axidraw_conf.py +++ b/inkscape driver/axidraw_conf.py @@ -2,7 +2,7 @@ # Part of the AxiDraw driver software # # https://github.com/evil-mad/axidraw -# Version 3.5.0, dated 2022-07-31. +# Version 3.6.0, dated 2022-10-01. # # Copyright 2022 Windell H. Oskay, Evil Mad Scientist Laboratories # @@ -18,48 +18,59 @@ behavior and performance of your AxiDraw to your application and taste. These parameters are used as defaults when using AxiDraw with the command- - line interface (CLI) or with the python library. With the CLI, you can - make copies of this configuration file and specify a configuration file. + line interface (CLI) or with the python API. With the CLI, you can make + copies of this configuration file and specify one as a configuration file. + When using the Python API, override individual settings within your script. -If you are operating the AxiDraw from within Inkscape, please set your - preferences within Inkscape, using the AxiDraw Control dialog. - Most values listed here are ignored when running within Inkscape. + If you are operating the AxiDraw from within Inkscape, please set your + preferences within Inkscape, using the AxiDraw Control dialog. Settings + that appear both here and in AxiDraw Control will be ignored; those + from AxiDraw Control will be used. Other settings can be configured here. -Similarly, values set within Inkscape are ignored when using the CLI or - python library. +Settings within Inkscape only affect use within Inkscape, and do not affect + the behavior of the AxiDraw CLI or Python APIs. ''' # DEFAULT VALUES -speed_pendown = 25 # Maximum plotting speed, when pen is down (1-100) -speed_penup = 75 # Maximum transit speed, when pen is up (1-100) -accel = 75 # Acceleration rate factor (1-100) +mode = 'plot' # Operational mode or GUI tab. Default: 'plot' -pen_pos_up = 60 # Height of pen when raised (0-100) -pen_pos_down = 30 # Height of pen when lowered (0-100) +speed_pendown = 25 # Maximum plotting speed, when pen is down (1-100). Default 25 +speed_penup = 75 # Maximum transit speed, when pen is up (1-100). Default 75 +accel = 75 # Acceleration rate factor (1-100). Default 75 -pen_rate_raise = 75 # Rate of raising pen (1-100) -pen_rate_lower = 50 # Rate of lowering pen (1-100) +pen_pos_up = 60 # Height of pen when raised (0-100). Default 60 +pen_pos_down = 30 # Height of pen when lowered (0-100). Default 30 -pen_delay_up = 0 # Optional delay after pen is raised (ms) -pen_delay_down = 0 # Optional delay after pen is lowered (ms) +pen_rate_raise = 75 # Rate of raising pen (1-100). Default 75 +pen_rate_lower = 50 # Rate of lowering pen (1-100). Default 50 -const_speed = False # Use constant velocity mode when pen is down -report_time = False # Report time elapsed -default_layer = 1 # Layer(s) selected for layers mode (1-1000) +pen_delay_up = 0 # Optional delay after pen is raised (ms). Default 0 +pen_delay_down = 0 # Optional delay after pen is lowered (ms). Default 0 + +const_speed = False # Use constant velocity mode when pen is down. Default False +report_time = False # Report time elapsed. Default False + +default_layer = 1 # Layer(s) selected for layers mode (1-1000). Default 1 + +manual_cmd = 'fw_version' # Manual command to execute when in manual mode. + # Default 'fw_version' + +walk_dist = 1.0 # Distance to walk in "walking" manual commands. Units may be + # selected with the value of manual_cmd. Default 1.0 copies = 1 # Copies to plot, or 0 for continuous plotting. Default: 1 -page_delay = 15 # Optional delay between copies (s). +page_delay = 15 # Optional delay between copies (s). Default 15 -preview = False # Preview mode; simulate plotting only. +preview = False # Preview mode; simulate plotting only. Default False rendering = 3 # Preview mode rendering option (0-3): # 0: Do not render previews # 1: Render only pen-down movement # 2: Render only pen-up movement # 3: Render all movement (Default) -model = 1 # AxiDraw Model (1-6) +model = 1 # AxiDraw Model (1-6). # 1: AxiDraw V2 or V3 (Default). 2: AxiDraw V3/A3 or SE/A3. # 3: AxiDraw V3 XLX. 4: AxiDraw MiniKit. # 5: AxiDraw SE/A1. 6: AxiDraw SE/A2. @@ -81,16 +92,12 @@ # 2: Full; Also allow path reversal # 4: None; Strictly preserve file order -random_start = False # Randomize start locations of closed paths. (Default: False) - -resolution = 1 # Resolution: (1-2): - # 1: High resolution (smoother, slightly slower) (Default) - # 2: Low resolution (coarser, slightly faster) +random_start = False # Randomize start locations of closed paths. Default False webhook = False # Enable webhook alerts when True - # Default: False + # Default False -webhook_url = None # URL for webhook alerts +webhook_url = None # URL for webhook alerts. Default None digest = 0 # Plot digest output option. (NOT supported in Inkscape context.) # 0: Disabled; No change to behavior or output (Default) @@ -98,9 +105,13 @@ # 2: Disable plots and previews; generate digest only progress = False # Enable progress bar display in AxiDraw CLI, when True - # Default: False + # Default False # This option has no effect in Inkscape or Python API contexts. +resolution = 1 # Resolution: (1-2): + # 1: High resolution (smoother, slightly slower) (Default) + # 2: Low resolution (coarser, slightly faster) + # Effective motor resolution is approx. 1437 or 2874 steps per inch, in the two modes respectively. # Note that these resolutions are defined along the native axes of the machine (X+Y) and (X-Y), # not along the XY axes of the machine. This parameter chooses 8X or 16X motor microstepping. diff --git a/inkscape driver/axidraw_control.py b/inkscape driver/axidraw_control.py index 126a8ed..faca4a3 100644 --- a/inkscape driver/axidraw_control.py +++ b/inkscape driver/axidraw_control.py @@ -62,6 +62,7 @@ def __init__( self, default_logging = True, params = None ): # use default configuration file params = import_module("axidrawinternal.axidraw_conf") # Configuration file self.params = params + self.status_code = 0 inkex.Effect.__init__( self ) @@ -188,7 +189,6 @@ def effect( self ): self.options.port = None self.plot_to_axidraw(self.options.port, True) - def plot_to_axidraw( self, port, primary): """ Delegate the plot to a particular AxiDraw """ # if primary: @@ -242,6 +242,7 @@ def plot_to_axidraw( self, port, primary): if primary: self.document = ad.document self.outdoc = ad.get_output() # Collect output from axidraw.py + self.status_code = ad.plot_status.stopped else: if ad.error_out: if port is not None: diff --git a/inkscape driver/axidraw_options/common_options.py b/inkscape driver/axidraw_options/common_options.py index a3f34fc..9a9ff74 100644 --- a/inkscape driver/axidraw_options/common_options.py +++ b/inkscape driver/axidraw_options/common_options.py @@ -127,10 +127,10 @@ def core_options(parser, config): type="int", action="store", dest="reordering",\ default=config["reordering"],\ help="SVG reordering option (0-4; 3 deprecated)."\ - + " 0: Least; Only connect adjoining paths."\ - + " 1: Basic; Also reorder paths for speed."\ - + " 2: Full; Also allow path reversal."\ - + " 4: None; Strictly preserve file order.") + + " 0: Least: Only connect adjoining paths."\ + + " 1: Basic: Also reorder paths for speed."\ + + " 2: Full: Also allow path reversal."\ + + " 4: None: Strictly preserve file order.") options.add_option("--resolution",\ type="int", action="store", dest="resolution",\ @@ -158,11 +158,11 @@ def core_options(parser, config): return options def core_mode_options(parser, config): - options = OptionGroup(parser, "Mode Options") + options = OptionGroup(parser, "Mode Options") options.add_option("--mode",\ action="store", type="string", dest="mode",\ - default="plot", \ + default=config["mode"], \ help="Mode or GUI tab. One of: [plot, layers, align, toggle, cycle"\ + ", manual, sysinfo, version, res_plot, res_home]. Default: plot.") @@ -173,7 +173,7 @@ def core_mode_options(parser, config): options.add_option("--manual_cmd",\ type="string", action="store", dest="manual_cmd",\ - default="fw_version",\ + default=config["manual_cmd"],\ help="Manual command. One of: [fw_version, raise_pen, lower_pen, "\ + "walk_x, walk_y, walk_mmx, walk_mmy, walk_home, enable_xy, "\ + "disable_xy, bootload, strip_data, read_name, list_names, "\ @@ -181,8 +181,8 @@ def core_mode_options(parser, config): options.add_option("--walk_dist",\ type="float", action="store", dest="walk_dist",\ - default=1,\ - help="Distance for manual walk (inches)") + default=config["walk_dist"],\ + help="Distance for manual walk") options.add_option("--layer",\ type="int", action="store", dest="layer",\ diff --git a/inkscape driver/axidraw_options/versions.py b/inkscape driver/axidraw_options/versions.py index 47dceac..6af4362 100644 --- a/inkscape driver/axidraw_options/versions.py +++ b/inkscape driver/axidraw_options/versions.py @@ -45,10 +45,12 @@ def get_versions_online(): url = "https://evilmadscience.s3.amazonaws.com/sites/axidraw/versions.txt" text = None try: - text = requests.get(url).text + text = requests.get(url, timeout=15).text + except requests.exceptions.Timeout as err: + raise RuntimeError("Unable to check for updates online; connection timed out.\n") from err except (RuntimeError, requests.exceptions.ConnectionError) as err_info: raise RuntimeError("Could not contact server to check for updates. " + - f"Are you connected to the internet?\n\n(Error details: {err_info})\n") + f"Are you connected to the internet?\n\n(Error details: {err_info})\n") from err_info if text: try: @@ -76,8 +78,8 @@ def get_fw_version(serial_port): fw_version_string = fw_version_string.strip() # For number comparisons return fw_version_string except RuntimeError as err_info: - raise RuntimeError(f"Error retrieving the EBB firmware version.\n\n(Error: {err_info})\n") - + raise RuntimeError(f"Error retrieving the EBB firmware version.\n\n(Error: {err_info})\n")\ + from err_info def get_current(serial_port): ''' @@ -104,8 +106,6 @@ def log_axidraw_control_version(online_versions, current_version_string, log_fun `online_versions` is a Versions namedtuple or False, e.g. the return value of get_versions_online ''' - log_fun(f"This is AxiDraw Control version {current_version_string}.") - if online_versions: if parse(online_versions.axidraw_control) > parse(current_version_string): log_fun("An update is available to a newer version, " + @@ -128,7 +128,6 @@ def log_ebb_version(fw_version_string, online_versions, log_fun): ''' `online_versions` is False if we failed or didn't try to get the online versions ''' - # log_fun("\nYour AxiDraw has firmware version {}.".format(fw_version_string)) log_fun(f"\nYour AxiDraw has firmware version {fw_version_string}.") if online_versions: @@ -144,6 +143,7 @@ def log_version_info(serial_port, check_updates, current_version_string, preview works whether or not `check_updates` is True, online versions were successfully retrieved, or `serial_port` is None (i.e. not connected AxiDraw) ''' + message_fun(f"This is AxiDraw Control version {current_version_string}.") online_versions = False if check_updates: try: diff --git a/inkscape driver/digest_svg.py b/inkscape driver/digest_svg.py index 894eed4..b64ecfb 100644 --- a/inkscape driver/digest_svg.py +++ b/inkscape driver/digest_svg.py @@ -73,11 +73,6 @@ def __init__(self, default_logging=True): self.doc_digest = path_objects.DocDigest() - self.style_dict = {} - self.style_dict['fill'] = None - self.style_dict['stroke'] = None - self.style_dict['fill_rule'] = None - # Variables that will be populated in process_svg(): self.bezier_tolerance = 0 self.supersample_tolerance = 0 @@ -88,7 +83,7 @@ def __init__(self, default_logging=True): self.diagonal_100 = 0 - def process_svg(self, node_list, warnings, digest_params, mat_current=None): + def process_svg(self, node_list, warnings, digest_params, mat_current=None): """ Wrapper around routine to recursively traverse an SVG document. @@ -135,12 +130,11 @@ def process_svg(self, node_list, warnings, digest_params, mat_current=None): self.current_layer = root_layer # Layer that graphical elements should be added to self.current_layer_name = root_layer.name - self.traverse(node_list, warnings, mat_current) + self.traverse(node_list, None, warnings, mat_current) return self.doc_digest - def traverse(self, node_list, warnings, mat_current=None,\ - parent_visibility='visible'): + def traverse(self, node_list, parent_style, warnings, mat_current): """ Recursively traverse the SVG file and process all of the paths. Keep track of the composite transformation applied to each path. @@ -156,34 +150,19 @@ def traverse(self, node_list, warnings, mat_current=None,\ Inkscape or another vector graphics editor. """ - # Future work: - # Ensure that fill and stroke attributes are correctly inherited - # from parents where applicable. E.g., If a group has a stroke. - # Guideline: Match Inkscape's style inheritance behavior - if mat_current is None: mat_current = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]] for node in node_list: + node_visibility = node.get('visibility') element_style = simplestyle.parseStyle(node.get('style')) + style_dict = inherit_style(parent_style, element_style, node_visibility) - # Check for "display:none" in the node's style attribute: - if 'display' in element_style.keys() and element_style['display'] == 'none': + if style_dict['display'] == 'none': continue # Do not plot this object or its children - # The node may have a display="none" attribute as well: - if node.get('display') == 'none': + if node.get('display') == 'none': # Possible SVG attribute as well continue # Do not plot this object or its children - # Visibility attributes control whether a given object will plot. - # Children of hidden (not visible) parents may be plotted if - # they assert visibility. - visibility = node.get('visibility', parent_visibility) - if visibility == 'inherit': - visibility = parent_visibility - - if 'visibility' in element_style.keys(): - visibility = element_style['visibility'] # Style may override attribute. - # first apply the current matrix transform to this node's transform mat_new = simpletransform.composeTransform(mat_current, \ simpletransform.parseTransform(node.get("transform"))) @@ -240,7 +219,7 @@ def traverse(self, node_list, warnings, mat_current=None,\ self.current_layer = new_layer self.current_layer_name = str(str_layer_name) - self.traverse(node, warnings, mat_new, parent_visibility=visibility) + self.traverse(node, style_dict, warnings, mat_new) # After parsing a layer, add a new "root layer" for any objects # that may appear in root before the next layer: @@ -253,26 +232,26 @@ def traverse(self, node_list, warnings, mat_current=None,\ self.current_layer = new_layer self.current_layer_name = new_layer.name else: # Regular group or sublayer that we treat as a group. - self.traverse(node, warnings, mat_new, parent_visibility=visibility) + self.traverse(node, style_dict, warnings, mat_new) continue if node.tag == inkex.addNS('symbol', 'svg') or node.tag == 'symbol': # A symbol is much like a group, except that it should only # be rendered when called within a "use" tag. if self.use_tag_nest_level > 0: - self.traverse(node, warnings, mat_new, parent_visibility=visibility) + self.traverse(node, style_dict, warnings, mat_new) continue if node.tag == inkex.addNS('a', 'svg') or node.tag == 'a': # An 'a' is much like a group, in that it is a generic container element. - self.traverse(node, warnings, mat_new, parent_visibility=visibility) + self.traverse(node, style_dict, warnings, mat_new) continue if node.tag == inkex.addNS('switch', 'svg') or node.tag == 'switch': # A 'switch' is much like a group, in that it is a generic container element. # We are not presently evaluating conditions on switch elements, but parsing # their contents to the extent possible. - self.traverse(node, warnings, mat_new, parent_visibility=visibility) + self.traverse(node, style_dict, warnings, mat_new) continue if node.tag == inkex.addNS('use', 'svg') or node.tag == 'use': @@ -306,9 +285,8 @@ def traverse(self, node_list, warnings, mat_current=None,\ simpletransform.parseTransform(f'translate({x_val:.6E},{y_val:.6E})')) else: mat_new2 = mat_new - visibility = node.get('visibility', visibility) self.use_tag_nest_level += 1 # Keep track of nested "use" elements. - self.traverse(refnode, warnings, mat_new2, parent_visibility=visibility) + self.traverse(refnode, style_dict, warnings, mat_new2) self.use_tag_nest_level -= 1 continue @@ -318,33 +296,14 @@ def traverse(self, node_list, warnings, mat_current=None,\ if self.current_layer_name == '__digest-root__': continue # Do not print root elements if layer_selection >= 0 - if visibility in ('hidden', 'collapse'): - # Do not plot this node if it is not visible. - # This comes after use, a, and group tags because - # items within a hidden item may be visible. + if style_dict['visibility'] in ('hidden', 'collapse'): + # Not visible; Do not plot. (This comes after the container tags; + # visible children of hidden elements can still plot.) continue - element_style = simplestyle.parseStyle(node.get('style')) - - if 'fill' in element_style.keys(): - self.style_dict['fill'] = element_style['fill'] - else: - self.style_dict['fill'] = None - - if 'stroke' in element_style.keys(): - self.style_dict['stroke'] = element_style['stroke'] - else: - self.style_dict['stroke'] = None - - fill_rule = node.get('fill-rule') - if fill_rule: - self.style_dict['fill_rule'] = fill_rule - else: - self.style_dict['fill_rule'] = None - if node.tag == inkex.addNS('path', 'svg'): path_d = node.get('d') - self.digest_path(path_d, mat_new) + self.digest_path(path_d, style_dict, mat_new) continue if node.tag == inkex.addNS('rect', 'svg') or node.tag == 'rect': """ @@ -355,10 +314,13 @@ def traverse(self, node_list, warnings, mat_current=None,\ https://www.w3.org/TR/SVG11/shapes.html#RectElement """ - x, r_x, width = [plot_utils.unitsToUserUnits(node.get(attr), - self.doc_width_100) for attr in ['x', 'rx', 'width']] - y, r_y, height = [plot_utils.unitsToUserUnits(node.get(attr), - self.doc_height_100) for attr in ['y', 'ry', 'height']] + x = plot_utils.unitsToUserUnits(node.get('x', '0'), self.doc_width_100) + y = plot_utils.unitsToUserUnits(node.get('y', '0'), self.doc_height_100) + + r_x, width = [plot_utils.unitsToUserUnits(node.get(attr), + self.doc_width_100) for attr in ['rx', 'width']] + r_y, height = [plot_utils.unitsToUserUnits(node.get(attr), + self.doc_height_100) for attr in ['ry', 'height']] def calc_r_attr(attr, other_attr, twice_maximum): value = (attr if attr is not None else @@ -387,7 +349,7 @@ def calc_r_attr(attr, other_attr, twice_maximum): instr.append([' L ', [x, y + height]]) instr.append([' L ', [x, y]]) - self.digest_path(simplepath.formatPath(instr), mat_new) + self.digest_path(simplepath.formatPath(instr), style_dict, mat_new) continue if node.tag == inkex.addNS('line', 'svg') or node.tag == 'line': """ @@ -402,7 +364,7 @@ def calc_r_attr(attr, other_attr, twice_maximum): path_a = [] path_a.append(['M ', [x_1, y_1]]) path_a.append([' L ', [x_2, y_2]]) - self.digest_path(simplepath.formatPath(path_a), mat_new) + self.digest_path(simplepath.formatPath(path_a), style_dict, mat_new) continue if node.tag in [inkex.addNS('polyline', 'svg'), 'polyline', @@ -438,7 +400,7 @@ def calc_r_attr(attr, other_attr, twice_maximum): if node.tag in [inkex.addNS('polygon', 'svg'), 'polygon']: path_d += " Z" - self.digest_path(path_d, mat_new) # Vertices are already in user coordinate system + self.digest_path(path_d, style_dict, mat_new) # Vertices are already in user coordinate system continue if node.tag in [inkex.addNS('ellipse', 'svg'), 'ellipse', inkex.addNS('circle', 'svg'), 'circle']: @@ -473,7 +435,7 @@ def calc_r_attr(attr, other_attr, twice_maximum): f'0 1 0 {x_2:f},{c_y:f} ' + \ f'A {r_x:f},{r_y:f} ' + \ f'0 1 0 {x_1:f},{c_y:f}' - self.digest_path(path_d, mat_new) + self.digest_path(path_d, style_dict, mat_new) continue if node.tag == inkex.addNS('metadata', 'svg') or node.tag == 'metadata': self.doc_digest.metadata.update(dict(node.attrib)) @@ -530,7 +492,7 @@ def calc_r_attr(attr, other_attr, twice_maximum): text = str(node.tag).split('}') warnings.add_new(str(text[-1]), self.current_layer_name) - def digest_path(self, path_d, mat_transform): + def digest_path(self, path_d, style_dict, mat_transform): """ Parse the path while applying the matrix transformation mat_transform. - Input is the "d" string attribute from an SVG path. @@ -577,9 +539,9 @@ def digest_path(self, path_d, mat_transform): return # At least one sub-path required new_path = path_objects.PathItem() - new_path.fill = self.style_dict['fill'] - new_path.stroke = self.style_dict['stroke'] - new_path.fill_rule = self.style_dict['fill_rule'] + new_path.fill = style_dict['fill'] + new_path.stroke = style_dict['stroke'] + new_path.fill_rule = style_dict['fill-rule'] new_path.item_id = str(self.next_id) self.next_id += 1 @@ -591,6 +553,46 @@ def digest_path(self, path_d, mat_transform): logger.debug('End of digest_path()\n') +def inherit_style(parent_style, node_style, visibility): + ''' + Parse style dict of node for fill and stroke information only. + Inherit style from parent, but supersede it when a local style is defined. + Also handle precedence of SVG "visibility" attribute, separate from the style. + Note that children of hidden parents may be plotted if they assert visibility. + ''' + + default_style = dict() + default_style['fill'] = None + default_style['stroke'] = None + default_style['fill-rule'] = None # Future work: Add support for 'nonzero' and 'evenodd' + default_style['visibility'] = 'visible' + default_style['display'] = None # A null value; not "display:none". + + if parent_style is None: # Use default values when there is no parent + parent_style = default_style + + # Use copy, not assignment, so that new_style represents an independent dict: + new_style = parent_style.copy() + + if visibility: # Update first, allowing it to be overruled by style attributes + new_style['visibility'] = visibility + + if node_style is None: # No additional new style information provided. + return new_style + + for attrib in ['fill', 'stroke', 'fill-rule', 'visibility', 'display',]: + # Valid for "string" attributes that DO NOT have units that need scaling; + # Do not extend this to other style attributes without accounting for that. + value = node_style.get(attrib) # Defaults to None, preventing KeyError + if value: + if value in ['inherit']: + new_style[attrib] = parent_style[attrib] + else: + new_style[attrib] = value + + return new_style + + def verify_plob(svg, model): """ Check to see if the provided SVG is a valid plob that can be automatically converted diff --git a/inkscape driver/pen_handling.py b/inkscape driver/pen_handling.py index 4fdf1b3..27779c4 100644 --- a/inkscape driver/pen_handling.py +++ b/inkscape driver/pen_handling.py @@ -34,7 +34,6 @@ * PenStatus: Data storage class for pen lift status variables - """ import time @@ -45,7 +44,6 @@ # inkex = from_dependency_import('ink_extensions.inkex') - class PenHeight: """ PenHeight: Class to manage pen-down height settings. @@ -54,8 +52,7 @@ class PenHeight: def __init__(self): self.pen_pos_down = None # Initial values must be set by update(). - self.pen_pos_down_default = None - self.use_temp_pen_height = None # Boolean set true while using temporary value + self.use_temp_pen_height = False # Boolean set true while using temporary value self.times = PenLiftTiming() def update(self, options, params): @@ -63,9 +60,8 @@ def update(self, options, params): Set initial/default values of options, after __init__. Call this function after changing option values to update pen height settings. ''' - self.pen_pos_down = options.pen_pos_down - self.pen_pos_down_default = options.pen_pos_down - self.use_temp_pen_height = False + if not self.use_temp_pen_height: + self.pen_pos_down = options.pen_pos_down self.times.update(options, params, self.pen_pos_down) def set_temp_height(self, options, params, temp_height): @@ -76,6 +72,7 @@ def set_temp_height(self, options, params, temp_height): if self.pen_pos_down == temp_height: return False self.pen_pos_down = temp_height + self.times.update(options, params, temp_height) return True @@ -84,9 +81,9 @@ def end_temp_height(self, options, params): End using temporary pen height position. Return True if the position has changed. ''' self.use_temp_pen_height = False - if self.pen_pos_down == self.pen_pos_down_default: + if self.pen_pos_down == options.pen_pos_down: return False - self.pen_pos_down = self.pen_pos_down_default + self.pen_pos_down = options.pen_pos_down self.times.update(options, params, self.pen_pos_down) return True @@ -156,6 +153,12 @@ def reset(self): self.virtual_pen_up = False self.lifts = 0 + def report(self, params, message_fun): + """ report: Print pen lift statistics """ + if not params.report_lifts: + return + message_fun(f"Number of pen lifts: {self.lifts}\n") + class PenHandler: """ PenHandler: Main class for managing pen lifting, lowering, and status @@ -191,22 +194,15 @@ def pen_raise(self, options, params, plot_status): self.status.lifts += 1 v_time = self.heights.times.raise_time - if options.preview: - pass - # Old functionality in axidraw.py: - # self.update_v_charts(0, 0, 0) - # self.vel_data_time += v_time - # self.update_v_charts(0, 0, 0) - # self.pt_estimate += v_time - else: - ebb_motion.sendPenUp(plot_status.port, v_time, params.servo_pin) - if params.use_b3_out: - ebb_motion.PBOutValue( plot_status.port, 3, 0 ) # I/O Pin B3 output: low + if not options.preview: + ebb_motion.sendPenUp(plot_status.port, v_time, params.servo_pin, False) if (v_time > 50) and (options.mode not in ["manual", "align", "toggle", "cycle"]): time.sleep(float(v_time - 30) / 1000.0) # pause before issuing next command + if params.use_b3_out: + ebb_motion.PBOutValue( plot_status.port, 3, 0, False) # I/O Pin B3 output: low self.status.pen_up = True if not self.status.ebblv_set: - ebb_motion.setEBBLV(plot_status.port, options.pen_pos_up + 1) + ebb_motion.setEBBLV(plot_status.port, options.pen_pos_up + 1, False) self.status.ebblv_set = True return v_time @@ -221,24 +217,17 @@ def pen_lower(self, options, params, plot_status): return 0 # skip if pen is state is _known_ and is down # Skip if stopped, or if resuming: - if plot_status.resume.resume_mode or plot_status.b_stopped: + if plot_status.resume.resume_mode or plot_status.stopped: return 0 v_time = self.heights.times.lower_time - if options.preview: - pass - # URGENT TODO: Replace this functionality in axidraw.py - # self.update_v_charts(0, 0, 0) - # self.vel_data_time += v_time - # self.update_v_charts(0, 0, 0) - # self.pt_estimate += v_time - else: - ebb_motion.sendPenDown(plot_status.port, v_time, params.servo_pin) - if params.use_b3_out: - ebb_motion.PBOutValue( plot_status.port, 3, 1 ) # I/O Pin B3 output: high + if not options.preview: + ebb_motion.sendPenDown(plot_status.port, v_time, params.servo_pin, False) if (v_time > 50) and (options.mode not in ["manual", "align", "toggle", "cycle"]): time.sleep(float(v_time - 30) / 1000.0) # pause before issuing next command + if params.use_b3_out: + ebb_motion.PBOutValue( plot_status.port, 3, 1, False) # I/O Pin B3 output: high self.status.pen_up = False return v_time @@ -262,8 +251,8 @@ def cycle(self, options, params, plot_status): Call only after servo_setup_wrapper(). This function should only be used as a setup utility. """ - v_time = self.pen_lower(options, params, plot_status) - time.sleep((v_time + 500.0) / 1000.0) + self.pen_lower(options, params, plot_status) + ebb_serial.command(plot_status.port, 'SM,500,0,0\r') self.pen_raise(options, params, plot_status) @@ -326,10 +315,12 @@ def servo_setup_wrapper(self, options, params, status): return # Need to figure out if we're in the pen-up or pen-down state, or indeterminate: - value = ebb_motion.queryEBBLV(status.port) + value = ebb_motion.queryEBBLV(status.port, False) + if value is None: + return if int(value) != options.pen_pos_up + 1: # See "Methods" above for what's going on here. - ebb_motion.setEBBLV(status.port, 0) + ebb_motion.setEBBLV(status.port, 0, False) self.status.ebblv_set = False self.status.virtual_pen_up = False @@ -337,7 +328,7 @@ def servo_setup_wrapper(self, options, params, status): # Note, however, that this does not ensure that the current # Z position matches that in the settings. self.status.ebblv_set = True - if ebb_motion.QueryPenUp(status.port): + if ebb_motion.QueryPenUp(status.port, False): self.status.pen_up = True self.status.virtual_pen_up = True else: @@ -361,18 +352,18 @@ def servo_setup(self, options, params, status): return self.heights.update(options, params) # Ensure heights and transit times are known - servo_range = params.servo_max - params.servo_min servo_slope = float(servo_range) / 100.0 + int_temp = int(round(params.servo_min + servo_slope * options.pen_pos_up)) - ebb_motion.setPenUpPos(status.port, int_temp) + ebb_motion.setPenUpPos(status.port, int_temp, False) int_temp = int(round(params.servo_min + servo_slope * self.heights.pen_pos_down)) - ebb_motion.setPenDownPos(status.port, int_temp) + ebb_motion.setPenDownPos(status.port, int_temp, False) servo_rate_scale = float(servo_range) * 0.24 / params.servo_sweep_time int_temp = int(round(servo_rate_scale * options.pen_rate_raise)) - ebb_motion.setPenUpRate(status.port, int_temp) - int_temp = int(round(servo_rate_scale * options.pen_rate_lower)) - ebb_motion.setPenDownRate(status.port, int_temp) + ebb_motion.setPenUpRate(status.port, int_temp, False) - ebb_motion.servo_timeout(status.port, params.servo_timeout) # Set servo power timeout + int_temp = int(round(servo_rate_scale * options.pen_rate_lower)) + ebb_motion.setPenDownRate(status.port, int_temp, False) + ebb_motion.servo_timeout(status.port, params.servo_timeout, None, False) diff --git a/inkscape driver/plot_status.py b/inkscape driver/plot_status.py index 24c8e43..8782894 100644 --- a/inkscape driver/plot_status.py +++ b/inkscape driver/plot_status.py @@ -37,23 +37,23 @@ # """ # PlotData: Class for data items stored in plotdata elements within the SVG file # Not in use yet -# +# # """ -# +# # ITEM_NAMES = ['layer', 'node', 'last_path', 'node_after_path', 'last_known_x', # 'last_known_y', 'paused_x', 'paused_y', 'application', # 'plob_version', 'row', 'randseed'] -# +# # def __init__(self): # for key in self.ITEM_NAMES: # Create instance variables in __init__ # setattr(self, key, None) # self.reset() # Set defaults via reset function -# +# # def reset(self): # '''Set default values''' # for key in self.ITEM_NAMES: # Set all to 0 except those specified below. # setattr(self, key, 0) -# +# # self.svg_layer = -2 # self.svg_rand_seed = 1 # self.svg_application = None @@ -63,9 +63,9 @@ # """ # PlotData: Class for managing plotdata elements # Not in use yet -# +# # """ -# +# # def __init__(self): # self.svg_data_read = False # self.svg_data_written = False @@ -79,9 +79,53 @@ class PlotStats: # pylint: disable=too-few-public-methods """ def __init__(self): - # self.pen_up_travel_inches = 0 # not yet implemented - # self.pen_down_travel_inches = 0 # not yet implemented + self.up_travel_inch = 0 # Pen-up travel distance, inches + self.down_travel_inch = 0 # Pen-down travel distance, inches self.pt_estimate = 0 # Plot time estimate, ms + self.page_delays = 0 # Delays between pages, ms + + def reset(self): + ''' Reset attributes to defaults ''' + self.up_travel_inch = 0 + self.down_travel_inch = 0 + self.pt_estimate = 0 + self.page_delays = 0 + + + def add_dist(self, pen_up, distance_inch): + """ add_dist: Add distance of the current plot segment to total distances """ + if pen_up: + self.up_travel_inch += distance_inch + else: + self.down_travel_inch += distance_inch + + def report(self, options, message_fun, elapsed_time): + """ report: Format and print time and distance statistics """ + + if not options.report_time: + return + + d_dist = 0.0254 * self.down_travel_inch + u_dist = 0.0254 * self.up_travel_inch + t_dist = d_dist + u_dist # Total distance + + delay_text = "" + elapsed_text = text_utils.format_hms(elapsed_time) + if self.page_delays > 0: + delay_text = ",\nincluding page delays of: " +\ + text_utils.format_hms(self.page_delays, True) + + if options.preview: + message_fun("Estimated print time: " +\ + text_utils.format_hms(self.pt_estimate, True) + delay_text) + message_fun(f"Length of path to draw: {d_dist:1.2f} m") + message_fun(f"Pen-up travel distance: {u_dist:1.2f} m") + message_fun(f"Total movement distance: {t_dist:1.2f} m") + message_fun("This estimate took " + elapsed_text + "\n") + else: + message_fun("Elapsed time: " + elapsed_text + delay_text) + message_fun(f"Length of path drawn: {d_dist:1.2f} m") + message_fun(f"Total distance moved: {t_dist:1.2f} m\n") class ProgressBar: @@ -152,7 +196,7 @@ def launch(self, status, options, delay=False, total_in=None): self.last = 0 - if total_in== None: + if total_in is None: total_val = self.total else: total_val = total_in @@ -224,17 +268,14 @@ class PlotStatus: PlotStatus: Data storage class for plot status variables """ - CONFIG_ITEMS = ['secondary', 'called_externally', 'cli_api'] - PAUSE_ITEMS = ['force_pause', 'delay_between_copies'] + CONFIG_ITEMS = ['secondary', 'called_externally', 'cli_api', 'delay_between_copies'] def __init__(self): self.port = None self.copies_to_plot = 1 - self.b_stopped = False + self.stopped = 0 # Status code. If a plot is stopped, record why. for key in self.CONFIG_ITEMS: # Create instance variables in __init__ setattr(self, key, False) - for key in self.PAUSE_ITEMS: # Create instance variables in __init__ - setattr(self, key, False) self.apply_defaults() # Apply default values of the above attributes self.resume = ResumeStatus() self.progress = ProgressBar() @@ -243,9 +284,8 @@ def __init__(self): def apply_defaults(self): ''' Reset attributes to defaults ''' self.port = None - self.b_stopped = False - for key in self.PAUSE_ITEMS: - setattr(self, key, False) + self.stopped = 0 # Default value 0 ("not stopped") + self.delay_between_copies = False def reset(self): ''' Reset attributes and resume attributes to defaults ''' diff --git a/inkscape driver/plot_warnings.py b/inkscape driver/plot_warnings.py index d725650..cd3b98e 100644 --- a/inkscape driver/plot_warnings.py +++ b/inkscape driver/plot_warnings.py @@ -79,16 +79,16 @@ def return_text_list(self): ) self.warning_dict.pop('voltage') - if 'bounds' in self.warning_dict: - if 'bounds' not in self.suppress_list: - warning_text_list.append( - "Warning (bounds): " + - "AxiDraw movement was limited by its physical range of motion." + - "\nIf everything else looks correct, you may have an issue with" + - "your document size, or you may have the wrong AxiDraw model selected." + - "\nPlease contact technical support if you need assistance.\n" - ) - self.warning_dict.pop('bounds') + if 'bounds' in self.warning_dict: + if 'bounds' not in self.suppress_list: + warning_text_list.append( + "Warning (bounds): AxiDraw movement was limited by its" + + "\nphysical range of motion. If everything else looks" + + "\ncorrect, there may be an issue with the document size," + + "\nor the wrong model of AxiDraw may be selected." + + "\nPlease contact technical support if you need assistance.\n" + ) + self.warning_dict.pop('bounds') if 'image' in self.warning_dict: if 'image' not in self.suppress_list: @@ -103,12 +103,12 @@ def return_text_list(self): if 'text' in self.warning_dict: if 'text' not in self.suppress_list: warning_text_list.append( - 'Note (plain-text): This file contains some plain text' + + 'Note (plain-text): This file contains some plain text\n' + layer_name_text(self.warning_dict['text']) + - "\nPlease convert text into paths vectors before plotting." + + "\nPlease convert text into vector paths before plotting." + "\nConsider using the Inkscape Path > Object to Path tool." + - "\nAlternately, consider using Hershey Text to render your text " + - "with stroke-based fonts.\n" + "\nAlternately, consider using Hershey Text to render your" + + "\ntext with stroke-based fonts.\n" ) self.warning_dict.pop('text') diff --git a/inkscape driver/serial_utils.py b/inkscape driver/serial_utils.py new file mode 100644 index 0000000..941b560 --- /dev/null +++ b/inkscape driver/serial_utils.py @@ -0,0 +1,75 @@ +# coding=utf-8 +# +# Copyright 2022 Windell H. Oskay, Evil Mad Scientist Laboratories +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +""" +serial_utils.py + +This module modularizes some serial functions.. + +Part of the AxiDraw driver for Inkscape +https://github.com/evil-mad/AxiDraw + +Requires Python 3.7 or newer. + +""" + +from axidrawinternal.plot_utils_import import from_dependency_import +ebb_serial = from_dependency_import('plotink.ebb_serial') # https://github.com/evil-mad/plotink +ebb_motion = from_dependency_import('plotink.ebb_motion') + +def connect(options, plot_status, message_fun, logger): + """ Connect to AxiDraw over USB """ + port_name = None + if options.port_config == 1: # port_config value "1": Use first available AxiDraw. + options.port = None + if not options.port: # Try to connect to first available AxiDraw. + plot_status.port = ebb_serial.openPort() + elif str(type(options.port)) in ( + "", "", ""): + # This function may be passed a port name to open (and later close). + options.port = str(options.port).strip('\"') + port_name = options.port + the_port = ebb_serial.find_named_ebb(options.port) + plot_status.port = ebb_serial.testPort(the_port) + options.port = None # Clear this input, to ensure that we close the port later. + else: + # options.port may be a serial port object of type serial.serialposix.Serial. + # In that case, interact with that given port object, and leave it open at the end. + plot_status.port = options.port + + if plot_status.port is None: + if port_name: + message_fun('Failed to connect to AxiDraw ' + str(port_name)) + else: + message_fun("Failed to connect to AxiDraw.") + return False + if port_name: + logger.debug('Connected successfully to port: ' + str(port_name)) + else: + logger.debug(" Connected successfully") + return True + + +def query_voltage(options, params, plot_status, warnings): + """ Check that power supply is detected. """ + if params.skip_voltage_check: + return + if plot_status.port is not None and not options.preview: + voltage_ok = ebb_motion.queryVoltage(plot_status.port, False) + if not voltage_ok: + warnings.add_new('voltage')