From 16dd8b3541b7dcdd92c1ab756d56ae8db16eaf29 Mon Sep 17 00:00:00 2001 From: bdring Date: Sat, 12 Oct 2019 18:16:43 -0500 Subject: [PATCH] Changes for piecewise linear fit spindle calcs --- Grbl_Esp32/config.h | 14 ++ Grbl_Esp32/spindle_control.cpp | 51 +++- Grbl_Esp32/spindle_control.h | 7 +- doc/script/fit_nonlinear_spindle.py | 373 ++++++++++++++++++++++++++++ doc/script/simple_stream.py | 66 +++++ doc/script/stream.py | 202 +++++++++++++++ 6 files changed, 701 insertions(+), 12 deletions(-) create mode 100644 doc/script/fit_nonlinear_spindle.py create mode 100644 doc/script/simple_stream.py create mode 100644 doc/script/stream.py diff --git a/Grbl_Esp32/config.h b/Grbl_Esp32/config.h index 649e6887..35a3ebb4 100644 --- a/Grbl_Esp32/config.h +++ b/Grbl_Esp32/config.h @@ -695,6 +695,7 @@ Some features should not be changed. See notes below. // the 'fit_nonlinear_spindle.py' script solution. Used only when ENABLE_PIECEWISE_LINEAR_SPINDLE // is enabled. Make sure the constant values are exactly the same as the script solution. // NOTE: When N_PIECES < 4, unused RPM_LINE and RPM_POINT defines are not required and omitted. +/* #define N_PIECES 4 // Integer (1-4). Number of piecewise lines used in script solution. #define RPM_MAX 11686.4 // Max RPM of model. $30 > RPM_MAX will be limited to RPM_MAX. #define RPM_MIN 202.5 // Min RPM of model. $31 < RPM_MIN will be limited to RPM_MIN. @@ -709,6 +710,19 @@ Some features should not be changed. See notes below. #define RPM_LINE_B3 4.881851e+02 #define RPM_LINE_A4 1.203413e-01 // Used N_PIECES = 4. A and B constants of line 4. #define RPM_LINE_B4 1.151360e+03 +*/ + +#define N_PIECES 3 +#define RPM_MAX 23935.2 +#define RPM_MIN 2412.2 +#define RPM_POINT12 6283.9 +#define RPM_POINT23 11866.0 +#define RPM_LINE_A1 4.390865e-03 +#define RPM_LINE_B1 7.591787e+00 +#define RPM_LINE_A2 1.074874e-02 +#define RPM_LINE_B2 4.754411e+01 +#define RPM_LINE_A3 9.528342e-03 +#define RPM_LINE_B3 3.306286e+01 /* --------------------------------------------------------------------------------------- diff --git a/Grbl_Esp32/spindle_control.cpp b/Grbl_Esp32/spindle_control.cpp index 7af15de6..44c77e7f 100644 --- a/Grbl_Esp32/spindle_control.cpp +++ b/Grbl_Esp32/spindle_control.cpp @@ -35,6 +35,7 @@ void spindle_init() #endif pwm_gradient = SPINDLE_PWM_RANGE/(settings.rpm_max-settings.rpm_min); + //grbl_sendf(CLIENT_SERIAL, "[MSG:pwm_gradient %4.4f SPINDLE_PWM_RANGE %d]\r\n", pwm_gradient, SPINDLE_PWM_RANGE); // Use DIR and Enable if pins are defined #ifdef SPINDLE_ENABLE_PIN @@ -58,12 +59,12 @@ void spindle_init() void spindle_stop() { - spindle_set_enable(false); + spindle_set_enable(false); #ifdef SPINDLE_PWM_PIN #ifndef INVERT_SPINDLE_PWM grbl_analogWrite(SPINDLE_PWM_CHANNEL, SPINDLE_PWM_OFF_VALUE); #else - grbl_analogWrite(SPINDLE_PWM_CHANNEL, (1< 3) + if (rpm > RPM_POINT34) { + pwm_value = floor(RPM_LINE_A4*rpm - RPM_LINE_B4); + } else + #endif + #if (N_PIECES > 2) + if (rpm > RPM_POINT23) { + pwm_value = floor(RPM_LINE_A3*rpm - RPM_LINE_B3); + } else + #endif + #if (N_PIECES > 1) + if (rpm > RPM_POINT12) { + pwm_value = floor(RPM_LINE_A2*rpm - RPM_LINE_B2); + } else + #endif + { + pwm_value = floor(RPM_LINE_A1*rpm - RPM_LINE_B1); + } + return pwm_value; +} + + + diff --git a/Grbl_Esp32/spindle_control.h b/Grbl_Esp32/spindle_control.h index 13a1b433..91f060c9 100644 --- a/Grbl_Esp32/spindle_control.h +++ b/Grbl_Esp32/spindle_control.h @@ -38,8 +38,9 @@ void spindle_set_speed(uint32_t pwm_value); uint32_t spindle_compute_pwm_value(float rpm); void spindle_set_state(uint8_t state, float rpm); - void spindle_sync(uint8_t state, float rpm); - void grbl_analogWrite(uint8_t chan, uint32_t duty); - void spindle_set_enable(bool enable); + void spindle_sync(uint8_t state, float rpm); + void grbl_analogWrite(uint8_t chan, uint32_t duty); + void spindle_set_enable(bool enable); + uint32_t piecewise_linear_fit(float rpm); #endif diff --git a/doc/script/fit_nonlinear_spindle.py b/doc/script/fit_nonlinear_spindle.py new file mode 100644 index 00000000..57ca5f3b --- /dev/null +++ b/doc/script/fit_nonlinear_spindle.py @@ -0,0 +1,373 @@ +""" +--------------------- +The MIT License (MIT) + +Copyright (c) 2017-2018 Sungeun K. Jeon for Gnea Research LLC + +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. +--------------------- +""" + + +""" +This Python script produces a continuous piece-wise line fit of actual spindle speed over +programmed speed/PWM, which must be measured and provided by the user. A plot of the data +and line fit will be auto-generated and saved in the working directory as 'line_fit.png'. + +REQUIREMENTS: + - Python 2.7 or 3.x with SciPy, NumPy, and Matplotlib Python Libraries + + - For the most people, the easiest way to run this script is on the free cloud service + https://repl.it/site/languages/python3. No account necessary. Unlimited runs. + + - Last checked on 4/5/2018. This site has been regularly evolving and becoming more + powerful. Things may not work exactly as shown. Please report any issues. + + - To use, go to the website and start the Python REPL. Copy and paste this script into + the main.py file in the browser editor. Click 'Run' and a solution should appear in + the text output in the REPL console window. You can edit the script directly in the + browser and re-run the script as many times as you need. A free account is only + necessary if you want to save files on their servers. + + - This script will also automatically generate a png image with the plot of the data + with the piece-wise linear fit over it, but this will not show up by default on this + website. To enable this, just click the 'Add File' icon and create a dummy file. + Name it anything, like dummy.py. Leave this file blank. Doing this places the REPL + in multiple file mode and will enable viewing the plot. Click the 'Run' icon. The + solution will be presented again in the console, and the data plot will appear in + the file list called 'line_fit.png'. Click the file to view the plot. + + - For offline Python installs, most Mac and Linux computers have Python pre-installed + with the required libraries. If not, a quick google search will show you how to + install them. For Windows, Python installations are bit more difficult. Anaconda and + Pyzo seem to work well. + +USAGE: + - First, make sure you are using the stock build of Grbl for the 328p processor. Most + importantly, the SPINDLE_PWM_MAX_VALUE and SPINDLE_PWM_MIN_VALUE should be unaltered + from defaults, otherwise change them back to 255.0 and 1.0 respectively for this test. + + - Next, program the max and min rpm Grbl settings to '$30=255' and '$31=1'. This sets + the internal PWM values equal to 'S' spindle speed for the standard Grbl build. + + - Check if your spindle does not turn on at very low voltages by setting 'S' spindle + speed to 'S1'. If it does not turn on or turns at a non-useful rpm, increase 'S' by + one until it does. Write down this 'S' value for later. You'll start the rpm data + collection from this point onward and will need to update the SPINDLE_PWM_MIN_VALUE + in cpu_map.h afterwards. + + - Collect actual spindle speed with a tachometer or similar means over a range of 'S' + and PWM values. Start by setting the spindle 'S' speed to the minimum useful 'S' from + the last step and measure and record actual spindle rpm. Next, increase 'S' spindle + speed over equally sized intervals and repeat the measurement. Increments of 20 rpm + should be more than enough, but decrease increment size, if highly nonlinear. Complete + the data collection the 'S' spindle speed equal to '$30' max rpm, or at the max useful + rpm, and record the actual rpm output. Make sure to collect rpm data all the way + throughout useful output rpm. The actual operating range within this model may be set + later within Grbl with the '$30' and '$31' settings. + + - In some cases, spindle PWM output can have discontinuities or not have a useful rpm + in certain ranges. For example, a known controller board has the spindle rpm drop + completely at voltages above ~4.5V. If you have discontinuities like this at the low + or high range of rpm, simply trim them from the data set. Don't include them. For + Grbl to compensate, you'll need to alter the SPINDLE_PWM_MIN_VALUE and/or + SPINDLE_PWM_MAX_VALUE in cpu_map.h to where your data set ends. This script will + indicate if you need to do that in the solution output. + + - Keep in mind that spindles without control electronics can slow down drastically when + cutting and under load. How much it slows down is dependent on a lot of factors, such + as feed rate, chip load, cutter diameter, flutes, cutter type, lubricant/coolant, + material being cut, etc. Even spindles with controllers can still slow down if the + load is higher than the max current the controller can provide. It's recommended to + frequently re-check and measure actual spindle speed during a job. You can always use + spindle speed overrides to tweak it temporarily to the desired speed. + + - Edit this script and enter the measured rpm values and their corresponding 'S' spindle + speed values in the data arrays below. Set the number of piecewise lines you would + like to use, from one to four lines. For most cases, four lines is perfectly fine. + In certain scenarios (laser engraving), this may significantly degrade performance and + should be reduced if possible. + + - Run the Python script. Visually assess the line fit from the plot. It will not likely + to what you want on the first go. Dial things in by altering the line fit junction + points 'PWM_pointX' in this script to move where the piecewise line junctions are + located along the plot x-axis. It may be desired to tweak the junction points so the + model solution is more accurate in the region that the spindle typically running. + Re-run the script and tweak the junction points until you are satified with the model. + + - Record the solution and enter the RPM_POINT and RPM_LINE values into config.h. Set the + number of piecewise lines used in this model in config.h. Also set the '$30' and '$31' + max and min rpm values to the solution values or in a range between them in Grbl '$' + settings. And finally, alter the SPINDLE_PWM_MIN_VALUE in cpu_map.h, if your spindle + needs to be above a certain voltage to produce a useful low rpm. + + - Once the solution is entered. Recompile and flash Grbl. This solution model is only + valid for this particular set of data. If the machine is altered, you will need to + perform this experiment again and regenerate a new model here. + +OUTPUT: + The solver produces a set of values that define the piecewise fit and can be used by + Grbl to quickly and efficiently compute spindle PWM output voltage for a desired RPM. + + The first two are the RPM_MAX ($30) and RPM_MIN ($31) Grbl settings. These must be + programmed into Grbl manually or setup in defaults.h for new systems. Altering these + values within Grbl after a piece-wise linear model is installed will not change alter + model. It will only alter the range of spindle speed rpm values Grbl output. + + For example, if the solver produces an RPM_MAX of 9000 and Grbl is programmed with + $30=8000, S9000 may be programmed, but Grbl will only produce the output voltage to run + at 8000 rpm. In other words, Grbl will only output voltages the range between + max(RPM_MIN,$31) and min(RPM_MAX,$30). + + The remaining values define the slopes and offsets of the line segments and the junction + points between line segments, like so for n_pieces=3: + + PWM_output = RPM_LINE_A1 * rpm - RPM_LINE_B1 [ RPM_MIN < rpm < RPM_POINT12 ] + PWM_output = RPM_LINE_A2 * rpm - RPM_LINE_B2 [ RPM_POINT12 < rpm < RPM_POINT23 ] + PWM_output = RPM_LINE_A3 * rpm - RPM_LINE_B3 [ RPM_POINT23 < rpm < RPM_MAX ] + + NOTE: The script solves in terms of PWM but the final equations and values are expressed + in terms of rpm in the form 'PWM = a*rpm - b'. + +""" + +from scipy import optimize +import numpy as np + +# ---------------------------------------------------------------------------------------- +# Configure spindle PWM line fit solver + +n_pieces = 4 # Number of line segments used for data fit. Only 1 to 4 line segments supported. + +# Programmed 'S' spindle speed values. Must start with minimum useful PWM or 'S' programmed +# value and end with the maximum useful PWM or 'S' programmed value. Order of the array must +# be synced with the RPM_measured array below. +# NOTE: ** DO NOT USE DATA FROM AN EXISTING PIECEWISE LINE FIT. USE DEFAULT GRBL MODEL ONLY. ** +PWM_set = np.array([2,18,36,55,73,91,109,127,146,164,182,200,218,237,254], dtype=float) + +# Actual RPM measured at the spindle. Must be in the ascending value and equal in length +# as the PWM_set array. Must include the min and max measured rpm output in the first and +# last array entries, respectively. +RPM_measured = np.array([213.,5420,7145,8282,9165,9765,10100,10500,10700,10900,11100,11250,11400,11550,11650], dtype=float) + +# Configure line fit points by 'S' programmed rpm or PWM value. Values must be between +# PWM_max and PWM_min. Typically, alter these values to space the points evenly between +# max and min PWM range. However, they may be tweaked to maximize accuracy in the places +# you normally operate for highly nonlinear curves. Plot to visually assess how well the +# solution fits the data. +PWM_point1 = 20.0 # (S) Point between segments 0 and 1. Used when n_pieces >= 2. +PWM_point2 = 80.0 # (S) Point between segments 1 and 2. Used when n_pieces >= 3. +PWM_point3 = 150.0 # (S) Point between segments 2 and 3. Used when n_pieces = 4. + +# ---------------------------------------------------------------------------------------- + +# Advanced settings + +# The optimizer requires an initial guess of the solution. Change value if solution fails. +slope_i = 100.0; # > 0.0 + +PWM_max = max(PWM_set) # Maximum PWM set in measured range +PWM_min = min(PWM_set) # Minimum PWM set in measured range +plot_figure = True # Set to False, if matplotlib is not available. + +# ---------------------------------------------------------------------------------------- +# DO NOT ALTER ANYTHING BELOW. + +def piecewise_linear_1(x,b,k1): + return np.piecewise(x, [(x>=PWM_min)&(x<=PWM_max)], [lambda x:k1*(x-PWM_min)+b]) + +def piecewise_linear_2(x,b,k1,k2): + c = [b, + b+k1*(PWM_point1-PWM_min)] + funcs = [lambda x:k1*(x-PWM_min)+c[0], + lambda x:k2*(x-PWM_point1)+c[1]] + conds = [(x=PWM_min), + (x<=PWM_max)&(x>=PWM_point1)] + return np.piecewise(x, conds, funcs) + +def piecewise_linear_3(x,b,k1,k2,k3): + c = [b, + b+k1*(PWM_point1-PWM_min), + b+k1*(PWM_point1-PWM_min)+k2*(PWM_point2-PWM_point1)] + funcs = [lambda x:k1*(x-PWM_min)+c[0], + lambda x:k2*(x-PWM_point1)+c[1], + lambda x:k3*(x-PWM_point2)+c[2]] + conds = [(x=PWM_min), + (x=PWM_point1), + (x<=PWM_max)&(x>=PWM_point2)] + return np.piecewise(x, conds, funcs) + +def piecewise_linear_4(x,b,k1,k2,k3,k4): + c = [b, + b+k1*(PWM_point1-PWM_min), + b+k1*(PWM_point1-PWM_min)+k2*(PWM_point2-PWM_point1), + b+k1*(PWM_point1-PWM_min)+k2*(PWM_point2-PWM_point1)+k3*(PWM_point3-PWM_point2)] + funcs = [lambda x:k1*(x-PWM_min)+c[0], + lambda x:k2*(x-PWM_point1)+c[1], + lambda x:k3*(x-PWM_point2)+c[2], + lambda x:k4*(x-PWM_point3)+c[3]] + conds = [(x=PWM_min), + (x=PWM_point1), + (x=PWM_point2), + (x<=PWM_max)&(x>=PWM_point3)] + return np.piecewise(x, conds, funcs) + +# ---------------------------------------------------------------------------------------- + +print("\nCONFIG:") +print(" N_pieces: %i" % n_pieces) +print(" PWM_min: %.1f" % PWM_min) +print(" PWM_max: %.1f" % PWM_max) +if n_pieces > 1: + print(" PWM_point1: %.1f" % PWM_point1) +if n_pieces > 2: + print(" PWM_point2: %.1f" % PWM_point2) +if n_pieces > 3: + print(" PWM_point3: %.1f" % PWM_point3) +print(" N_data: %i" % len(RPM_measured)) +print(" PWM_set: ", PWM_set) +print(" RPM_measured: ", RPM_measured) + +if n_pieces == 1: + piece_func = piecewise_linear_1 + p_initial = [RPM_measured[0],slope_i] + + p , e = optimize.curve_fit(piece_func, PWM_set, RPM_measured, p0=p_initial) + a = [p[1]] + b = [ p[0]-p[1]*PWM_min] + rpm = [ p[0], + p[0]+p[1]*(PWM_point1-PWM_min)] + +elif n_pieces == 2: + piece_func = piecewise_linear_2 + p_initial = [RPM_measured[0],slope_i,slope_i] + + p , e = optimize.curve_fit(piece_func, PWM_set, RPM_measured, p0=p_initial) + a = [p[1],p[2]] + b = [ p[0]-p[1]*PWM_min, + p[0]+p[1]*(PWM_point1-PWM_min)-p[2]*PWM_point1] + rpm = [ p[0], + p[0]+p[1]*(PWM_point1-PWM_min), + p[0]+p[1]*(PWM_point1-PWM_min)+p[2]*(PWM_max-PWM_point1)] + +elif n_pieces == 3: + piece_func = piecewise_linear_3 + p_initial = [RPM_measured[0],slope_i,slope_i,slope_i] + + p , e = optimize.curve_fit(piece_func, PWM_set, RPM_measured, p0=p_initial) + a = [p[1],p[2],p[3]] + b = [ p[0]-p[1]*PWM_min, + p[0]+p[1]*(PWM_point1-PWM_min)-p[2]*PWM_point1, + p[0]+p[1]*(PWM_point1-PWM_min)+p[2]*(PWM_point2-PWM_point1)-p[3]*PWM_point2] + rpm = [ p[0], + p[0]+p[1]*(PWM_point1-PWM_min), + p[0]+p[1]*(PWM_point1-PWM_min)+p[2]*(PWM_point2-PWM_point1), + p[0]+p[1]*(PWM_point1-PWM_min)+p[2]*(PWM_point2-PWM_point1)+p[3]*(PWM_max-PWM_point2) ] + +elif n_pieces == 4: + piece_func = piecewise_linear_4 + p_initial = [RPM_measured[0],slope_i,slope_i,slope_i,slope_i] + + p , e = optimize.curve_fit(piece_func, PWM_set, RPM_measured, p0=p_initial) + a = [p[1],p[2],p[3],p[4]] + b = [ p[0]-p[1]*PWM_min, + p[0]+p[1]*(PWM_point1-PWM_min)-p[2]*PWM_point1, + p[0]+p[1]*(PWM_point1-PWM_min)+p[2]*(PWM_point2-PWM_point1)-p[3]*PWM_point2, + p[0]+p[1]*(PWM_point1-PWM_min)+p[2]*(PWM_point2-PWM_point1)+p[3]*(PWM_point3-PWM_point2)-p[4]*PWM_point3 ] + rpm = [ p[0], + p[0]+p[1]*(PWM_point1-PWM_min), + p[0]+p[1]*(PWM_point1-PWM_min)+p[2]*(PWM_point2-PWM_point1), + p[0]+p[1]*(PWM_point1-PWM_min)+p[2]*(PWM_point2-PWM_point1)+p[3]*(PWM_point3-PWM_point2), + p[0]+p[1]*(PWM_point1-PWM_min)+p[2]*(PWM_point2-PWM_point1)+p[3]*(PWM_point3-PWM_point2)+p[4]*(PWM_max-PWM_point3) ] + +else : + print("ERROR: Unsupported number of pieces. Check and alter n_pieces") + quit() + +print("\nSOLUTION:\n\n[Update these #define values and uncomment]\n[ENABLE_PIECEWISE_LINEAR_SPINDLE in config.h.]") +print("#define N_PIECES %.0f" % n_pieces) +print("#define RPM_MAX %.1f" % rpm[-1]) +print("#define RPM_MIN %.1f" % rpm[0]) + +if n_pieces > 1: + print("#define RPM_POINT12 %.1f" % rpm[1]) +if n_pieces > 2: + print("#define RPM_POINT23 %.1f" %rpm[2]) +if n_pieces > 3: + print("#define RPM_POINT34 %.1f" %rpm[3]) + +print("#define RPM_LINE_A1 %.6e" % (1./a[0])) +print("#define RPM_LINE_B1 %.6e" % (b[0]/a[0])) +if n_pieces > 1: + print("#define RPM_LINE_A2 %.6e" % (1./a[1])) + print("#define RPM_LINE_B2 %.6e" % (b[1]/a[1])) +if n_pieces > 2: + print("#define RPM_LINE_A3 %.6e" % (1./a[2])) + print("#define RPM_LINE_B3 %.6e" % (b[2]/a[2])) +if n_pieces > 3: + print("#define RPM_LINE_A4 %.6e" % (1./a[3])) + print("#define RPM_LINE_B4 %.6e" % (b[3]/a[3])) + +print("\n[To operate over full model range, manually write these]") +print("['$' settings or alter values in defaults.h. Grbl will]") +print("[operate between min($30,RPM_MAX) and max($31,RPM_MIN)]") +print("$30=%.1f (rpm max)" % rpm[-1]) +print("$31=%.1f (rpm min)" % rpm[0]) + +if (PWM_min > 1)|(PWM_max<255): + print("\n[Update the following #define values in cpu_map.h]") + if (PWM_min >1) : + print("#define SPINDLE_PWM_MIN_VALUE %.0f" % PWM_min) + if PWM_max <255: + print("#define SPINDLE_PWM_MAX_VALUE %.0f" % PWM_max) +else: + print("\n[No cpu_map.h changes required.]") +print("\n") + +test_val = (1./a[0])*rpm[0] - (b[0]/a[0]) +if test_val < 0.0 : + print("ERROR: Solution is negative at RPM_MIN. Adjust junction points or increase n_pieces.\n") + +if plot_figure: + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + fig = plt.figure() + ax = fig.add_subplot(111) + xd = np.linspace(PWM_min, PWM_max, 10000) + ax.plot(PWM_set, RPM_measured, "o") + ax.plot(xd, piece_func(xd, *p),'g') + plt.xlabel("Programmed PWM") + plt.ylabel("Measured RPM") + + # Check solution by plotting in terms of rpm. +# x = np.linspace(rpm[0], rpm[1], 10000) +# ax.plot((1./a[0])*x-(b[0]/a[0]),x,'r:') +# if n_pieces > 1: +# x = np.linspace(rpm[1], rpm[2], 10000) +# ax.plot((1./a[1])*x-(b[1]/a[1]),x,'r:') +# if n_pieces > 2: +# x = np.linspace(rpm[2], rpm[3], 10000) +# ax.plot((1./a[2])*x-(b[2]/a[2]),x,'r:') +# if n_pieces > 3: +# x = np.linspace(rpm[3], rpm[-1], 10000) +# ax.plot((1./a[3])*x-(b[3]/a[3]),x,'r:') + + fig.savefig("line_fit.png") diff --git a/doc/script/simple_stream.py b/doc/script/simple_stream.py new file mode 100644 index 00000000..67c2a2c3 --- /dev/null +++ b/doc/script/simple_stream.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python +"""\ +Simple g-code streaming script for grbl + +Provided as an illustration of the basic communication interface +for grbl. When grbl has finished parsing the g-code block, it will +return an 'ok' or 'error' response. When the planner buffer is full, +grbl will not send a response until the planner buffer clears space. + +G02/03 arcs are special exceptions, where they inject short line +segments directly into the planner. So there may not be a response +from grbl for the duration of the arc. + +--------------------- +The MIT License (MIT) + +Copyright (c) 2012 Sungeun K. Jeon + +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 serial +import time + +# Open grbl serial port +s = serial.Serial('/dev/tty.usbmodem1811',115200) + +# Open g-code file +f = open('grbl.gcode','r'); + +# Wake up grbl +s.write("\r\n\r\n") +time.sleep(2) # Wait for grbl to initialize +s.flushInput() # Flush startup text in serial input + +# Stream g-code to grbl +for line in f: + l = line.strip() # Strip all EOL characters for consistency + print 'Sending: ' + l, + s.write(l + '\n') # Send g-code block to grbl + grbl_out = s.readline() # Wait for grbl response with carriage return + print ' : ' + grbl_out.strip() + +# Wait here until grbl is finished to close serial port and file. +raw_input(" Press to exit and disable grbl.") + +# Close file and serial port +f.close() +s.close() \ No newline at end of file diff --git a/doc/script/stream.py b/doc/script/stream.py new file mode 100644 index 00000000..4a637ab9 --- /dev/null +++ b/doc/script/stream.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python +"""\ + +Stream g-code to grbl controller + +This script differs from the simple_stream.py script by +tracking the number of characters in grbl's serial read +buffer. This allows grbl to fetch the next line directly +from the serial buffer and does not have to wait for a +response from the computer. This effectively adds another +buffer layer to prevent buffer starvation. + +CHANGELOG: +- 20170531: Status report feedback at 1.0 second intervals. + Configurable baudrate and report intervals. Bug fixes. +- 20161212: Added push message feedback for simple streaming +- 20140714: Updated baud rate to 115200. Added a settings + write mode via simple streaming method. MIT-licensed. + +TODO: +- Add realtime control commands during streaming. + +--------------------- +The MIT License (MIT) + +Copyright (c) 2012-2017 Sungeun K. Jeon + +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 serial +import re +import time +import sys +import argparse +import threading + +RX_BUFFER_SIZE = 128 +BAUD_RATE = 115200 +ENABLE_STATUS_REPORTS = True +REPORT_INTERVAL = 1.0 # seconds + +is_run = True # Controls query timer + +# Define command line argument interface +parser = argparse.ArgumentParser(description='Stream g-code file to grbl. (pySerial and argparse libraries required)') +parser.add_argument('gcode_file', type=argparse.FileType('r'), + help='g-code filename to be streamed') +parser.add_argument('device_file', + help='serial device path') +parser.add_argument('-q','--quiet',action='store_true', default=False, + help='suppress output text') +parser.add_argument('-s','--settings',action='store_true', default=False, + help='settings write mode') +parser.add_argument('-c','--check',action='store_true', default=False, + help='stream in check mode') +args = parser.parse_args() + +# Periodic timer to query for status reports +# TODO: Need to track down why this doesn't restart consistently before a release. +def send_status_query(): + s.write('?') + +def periodic_timer() : + while is_run: + send_status_query() + time.sleep(REPORT_INTERVAL) + + +# Initialize +s = serial.Serial(args.device_file,BAUD_RATE) +f = args.gcode_file +verbose = True +if args.quiet : verbose = False +settings_mode = False +if args.settings : settings_mode = True +check_mode = False +if args.check : check_mode = True + +# Wake up grbl +print "Initializing Grbl..." +s.write("\r\n\r\n") + +# Wait for grbl to initialize and flush startup text in serial input +time.sleep(2) +s.flushInput() + +if check_mode : + print "Enabling Grbl Check-Mode: SND: [$C]", + s.write("$C\n") + while 1: + grbl_out = s.readline().strip() # Wait for grbl response with carriage return + if grbl_out.find('error') >= 0 : + print "REC:",grbl_out + print " Failed to set Grbl check-mode. Aborting..." + quit() + elif grbl_out.find('ok') >= 0 : + if verbose: print 'REC:',grbl_out + break + +start_time = time.time(); + +# Start status report periodic timer +if ENABLE_STATUS_REPORTS : + timerThread = threading.Thread(target=periodic_timer) + timerThread.daemon = True + timerThread.start() + +# Stream g-code to grbl +l_count = 0 +error_count = 0 +if settings_mode: + # Send settings file via simple call-response streaming method. Settings must be streamed + # in this manner since the EEPROM accessing cycles shut-off the serial interrupt. + print "SETTINGS MODE: Streaming", args.gcode_file.name, " to ", args.device_file + for line in f: + l_count += 1 # Iterate line counter + # l_block = re.sub('\s|\(.*?\)','',line).upper() # Strip comments/spaces/new line and capitalize + l_block = line.strip() # Strip all EOL characters for consistency + if verbose: print "SND>"+str(l_count)+": \"" + l_block + "\"" + s.write(l_block + '\n') # Send g-code block to grbl + while 1: + grbl_out = s.readline().strip() # Wait for grbl response with carriage return + if grbl_out.find('ok') >= 0 : + if verbose: print " REC<"+str(l_count)+": \""+grbl_out+"\"" + break + elif grbl_out.find('error') >= 0 : + if verbose: print " REC<"+str(l_count)+": \""+grbl_out+"\"" + error_count += 1 + break + else: + print " MSG: \""+grbl_out+"\"" +else: + # Send g-code program via a more agressive streaming protocol that forces characters into + # Grbl's serial read buffer to ensure Grbl has immediate access to the next g-code command + # rather than wait for the call-response serial protocol to finish. This is done by careful + # counting of the number of characters sent by the streamer to Grbl and tracking Grbl's + # responses, such that we never overflow Grbl's serial read buffer. + g_count = 0 + c_line = [] + for line in f: + l_count += 1 # Iterate line counter + l_block = re.sub('\s|\(.*?\)','',line).upper() # Strip comments/spaces/new line and capitalize + # l_block = line.strip() + c_line.append(len(l_block)+1) # Track number of characters in grbl serial read buffer + grbl_out = '' + while sum(c_line) >= RX_BUFFER_SIZE-1 | s.inWaiting() : + out_temp = s.readline().strip() # Wait for grbl response + if out_temp.find('ok') < 0 and out_temp.find('error') < 0 : + print " MSG: \""+out_temp+"\"" # Debug response + else : + if out_temp.find('error') >= 0 : error_count += 1 + g_count += 1 # Iterate g-code counter + if verbose: print " REC<"+str(g_count)+": \""+out_temp+"\"" + del c_line[0] # Delete the block character count corresponding to the last 'ok' + s.write(l_block + '\n') # Send g-code block to grbl + if verbose: print "SND>"+str(l_count)+": \"" + l_block + "\"" + # Wait until all responses have been received. + while l_count > g_count : + out_temp = s.readline().strip() # Wait for grbl response + if out_temp.find('ok') < 0 and out_temp.find('error') < 0 : + print " MSG: \""+out_temp+"\"" # Debug response + else : + if out_temp.find('error') >= 0 : error_count += 1 + g_count += 1 # Iterate g-code counter + del c_line[0] # Delete the block character count corresponding to the last 'ok' + if verbose: print " REC<"+str(g_count)+": \""+out_temp + "\"" + +# Wait for user input after streaming is completed +print "\nG-code streaming finished!" +end_time = time.time(); +is_run = False; +print " Time elapsed: ",end_time-start_time,"\n" +if check_mode : + if error_count > 0 : + print "CHECK FAILED:",error_count,"errors found! See output for details.\n" + else : + print "CHECK PASSED: No errors found in g-code program.\n" +else : + print "WARNING: Wait until Grbl completes buffered g-code blocks before exiting." + raw_input(" Press to exit and disable Grbl.") + +# Close file and serial port +f.close() +s.close()