The Imperfect Robot

I’ve been researching Harold Cohen’s work recently. He was an acclaimed “traditional” painter until discovering computers when he changed track to pursue work made with a floor-roaming, pen-wielding wheeled robot controlled by his AARON software, which seems like his investigation into the mechanics of painting. He made work in this way from the 1960s to his passing in 2016. I enjoy hearing him talk about his work, and there are several videos online to watch, for example from Youtube:

Of course, Cohen used a precision robot to make his work, but I wondered if I could at least start to think about how to use a similar machine. I found a cheap way to at least explore the possibilities, using a pair of BBC Microbit microcontrollers and a compatible wheeled buggy along with a Sharpie pen and a bit sheet of paper.

My idea was to make up some simple instructions for driving the buggy, and have my laptop send those commands to it: keep the buggy side very simple and have any complicated stuff running on the laptop.

Experimenting with a pair of BBC Microbits and a buggy
Experimenting with a pair of BBC Microbits and a buggy

The protocol itself included simple instructions like “move forward a bit”, “turn right a bit”, “how far to the obstruction in front of you?”.

I sent the instructions between laptop and buggy using the Microbit’s built-in radio channel and made the buggy light up its display to show me what it thought it was doing.

In this way, I made a very simple drawing… and realised that my robot was, shall we say, “imperfect”. It’s only a basic educational machine, which manifests in how very inexactly it can be controlled. Attempts to make a closed shape resulted in (much more interesting) outlined blobs.

It was also very apparent that to use this for making real work would need acres of space and similarly oversized sheets of paper for my clunky buggy to roll around on!

But it’s interesting, so I coded up an app that would try to draw lines up to any obstruction and then rotate away to a clear space and continue. I found that cardboard walls didn’t reliably trip the “obstruction sensor” (which is based on echo-location like a bat!) so I ran round holding my hands at the edges of the paper to try and make a wall that the robot could detect to stop it driving itself off!


It worked. Sort of. The end result was a drawing on oversized newsprint:

Imperfect Robot Drawing: Sharpie pen on A1+ newsprint
Imperfect Robot Drawing: Sharpie pen on A1+ newsprint

Addendum

For anyone interested in what the code for something like this looks like, here’s where I ended up. It’s unapologetically naive and simple.

The code running on the Microbit plugged into the buggy is python, responding to a simple protocol sent to it over its radio receiver:

from microbit import *
import radio
import neopixel
import utime

RED = (255, 0, 0)
ORANGE = (255, 170, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
BLACK = (0, 0, 0)

radio.on()
np = neopixel.NeoPixel(pin13, 12)
for id in range(0, 7):
    np[id] = BLUE
np.show()

def reset_pixels():
    for id in range(0, 7):
        np[id] = BLACK
    np.show()

def forward():
    np[3] = GREEN; np[4] = GREEN; np.show()
    pin0.write_digital(1)
    pin8.write_digital(0)
    pin1.write_digital(1)
    pin12.write_digital(0)
    utime.sleep_ms(150)
    pin0.write_digital(0)
    pin1.write_digital(0)
    reset_pixels()
    radio.send("1")

def right():
    np[4] = ORANGE; np[5] = ORANGE; np[6] = ORANGE; np.show()
    pin0.write_digital(1)
    pin8.write_digital(0)
    pin1.write_digital(0)
    pin12.write_digital(1)
    utime.sleep_ms(100)
    pin0.write_digital(0)
    pin12.write_digital(0)
    reset_pixels()
    radio.send("1")

def get_distance():
    pin15.write_digital(1) # Send 10us Ping pulse
    utime.sleep_us(10)
    pin15.write_digital(0)
    while pin15.read_digital() == 0: # ensure Ping pulse has cleared
        pass
    start = utime.ticks_us()
    while pin15.read_digital() == 1: # wait for Echo pulse to return
        pass
    end = utime.ticks_us()
    echo = end - start
    distance_cm = int(0.01715 * echo)
    return distance_cm

def sense_distance():
    np[3] = RED; np[4] = RED; np.show()
    distance_cm = get_distance()
    reset_pixels()
    scaled_distance = min(int(distance_cm / 20), 5)
    radio.send(str(scaled_distance))

sleep(100)
reset_pixels()

while True:
    recd = radio.receive()
    if recd != None:
        if recd == 'm':
            forward()
        elif recd == 'r':
            right()    
        elif recd == 'd':
            sense_distance()    
    utime.sleep_ms(50)

The Microbit plugged into my laptop relays instructions from its USB connection to the radio channel, again python code:

from microbit import *
import radio

radio.on()

uart.init(baudrate=115200, bits=8, parity=None)
uart.write('\r') # flush junk
sleep(100)

while True:
    while not uart.any():
        sleep(100)
    from_laptop = uart.read(1)
    radio.send(from_laptop)
    display.scroll(from_laptop)
    
    from_buggy = None
    while not from_buggy:
        sleep(20)
        from_buggy = radio.receive()
    uart.write(from_buggy)

And the program that makes the drawing is ruby code running on my laptop that sends instructions to the connected Microbit via USB:

require 'rubygems'
require 'rubyserial'

$serial = Serial.new("/dev/cu.usbmodem102", 115200, 8, :none)

def waitAck
  begin
    response = $serial.read(1)
    sleep(0.05)
  end while response.empty?
  response
end

def forward
  $serial.write("m")
  waitAck
end

def right
  $serial.write("r")
  waitAck
end

def senseDistance
  $serial.write("d")
  waitAck.to_i
end

def avoid
  loop do
    right
    right
    d = senseDistance
    break if d > 0
  end
end

loop do
  d = senseDistance
  puts "Distance: #{d}"
  avoid if d < 1
  [(d/2).to_i, 1].max.times do
    forward
  end
end

Published by Steve Meyfroidt

Artist, Scientist, Technologist

%d bloggers like this: