Audino Drawing Robot

This project is a recreation of another bot (Drawing Robot for Arduino : 18 Steps (with Pictures) - Instructables). This version takes a hand drawn image and converts it into lines through MATLAB for a two wheeled bot to draw. The modifications to this bot done in order to make the bot use a wide range of different parts and pens.
Supplies
Electronics
- 1- Arduino UNO or equivalent- adafruit.com/products/50
- Adafruit is now the US Manufacture for Genuine Arduinos! Get them from the source.
- 2- Geared 5V Stepper- adafruit.com/products/858
- 1- ULN2803 Darlington Driver - adafruit.com/products/970
- 1- Half-size breadboard- adafruit.com/products/64
- 12- Male-male jumpers- adafruit.com/products/1956
- At least two should be 6", the rest can be 3".
- 1- Micro servo- adafruit.com/products/169
- 1- Male pin header- digikey.com/short/t93cbd
- 1- 2 x AA Holder- digikey.com/short/tz5bd1
- 1 -3 x AA Holder- digikey.com/short/t5nw1c
- 1 -470 uF 25V capacitor - www.digikey.com/product-detail/en/ECA-1EM471/P5155-ND/245014
- 1 -SPDT slide switch - www.digikey.com/product-detail/en/EG1218/EG1903-ND/101726
- 1- USB micro cable http://amzn.com/B00OC6WR22
- 5 - AA Batteries
Hareware:
- 2- 1 7/8" ID x 1/8" O-ring- mcmaster.com/#9452K96
- 1- Caster 5/8" bearing- mcmaster.com/#96455k58/=yskbki
Install Caster

The caster can be pressed in by hand or by preheating the bracket for an easy fit. Then, screw in the caster holder to the plate.
Optional: Use washers to support plastic
Install Stepper Brackets


Using the same screws, install the stepper brackets on either side of the plate. Be careful to not over torque the screws as this part is delicate.
Install Stepper Motors


Using either bolts or screws with backing spacers, install the stepper motors. There is a small recess in the bracket to help align the stepper motors with their holes.
Pen Holder Bracket

Install the pen holder bracket with screws and washers. The "tongue" of the bracket should face away from the protruding holes on the plate. At this point in time, install the rubber O-ring onto the wheels and press the wheels onto the stepper motor shafts.
Audino

Install the Audino onto the protruding holes on the plate using the screws. Depending on the number of screws, be sure to add the backing spacers to prevent sharp screws from sticking out of the bottom of the plate. At this step also adhere the bread board onto the other side of the plate.
Stepper Motor

Install the servo motor onto the bracket. If using an aftermarket servo, use the backing spacers in order to adjust the fit. Then, install the servo motor arm.
Wiring


Install the wiring for the bot.
Quote from original post:
"
The microcontroller provides 5 volt signals to the darlington array which in turn, provides VCC to the stepper coils:
- Start with pin next to the ground pin on the darlington driver, and install orange, yellow, green, and blue wires in that order (Image 1).
- Attach the jumpers to the following arduino pins (Image 2):
- orange - Digital pin 4
- yellow - Digital pin 5
- green - Digital pin 6
- blue - Digital pin 7
- Back at the darlington, continue jumper for the other stepper in the reverse of the others:
- blue, green, yellow, and orange (Image 3).
- Attach the jumpers to the following arduino pins (Image 4):
- blue - Digital pin 9 (pin 8 used latter for the servo).
- green - Digital pin 10
- yellow - Digital pin 11
- orange - Digital pin 12
"
Matlab
%Jacob Lehrman
%University of Kansas, Dr. Wilson
%Last Updated: 3/27/25
%Desc: Script to load an image, convert it to lineart, and then translate
%that lineart into stepper motor steps (516.096 steps/rev)
%Functions and code to interpret images by Mathworks
clear
clf
clc
fignum = 1;%Value to make updating figures easier
%User-Defined Values
longest = 270;%Length of the longest side of the physical drawing space in mm
%Note: this assumes that your image will fit on the physical space!
indent = 10;%amount of distance the bot should move in and over when starting. Makes a margin on the left and top of the image. Units:mm.
wheelDiameter = 57.9247;%Diameter of the drive wheels in mm.
wheelBase = 108.0516;%Center to Center distance between the drive wheels. Pen should located at half of this distance.
stepperSteps = 516.096;%number of steps in 1 revolution of the stepper motor.
%Code to load the image
pic = imread("DarkstoneTemple1-6-23.png");%Your picture goes here
%pic = imcomplement(pic);%Comment this line out if your image is black lines on a white background
figure(fignum)
imshow(pic)
fignum = fignum + 1;
%Code to binarize and convert the image to paths
[picSegmentsPixel,xLimPixel,yLimPixel] = imageToPixelSegments(pic);
%Plot the segments to visualize whats going on.
figure(fignum)
clf
fignum = fignum + 1;
for ii = 1:length(picSegmentsPixel)
coords = picSegmentsPixel{ii};
plot(coords(:,2),coords(:,1),'-*')
hold on
end
hold off
axis equal ij
axis([0 size(pic,2) 0 size(pic,1)])
title('Paths to draw (pixels)')
%Code to optimize the paths
%Eliminate exceptionally short paths
shortPathLength = 5;
%Five is arbitraily chosen as the number to define a long enough path here to eliminate the shortest paths in our test image.
%You should pick a number that best suits your usecase, or comment this block out entirely.
ii = 1;
while ii <= length(picSegmentsPixel)
if size(picSegmentsPixel{ii},1) <= shortPathLength
picSegmentsPixel(ii) = [];
else
ii = ii + 1;
end
end
%Eliminate redundant points from paths
firstPassSegments = [];%empty array to hold thge segments for the first pass of optimization.
for ii = 1:length(picSegmentsPixel)
%Grab the segment's points
segment = picSegmentsPixel{ii};
%Compute an initial unit vector to compare against
%Note that since we are still computing pixelwise, we don't need to
%normalize our vectors as each pixel is within 1 unit of every other.
startIndex = 1; %Save this for later checking
Point1 = segment(1,:);
Point2 = segment(2,:);
baseVector = Point2 - Point1;
jj = 3;
while jj <= length(segment)
%code to check if the points can be replaced
%Compute the unit vector to the next point
Point1 = segment(jj-1,:);
Point2 = segment(jj,:);
vector = Point2 - Point1;
%If its in-line, it can be replaced
if (vector == baseVector) & (jj < length(segment))
jj = jj + 1;
elseif jj == (2 + startIndex) & (jj < length(segment))%if the part is 2-points long, just update it unless its the end of a segment
jj = 3 + startIndex;%Move jj's starting position up by the number of optimizations that have been done.
startIndex = startIndex + 1;%Increment the Starting Position
Point1 = segment(startIndex,:);%Recompute the base Vector
Point2 = segment(startIndex+1,:);
baseVector = Point2 - Point1;
else
%Remove unneeded points
if jj == length(segment)%Fixes a bug where the end of a segment would have an improperly placed extra point
jj = jj+1;
for k = startIndex:(jj-3)
segment(1+startIndex,:) = [];%Removes points between endpoints of a continuous direction
end
else
for k = startIndex:(jj-3)
segment(1+startIndex,:) = [];%Removes points between endpoints of a continuous direction
end
jj = 3 + startIndex;%Move jj's starting position up by the number of optimizations that have been done.
startIndex = startIndex + 1;%Increment the Starting Position
Point1 = segment(startIndex,:);%Recompute the base Vector
Point2 = segment(startIndex+1,:);
baseVector = Point2 - Point1;
end
end
end
firstPassSegments{ii} = segment;
end
%First Straight Line approx. Optimization pass. Checks for larger patterns to see
%if any of them can be reasonably replaced with straight lines
secondPassSegments = straightLineOptUnweighted(firstPassSegments,1.5,4);
%Redraw for testing
figure(fignum)
fignum = fignum + 1;
for ii = 1:length(secondPassSegments)
coords = secondPassSegments{ii};
plot(coords(:,2),coords(:,1),'-*')
hold on
end
hold off
axis equal ij
axis([0 size(pic,2) 0 size(pic,1)])
title('4 Point')
%Second Straight Line approx. Optimization pass. Refining previous
thirdPassSegments = straightLineOptUnweighted(secondPassSegments,1,3);
thirdPassSegments = straightLineOptUnweighted(thirdPassSegments,1,3);
%Redraw for testing
figure(fignum)
fignum = fignum + 1;
for ii = 1:length(thirdPassSegments)
coords = thirdPassSegments{ii};
plot(coords(:,2),coords(:,1),'-*')
hold on
end
hold off
axis equal ij
axis([0 size(pic,2) 0 size(pic,1)])
title('3 Point')
%Third Straight Line approx. Optimization pass. Refining previous
fourthPassSegments = straightLineOptUnweighted(thirdPassSegments,2,2);
for ii = 2:3
fourthPassSegments = straightLineOptUnweighted(fourthPassSegments,1,2);
end
%Redraw for testing
figure(fignum)
fignum = fignum + 1;
for ii = 1:length(fourthPassSegments)
coords = fourthPassSegments{ii};
plot(coords(:,2),coords(:,1),'-*')
hold on
%pause(.5)
end
hold off
axis equal ij
axis([0 size(pic,2) 0 size(pic,1)])
title('2 Point')
%Code to convert the paths into stepper motor instructions
%Step 1: make it so the bot goes to the next closest path each time. This is
%because angle errors will make for nasty pics
for ii = 1:length(fourthPassSegments)-1
Path1 = fourthPassSegments{ii};
dist = zeros(length(fourthPassSegments)-ii,1);%Preallocating
for jj = 1:length(fourthPassSegments)-ii
Path2 = fourthPassSegments{jj+ii};
dist(jj) = sqrt(sum((Path1(end,:)-Path2(1,:)).^2));
end
[~,Index] = min(dist);%find the index of the minimum distance
Path3 = fourthPassSegments{ii+1};%Temporary for moving values
Path4 = fourthPassSegments{Index+ii};
fourthPassSegments{ii+1} = [];%since Matlab does not support moving lists like variables, delete them first then reassign
fourthPassSegments{Index+ii} = [];
fourthPassSegments{ii+1} = Path4;%Move the closest path to be next
fourthPassSegments{Index+ii} = Path3;%Put the previous closest where the previous path was.
end
%Step 2: convert pixels to metric distances
longestPixel = max(xLimPixel(1,2), yLimPixel(1,2));
mmPerPixel = longest/longestPixel;%Obtain ratio of mm/pixel
for ii = 1:length(fourthPassSegments)
fourthPassSegments{ii} = (fourthPassSegments{ii}.*mmPerPixel)+indent;%Actually convert the paths
end
%Step 3: compute how far a 'step' is for the bot, and how much turning will
%occur
stepSize = (wheelDiameter*pi)/stepperSteps;%step size in mm
stepAngle = (2*stepSize)/(wheelBase);%angle turned when moving a single step tankwise. Measured in radians
%Step 4: Compute vector Paths to each point, and if the pen should be up or
%down. Save this as an array so it can be exported to arduino easily.
botPos = [0; 0];%Assume bot starts at 0,0 on grind.
botStartingVector = [1; 0];%assume bot is facing to the right. Has to be a collum due to math.
botVector = botStartingVector;
rotate = @(angle) [cos(angle) -sin(angle); sin(angle) cos(angle)];%Vector Rotation Matrix
angleBetween = @(v1,v2) acos(sum(v1.*v2)/(sqrt(sum(v1.^2))*sqrt(sum(v2.^2))));%Function to compute angle between vectors
xAxis = [1;0];%Vector for x-axis. Angles will be measured to here.
motorSteps = [];%Empty array to hold the motor steps
%Note: due to how the image is processed, it is mirrored over the x axis for
%some reason. this means that all y-coordinates will have to be inverted to
%plot correctly.
%motorSteps are saved in the order Pen, angle, forward.
%Angle steps should follow RHR, that is CCW rotation is Positive
angleBuffer = 0;%Buffers for round-offs of motorsteps.
lengthBuffer = 0;
for ii = 1:length(fourthPassSegments)
%Lead with calc from end to start of next
segment = fourthPassSegments{ii}';
holding = segment(1,:);%Swapping rows to make it [X; Y]
segment(1,:) = segment(2,:);
segment(2,:) = -holding;
start = segment(:,1)-botPos;%vector pointing to start of the new segment
angleToAxis = angleBetween(botVector,xAxis);%This should compute the angle between the bot and a hypothetical x axis.
if botVector(2) > 0
angleToAxis = -angleToAxis;
end
botVector = rotate(angleToAxis)*botVector;%Rotate all vectors to the imaginary horizontal axis.
start = rotate(angleToAxis)*start;
rotationAngle = angleBetween(botVector,start);%Computes the angle the bot needs to turn to get to the next point
angleSteps = rotationAngle/stepAngle;%Computes the number of rotation steps needed to get close enough to the angle.
if start(2) < 0
angleSteps = angleSteps*-1;
end
lengthSteps = sqrt(sum(start.^2))/stepSize;%Computes the number of steps needed to get to the next position.
botVector = segment(:,1)-botPos;%Update the direction the bot is facing.
botPos = segment(:,1);%Update the bots position to the next point
angleBuffer = angleBuffer + angleSteps - round(angleSteps);%Saves the total distance rounded off.
lengthBuffer = lengthBuffer + lengthSteps - round(lengthSteps);
if abs(round(angleBuffer)) >= 1
angleSteps = round(angleSteps) + round(angleBuffer);
angleBuffer = angleBuffer - round(angleBuffer);
else
angleSteps = round(angleSteps);
end
if abs(round(lengthBuffer)) >= 1
lengthSteps = round(lengthSteps) + round(lengthBuffer);
lengthBuffer = lengthBuffer - round(lengthBuffer);
else
lengthSteps = round(lengthSteps);
end
%Save Data
motorSteps(end+1) = 0;%Set value to lift the pen up
motorSteps(end+1) = angleSteps;%Save number of angle steps to rotate
motorSteps(end+1) = lengthSteps;%Save distance to travel after rotating
for jj = 2:length(segment)
%Now to trace the segment
start = segment(:,jj)-botPos;%vector pointing to next point
angleToAxis = angleBetween(botVector,xAxis);%This should compute the angle between the bot and a hypothetical x axis.
if botVector(2) > 0
angleToAxis = -angleToAxis;
end
botVector = rotate(angleToAxis)*botVector;%Rotate all vectors relative to the imaginary horizontal axis.
start = rotate(angleToAxis)*start;
rotationAngle = angleBetween(botVector,start);%Computes the angle the bot needs to turn to get to the next point
angleSteps = rotationAngle/stepAngle;%Computes the number of rotation steps needed to get close enough to the angle.
if start(2) < 0
angleSteps = angleSteps*-1;
end
lengthSteps = sqrt(sum(start.^2))/stepSize;%Computes the number of steps needed to get to the next position.
botVector = segment(:,jj)-botPos;%Update the direction the bot is facing.
botPos = segment(:,jj);%Update the bots position to the next point
%So apparently the round-off errors are big enough to make the image
%unviewable when drawn. time to correct them.
angleBuffer = angleBuffer + angleSteps - round(angleSteps);%Saves the total distance rounded off.
lengthBuffer = lengthBuffer + lengthSteps - round(lengthSteps);
if abs(round(angleBuffer)) >= 1
angleSteps = round(angleSteps) + round(angleBuffer);
angleBuffer = angleBuffer - round(angleBuffer);
else
angleSteps = round(angleSteps);
end
if abs(round(lengthBuffer)) >= 1
lengthSteps = round(lengthSteps) + round(lengthBuffer);
lengthBuffer = lengthBuffer - round(lengthBuffer);
else
lengthSteps = round(lengthSteps);
end
%Save Data
motorSteps(end+1) = 1;%Set value to set the pen down
motorSteps(end+1) = angleSteps;%Save number of angle steps to rotate
motorSteps(end+1) = lengthSteps;%Save distance to travel after rotating
end
end
%Plot the path the bot should follow
figure(fignum)
clf
fignum = fignum + 1;
plottingAngle = 0;
plottingPoint = [0 0];
for ii = 1:3:length(motorSteps)
plottingAngle = plottingAngle + (motorSteps(ii+1)*stepAngle);
ppOld = plottingPoint;%Save the old plotting point
plottingPoint = plottingPoint + ([cos(plottingAngle) sin(plottingAngle)].*stepSize*motorSteps(ii+2));
plotMatrix = [ppOld(2) ppOld(1); plottingPoint(2) plottingPoint(1)];
if motorSteps(ii) == 1
plot(plotMatrix(:,2),plotMatrix(:,1),'-*')
hold on
pause(.05)
end
end
hold off
axis([indent xLimPixel(2)*mmPerPixel+indent -(yLimPixel(2)*mmPerPixel+indent) -indent])
title('Predicted Bot Path')
%Save bot path in text format for copying to Arduino
writematrix(motorSteps)
Matlab 2
function passSegmentsUn = straightLineOptUnweighted(segmentsPix,offLim,optLevel)
%Jacob Lehrman
%Last Updated: 2/23/25
%Desc: Function to optimize paths based on a given number of points
for ii = 1:length(segmentsPix)
%Grab the segment's points
segment = segmentsPix{ii};
if length(segment) > optLevel
startIndex = 1; %Save this for later checking
%Compute an initial unit vector to compare against
unitVector = unitVectorUnweight(segment,startIndex,optLevel);
jj = optLevel+startIndex;%Set the starting position
while jj <= length(segment)
%compute the vector to the next point and its magnitude
vector = segment(jj,:) - segment(startIndex,:);
magVec = sqrt(sum(vector.^2));
%compute the angle between it and the unit vector
%Don't have to have unit vectors magnitude on bottom as it is 1 by definition
theta = acos(sum(vector.*unitVector)/magVec);
%Compute how far away from the unit vector line the point is
offset = magVec*sin(theta);
if offset < offLim && jj < length(segment)
jj = jj + 1;
elseif jj == (optLevel + startIndex)
%If the very next point is not in line, don't change
%the data, then move up a point
startIndex = startIndex + 1;
jj = jj + 1;
%recompute unit vector
unitVector = unitVectorUnweight(segment,startIndex,optLevel);
else
for k = startIndex:(jj-3)
segment(1+startIndex,:) = [];%Removes points between endpoints of a continuous direction
end
startIndex = startIndex + 1;%Increment the Starting Position
jj = optLevel + startIndex;%Move jj's starting position up by the number of optimizations that have been done.
if jj <= length(segment)
%recompute the unit vector if there are enough points
unitVector = unitVectorUnweight(segment,startIndex,optLevel);
end
end
end
end
passSegmentsUn{ii} = segment;
end
Matlab 3
function unweightedUnitVector = unitVectorUnweight(segment,start,optLevel)
%This function should only be called after checking that the segment is
%long enough
%use 4 points to make sure that there is a pattern
Points = segment(start:start+optLevel-1,:);
%Compute vector(s), measured from point 1
Vec = [Points(2:optLevel,:)] - Points(1,:);
%Magnitudes of each vector for weighting
Mag = sqrt(sum(Vec.^2,2));
%Unit Vectors in each direction
uVec = Vec./Mag;
%Compute the final unit vector
if optLevel > 2
unweightedUnitVector = sum(uVec)./length(Vec);
else
unweightedUnitVector = uVec;
end
end
Enjoy!!!
