ord

[WIP] Mindstorms 51515 Plotter

Recommended Posts

Hi,

I am working on a classic XY Plotter that is controlled by the new Mindstorms hub. It's a work in progress and hopefully I can show some improvements here over time.

I made it with the Robot Inventor Hub instead of the Technic Hub simply because I wanted to try programming with the bundled Mindstorms MicroPython software. I was unable to achieve some crucial things with this software (most importantly a simple way to run multiple motors in parallel?) so I switched to Pybricks but kept the Mindstorms Hub. The code was adapted from my Cartesian Parallel Robot - easy!

51330109889_1282784316_c_d.jpg

Features:

  • Lightweight XY carriage with all motors mounted off the moving parts
  • Pen raising and lowering
  • Programmed to 0.1mm accuracy
  • Programmed with backlash compensation (0.9mm in X, 0.5mm in Y)

I hope to reduce the mechanical backlash some more. For the moment though, programming it out seems to help. Also, I have implemented the method described by Didumos69 here to maintain tension in 'loops' going through the chains and remove backlash in them (I found this to be a better solution than tensioning the chains with springs). In this video the hub's display shows when backlash is compensated for in each axis:

 

Plot #1:

  • File: Gaudi's Honeycomb (SVG)
  • Paper: A5 200gsm
  • Speed: 20mm/s
  • Plot time: ~30min
  • Pen: Rotring 0.5mm graphic pen

I found a good website with some free SVG files to start with (link above). This particular file required around 4000 lines of Pybricks code to plot. I'm approaching the point where files are too big for the Inventor Hub (and its ~252KB of available RAM). Perhaps someone could tell me if there's some way to load larger files onto the hub using Pybricks or is this something that's only possible with the Mindstorms software or am I stuck with splitting larger files into multiple plots?

51329484621_6386da810f_c_d.jpg51330110164_d8cdc20ee7_c_d.jpg

I'm reasonably happy with the result. There are some obvious defects though and I would like to make it faster. Here's a short real-time video.

 

What's next?

I have a bricklink order on the way with parts to increase the plotting area to A4. I think this is the maximum size that this design can be, given the 32L axles along the Y-axis and for the pen raising/lowering. Aside from that, I will try to further reduce the backlash and increase the speed. Mostly though, I just want to plot some different things (which I will post here).

Thank you for reading. More to come soon...

Edited by ord
Changed to WIP topic

Share this post


Link to post
Share on other sites

Wow, this is very impressive! I will check out the video later.

I am not sure how to load bigger files.

Share this post


Link to post
Share on other sites

Amazing! It is really cleverly designed plotter which seems to be rather precise - for Lego plastic components. I look forward to your next steps and hope you are going to show us some construction details.

Share this post


Link to post
Share on other sites

This is really stunning...
And fast.

Now up for a LEGO 3D printer that makes Lego that makes 3D printers that...
Hey wait... Von Neumann arrived in the building:laugh:

Share this post


Link to post
Share on other sites

Thanks all! @Jonas I plan to make a Studio model of it once it's finished - it's still very much a work in progress.

A small update: I found a very cool tool called SquiggleCam - it converts any image into a bunch of wavy lines for plotting. I love it. Here are two plots I've done with it so far...

Plot #2:

  • File: My desktop background (The Matterhorn), in squiggle format
  • Paper: A5 200gsm
  • Speed: 40mm/s*
  • Plot time: ~30min (divided into 2 roughly equal parts)
  • Pen: Rotring 0.5mm graphic pen

51335481517_6ea45eaa07_c_d.jpg

 

Plot #3:

  • File: A portrait of a friend of mine, in squiggle format
  • Paper: A5 200gsm
  • Speed: 40mm/s*
  • Plot time: ~35min (divided into 3 roughly equal parts)
  • Pen: Rotring 0.5mm graphic pen

51336208046_6349b9c386_c_d.jpg

51337222665_37344d7643_c_d.jpg

*The programmed speed of each linear movement, not necessarily the overall speed, as I'm still working on timing between linear movements

Edited by ord

Share this post


Link to post
Share on other sites

Very nice machine! Those last pictures are really nice. Most accurate Lego plotter I've seen so far.

Share this post


Link to post
Share on other sites

Wow! We're impressed by what you've made (again!)

Glad to hear you found the Pybricks motor features useful.

And indeed, we still have to enable file system access. Then you can make drawings up to 30 MB.

 

Quote

This particular file required around 4000 lines of Pybricks code to plot

I don't think we've ever tried this. Thanks for testing these edge cases :)

For now, maybe it's already possible to do larger images by storing the image data in a different way. How do you currently do it?

Share this post


Link to post
Share on other sites

Thank you Jos and Pybricks!

5 hours ago, Pybricks said:

Then you can make drawings up to 30 MB. 

That would be great!

As a side note, I am continually impressed by the power of Pybricks and its many advanced features. So thank you @Pybricks for the work you've done to make such useful software and the documentation to go with it :)

6 hours ago, Pybricks said:

For now, maybe it's already possible to do larger images by storing the image data in a different way. How do you currently do it?

I convert the SVG file to a sequence of move(), head_down() and head_up() commands which I paste into Pybricks. Here is the start of such a sequence:

def program(speed):
	move(581,10,speed) #Move to x = 58.1mm, y = 1.0mm at some speed
	head_down()
	move(583,16,speed)
	move(585,17,speed)
	move(587,13,speed)
	move(587,10,speed)
	head_up()
	move(594,10,speed)
	head_down()
	move(594,11,speed)
	move(596,17,speed)
	move(598,17,speed)
	...

Later, I call program() with speed as a parameter. The first two parameters of the move() function are X and Y coordinates. They are stored in 10−1mm units, which allows for 0.1mm accuracy while storing them as integers (this reduces the program size by about 75% by my measurements, compared to storing them as floats). Without an in-depth knowledge of how programs are sent to the hub though, it's hard to know how to make it more efficient. Would trimming down the move() function itself help, since it is called so many times?

Share this post


Link to post
Share on other sites

Thanks for sharing that. That is quite good already --- I was indeed curious about whether you used floats or not.

Since speed is mostly constant, maybe it's worth removing it as a function argument. You could leave it constant altogether or use a global variable. And then you might be able to drop the program() function as well, so you're just left with a sequence of move/head calls.

Scripts are compiled to MPY format and then sent to the hub. It might be possible to optimize more by storing the data differently or even compressing it, but this is probably not worth the effort.

We better get that file system going! :classic:

 

Share this post


Link to post
Share on other sites

Just some wild thoughts as I don't know much about data saving, and how you could convert it, but can't you like store the X / Y / Head position in a list?

Just add every X Y and the Head position at the end of that movement (or beginning, just switch around the order of motor movements) to a list.

speed = 700
position_list = [58110True58316True58517True58713True58710False59410True59411True59617True59817True]
 
for x in range(len(position_list) / 3):
    move(position_list[x * 3], position_list[1 + (x * 3)], position_list[2 + (x * 3)])
 
def move(x_posy_poshead_down):
    global speed
    motor_x.run_target(x_pos, speed, then=Stop.HOLD, wait=False)
    motor_y.run_target(y_pos, speed, then=Stop.HOLD, wait=True)
    if head_down == True: motor_z.run_target(500, speed, then=Stop.HOLD)
    else: motor_z.run_target(0, speed, then=Stop.HOLD)

Position 0,3,6,9,... in the list will be the X_coordinates (Integers), 1,4,7,10,... Y_coordinates (Integers), 2,5,8,11,... head positions (Booleans True/False). If needed you can do just X/Y in the list and make head in a seperate list.

This is just a basic example ofcourse, as you will need to adjust the speed of 1 of the motors to make them end at the same time, but that you'll know already how to do.

Edited by Mr Jos

Share this post


Link to post
Share on other sites
Quote

I made it with the Robot Inventor Hub instead of the Technic Hub simply because I wanted to try programming with the bundled Mindstorms MicroPython software. I was unable to achieve some crucial things with this software (most importantly a simple way to run multiple motors in parallel?) so I switched to Pybricks but kept the Mindstorms Hub.

There's also this internal Hub API you can use within the official app.

Share this post


Link to post
Share on other sites

Thanks for the suggestions. They all sound good and I look forward to trying them.

36 minutes ago, Pybricks said:

There's also this internal Hub API you can use within the official app.

Is that different to what's shown in the official app's documentation? I'm confused.

Either way, I checked out the Motor class on that page and couldn't see anything similar to run_target() with wait=False as a parameter, like what I use in Pybricks.

Share this post


Link to post
Share on other sites

Back to the mechanical side of things... I now own four of each of the tread link sprockets and need to decide which will be best for the Y-axis.

The smallest ones would make the whole MOC nice and slim but would be more prone to chordal action (variations in speed and position of the chain), so I investigated to see just how much...

51339423430_b055a21b96_o_d.png

My conclusion: the speed and hence positional accuracy of the Y-axis will vary by a maximum of 13.4% if using the smallest sprockets, cyclically every 12mm. This isn't a huge amount, but combined with a radius variation of 1.6mm (chain will move up and down by this much every 12mm) I think I'll stick with the 10-teeth sprockets, or maybe even use the 14-teeth ones for maximum smoothness/accuracy.

Share this post


Link to post
Share on other sites

I've tried the suggestions for reducing file size and here are the results (only tested with one SVG file):

I called mem_info() at the very start of the code (after imports) and at the very end of the code. Shown is the memory used at each point.

  • Original code, program(speed): start 78KB, end 188KB
  • Speed parameter set as constant, program(): start 73KB, end 80KB
  • 'Program' function removed, move() calls only: start 73KB, end 78KB  (speed constant)
  • Positions stored in list, position_list[]: start 106KB, end ?  (speed constant)

I didn't wait for the plot to finish with the positions stored in a list, as it was taking a very long time (I think due to motor_z.run_target() being called with every move) and the memory used at the start of the program had already exceeded the memory used with the other suggestions.

So I think setting speed as a constant will be the most helpful thing to reduce file size. I am now, however, running into errors such as ValueError('incompatible .mpy file') and MemoryError: memory allocation failed when simply pasting all of the moves in twice (which I thought would use <2x80KB<252KB). Maybe I'm making some other mistake here though.

Anyway, it's not a huge problem to run a few programs for one plot if I need to.

Edited by ord

Share this post


Link to post
Share on other sites

Glad to hear the speed parameter helped a lot. That might be the best for now.

Another thing to keep in mind is that both the program and all your variables share the same memory. This is especially tricky if you have one giant list, as you have found.

This will no longer be an issue once the file system is enabled. Then you can read coordinates from a file, one at a time instead of loading the whole file at once.

Share this post


Link to post
Share on other sites

Update:

  • Plot area increased to A4 (not plotting yet - soon!)
  • Y-axis driven by 14-teeth sprockets on turntables. This is tentative - turntables seem to have more friction than axles, which could cause vibrations. They do however allow for a large gear reduction (5:1), which reduces torque in the drivetrain. Testing to come with new motors and a stiffened drivetrain
  • X-axis carriage rebuilt to move pen tip closer to chain attachment point (see renders below)
  • Soon: two large angular motors to drive the Y-axis

I'm really excited to get this thing plotting. One unfortunate thing about the current generation motors is their lack of (official) extension cables, which means I have to locate the hub in an inconvenient position at the back, or buy a bunch of 3rd-party cables.

51498846734_33b7a546ff_o_d.png
X-axis carriage. Grey - rails; Red - carriage; Blue - pen holder; Yellow - pen lifter; Green - rail clamp.

Edited by ord

Share this post


Link to post
Share on other sites

You might want to try using these parts:

lego 23421 and lego 25195 or lego 23422

instead of using chain links, the spindle axle can be driven by a motor and using a clutch ring and clutch gear you can have i where the nut is not spinning but can move along the spindle, or (when you engage the clutch ring), spinning and moving along the spindle.

Regards, Snipe

Share this post


Link to post
Share on other sites

Thanks for the suggestion Snipe. 

I remember seeing these in another thread - they look interesting but the ends seem to be held by friction only. There might be ways to secure them, however I am quite happy with my current setup for the plotter. 

I'd definitely like to try these out for something else though :).

Regards, ord

Share this post


Link to post
Share on other sites

A4 plot #1:

  • File: generated with vpype and vpype-gcode
  • Paper: A4 200gsm
  • Speed: ~20mm/s
  • Plot time: ~7min
  • Pen: Rotring 0.5mm graphic pen

 


It's working again :classic:. There are a few artefacts that need fixing - it probably needs the backlash recalibrated and maybe a 'clamp' on the y-axis (like what's on the x-axis). I'll work on them next.

@Carsten Svendsen thanks, it's programmed in Pybricks. Here's the code for this plot if you're interested:

Spoiler

from pybricks.hubs import InventorHub
from pybricks.pupdevices import Motor
from pybricks.parameters import Port, Stop, Color, Direction, Icon, Side, Button
from pybricks.tools import wait, StopWatch
from pybricks.geometry import Matrix
from math import sin, pi, fabs, sqrt
from urandom import randint
from micropython import mem_info


print("START")

# Initialize the hub and stopwatch
hub = InventorHub()
stop_watch = StopWatch()

# Initialize the motors.
# First gear sets are actual gears e.g. 8 and 40
# Second gear sets allow motor commands to be given in 0.1mm units rather than degrees e.g. 130mm = 360 degrees
motor_x = Motor(Port.B, Direction.COUNTERCLOCKWISE, [[8, 40], [1300, 360]])
motor_y1 = Motor(Port.E, Direction.COUNTERCLOCKWISE, [[12, 60], [1680, 360]])
motor_y2 = Motor(Port.F, Direction.CLOCKWISE, [[12, 60], [1680, 360]])
motor_z = Motor(Port.D, Direction.CLOCKWISE, [24, 8])

# Initialize variables
old_x = 0
old_y = 0
old_x_direction = "-"
old_y_direction = "-"
x_backlash = 0
y_backlash = 0


def head_up():
    motor_z.run_target(500, 0, then=Stop.BRAKE, wait=True)


def head_down():
    wait(200)
    motor_z.run_target(500, 180, then=Stop.BRAKE, wait=True)
    wait(200)


def move(x, y):
    global old_x
    global old_y
    global old_x_direction
    global old_y_direction
    global x_backlash
    global y_backlash
    
    
    # Determine the relative move coordinates
    relative_x = x - old_x
    relative_y = y - old_y
    
    
    # Determine x move direction
    if relative_x > 0:
        new_x_direction = "+"
        x_backlash = 9 # Set x backlash amount here
        hub.display.pixel(4, 3)
        hub.display.pixel(4, 4)
    elif relative_x < 0:
        new_x_direction = "-"
        x_backlash = 0
        hub.display.pixel(4, 3, 0)
        hub.display.pixel(4, 4, 0)
    else:
        # Backlash is unchanged
        new_x_direction = old_x_direction
    
    
    # Determine y move direction
    if relative_y > 0:
        new_y_direction = "+"
        y_backlash = 5  # Set y backlash amount here
        hub.display.pixel(0, 0)
        hub.display.pixel(1, 0)
    elif relative_y < 0:
        new_y_direction = "-"
        y_backlash = 0
        hub.display.pixel(0, 0, 0)
        hub.display.pixel(1, 0, 0)
    else:
        # Backlash is unchanged
        new_y_direction = old_y_direction
    
    
    # If x direction has changed, remove backlash
    if new_x_direction != old_x_direction:
        if new_x_direction == "+":
            motor_x.run_angle(300, 9, then=Stop.BRAKE, wait=False)
        elif new_x_direction == "-":
            motor_x.run_angle(300, -9, then=Stop.BRAKE, wait=False)
    
    
    # If y direction has changed, remove backlash
    if new_y_direction != old_y_direction:
        if new_y_direction == "+":
            motor_y1.run_angle(300, 5, then=Stop.BRAKE, wait=False)
            motor_y2.run_angle(300, 5, then=Stop.BRAKE, wait=False)
        elif new_y_direction == "-":
            motor_y1.run_angle(300, -5, then=Stop.BRAKE, wait=False)
            motor_y2.run_angle(300, -5, then=Stop.BRAKE, wait=False)
    
    
    # If any backlash is removed, allow time for it
    if new_x_direction != old_x_direction or new_y_direction != old_y_direction:
        wait(200)
    
    
    segment_length = sqrt(pow(relative_x,2) + pow(relative_y,2))
    time = segment_length / 200 # Set speed here
    if time == 0: time = 0.01 # In case of zero length segment
    x_speed = fabs(relative_x) / time
    y_speed = fabs(relative_y) / time
    
    if x_speed > 1: motor_x.run_target(x_speed, x + x_backlash, then=Stop.BRAKE, wait=False)
    if y_speed > 1:
        motor_y1.run_target(y_speed, y + y_backlash, then=Stop.BRAKE, wait=False)
        motor_y2.run_target(y_speed, y + y_backlash, then=Stop.BRAKE, wait=False)

    wait(time * 2000 + 50)
    old_x = x
    old_y = y
    old_x_direction = new_x_direction
    old_y_direction = new_y_direction


def home():
    print("Finding origin...")
    wait(1000)
    head_up()
    
    hub.display.image(Icon.ARROW_LEFT)
    motor_x.run_until_stalled(-100, duty_limit=40)
    print("x zero set.")
    hub.display.off()
    motor_x.reset_angle(0)
    move(50,0)
    motor_x.reset_angle(0)
    wait(1000)
    
    hub.display.image(Icon.ARROW_UP)
    print("\ny zero set.")
    hub.display.off()
    motor_y1.reset_angle(0)
    motor_y2.reset_angle(0)
    wait(1000)
    move(0,0)






def program():
	move(430,310)
	head_down()
	move(430,1790)
	move(2530,1790)
	move(2530,310)
	move(430,310)
	head_up()
	move(740,520)
	head_down()
	move(740,1570)
	move(2220,1570)
	move(2220,520)
	move(740,520)
	head_up()
	move(1345,1452)
	head_down()
	move(1365,1504)
	head_up()
	move(1358,1487)
	head_down()
	move(1333,1487)
	head_up()
	move(1325,1504)
	head_down()
	move(1345,1452)
	head_up()
	move(1377,1487)
	head_down()
	move(1380,1479)
	move(1385,1474)
	move(1392,1472)
	move(1395,1472)
	move(1402,1474)
	move(1407,1479)
	move(1410,1487)
	move(1410,1489)
	move(1407,1497)
	move(1402,1502)
	move(1395,1504)
	move(1392,1504)
	move(1385,1502)
	move(1380,1497)
	move(1377,1487)
	move(1377,1474)
	move(1380,1462)
	move(1385,1454)
	move(1392,1452)
	move(1397,1452)
	move(1405,1454)
	move(1407,1459)
	head_up()
	move(1467,1482)
	head_down()
	move(1511,1482)
	head_up()
	move(1576,1462)
	head_down()
	move(1581,1459)
	move(1588,1452)
	move(1588,1504)
	head_up()
	move(1643,1504)
	head_down()
	move(1643,1452)
	head_up()
	move(1643,1452)
	head_down()
	move(1618,1487)
	move(1655,1487)
	head_up()
	move(1680,1452)
	head_down()
	move(1673,1454)
	move(1670,1459)
	move(1670,1464)
	move(1673,1469)
	move(1678,1472)
	move(1688,1474)
	move(1695,1477)
	move(1700,1482)
	move(1702,1487)
	move(1702,1494)
	move(1700,1499)
	move(1697,1502)
	move(1690,1504)
	move(1680,1504)
	move(1673,1502)
	move(1670,1499)
	move(1668,1494)
	move(1668,1487)
	move(1670,1482)
	move(1675,1477)
	move(1683,1474)
	move(1693,1472)
	move(1697,1469)
	move(1700,1464)
	move(1700,1459)
	move(1697,1454)
	move(1690,1452)
	move(1680,1452)
	head_up()
	move(1717,1469)
	head_down()
	move(1745,1504)
	head_up()
	move(1717,1504)
	head_down()
	move(1745,1469)
	head_up()
	move(1767,1462)
	head_down()
	move(1772,1459)
	move(1779,1452)
	move(1779,1504)
	head_up()
	move(1824,1452)
	head_down()
	move(1817,1454)
	move(1812,1462)
	move(1809,1474)
	move(1809,1482)
	move(1812,1494)
	move(1817,1502)
	move(1824,1504)
	move(1829,1504)
	move(1836,1502)
	move(1841,1494)
	move(1844,1482)
	move(1844,1474)
	move(1841,1462)
	move(1836,1454)
	move(1829,1452)
	move(1824,1452)
	head_up()
	move(1859,1494)
	head_down()
	move(1861,1499)
	move(1864,1502)
	move(1871,1504)
	move(1879,1504)
	move(1886,1502)
	move(1891,1497)
	move(1893,1489)
	move(1893,1484)
	move(1891,1477)
	move(1886,1472)
	move(1879,1469)
	move(1871,1469)
	move(1864,1472)
	move(1861,1474)
	move(1864,1452)
	move(1888,1452)
	head_up()
	move(1911,1469)
	head_down()
	move(1911,1504)
	head_up()
	move(1911,1479)
	head_down()
	move(1918,1472)
	move(1923,1469)
	move(1931,1469)
	move(1936,1472)
	move(1938,1479)
	move(1938,1504)
	head_up()
	move(1938,1479)
	head_down()
	move(1946,1472)
	move(1950,1469)
	move(1958,1469)
	move(1963,1472)
	move(1965,1479)
	move(1965,1504)
	head_up()
	move(1985,1504)
	head_down()
	move(1985,1469)
	head_up()
	move(1985,1479)
	head_down()
	move(1993,1472)
	move(1998,1469)
	move(2005,1469)
	move(2010,1472)
	move(2012,1479)
	move(2012,1504)
	head_up()
	move(2012,1479)
	head_down()
	move(2020,1472)
	move(2025,1469)
	move(2032,1469)
	move(2037,1472)
	move(2040,1479)
	move(2040,1504)
	head_up()
	move(2084,1680)
	head_down()
	move(2084,1750)
	head_up()
	move(2100,1726)
	head_down()
	move(2051,1726)
	move(2084,1680)
	head_up()
	move(2133,1680)
	head_down()
	move(2123,1683)
	move(2120,1690)
	move(2120,1697)
	move(2123,1703)
	move(2130,1707)
	move(2143,1710)
	move(2153,1713)
	move(2160,1720)
	move(2163,1726)
	move(2163,1736)
	move(2160,1743)
	move(2156,1746)
	move(2146,1750)
	move(2133,1750)
	move(2123,1746)
	move(2120,1743)
	move(2117,1736)
	move(2117,1726)
	move(2120,1720)
	move(2127,1713)
	move(2137,1710)
	move(2150,1707)
	move(2156,1703)
	move(2160,1697)
	move(2160,1690)
	move(2156,1683)
	move(2146,1680)
	move(2133,1680)
	head_up()
	move(2186,1703)
	head_down()
	move(2186,1750)
	head_up()
	move(2186,1716)
	head_down()
	move(2196,1707)
	move(2203,1703)
	move(2213,1703)
	move(2219,1707)
	move(2222,1716)
	move(2222,1750)
	head_up()
	move(2222,1716)
	head_down()
	move(2232,1707)
	move(2239,1703)
	move(2249,1703)
	move(2256,1707)
	move(2259,1716)
	move(2259,1750)
	head_up()
	move(2285,1750)
	head_down()
	move(2285,1703)
	head_up()
	move(2285,1716)
	head_down()
	move(2295,1707)
	move(2302,1703)
	move(2312,1703)
	move(2318,1707)
	move(2322,1716)
	move(2322,1750)
	head_up()
	move(2322,1716)
	head_down()
	move(2332,1707)
	move(2338,1703)
	move(2348,1703)
	move(2355,1707)
	move(2358,1716)
	move(2358,1750)
	head_up()
	move(2410,1925)
	head_down()
	move(2418,1921)
	move(2431,1908)
	move(2431,1995)
	head_up()
	move(2373,1995)
	head_down()
	move(2315,1995)
	move(2356,1954)
	move(2365,1941)
	move(2369,1933)
	move(2369,1925)
	move(2365,1917)
	move(2361,1912)
	move(2352,1908)
	move(2336,1908)
	move(2328,1912)
	move(2323,1917)
	move(2319,1925)
	move(2319,1929)
	head_up()
	move(2290,1937)
	head_down()
	move(2245,1995)
	head_up()
	move(2290,1995)
	head_down()
	move(2245,1937)
	head_up()
	move(2220,1908)
	head_down()
	move(2179,1995)
	head_up()
	move(2133,1937)
	head_down()
	move(2129,1950)
	move(2121,1958)
	move(2108,1962)
	move(2104,1962)
	move(2092,1958)
	move(2084,1950)
	move(2079,1937)
	move(2079,1933)
	move(2084,1921)
	move(2092,1912)
	move(2104,1908)
	move(2108,1908)
	move(2121,1912)
	move(2129,1921)
	move(2133,1937)
	move(2133,1958)
	move(2129,1979)
	move(2121,1991)
	move(2108,1995)
	move(2100,1995)
	move(2088,1991)
	move(2084,1983)
	head_up()
	move(2055,1995)
	head_down()
	move(1997,1995)
	move(2038,1954)
	move(2046,1941)
	move(2051,1933)
	move(2051,1925)
	move(2046,1917)
	move(2042,1912)
	move(2034,1908)
	move(2017,1908)
	move(2009,1912)
	move(2005,1917)
	move(2001,1925)
	move(2001,1929)
	head_up()
	move(1902,1958)
	head_down()
	move(1827,1958)
	head_up()
	move(1736,1966)
	head_down()
	move(1674,1966)
	move(1716,1908)
	head_up()
	move(1716,1908)
	head_down()
	move(1716,1995)
	head_up()
	move(1658,1995)
	head_down()
	move(1625,1908)
	head_up()
	move(1625,1908)
	head_down()
	move(1592,1995)
	head_up()
	move(1604,1966)
	head_down()
	move(1645,1966)
	head_up()
	move(1654,1720)
	head_down()
	move(1594,1720)
	head_up()
	move(1511,1680)
	head_down()
	move(1478,1680)
	move(1475,1710)
	move(1478,1707)
	move(1488,1703)
	move(1498,1703)
	move(1508,1707)
	move(1515,1713)
	move(1518,1723)
	move(1518,1730)
	move(1515,1740)
	move(1508,1746)
	move(1498,1750)
	move(1488,1750)
	move(1478,1746)
	move(1475,1743)
	move(1472,1736)
	head_up()
	move(1459,1750)
	head_down()
	move(1432,1680)
	head_up()
	move(1432,1680)
	head_down()
	move(1406,1750)
	head_up()
	move(1416,1726)
	head_down()
	move(1449,1726)
	head_up()
	move(1733,1697)
	head_down()
	move(1733,1693)
	move(1736,1687)
	move(1740,1683)
	move(1746,1680)
	move(1759,1680)
	move(1766,1683)
	move(1769,1687)
	move(1773,1693)
	move(1773,1700)
	move(1769,1707)
	move(1763,1716)
	move(1730,1750)
	move(1776,1750)
	head_up()
	move(1822,1750)
	head_down()
	move(1822,1680)
	move(1812,1690)
	move(1806,1693)
	head_up()
	move(1882,1680)
	head_down()
	move(1872,1683)
	move(1865,1693)
	move(1862,1710)
	move(1862,1720)
	move(1865,1736)
	move(1872,1746)
	move(1882,1750)
	move(1888,1750)
	move(1898,1746)
	move(1905,1736)
	move(1908,1720)
	move(1908,1710)
	move(1905,1693)
	move(1898,1683)
	move(1888,1680)
	move(1882,1680)
	head_up()
	move(1928,1703)
	head_down()
	move(1965,1750)
	head_up()
	move(1928,1750)
	head_down()
	move(1965,1703)
	head_up()
	move(1994,1693)
	head_down()
	move(2001,1690)
	move(2011,1680)
	move(2011,1750)
	head_up()
	move(2162,1908)
	head_down()
	move(2220,1908)
	head_up()
	move(2505,1908)
	head_down()
	move(2493,1912)
	move(2485,1925)
	move(2480,1946)
	move(2480,1958)
	move(2485,1979)
	move(2493,1991)
	move(2505,1995)
	move(2514,1995)
	move(2526,1991)
	move(2534,1979)
	move(2538,1958)
	move(2538,1946)
	move(2534,1925)
	move(2526,1912)
	move(2514,1908)
	move(2505,1908)
	head_up()
	move(2567,1937)
	head_down()
	move(2567,1995)
	head_up()
	move(2567,1954)
	head_down()
	move(2580,1941)
	move(2588,1937)
	move(2600,1937)
	move(2609,1941)
	move(2613,1954)
	move(2613,1995)
	head_up()
	move(2613,1954)
	head_down()
	move(2625,1941)
	move(2633,1937)
	move(2646,1937)
	move(2654,1941)
	move(2658,1954)
	move(2658,1995)
	head_up()
	move(2691,1995)
	head_down()
	move(2691,1937)
	head_up()
	move(2691,1954)
	head_down()
	move(2704,1941)
	move(2712,1937)
	move(2724,1937)
	move(2733,1941)
	move(2737,1954)
	move(2737,1995)
	head_up()
	move(2737,1954)
	head_down()
	move(2749,1941)
	move(2757,1937)
	move(2770,1937)
	move(2778,1941)
	move(2782,1954)
	move(2782,1995)
	head_up()









# PROGRAM HERE
home()
program()
move(0,0)



print("Time elapsed: " + str(stop_watch.time()/1000) + " seconds")
print("Memory info:")
mem_info()
print("END\n")

 

 

Share this post


Link to post
Share on other sites

Actually, not as advanced a code as I would have expected. Probably mostly because the perimeters are... hard coded? That seems odd, especially for that complex art piece in the first post.
How do you convert an .svg image into plotting perimeters?

I really like your design by the way. Nice idea of tension by shifting gears to twist the axles slightly, great use of the 8t slipping gear which I still haven't found a project for, and the big tracks compared to small tracks for accuracy. And it's not even an over the top complex design! I love it

Edited by Carsten Svendsen

Share this post


Link to post
Share on other sites

Thank you, I appreciate the comments :).

I use a handy tool called vpype (that's specifically made for plotters) and a plugin for it called vpype-gcode to convert the .svg file into the code that you see under program(). That art in the first plot was just an .svg file that I downloaded.

Vpype sorts and scales the line segments of the .svg...

vpype read C:\input.svg layout --fit-to-margins 20cm --landscape 297x210cm show linesort gwrite --profile pybricks C:\output.txt

...while vpype-gcode (referenced by gwrite above) turns it into the code that I paste into Pybricks, by referencing this custom profile...

[gwrite.pybricks]
document_start = "def program():\n"
segment_first = "	move({x:.0f},{y:.0f})\n	head_down()\n"
segment = 	"	move({x:.0f},{y:.0f})\n"
segment_last = 	"	move({x:.0f},{y:.0f})\n	head_up()\n"

The last plot I did (A4 plot #1) didn't use an .svg file but rather was created entirely with the vpype command line interface, because it was easier that way.

Hope that makes sense!

Share this post


Link to post
Share on other sites

I see, very interesting! It's amazing to see how things work to a detailed level. While the physical construction is key, it's nothing without a functional program.

Keep at it!

Share this post


Link to post
Share on other sites
On 9/27/2021 at 10:17 AM, Carsten Svendsen said:

it's nothing without a functional program.

:thumbup::thumbup:. After tweaking the backlash numbers, it's plotting a lot better now.

I also faced a curious problem that took some time to figure out - the speed of the print head was fluctuating in the x-axis, with each rotation of the x-motor:

51528732422_2df11a454b_w_d.jpg
51530464005_68128ab12b_w_d.jpg

After checking gears, shafts, and switching out the motor with no improvement I was stumped. I then removed the pen-lifting motor that was mounted to the side of the x-motor and success - the problem disappeared! The motors must interfere with each other when mounted too close together. They are now mounted far apart.

With this sorted, the plotter was ready for its first real A4 plot...

Bunny:

  • File: Bunny (SVG)
  • Paper: A4 200gsm
  • Speed: ~20mm/s
  • Plot time: ~40min
  • Pen: Rotring 0.5mm graphic pen

 

51529778013_a2d3c98bf8_c_d.jpg

 

Share this post


Link to post
Share on other sites

What a difference a few lines of code can make...

By adding the following line for each of the motors, their accelerations are effectively no longer limited by the control, meaning a lot faster plots:

motor_x.control.limits(acceleration=20000)

My only concern is whether this will quickly cause damage to the motors. Perhaps someone who knows more about motors can answer this?

I've been experimenting with different media too (before the acceleration limits were changed):

IcoSphere:

  • File: IcoSphere (SVG)
  • Paper: A4 200gsm
  • Speed: ~10mm/s
  • Plot time: ~52min
  • Pen: Lilac gel pen

800x600.jpg
 

The Great Wave off Kanagawa:

  • File: The Great Wave off Kanagawa (SVG, stripped to a few layers)
  • Paper: Brown A4 250gsm
  • Speed: ~20mm/s
  • Plot time: ~81min
  • Pen: Rotring 0.5mm graphic pen

800x600.jpg

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.