Table of Contents
Recently I discovered Pybricks and their announcement that Pybricks will support the Powered Up hubs in the near future.
I was able to buy a Technic hub, 2 XL motors and 1 L motor for a nice price on Bricklink and I decided that my old Lego 8094 set was the perfect candidate to test it on.
Prepare the Lego set
First I had to make some modifications such as removing the old motors and replace them with the Powered Up motors and making the pen lift/drop mechanism driven as well:
I made sure that the motors drive all axis directly because I wanted to use the sensorless load measurement option that these Powered Up motors support.
The sensorless load measurement makes it possible to move the motor until it stalls because of an obstacle and use that for homing the X, Y and Z-axis.
Pybricks
I was lucky enough to get access to the early beta testing program of Pybricks so I could get started.
All code I created for this project are in beta!!! It is still a proof of concept so the code is absolutely not optimized yet!!
The first important step was creating a homing routine to detect the limits of the axis so the machine knows its current position.
from pybricks.hubs import CPlusHub from pybricks.pupdevices import Motor from pybricks.parameters import Port, Stop from pybricks.tools import wait import math TechnicHub = CPlusHub() # theoretical XY limits Xmax = -6000 Ymax = -6000 X_DegMm = 0 Y_DegMm = 0 Xcurr = 0 Ycurr = 0 # Define the XYZ axis motor ports Xaxis = Motor(Port.B) Yaxis = Motor(Port.A) Zaxis = Motor(Port.C) def G28_homingXYZ(Xmax, Ymax): global Xcurr global Ycurr global X_DegMm global Y_DegMm # Move the Z-axis to its 0deg reference location print("Homing Z-axis: ") stallAngle = Zaxis.run_until_stalled(90,Stop.COAST,30) Zaxis.reset_angle(90) Zaxis.run_target(90,0,Stop.COAST) print("finished") # Home the Y-axis print("Homing Y-axis: ") stallAngle = Yaxis.run_until_stalled(+3600,Stop.COAST,20) Yaxis.reset_angle(0) Yaxis.run_target(3600, Ymax, Stop.COAST) Ymax = Yaxis.run_until_stalled(-3600,Stop.COAST,20) print("Ymax = " + str(Ymax) + "°") print("finished") # Home the X-axis print("Homing X-axis: ") stallAngle = Xaxis.run_until_stalled(3600,Stop.COAST,20) Xaxis.reset_angle(0) Xaxis.run_target(3600, Xmax, Stop.COAST) Xmax = Xaxis.run_until_stalled(-3600,Stop.COAST,20) print("Xmax = " + str(Xmax) + "°") print("finished") X_DegMm = Xmax / 145 Y_DegMm = Ymax / 140 Xcurr = 145 Ycurr = 140 print("Current location = X", Xcurr, " Y", Ycurr) # Say hello :) print("Lego 8094 says hello!") print("Battery voltage = " + str(TechnicHub.battery.voltage()) + "mV") G28_homingXYZ(Xmax, Ymax)
After homing was working I had to make sure the machine can move in XY along a line other than 0°, 45° and 90° as that was the drawback of the original set. To be able to make a strait line from A to B along an angled (other than 45°) the feedrate for each motor needs to be proportional.
def G1move(X,Y): global Xcurr global Ycurr ERROR = False print("G1 X", X, " Y", Y) if X > 145: print("X+ axis overtravel!!!") ERROR = True elif X < 0: print("X- axis overtravel!!!") ERROR = True if Y > 140: print("Y+ axis overtravel!!!") ERROR = True elif Y < 0: print("Y- axis overtravel!!!") ERROR = True if ERROR == False: PenDown() #print(" Xcurr", Xcurr, " Ycurr", Ycurr) Xinc = X - Xcurr Yinc = Y - Ycurr #print(" Xinc", Xinc, " Yinc", Yinc) Xangle = Xaxis.angle() + (Xinc * X_DegMm) Yangle = Yaxis.angle() + (Yinc * Y_DegMm) #print(" X_DegMm", X_DegMm, " Y_DegMm", Y_DegMm) #print(" Xangle", Xangle, " Yangle", Yangle) factor = min(X,Y) / max(X,Y) if min(X,Y) == X: Xfeed = 500*factor Yfeed = 500 #print("Xfeed = ", Xfeed, " Yfeed = ", Yfeed) Xaxis.run_target(Xfeed, Xangle, Stop.COAST, False) Yaxis.run_target(Yfeed, Yangle, Stop.COAST, False) else: Xfeed = 500 Yfeed = 500*factor #print("Xfeed = ", Xfeed, " Yfeed = ", Yfeed) Xaxis.run_target(Xfeed, Xangle, Stop.COAST, False) Yaxis.run_target(Yfeed, Yangle, Stop.COAST, False) wait(1000) # give the motor time to start moving while True: if Xaxis.speed() == 0 and Yaxis.speed() == 0: break #print(" Xang", Xaxis.angle(), " Yang", Yaxis.angle()) Xcurr = X Ycurr = Y
I tried to draw the Pybricks logo and write Pybricks beneath it but I seem to run into some memory problems. I created a postprocessor in my CAM system to get the correct coordinates for the drawing but I forget to change the output from 0.001mm accuracy to something like 0.1mm accuracy. I hope that that will reduce the memory issue I’m facing.
I also really need to find some time to fine tune the used calculations.
Feel free to play around and suggest modifications 🙂
Complete code
from pybricks.hubs import CPlusHub from pybricks.pupdevices import Motor from pybricks.parameters import Port, Stop from pybricks.tools import wait import math TechnicHub = CPlusHub() # theoretical XY limits Xmax = -6000 Ymax = -6000 X_DegMm = 0 Y_DegMm = 0 Xcurr = 0 Ycurr = 0 # Define the XYZ axis motor ports Xaxis = Motor(Port.B) Yaxis = Motor(Port.A) Zaxis = Motor(Port.C) def G28_homingXYZ(Xmax, Ymax): global Xcurr global Ycurr global X_DegMm global Y_DegMm # Move the Z-axis to its 0deg reference location print("Homing Z-axis: ") stallAngle = Zaxis.run_until_stalled(90,Stop.COAST,30) Zaxis.reset_angle(90) Zaxis.run_target(90,0,Stop.COAST) print("finished") # Home the Y-axis print("Homing Y-axis: ") stallAngle = Yaxis.run_until_stalled(+3600,Stop.COAST,20) Yaxis.reset_angle(0) Yaxis.run_target(3600, Ymax, Stop.COAST) Ymax = Yaxis.run_until_stalled(-3600,Stop.COAST,20) print("Ymax = " + str(Ymax) + "°") print("finished") # Home the X-axis print("Homing X-axis: ") stallAngle = Xaxis.run_until_stalled(3600,Stop.COAST,20) Xaxis.reset_angle(0) Xaxis.run_target(3600, Xmax, Stop.COAST) Xmax = Xaxis.run_until_stalled(-3600,Stop.COAST,20) print("Xmax = " + str(Xmax) + "°") print("finished") X_DegMm = Xmax / 145 Y_DegMm = Ymax / 140 Xcurr = 145 Ycurr = 140 print("Current location = X", Xcurr, " Y", Ycurr) # NOTE PenDown(): puts the pen down on the paper def PenDown(): Zaxis.run_target(90,-500,Stop.COAST) # NOTE PenUp(): retracts the pen away from the paper def PenUp(): Zaxis.run_target(90,0,Stop.COAST) def G0move(X,Y): global Xcurr global Ycurr ERROR = False print("G0 X", X, " Y", Y) if X > 145: print("X+ axis overtravel!!!") ERROR = True elif X < 0: print("X- axis overtravel!!!") ERROR = True if Y > 140: print("Y+ axis overtravel!!!") ERROR = True elif Y < 0: print("Y- axis overtravel!!!") ERROR = True if ERROR == False: PenUp() #print(" Xcurr", Xcurr, " Ycurr", Ycurr) Xinc = X - Xcurr Yinc = Y - Ycurr #print(" Xinc", Xinc, " Yinc", Yinc) Xangle = Xaxis.angle() + (Xinc * X_DegMm) Yangle = Yaxis.angle() + (Yinc * Y_DegMm) #print(" X_DegMm", X_DegMm, " Y_DegMm", Y_DegMm) #print(" Xangle", Xangle, " Yangle", Yangle) Xfeed = 3000 Yfeed = 3000 #print(" Xang", Xaxis.angle(), " Yang", Yaxis.angle()) Xaxis.run_target(Xfeed, Xangle, Stop.COAST, False) Yaxis.run_target(Yfeed, Yangle, Stop.COAST, False) wait(1000) # give the motor time to start moving while True: if Xaxis.speed() == 0 and Yaxis.speed() == 0: break #print(" Xang", Xaxis.angle(), " Yang", Yaxis.angle()) Xcurr = X Ycurr = Y def G1move(X,Y): global Xcurr global Ycurr ERROR = False print("G1 X", X, " Y", Y) if X > 145: print("X+ axis overtravel!!!") ERROR = True elif X < 0: print("X- axis overtravel!!!") ERROR = True if Y > 140: print("Y+ axis overtravel!!!") ERROR = True elif Y < 0: print("Y- axis overtravel!!!") ERROR = True if ERROR == False: PenDown() #print(" Xcurr", Xcurr, " Ycurr", Ycurr) Xinc = X - Xcurr Yinc = Y - Ycurr #print(" Xinc", Xinc, " Yinc", Yinc) Xangle = Xaxis.angle() + (Xinc * X_DegMm) Yangle = Yaxis.angle() + (Yinc * Y_DegMm) #print(" X_DegMm", X_DegMm, " Y_DegMm", Y_DegMm) #print(" Xangle", Xangle, " Yangle", Yangle) factor = min(X,Y) / max(X,Y) if min(X,Y) == X: Xfeed = 500*factor Yfeed = 500 #print("Xfeed = ", Xfeed, " Yfeed = ", Yfeed) Xaxis.run_target(Xfeed, Xangle, Stop.COAST, False) Yaxis.run_target(Yfeed, Yangle, Stop.COAST, False) else: Xfeed = 500 Yfeed = 500*factor #print("Xfeed = ", Xfeed, " Yfeed = ", Yfeed) Xaxis.run_target(Xfeed, Xangle, Stop.COAST, False) Yaxis.run_target(Yfeed, Yangle, Stop.COAST, False) wait(1000) # give the motor time to start moving while True: if Xaxis.speed() == 0 and Yaxis.speed() == 0: break #print(" Xang", Xaxis.angle(), " Yang", Yaxis.angle()) Xcurr = X Ycurr = Y # NOTE ReturnToReferencePosition(Xmax, Ymax) def ReturnToReferencePosition(): global Xcurr global Ycurr G0move(70,130) # Say hello :) print("Lego 8094 says hello!") print("Battery voltage = " + str(TechnicHub.battery.voltage()) + "mV") G28_homingXYZ(Xmax, Ymax) G0move(95.677,67.667) G1move(96.166,64.577) G1move(97.587,61.789) G1move(99.799,59.577) G1move(102.587,58.157) G1move(105.677,57.667) G1move(113.659,57.667) G1move(116.749,58.157) G1move(119.536,59.577) G1move(121.749,61.789) G1move(123.169,64.577) G1move(123.659,67.667) G1move(123.659,104.908) G1move(123.169,107.998) G1move(121.749,110.785) G1move(119.536,112.998) G1move(116.749,114.418) G1move(113.659,114.908) G1move(30.194,114.908) G1move(27.104,114.418) G1move(24.316,112.998) G1move(22.104,110.785) G1move(20.684,107.998) G1move(20.194,104.908) G1move(20.194,67.667) G1move(20.684,64.577) G1move(22.104,61.789) G1move(24.316,59.577) G1move(27.104,58.157) G1move(30.194,57.667) G1move(39.095,57.667) G1move(42.185,58.157) G1move(44.973,59.577) G1move(47.185,61.789) G1move(48.606,64.577) G1move(49.095,67.667) G1move(95.677,67.667) G0move(105.289,76.983) G1move(105.289,76.983) G1move(105.289,67.608) G1move(114.477,67.608) G1move(114.477,105.108) G1move(30.289,105.108) G1move(30.289,67.608) G1move(39.47,67.608) G1move(39.477,76.983) G1move(47.539,76.983) G1move(47.539,80.546) G1move(50.352,80.546) G1move(50.352,76.983) G1move(56.727,76.983) G1move(56.727,80.546) G1move(59.727,80.546) G1move(59.727,76.983) G1move(66.102,76.983) G1move(66.102,80.546) G1move(69.102,80.546) G1move(69.102,76.983) G1move(75.477,76.983) G1move(75.477,80.546) G1move(78.477,80.546) G1move(78.477,76.983) G1move(84.852,76.983) G1move(84.852,80.546) G1move(87.852,80.546) G1move(87.852,76.983) G1move(94.227,76.983) G1move(94.227,80.546) G1move(97.227,80.546) G1move(97.227,76.983) G1move(105.289,76.983) G0move(97.048,96.992) G1move(97.048,96.992) G1move(96.161,98.735) G1move(94.778,100.117) G1move(93.036,101.005) G1move(91.104,101.311) G1move(89.173,101.005) G1move(87.431,100.117) G1move(86.048,98.735) G1move(85.16,96.992) G1move(84.854,95.061) G1move(85.16,93.13) G1move(86.048,91.387) G1move(87.431,90.005) G1move(89.173,89.117) G1move(91.104,88.811) G1move(93.036,89.117) G1move(94.778,90.005) G1move(96.161,91.387) G1move(97.048,93.13) G1move(97.354,95.061) G1move(97.048,96.992) G0move(59.623,97.143) G1move(59.623,97.143) G1move(58.736,98.885) G1move(57.353,100.268) G1move(55.611,101.156) G1move(53.679,101.462) G1move(51.748,101.156) G1move(50.006,100.268) G1move(48.623,98.885) G1move(47.735,97.143) G1move(47.429,95.212) G1move(47.735,93.28) G1move(48.623,91.538) G1move(50.006,90.155) G1move(51.748,89.268) G1move(53.679,88.962) G1move(55.611,89.268) G1move(57.353,90.155) G1move(58.736,91.538) G1move(59.623,93.28) G1move(59.929,95.212) G1move(59.623,97.143) G0move(18.82,36.252) G1move(18.82,36.252) G1move(18.82,48.752) G1move(22.57,48.752) G1move(23.82,48.156) G1move(24.237,47.561) G1move(24.654,46.371) G1move(24.654,44.585) G1move(24.237,43.395) G1move(23.82,42.799) G1move(22.57,42.204) G1move(18.82,42.204) G0move(34.237,44.585) G1move(34.237,44.585) G1move(36.737,36.252) G0move(36.737,36.252) G0move(39.237,44.585) G1move(39.237,44.585) G1move(36.737,36.252) G1move(35.904,33.871) G1move(35.07,32.68) G1move(34.237,32.085) G1move(33.82,32.085) G0move(48.82,36.252) G1move(48.82,36.252) G1move(48.82,48.752) G1move(52.57,48.752) G1move(53.82,48.156) G1move(54.237,47.561) G1move(54.654,46.371) G1move(54.654,45.18) G1move(54.237,43.99) G1move(53.82,43.395) G1move(52.57,42.799) G0move(48.82,42.799) G1move(48.82,42.799) G1move(52.57,42.799) G1move(53.82,42.204) G1move(54.237,41.609) G1move(54.654,40.418) G1move(54.654,38.633) G1move(54.237,37.442) G1move(53.82,36.847) G1move(52.57,36.252) G1move(48.82,36.252) G0move(63.82,36.252) G1move(63.82,36.252) G1move(63.82,44.585) G0move(63.82,41.014) G1move(63.82,41.014) G1move(64.237,42.799) G1move(65.07,43.99) G1move(65.904,44.585) G1move(67.154,44.585) G0move(78.82,48.156) G1move(78.82,48.156) G1move(79.237,47.561) G1move(79.654,48.156) G1move(79.237,48.752) G1move(78.82,48.156) G0move(79.237,44.585) G1move(79.237,44.585) G1move(79.237,36.252) G0move(98.82,42.799) G1move(98.82,42.799) G1move(97.987,43.99) G1move(97.154,44.585) G1move(95.904,44.585) G1move(95.07,43.99) G1move(94.237,42.799) G1move(93.82,41.014) G1move(93.82,39.823) G1move(94.237,38.037) G1move(95.07,36.847) G1move(95.904,36.252) G1move(97.154,36.252) G1move(97.987,36.847) G1move(98.82,38.037) G0move(108.82,36.252) G1move(108.82,36.252) G1move(108.82,48.752) G0move(112.987,44.585) G1move(112.987,44.585) G1move(108.82,38.633) G0move(110.487,41.014) G1move(110.487,41.014) G1move(113.404,36.252) G0move(128.404,42.799) G1move(128.404,42.799) G1move(127.987,43.99) G1move(126.737,44.585) G1move(125.487,44.585) G1move(124.237,43.99) G1move(123.82,42.799) G1move(124.237,41.609) G1move(125.07,41.014) G1move(127.154,40.418) G1move(127.987,39.823) G1move(128.404,38.633) G1move(128.404,38.037) G1move(127.987,36.847) G1move(126.737,36.252) G1move(125.487,36.252) G1move(124.237,36.847) G1move(123.82,38.037) G0move(123.82,38.037) ReturnToReferencePosition()