Sat Jan 2 00:24:00 PDT 2021

sawbot

(Programming note: this is a blog post I wrote for my previous employer, now defunct, preserved here. Some of the links won't work, since they point at a domain that no longer exists)

This year's Worlds demo is something practical: a robot that cuts shafts for you!

A power hacksaw is testing the limits of the VEX hardware. Power tools need big motors and stiff, precise bearings: VEX 393 motors have a peak power of 35 watts, (At that power level they overheat and trip the internal polyfuse in seconds) and unlubricated plastic-on-steel bushings.

The robot is basically just a hacksaw blade (clamped between two 1x2x1x35 c-channels) mounted to a crank. The linear part uses a linear motion kit with all four wide slide trucks mounted, and greased with Super Lube 21030. The rotational bit uses a turntable bearing packed with grease. The saw is powered by four 393 motors ganged together with standard gears, which have less friction than the high-strength versions.

In practice you push a shaft through a set of four shaft bar locks until it hits the stop, then tell it what length you want it cut to. The shaft feed moves it to the correct position, a pair of pneumatic cylinders push the blade into the shaft, (using the same air compressor we used in the Pongbot from last year) and it starts cutting. Once it cuts through, the saw sled trips a limit switch, which retracts the saw, and then the shaft feed ejects the cut shaft. And that's it!

The software half of the robot is uncomplicated, basically just turning motors on and off in response to user input. But we get that user input from the LCD buttons, which means UI code!

UI programming tends to be tricky because it requires a little design, and a lot of input validation. You have to make sure the user can't wiggle the system into a bad state, which requires thinking very hard about how inputs affect everything. Let's do a quick, easy prototype, that also happens to be totally broken.

def UpdateLCD(length):
    lcd.write_top(length + " - " + (12.0 - length))
    lcd.write_bottom("UP   START  DOWN")

def ChooseLength():
    length = 1.875
    while True:
        if lcd.button_left():
            length += 0.125
            UpdateLCD(length)
        elif lcd.button_middle():
            return length
        elif lcd.button_right():
            length -= 0.125
            UpdateLCD(length)

def FeedShaft(length):
    ticks = (length - 1.875) * TICKS_PER_INCH
    while feed_encoder.value() < ticks:
        shaft_feed.run(50)
    shaft_feed.off()

FeedShaft(ChooseLength())

This code works, in the sense that it will faithfully do exactly what the user commands. Unfortunately, you can keep pressing down forever until you get a negative number. FeedShaft won't do anything outrageously stupid if you give it a negative number, but it'll quite happily run the shaft feed sled into the far end if you give it a large positive number.

We could fix the problem by changing FeedShaft to clamp input values to 1.875-6.0, but input validation should be done at the point where the user enters the input:

while True:
    if lcd.button_left() and length <= 5.875:
        length += 0.125
        UpdateLCD(length)
    elif lcd.button_mid():
        return length
    elif lcd.button_right() and length >= 2:
        length -= 0.125
        UpdateLCD(length)

This disables the up and down buttons when we're at 1.875 and 6. But it leaves the button labels on the LCD, a false afforance that tricks the user into thinking they can keep going up or down, so let's fix that too.

def UpdateLCD(length):
    lcd.write_top(str(length) + " - " + str((12.0 - length)))
    if length == 1.875:
        lcd.write_bottom("UP   START      ")
    elif length == 6:
        lcd.write_bottom("     START  DOWN")
    else:
        lcd.write_bottom("UP   START  DOWN")

Note the implemtation detail here: the length check in ChooseLength is <= 5.875, while the equivalent value in UpdateLCD is == 6. If it was 5.875 too, of course, then the up button would have its label removed one increment early, and be invisibly functional, an interesting example of an off-by-one error.

A slightly different problem arises. On the first update, the LCD prints 1.875 - 10.125 but when you press up once, it prints 2.0 - 10.0, which is a couple characters shorter. In practice, the numbers thrash around a lot while you hold up or down, which makes it hard to read them. If we pad short strings with zeros, then it will keep the decimal point in the same place while we scroll through sizes. Zero padding is easy enough:

def ZeroPad(float):
    string = str(float)
    if len(string) == 3:
        return string + "00"
    elif len(string) == 4:
        return string + "0"
    else:
        return string

This function takes a number, turns it into a string, then counts how many characters it contains, adding variable numbers of 0's to the end, and returning the concatenated string. (Embarassingly, my first try at this function had another off-by-one-ish bug: I looked at the string "1.875" and concluded it had four characters in it, instead of five, because I was counting the digits. I wrote my function as a string of elifs, and became immensely confused when it returned None instead of a zero-padded number. (A function returns None by default if you don't override it by passing something to the return keyword.)

And that's basically it! You can check out the full program here.


①: Code inside a while block will only execute if the statement after the keyword evaluates to True. feed_encoder.value() is always 0 at this point in the code, so an expression testing if it is less than a negative number will return False. ^

②: "Well, what if you wanted a shaft that was 7 inches long?" The robot can only perform one cut, and it gives you both halves of the cut shaft. If you want a 7 inch shaft, then you select 5.0 on the LCD. (It displays the length of both segments on the screen-- that's what (12.0 - length) is doing in the lcd.write_top call. ^


Posted by Samuel Bierwagen | Permanent link | File under: nerdery