From a87e2a379be00c0be7f099bc3c8b3f93cb188cd3 Mon Sep 17 00:00:00 2001 From: Nikolay Khabarov <2xl@mail.ru> Date: Sat, 27 May 2017 18:47:01 +0300 Subject: [PATCH] add E axis --- cnc/config.py | 3 ++ cnc/coordinates.py | 39 ++++++++++++-------- cnc/gcode.py | 8 ++-- cnc/gmachine.py | 36 ++++++++++-------- cnc/hal_raspberry/hal.py | 13 ++++++- cnc/hal_virtual.py | 30 ++++++++++----- cnc/pulses.py | 44 ++++++++++++++-------- tests/test_coordinates.py | 71 +++++++++++++++++++++++------------- tests/test_gcode.py | 19 +++++++--- tests/test_gmachine.py | 77 ++++++++++++++++++++------------------- tests/test_pulses.py | 45 ++++++++++++++--------- 11 files changed, 238 insertions(+), 147 deletions(-) diff --git a/cnc/config.py b/cnc/config.py index ff905e2..f230cbb 100644 --- a/cnc/config.py +++ b/cnc/config.py @@ -6,6 +6,7 @@ STEPPER_MAX_ACCELERATION_MM_PER_S2 = 200 # mm per sec^2 STEPPER_PULSES_PER_MM_X = 400 STEPPER_PULSES_PER_MM_Y = 400 STEPPER_PULSES_PER_MM_Z = 400 +STEPPER_PULSES_PER_MM_E = 80 TABLE_SIZE_X_MM = 200 TABLE_SIZE_Y_MM = 300 @@ -17,10 +18,12 @@ SPINDLE_MAX_RPM = 10000 STEPPER_STEP_PIN_X = 16 STEPPER_STEP_PIN_Y = 20 STEPPER_STEP_PIN_Z = 21 +STEPPER_STEP_PIN_E = 25 STEPPER_DIR_PIN_X = 13 STEPPER_DIR_PIN_Y = 19 STEPPER_DIR_PIN_Z = 26 +STEPPER_DIR_PIN_E = 8 SPINDLE_PWM_PIN = 7 diff --git a/cnc/coordinates.py b/cnc/coordinates.py index 490ecf5..06adf97 100644 --- a/cnc/coordinates.py +++ b/cnc/coordinates.py @@ -6,7 +6,7 @@ class Coordinates(object): """ This object represent machine coordinates. Machine supports 3 axis, so there are X, Y and Z. """ - def __init__(self, x, y, z): + def __init__(self, x, y, z, e): """ Create object. :param x: x coordinated. :param y: y coordinated. @@ -15,16 +15,18 @@ class Coordinates(object): self.x = round(x, 10) self.y = round(y, 10) self.z = round(z, 10) + self.e = round(e, 10) def is_zero(self): """ Check if all coordinates are zero. :return: boolean value. """ - return self.x == 0.0 and self.y == 0.0 and self.z == 0.0 + return self.x == 0.0 and self.y == 0.0 and self.z == 0.0 and \ + self.e == 0.0 def is_in_aabb(self, p1, p2): """ Check coordinates are in aabb(Axis-Aligned Bounding Box). - aabb is specified with two points. + aabb is specified with two points. E is ignored. :param p1: First point in Coord object. :param p2: Second point in Coord object. :return: boolean value. @@ -42,46 +44,53 @@ class Coordinates(object): """ Calculate the length of vector. :return: Vector length. """ - return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z) + return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z + + self.e * self.e) - def round(self, base_x, base_y, base_z): + def round(self, base_x, base_y, base_z, base_e): """ Round values to specified base, ie 0.49 with base 0.25 will be 0.5. :param base_x: Base for x axis. :param base_y: Base for y axis. :param base_z: Base for z axis. + :param base_e: Base for e axis. :return: New rounded object. """ return Coordinates(round(self.x / base_x) * base_x, round(self.y / base_y) * base_y, - round(self.z / base_z) * base_z) + round(self.z / base_z) * base_z, + round(self.e / base_e) * base_e) def find_max(self): """ Find a maximum value of all values. :return: maximum value. """ - return max(self.x, self.y, self.z) + return max(self.x, self.y, self.z, self.e) # build in function implementation def __add__(self, other): - return Coordinates(self.x + other.x, self.y + other.y, self.z + other.z) + return Coordinates(self.x + other.x, self.y + other.y, + self.z + other.z, self.e + other.e) def __sub__(self, other): - return Coordinates(self.x - other.x, self.y - other.y, self.z - other.z) + return Coordinates(self.x - other.x, self.y - other.y, + self.z - other.z, self.e - other.e) def __mul__(self, v): - return Coordinates(self.x * v, self.y * v, self.z * v) + return Coordinates(self.x * v, self.y * v, self.z * v, self.e * v) def __div__(self, v): - return Coordinates(self.x / v, self.y / v, self.z / v) + return Coordinates(self.x / v, self.y / v, self.z / v, self.e / v) def __truediv__(self, v): - return Coordinates(self.x / v, self.y / v, self.z / v) + return Coordinates(self.x / v, self.y / v, self.z / v, self.e / v) def __eq__(self, other): - return self.x == other.x and self.y == other.y and self.z == other.z + return self.x == other.x and self.y == other.y and self.z == other.z \ + and self.e == other.e def __str__(self): - return '(' + str(self.x) + ', ' + str(self.y) + ', ' + str(self.z) + ')' + return '(' + str(self.x) + ', ' + str(self.y) + ', ' + str(self.z) \ + + ', ' + str(self.e) + ')' def __abs__(self): - return Coordinates(abs(self.x), abs(self.y), abs(self.z)) + return Coordinates(abs(self.x), abs(self.y), abs(self.z), abs(self.e)) diff --git a/cnc/gcode.py b/cnc/gcode.py index 837adee..a7e4aad 100644 --- a/cnc/gcode.py +++ b/cnc/gcode.py @@ -44,13 +44,15 @@ class GCode(object): x = self.get('X', default.x, multiply) y = self.get('Y', default.y, multiply) z = self.get('Z', default.z, multiply) - return Coordinates(x, y, z) + e = self.get('E', default.e, multiply) + return Coordinates(x, y, z, e) def has_coordinates(self): """ Check if at least one of the coordinates is present. :return: Boolean value. """ - return 'X' in self.params or 'Y' in self.params or 'Z' in self.params + return 'X' in self.params or 'Y' in self.params or 'Z' in self.params \ + or 'E' in self.params def radius(self, default, multiply): """ Get radius for circular interpolation(I, J, K or R). @@ -61,7 +63,7 @@ class GCode(object): i = self.get('I', default.x, multiply) j = self.get('J', default.y, multiply) k = self.get('K', default.z, multiply) - return Coordinates(i, j, k) + return Coordinates(i, j, k, 0) def command(self): """ Get value from gcode line. diff --git a/cnc/gmachine.py b/cnc/gmachine.py index d3f3b8b..3909061 100644 --- a/cnc/gmachine.py +++ b/cnc/gmachine.py @@ -22,7 +22,7 @@ class GMachine(object): def __init__(self): """ Initialization. """ - self._position = Coordinates(0.0, 0.0, 0.0) + self._position = Coordinates(0.0, 0.0, 0.0, 0.0) # init variables self._velocity = 0 self._spindle_rpm = 0 @@ -48,11 +48,11 @@ class GMachine(object): self._velocity = 1000 self._spindle_rpm = 1000 self._pause = 0 - self._local = Coordinates(0.0, 0.0, 0.0) + self._local = Coordinates(0.0, 0.0, 0.0, 0.0) self._convertCoordinates = 1.0 self._absoluteCoordinates = True self._plane = PLANE_XY - self._radius = Coordinates(0.0, 0.0, 0.0) + self._radius = Coordinates(0.0, 0.0, 0.0, 0.0) def _spindle(self, spindle_speed): hal.join() @@ -60,14 +60,15 @@ class GMachine(object): def __check_delta(self, delta): pos = self._position + delta - if not pos.is_in_aabb(Coordinates(0.0, 0.0, 0.0), - Coordinates(TABLE_SIZE_X_MM, TABLE_SIZE_Y_MM, TABLE_SIZE_Z_MM)): + if not pos.is_in_aabb(Coordinates(0.0, 0.0, 0.0, 0.0), + Coordinates(TABLE_SIZE_X_MM, TABLE_SIZE_Y_MM, TABLE_SIZE_Z_MM, 0)): raise GMachineException("out of effective area") def _move_linear(self, delta, velocity): delta = delta.round(1.0 / STEPPER_PULSES_PER_MM_X, 1.0 / STEPPER_PULSES_PER_MM_Y, - 1.0 / STEPPER_PULSES_PER_MM_Z) + 1.0 / STEPPER_PULSES_PER_MM_Z, + 1.0 / STEPPER_PULSES_PER_MM_E) if delta.is_zero(): return self.__check_delta(delta) @@ -130,10 +131,11 @@ class GMachine(object): def _circular(self, delta, radius, velocity, direction): delta = delta.round(1.0 / STEPPER_PULSES_PER_MM_X, 1.0 / STEPPER_PULSES_PER_MM_Y, - 1.0 / STEPPER_PULSES_PER_MM_Z) + 1.0 / STEPPER_PULSES_PER_MM_Z, + 1.0 / STEPPER_PULSES_PER_MM_E) self.__check_delta(delta) # get delta vector and put it on circle - circle_end = Coordinates(0,0,0) + circle_end = Coordinates(0, 0, 0, 0) if self._plane == PLANE_XY: circle_end.x, circle_end.y = self.__adjust_circle(delta.x, delta.y, radius.x, radius.y, direction, @@ -152,9 +154,11 @@ class GMachine(object): self._position.z, self._position.x, TABLE_SIZE_Z_MM, TABLE_SIZE_X_MM) circle_end.y = delta.y + circle_end.e = delta.e circle_end = circle_end.round(1.0 / STEPPER_PULSES_PER_MM_X, 1.0 / STEPPER_PULSES_PER_MM_Y, - 1.0 / STEPPER_PULSES_PER_MM_Z) + 1.0 / STEPPER_PULSES_PER_MM_Z, + 1.0 / STEPPER_PULSES_PER_MM_E) hal.move_circular(circle_end, radius, self._plane, velocity, direction) # if finish coords is not on circle, move some distance linearly linear_delta = delta - circle_end @@ -168,9 +172,9 @@ class GMachine(object): def home(self): """ Move head to park position """ - d = Coordinates(0, 0, -self._position.z) + d = Coordinates(0, 0, -self._position.z, 0) self._move_linear(d, STEPPER_MAX_VELOCITY_MM_PER_MIN) - d = Coordinates(-self._position.x, -self._position.y, 0) + d = Coordinates(-self._position.x, -self._position.y, 0, 0) self._move_linear(d, STEPPER_MAX_VELOCITY_MM_PER_MIN) def position(self): @@ -207,7 +211,8 @@ class GMachine(object): coord = coord + self._local delta = coord - self._position else: - delta = gcode.coordinates(Coordinates(0.0, 0.0, 0.0), self._convertCoordinates) + delta = gcode.coordinates(Coordinates(0.0, 0.0, 0.0, 0.0), + self._convertCoordinates) coord = self._position + delta velocity = gcode.get('F', self._velocity) spindle_rpm = gcode.get('S', self._spindle_rpm) @@ -245,14 +250,14 @@ class GMachine(object): elif c == 'G28': # home self.home() elif c == 'G53': # switch to machine coords - self._local = Coordinates(0.0, 0.0, 0.0) + self._local = Coordinates(0.0, 0.0, 0.0, 0.0) elif c == 'G90': # switch to absolute coords self._absoluteCoordinates = True elif c == 'G91': # switch to relative coords self._absoluteCoordinates = False elif c == 'G92': # switch to local coords self._local = self._position - \ - gcode.coordinates(Coordinates(0.0, 0.0, 0.0), + gcode.coordinates(Coordinates(0.0, 0.0, 0.0, 0.0), self._convertCoordinates) elif c == 'M3': # spinle on self._spindle(spindle_rpm) @@ -271,5 +276,4 @@ class GMachine(object): self._spindle_rpm = spindle_rpm self._pause = pause self._radius = radius - logging.debug("position {}, {}, {}".format( - self._position.x, self._position.y, self._position.z)) + logging.debug("position {}".format(self._position)) diff --git a/cnc/hal_raspberry/hal.py b/cnc/hal_raspberry/hal.py index 3fec80c..848c427 100644 --- a/cnc/hal_raspberry/hal.py +++ b/cnc/hal_raspberry/hal.py @@ -27,6 +27,7 @@ pwm = rpgpio.DMAPWM() STEP_PIN_MASK_X = 1 << STEPPER_STEP_PIN_X STEP_PIN_MASK_Y = 1 << STEPPER_STEP_PIN_Y STEP_PIN_MASK_Z = 1 << STEPPER_STEP_PIN_Z +STEP_PIN_MASK_E = 1 << STEPPER_STEP_PIN_E def init(): """ Initialize GPIO pins and machine itself, including callibration if @@ -35,9 +36,11 @@ def init(): gpio.init(STEPPER_STEP_PIN_X, rpgpio.GPIO.MODE_OUTPUT) gpio.init(STEPPER_STEP_PIN_Y, rpgpio.GPIO.MODE_OUTPUT) gpio.init(STEPPER_STEP_PIN_Z, rpgpio.GPIO.MODE_OUTPUT) + gpio.init(STEPPER_STEP_PIN_E, rpgpio.GPIO.MODE_OUTPUT) gpio.init(STEPPER_DIR_PIN_X, rpgpio.GPIO.MODE_OUTPUT) gpio.init(STEPPER_DIR_PIN_Y, rpgpio.GPIO.MODE_OUTPUT) gpio.init(STEPPER_DIR_PIN_Z, rpgpio.GPIO.MODE_OUTPUT) + gpio.init(STEPPER_DIR_PIN_E, rpgpio.GPIO.MODE_OUTPUT) gpio.init(ENDSTOP_PIN_X, rpgpio.GPIO.MODE_INPUT_PULLUP) gpio.init(ENDSTOP_PIN_X, rpgpio.GPIO.MODE_INPUT_PULLUP) gpio.init(ENDSTOP_PIN_X, rpgpio.GPIO.MODE_INPUT_PULLUP) @@ -130,15 +133,19 @@ def move_linear(delta, velocity): gpio.clear(STEPPER_DIR_PIN_Z) else: gpio.set(STEPPER_DIR_PIN_Z) + if delta.e > 0.0: + gpio.clear(STEPPER_DIR_PIN_E) + else: + gpio.set(STEPPER_DIR_PIN_E) # prepare and run dma dma.clear() prev = 0 is_ran = False st = time.time() - for tx, ty, tz in generator: + for tx, ty, tz, te in generator: pins = 0 - k = int(round(min(x for x in (tx, ty, tz) if x is not None) + k = int(round(min(x for x in (tx, ty, tz, te) if x is not None) * US_IN_SECONDS)) if tx is not None: pins |= STEP_PIN_MASK_X @@ -146,6 +153,8 @@ def move_linear(delta, velocity): pins |= STEP_PIN_MASK_Y if tz is not None: pins |= STEP_PIN_MASK_Z + if te is not None: + pins |= STEP_PIN_MASK_E if k - prev > 0: dma.add_delay(k - prev) dma.add_pulse(pins, STEPPER_PULSE_LINGTH_US) diff --git a/cnc/hal_virtual.py b/cnc/hal_virtual.py index e15c667..ff29c5c 100644 --- a/cnc/hal_virtual.py +++ b/cnc/hal_virtual.py @@ -31,13 +31,13 @@ def move_linear(delta, velocity): :param velocity: velocity in mm per min """ logging.info("move {} with velocity {}".format(delta, velocity)) - ix = iy = iz = 0 + ix = iy = iz = ie = 0 generator = PulseGeneratorLinear(delta, velocity) - lx, ly, lz = None, None, None - dx, dy, dz = 0, 0, 0 - mx, my, mz = 0, 0, 0 + lx, ly, lz, le = None, None, None, None + dx, dy, dz, de = 0, 0, 0, 0 + mx, my, mz, me = 0, 0, 0, 0 st = time.time() - for tx, ty, tz in generator: + for tx, ty, tz, te in generator: if tx is not None: if tx > mx: mx = tx @@ -71,16 +71,28 @@ def move_linear(delta, velocity): lz = tz else: dz = None + if te is not None: + if te > me: + me = te + te = int(round(te * 1000000)) + ie += 1 + if le is not None: + de = te - le + assert de > 0, "negative or zero time delta detected for e" + le = te + else: + de = None # very verbose, uncomment on demand - # logging.debug("Iteration {} is {} {} {}".format(max(ix, iy, iz), tx, ty, tz)) - f = list(x for x in (tx, ty, tz) if x is not None) + # logging.debug("Iteration {} is {} {} {} {}".format(max(ix, iy, iz, ie), tx, ty, tz, te)) + f = list(x for x in (tx, ty, tz, te) if x is not None) assert f.count(f[0]) == len(f), "fast forwarded pulse detected" pt = time.time() assert ix / STEPPER_PULSES_PER_MM_X == abs(delta.x), "x wrong number of pulses" assert iy / STEPPER_PULSES_PER_MM_Y == abs(delta.y), "y wrong number of pulses" assert iz / STEPPER_PULSES_PER_MM_Z == abs(delta.z), "z wrong number of pulses" - assert max(mx, my, mz) <= generator.total_time_s(), "interpolation time or pulses wrong" - logging.debug("Did {}, {}, {} iterations".format(ix, iy, iz)) + assert ie / STEPPER_PULSES_PER_MM_E == abs(delta.e), "e wrong number of pulses" + assert max(mx, my, mz, me) <= generator.total_time_s(), "interpolation time or pulses wrong" + logging.debug("Did {}, {}, {}, {} iterations".format(ix, iy, iz, ie)) logging.info("prepared in " + str(round(pt - st, 2)) \ + "s, estimated " + str(round(generator.total_time_s(), 2)) + "s") diff --git a/cnc/pulses.py b/cnc/pulses.py index 1c6e355..6422e25 100644 --- a/cnc/pulses.py +++ b/cnc/pulses.py @@ -41,6 +41,7 @@ class PulseGenerator(object): self._iteration_x = 0 self._iteration_y = 0 self._iteration_z = 0 + self._iteration_e = 0 self._acceleration_time_s = 0.0 self._linear_time_s = 0.0 self._2Vmax_per_a = 0.0 @@ -60,12 +61,15 @@ class PulseGenerator(object): """ raise NotImplemented - def _interpolation_function(self, pulse_number): + def _interpolation_function(self, ix, iy, iz, ie): """ Get function for interpolation path. This function should returned values as it is uniform movement. There is only one trick, function must be expressed in terms of position, i.e. t = S / V for linear, where S - distance would be increment on motor minimum step. - :param pulse_number: number of pulse. + :param ix: number of pulse for X axis. + :param iy: number of pulse for X axis. + :param iz: number of pulse for X axis. + :param ie: number of pulse for X axis. :return: time for each axis or None if movement for axis is finished. """ raise NotImplemented @@ -82,6 +86,7 @@ class PulseGenerator(object): self._iteration_x = 0 self._iteration_y = 0 self._iteration_z = 0 + self._iteration_e = 0 logging.debug(', '.join("%s: %s" % i for i in vars(self).items())) return self @@ -123,15 +128,16 @@ class PulseGenerator(object): the next pulse. If there is no pulses left None will be returned. """ - tx, ty, tz = self._interpolation_function(self._iteration_x, - self._iteration_y, - self._iteration_z) + tx, ty, tz, te = self._interpolation_function(self._iteration_x, + self._iteration_y, + self._iteration_z, + self._iteration_e) # check condition to stop - if tx is None and ty is None and tz is None: + if tx is None and ty is None and tz is None and te is None: raise StopIteration # convert to real time - m = min(x for x in (tx, ty, tz) if x is not None) + m = min(x for x in (tx, ty, tz, te) if x is not None) am = self._to_accelerated_time(m) # sort pulses in time if tx is not None: @@ -152,8 +158,14 @@ class PulseGenerator(object): else: tz = am self._iteration_z += 1 + if te is not None: + if te > m: + te = None + else: + te = am + self._iteration_e += 1 - return tx, ty, tz + return tx, ty, tz, te def total_time_s(self): """ Get total time for movement. @@ -173,24 +185,24 @@ class PulseGeneratorLinear(PulseGenerator): # this class doesn't care about direction self._distance_mm = abs(delta_mm) # velocity of each axis - distance_xyz_mm = self._distance_mm.length() + distance_total_mm = self._distance_mm.length() self.max_velocity_mm_per_sec = self._distance_mm * ( - velocity_mm_per_min / SECONDS_IN_MINUTE / distance_xyz_mm) + velocity_mm_per_min / SECONDS_IN_MINUTE / distance_total_mm) # acceleration time self.acceleration_time_s = self.max_velocity_mm_per_sec.find_max() \ / STEPPER_MAX_ACCELERATION_MM_PER_S2 # check if there is enough space to accelerate and brake, adjust time # S = a * t^2 / 2 if STEPPER_MAX_ACCELERATION_MM_PER_S2 * self.acceleration_time_s ** 2 \ - > distance_xyz_mm: - self.acceleration_time_s = math.sqrt(distance_xyz_mm / + > distance_total_mm: + self.acceleration_time_s = math.sqrt(distance_total_mm / STEPPER_MAX_ACCELERATION_MM_PER_S2) self.linear_time_s = 0.0 # V = a * t -> V = 2 * S / t, take half of total distance for acceleration and braking self.max_velocity_mm_per_sec = self._distance_mm / self.acceleration_time_s else: # calculate linear time - linear_distance_mm = distance_xyz_mm\ + linear_distance_mm = distance_total_mm\ - self.acceleration_time_s ** 2 \ * STEPPER_MAX_ACCELERATION_MM_PER_S2 self.linear_time_s = linear_distance_mm \ @@ -212,7 +224,7 @@ class PulseGeneratorLinear(PulseGenerator): # Linear movement, S = V * t -> t = S / V return position_mm / velocity_mm_per_sec - def _interpolation_function(self, ix, iy, iz): + def _interpolation_function(self, ix, iy, iz, ie): """ Calculate interpolation values for linear movement, see super class for details. """ @@ -222,4 +234,6 @@ class PulseGeneratorLinear(PulseGenerator): self.max_velocity_mm_per_sec.y) t_z = self.__linear(iz / STEPPER_PULSES_PER_MM_Z, self._distance_mm.z, self.max_velocity_mm_per_sec.z) - return t_x, t_y, t_z + t_e = self.__linear(ie / STEPPER_PULSES_PER_MM_E, self._distance_mm.e, + self.max_velocity_mm_per_sec.e) + return t_x, t_y, t_z, t_e diff --git a/tests/test_coordinates.py b/tests/test_coordinates.py index 2455e28..78537c6 100644 --- a/tests/test_coordinates.py +++ b/tests/test_coordinates.py @@ -5,7 +5,7 @@ from cnc.coordinates import * class TestCoordinates(unittest.TestCase): def setUp(self): - self.default = Coordinates(96, 102, 150) + self.default = Coordinates(96, 102, 150, 228) def tearDown(self): pass @@ -13,110 +13,129 @@ class TestCoordinates(unittest.TestCase): def test_constructor(self): # constructor rounds values to 10 digits after the point self.assertRaises(TypeError, Coordinates) - c = Coordinates(1.00000000005, 2.00000000004, -3.5000000009) + c = Coordinates(1.00000000005, 2.00000000004, -3.5000000009, 0.0) self.assertEqual(c.x, 1.0000000001) self.assertEqual(c.y, 2.0) self.assertEqual(c.z, -3.5000000009) + self.assertEqual(c.e, 0.0) def test_zero(self): - c = Coordinates(0, 0, 0) + c = Coordinates(0, 0, 0, 0) self.assertTrue(c.is_zero()) def test_aabb(self): # aabb - Axis Aligned Bounded Box. # original method checks if point belongs aabb. - p1 = Coordinates(0, 0, 0) - p2 = Coordinates(2, 2, 2) - c = Coordinates(1, 1, 1) + p1 = Coordinates(0, 0, 0, 0) + p2 = Coordinates(2, 2, 2, 0) + c = Coordinates(1, 1, 1, 0) self.assertTrue(c.is_in_aabb(p1, p2)) self.assertTrue(c.is_in_aabb(p2, p1)) - c = Coordinates(0, 0, 0) + c = Coordinates(0, 0, 0, 0) self.assertTrue(c.is_in_aabb(p1, p2)) - c = Coordinates(2, 2, 2) + c = Coordinates(2, 2, 2, 0) self.assertTrue(c.is_in_aabb(p1, p2)) - c = Coordinates(2, 3, 2) + c = Coordinates(2, 3, 2, 0) self.assertFalse(c.is_in_aabb(p1, p2)) - c = Coordinates(-1, 1, 1) + c = Coordinates(-1, 1, 1, 0) self.assertFalse(c.is_in_aabb(p1, p2)) - c = Coordinates(1, 1, 3) + c = Coordinates(1, 1, 3, 0) self.assertFalse(c.is_in_aabb(p1, p2)) def test_length(self): - c = Coordinates(-1, 0, 0) + c = Coordinates(-1, 0, 0, 0) self.assertEqual(c.length(), 1) - c = Coordinates(0, 3, -4) + c = Coordinates(0, 3, -4, 0) self.assertEqual(c.length(), 5) - c = Coordinates(3, 4, 12) + c = Coordinates(3, 4, 12, 0) self.assertEqual(c.length(), 13) + c = Coordinates(1, 1, 1, 1) + self.assertEqual(c.length(), 2) def test_round(self): # round works in another way then Python's round. # This round() rounds digits with specified step. - c = Coordinates(1.5, -1.4, 3.05) - r = c.round(1, 1, 1) + c = Coordinates(1.5, -1.4, 3.05, 3.5) + r = c.round(1, 1, 1, 1) self.assertEqual(r.x, 2.0) self.assertEqual(r.y, -1.0) self.assertEqual(r.z, 3.0) - r = c.round(0.25, 0.25, 0.25) + self.assertEqual(r.e, 4.0) + r = c.round(0.25, 0.25, 0.25, 0.25) self.assertEqual(r.x, 1.5) self.assertEqual(r.y, -1.5) self.assertEqual(r.z, 3.0) + self.assertEqual(r.e, 3.5) def test_max(self): self.assertEqual(self.default.find_max(), max(self.default.x, - self.default.y, - self.default.z)) + self.default.y, + self.default.z, + self.default.e)) # build-in function overriding tests def test_add(self): - r = self.default + Coordinates(1, 2, 3) + r = self.default + Coordinates(1, 2, 3, 4) self.assertEqual(r.x, self.default.x + 1) self.assertEqual(r.y, self.default.y + 2) self.assertEqual(r.z, self.default.z + 3) + self.assertEqual(r.e, self.default.e + 4) def test_sub(self): - r = self.default - Coordinates(1, 2, 3) + r = self.default - Coordinates(1, 2, 3, 4) self.assertEqual(r.x, self.default.x - 1) self.assertEqual(r.y, self.default.y - 2) self.assertEqual(r.z, self.default.z - 3) + self.assertEqual(r.e, self.default.e - 4) def test_mul(self): r = self.default * 2 self.assertEqual(r.x, self.default.x * 2) self.assertEqual(r.y, self.default.y * 2) self.assertEqual(r.z, self.default.z * 2) + self.assertEqual(r.e, self.default.e * 2) def test_div(self): r = self.default / 2 self.assertEqual(r.x, self.default.x / 2) self.assertEqual(r.y, self.default.y / 2) self.assertEqual(r.z, self.default.z / 2) + self.assertEqual(r.e, self.default.e / 2) def test_truediv(self): r = self.default / 3.0 self.assertEqual(r.x, self.default.x / 3.0) self.assertEqual(r.y, self.default.y / 3.0) self.assertEqual(r.z, self.default.z / 3.0) + self.assertEqual(r.e, self.default.e / 3.0) def test_eq(self): - a = Coordinates(self.default.x, self.default.y, self.default.z) + a = Coordinates(self.default.x, self.default.y, self.default.z, + self.default.e) self.assertTrue(a == self.default) - a = Coordinates(-self.default.x, self.default.y, self.default.z) + a = Coordinates(-self.default.x, self.default.y, self.default.z, + self.default.e) self.assertFalse(a == self.default) - a = Coordinates(self.default.x, -self.default.y, self.default.z) + a = Coordinates(self.default.x, -self.default.y, self.default.z, + self.default.e) self.assertFalse(a == self.default) - a = Coordinates(self.default.x, self.default.y, -self.default.z) + a = Coordinates(self.default.x, self.default.y, -self.default.z, + self.default.e) + self.assertFalse(a == self.default) + a = Coordinates(self.default.x, self.default.y, self.default.z, + -self.default.e) self.assertFalse(a == self.default) def test_str(self): self.assertTrue(isinstance(str(self.default), str)) def test_abs(self): - c = Coordinates(-1, -2.5, -99) + c = Coordinates(-1, -2.5, -99, -23) r = abs(c) self.assertEqual(r.x, 1.0) self.assertEqual(r.y, 2.5) self.assertEqual(r.z, 99.0) + self.assertEqual(r.e, 23.0) if __name__ == '__main__': diff --git a/tests/test_gcode.py b/tests/test_gcode.py index e95b944..3c56030 100644 --- a/tests/test_gcode.py +++ b/tests/test_gcode.py @@ -7,7 +7,7 @@ from cnc.gcode import * class TestGCode(unittest.TestCase): def setUp(self): - self.default = Coordinates(-7, 8, 9) + self.default = Coordinates(-7, 8, 9, -10) def tearDown(self): pass @@ -16,28 +16,31 @@ class TestGCode(unittest.TestCase): # GCode shouldn't be created with constructor, but since it uses # internally, let's check it. self.assertRaises(TypeError, GCode) - gc = GCode({"X": "1", "Y": "-2", "Z":"0", "G": "1"}) + gc = GCode({"X": "1", "Y": "-2", "Z":"0", "E": 99, "G": "1"}) self.assertEqual(gc.coordinates(self.default, 1).x, 1.0) self.assertEqual(gc.coordinates(self.default, 1).y, -2.0) self.assertEqual(gc.coordinates(self.default, 1).z, 0.0) + self.assertEqual(gc.coordinates(self.default, 1).e, 99.0) def test_parser(self): - gc = GCode.parse_line("G1X2Y-3Z4") + gc = GCode.parse_line("G1X2Y-3Z4E1.5") self.assertEqual(gc.command(), "G1") self.assertEqual(gc.coordinates(self.default, 1).x, 2.0) self.assertEqual(gc.coordinates(self.default, 1).y, -3.0) self.assertEqual(gc.coordinates(self.default, 1).z, 4.0) + self.assertEqual(gc.coordinates(self.default, 1).e, 1.5) gc = GCode.parse_line("") self.assertIsNone(gc) def test_defaults(self): # defaults are values which should be returned if corresponding # value doesn't exist in gcode. - default = Coordinates(11, -12, 14) + default = Coordinates(11, -12, 14, -10) gc = GCode.parse_line("G1") self.assertEqual(gc.coordinates(default, 1).x, 11.0) self.assertEqual(gc.coordinates(default, 1).y, -12.0) self.assertEqual(gc.coordinates(default, 1).z, 14.0) + self.assertEqual(gc.coordinates(default, 1).e, -10.0) def test_commands(self): gc = GCode({"G": "1"}) @@ -48,11 +51,12 @@ class TestGCode(unittest.TestCase): def test_case_sensitivity(self): gc = GCode.parse_line("m111") self.assertEqual(gc.command(), "M111") - gc = GCode.parse_line("g2X3y-4Z5") + gc = GCode.parse_line("g2X3y-4Z5e6") self.assertEqual(gc.command(), "G2") self.assertEqual(gc.coordinates(self.default, 1).x, 3.0) self.assertEqual(gc.coordinates(self.default, 1).y, -4.0) self.assertEqual(gc.coordinates(self.default, 1).z, 5.0) + self.assertEqual(gc.coordinates(self.default, 1).e, 6.0) def test_has_coordinates(self): gc = GCode.parse_line("X2Y-3Z4") @@ -65,6 +69,8 @@ class TestGCode(unittest.TestCase): self.assertTrue(gc.has_coordinates()) gc = GCode.parse_line("Z1") self.assertTrue(gc.has_coordinates()) + gc = GCode.parse_line("E1") + self.assertTrue(gc.has_coordinates()) def test_radius(self): gc = GCode.parse_line("G2I1J2K3") @@ -78,10 +84,11 @@ class TestGCode(unittest.TestCase): def test_multiply(self): # getting coordinates could modify value be specified multiplier. - gc = GCode.parse_line("X2 Y-3 Z4") + gc = GCode.parse_line("X2 Y-3 Z4 E5") self.assertEqual(gc.coordinates(self.default, 25.4).x, 50.8) self.assertEqual(gc.coordinates(self.default, 2).y, -6) self.assertEqual(gc.coordinates(self.default, 0).y, 0) + self.assertEqual(gc.coordinates(self.default, 5).e, 25) def test_whitespaces(self): gc = GCode.parse_line("X1 Y2") diff --git a/tests/test_gmachine.py b/tests/test_gmachine.py index 0793aa2..80732bd 100644 --- a/tests/test_gmachine.py +++ b/tests/test_gmachine.py @@ -20,28 +20,28 @@ class TestGMachine(unittest.TestCase): m.do_command(GCode.parse_line("G91")) m.do_command(GCode.parse_line("X1Y1Z1")) m.reset() - m.do_command(GCode.parse_line("X3Y4Z5")) - self.assertEqual(m.position(), Coordinates(3, 4, 5)) + m.do_command(GCode.parse_line("X3Y4Z5E6")) + self.assertEqual(m.position(), Coordinates(3, 4, 5, 6)) def test_release(self): # release homes head. m = GMachine() - m.do_command(GCode.parse_line("X1Y2Z3")) + m.do_command(GCode.parse_line("X1Y2Z3E4")) m.release() - self.assertEqual(m.position(), Coordinates(0, 0, 0)) + self.assertEqual(m.position(), Coordinates(0, 0, 0, 4)) def test_home(self): m = GMachine() - m.do_command(GCode.parse_line("X1Y2Z3")) + m.do_command(GCode.parse_line("X1Y2Z3E4")) m.home() - self.assertEqual(m.position(), Coordinates(0, 0, 0)) + self.assertEqual(m.position(), Coordinates(0, 0, 0, 4)) def test_none(self): # GMachine must ignore None commands, since GCode.parse_line() # returns None if no gcode found in line. m = GMachine() m.do_command(None) - self.assertEqual(m.position(), Coordinates(0, 0, 0)) + self.assertEqual(m.position(), Coordinates(0, 0, 0, 0)) def test_unknown(self): # Test commands which doesn't exists @@ -54,10 +54,10 @@ class TestGMachine(unittest.TestCase): # Test gcode commands. def test_g0_g1(self): m = GMachine() - m.do_command(GCode.parse_line("G0X3Y2Z1")) - self.assertEqual(m.position(), Coordinates(3, 2, 1)) - m.do_command(GCode.parse_line("G1X1Y2Z3")) - self.assertEqual(m.position(), Coordinates(1, 2, 3)) + m.do_command(GCode.parse_line("G0X3Y2Z1E-2")) + self.assertEqual(m.position(), Coordinates(3, 2, 1, -2)) + m.do_command(GCode.parse_line("G1X1Y2Z3E4")) + self.assertEqual(m.position(), Coordinates(1, 2, 3, 4)) self.assertRaises(GMachineException, m.do_command, GCode.parse_line("G1F-1")) self.assertRaises(GMachineException, @@ -86,7 +86,7 @@ class TestGMachine(unittest.TestCase): m.do_command, GCode.parse_line("G2X99999999Y99999999I1J1")) self.assertRaises(GMachineException, m.do_command, GCode.parse_line("G2X2Y2Z99999999I1J1")) - self.assertEqual(m.position(), Coordinates(0, 0, 0)) + self.assertEqual(m.position(), Coordinates(0, 0, 0, 0)) self.assertRaises(GMachineException, m.do_command, GCode.parse_line("G2X4Y4I2J2")) self.assertRaises(GMachineException, @@ -94,10 +94,10 @@ class TestGMachine(unittest.TestCase): m.do_command(GCode.parse_line("G1X1")) m.do_command(GCode.parse_line("G2J1")) m.do_command(GCode.parse_line("G3J1")) - self.assertEqual(m.position(), Coordinates(1, 0, 0)) + self.assertEqual(m.position(), Coordinates(1, 0, 0, 0)) m.do_command(GCode.parse_line("G1X5Y5")) m.do_command(GCode.parse_line("G2X0Y0Z5I-2J-2")) - self.assertEqual(m.position(), Coordinates(0, 0, 5)) + self.assertEqual(m.position(), Coordinates(0, 0, 5, 0)) def test_g4(self): @@ -120,47 +120,48 @@ class TestGMachine(unittest.TestCase): def test_g20_g21(self): m = GMachine() m.do_command(GCode.parse_line("G20")) - m.do_command(GCode.parse_line("X3Y2Z1")) - self.assertEqual(m.position(), Coordinates(76.2, 50.8, 25.4)) + m.do_command(GCode.parse_line("X3Y2Z1E0.5")) + self.assertEqual(m.position(), Coordinates(76.2, 50.8, 25.4, 12.7)) m.do_command(GCode.parse_line("G21")) - m.do_command(GCode.parse_line("X3Y2Z1")) - self.assertEqual(m.position(), Coordinates(3, 2, 1)) + m.do_command(GCode.parse_line("X3Y2Z1E0.5")) + self.assertEqual(m.position(), Coordinates(3, 2, 1, 0.5)) def test_g90_g91(self): m = GMachine() m.do_command(GCode.parse_line("G91")) + m.do_command(GCode.parse_line("X1Y1Z1E1")) m.do_command(GCode.parse_line("X1Y1Z1")) m.do_command(GCode.parse_line("X1Y1")) m.do_command(GCode.parse_line("X1")) - self.assertEqual(m.position(), Coordinates(3, 2, 1)) - m.do_command(GCode.parse_line("X-1Y-1Z-1")) + self.assertEqual(m.position(), Coordinates(4, 3, 2, 1)) + m.do_command(GCode.parse_line("X-1Y-1Z-1E-1")) m.do_command(GCode.parse_line("G90")) - m.do_command(GCode.parse_line("X1Y1Z1")) - self.assertEqual(m.position(), Coordinates(1, 1, 1)) + m.do_command(GCode.parse_line("X1Y1Z1E1")) + self.assertEqual(m.position(), Coordinates(1, 1, 1, 1)) def test_g90_g92(self): m = GMachine() - m.do_command(GCode.parse_line("G92X100Y100Z100")) - m.do_command(GCode.parse_line("X101Y102Z103")) - self.assertEqual(m.position(), Coordinates(1, 2, 3)) - m.do_command(GCode.parse_line("G92X-1Y-1Z-1")) - m.do_command(GCode.parse_line("X1Y1Z1")) - self.assertEqual(m.position(), Coordinates(3, 4, 5)) - m.do_command(GCode.parse_line("G92X3Y4Z5")) - m.do_command(GCode.parse_line("X0Y0Z0")) - self.assertEqual(m.position(), Coordinates(0, 0, 0)) + m.do_command(GCode.parse_line("G92X100Y100Z100E100")) + m.do_command(GCode.parse_line("X101Y102Z103E104")) + self.assertEqual(m.position(), Coordinates(1, 2, 3, 4)) + m.do_command(GCode.parse_line("G92X-1Y-1Z-1E-1")) + m.do_command(GCode.parse_line("X1Y1Z1E1")) + self.assertEqual(m.position(), Coordinates(3, 4, 5, 6)) + m.do_command(GCode.parse_line("G92X3Y4Z5E6")) + m.do_command(GCode.parse_line("X0Y0Z0E0")) + self.assertEqual(m.position(), Coordinates(0, 0, 0, 0)) m.do_command(GCode.parse_line("G90")) - m.do_command(GCode.parse_line("X6Y7Z8")) - self.assertEqual(m.position(), Coordinates(6, 7, 8)) + m.do_command(GCode.parse_line("X6Y7Z8E9")) + self.assertEqual(m.position(), Coordinates(6, 7, 8, 9)) def test_g53_g91_g92(self): m = GMachine() - m.do_command(GCode.parse_line("G92X-50Y-60Z-70")) - m.do_command(GCode.parse_line("X-45Y-55Z-65")) - self.assertEqual(m.position(), Coordinates(5, 5, 5)) + m.do_command(GCode.parse_line("G92X-50Y-60Z-70E-80")) + m.do_command(GCode.parse_line("X-45Y-55Z-65E-75")) + self.assertEqual(m.position(), Coordinates(5, 5, 5, 5)) m.do_command(GCode.parse_line("G91")) - m.do_command(GCode.parse_line("X-1Y-2Z-3")) - self.assertEqual(m.position(), Coordinates(4, 3, 2)) + m.do_command(GCode.parse_line("X-1Y-2Z-3E-4")) + self.assertEqual(m.position(), Coordinates(4, 3, 2, 1)) def test_m3_m5(self): m = GMachine() diff --git a/tests/test_pulses.py b/tests/test_pulses.py index 0e5be11..099f2f0 100644 --- a/tests/test_pulses.py +++ b/tests/test_pulses.py @@ -18,90 +18,101 @@ class TestPulses(unittest.TestCase): # PulseGenerator should never receive empty movement. self.assertRaises(ZeroDivisionError, PulseGeneratorLinear, - Coordinates(0, 0, 0), self.v) + Coordinates(0, 0, 0, 0), self.v) def test_step(self): # Check if PulseGenerator returns correctly single step movement. - g = PulseGeneratorLinear(Coordinates(1.0 / STEPPER_PULSES_PER_MM_X, 0, 0), + g = PulseGeneratorLinear(Coordinates(1.0 / STEPPER_PULSES_PER_MM_X, + 0, 0, 0), self.v) i = 0 - for px, py, pz in g: + for px, py, pz, pe in g: i += 1 self.assertEqual(px, 0) self.assertEqual(py, None) self.assertEqual(pz, None) + self.assertEqual(pe, None) self.assertEqual(i, 1) g = PulseGeneratorLinear(Coordinates( 1.0 / STEPPER_PULSES_PER_MM_X, 1.0 / STEPPER_PULSES_PER_MM_Y, - 1.0 / STEPPER_PULSES_PER_MM_Z), + 1.0 / STEPPER_PULSES_PER_MM_Z, + 1.0 / STEPPER_PULSES_PER_MM_E), self.v) i = 0 - for px, py, pz in g: + for px, py, pz, pe in g: i += 1 self.assertEqual(px, 0) self.assertEqual(py, 0) self.assertEqual(pz, 0) + self.assertEqual(pe, 0) self.assertEqual(i, 1) def test_linear_with_hal_virtual(self): # Using hal_virtual module for this test, it already contains plenty # of asserts for wrong number of pulses, pulse timing issues etc - hal_virtual.move_linear(Coordinates(1, 0, 0), self.v) - hal_virtual.move_linear(Coordinates(25.4, 0, 0), self.v) - hal_virtual.move_linear(Coordinates(25.4, 0, 0), self.v) - hal_virtual.move_linear(Coordinates(25.4, 0, 0), self.v) + hal_virtual.move_linear(Coordinates(1, 0, 0, 0), self.v) + hal_virtual.move_linear(Coordinates(25.4, 0, 0, 0), self.v) + hal_virtual.move_linear(Coordinates(25.4, 0, 0, 0), self.v) + hal_virtual.move_linear(Coordinates(25.4, 0, 0, 0), self.v) hal_virtual.move_linear(Coordinates(TABLE_SIZE_X_MM, TABLE_SIZE_Y_MM, - TABLE_SIZE_Z_MM), self.v) + TABLE_SIZE_Z_MM, + 100.0), self.v) def test_twice_faster(self): # Checks if one axis moves exactly twice faster, pulses are correct. - m = Coordinates(2, 4, 0) + m = Coordinates(2, 4, 0, 0) g = PulseGeneratorLinear(m, self.v) i = 0 - for px, py, pz in g: + for px, py, pz, pe in g: if i % 2 == 0: self.assertNotEqual(px, None) else: self.assertEqual(px, None) self.assertNotEqual(py, None) self.assertEqual(pz, None) + self.assertEqual(pe, None) i += 1 self.assertEqual(m.find_max() * STEPPER_PULSES_PER_MM_Y, i) def test_pulses_count_and_timings(self): # Check if number of pulses is equal to specified distance. - m = Coordinates(TABLE_SIZE_X_MM, TABLE_SIZE_Y_MM, TABLE_SIZE_Z_MM) + m = Coordinates(TABLE_SIZE_X_MM, TABLE_SIZE_Y_MM, TABLE_SIZE_Z_MM, + 100.0) g = PulseGeneratorLinear(m, self.v) ix = 0 iy = 0 iz = 0 + ie = 0 t = -1 - for px, py, pz in g: + for px, py, pz, pe in g: if px is not None: ix += 1 if py is not None: iy += 1 if pz is not None: iz += 1 - v = list(x for x in (px, py, pz) if x is not None) + if pe is not None: + ie += 1 + v = list(x for x in (px, py, pz, pe) if x is not None) self.assertEqual(min(v), max(v)) self.assertLess(t, min(v)) t = max(v) self.assertEqual(m.x * STEPPER_PULSES_PER_MM_X, ix) self.assertEqual(m.y * STEPPER_PULSES_PER_MM_Y, iy) self.assertEqual(m.z * STEPPER_PULSES_PER_MM_Z, iz) + self.assertEqual(m.e * STEPPER_PULSES_PER_MM_E, ie) self.assertLessEqual(t, g.total_time_s()) def test_acceleration_velocity(self): # Check if acceleration present in pulses sequence and if velocity # is correct - m = Coordinates(TABLE_SIZE_X_MM, 0, 0) + m = Coordinates(TABLE_SIZE_X_MM, 0, 0, 0) g = PulseGeneratorLinear(m, self.v) i = 0 lx = 0 - for px, py, pz in g: + for px, py, pz, pe in g: if i == 2: at = px - lx if i == TABLE_SIZE_X_MM * STEPPER_PULSES_PER_MM_X / 2: