Writing Robot v3
Version 2 of the writing robot was not very accurate. Let's see whether we can use the gyro sensor to make more precise turns. I'm always in favor of focusing on robotics applications that make use of the sensors because this better demonstrates the 'intelligence' of robots - writing an application that uses only motors won't teach you anything more than writing a program to move the Small Basic turtle or a Scratch sprite, so why bother using a robot?
Another excellent reason to use the gyro sensor to control the turns is that the code will then work for robots with differing wheel diameters and different wheel spacing, unlike the WriterBot code I have used so far.
We could get inspiration from Damien Kee's Spirograph robot, which uses the gyro sensor:
I decided to attach the sensor as shown here (there could be an argument for attaching the sensor further away from the brick to avoid interference). The sensor is attached to port 2, as per the standard convention.
First I decided to use the gyro only to control the turns but this gave me disappointing results. Then I realised that I could also use the sensor while the robot is making its straight line motions, to make sure that these motions are in the correct direction and that the direction is stable. The key code that calculates the error in the robot's straight line direction and then uses the error to adjust the direction is highlighted in green below. Since this code is running continuously inside a While loop while the robot makes its straight line motion it is necessary to use Motor.Count() to determine when the robot should terminate its straight line motion. Putting together all these wonderful ideas, I was able to get the robot to draw 'BASIC' as follows:
This is disappointing, to say the least! The gyro is failing to measure the angles correctly, even though I was careful to make sure that the sensor was perfectly still when the robot was powered up, so that the reading would not wander. Look at the 'vertical' line that forms the left edge of the letter 'B' above and the 'vertical' line that forms the central line of the letter 'I'. The sensor thinks that the robot was oriented in the same direction when these two lines were drawn - it thinks these two lines are parallel!
So I did some testing of the sensor (details HERE) and determined that my sensor had a bias - it was measuring counter-clockwise turns fairly accurately but was underestimating clockwise turn angles by about 2-3%. In other words when the sensor thought the robot had turned 100° it would have turned 102° in reality. The difference may seem small but after many turns these errors accumulate, which is why towards the end of the 'BASIC' drawing above the robot has accumulated a large clockwise deviation from the intended path. So in the code below I have included a line (highlighted in orange) that compensates for the 2% error in the sensor's estimation of clockwise rotations. It's likely that this correction will vary from sensor to sensor and even from session to session, so expect to have to do some work to find the value that works for you (for the current session!). Don't forget that smaller values will tend to make the text curve up as the text is drawn, to compensate for the 'drooping' of the text as shown above.
It may be that your sensor does not have the bias that mine has, but if you want to do serious work with the gyro sensor I suggest you do some tests of your own to make sure.
If you look at the 'S' in the above drawing you will notice another problem: the top of the S is thinner than the bottom of the S. I came to the conclusion that this was to do with the way I coded the use of the Motor.Count() function. This function returns the angle of a single motor. I had initially assumed that the displacement in straight line could terminate when either motor had turned through the desired angle since both motors should turn through the same angle when the robot moves in a straight line. But in fact since the robot is compensating for orientation errors the line is not quite straight and therefore it is necessary to verify that both motors have turned through the desired angle, as I have done in the lines highlighted in blue below.
Here below is the modified version of the program, ready for you to copy and paste it into Small Basic so you can try it yourself (and make improvements!). I have highlighted the parts that are new, relative to the previous version that did not use the gyro sensor. I have also modified the code to add two new characters ('!', '?' and '$') to the range of characters that the robot can draw (the '$' currently does nothing - I intend to use it later to draw a smiley...).
Don't forget that when you are using the gyro sensor it is vitally important that the robot be absolutely still when the robot is turned on, otherwise the sensor reading will wander even when the robot is stationary. You can check whether the reading is wandering using 'port view' in the brick's menus - if you see that the reading is wandering then disconnect and reconnect the sensor while making sure the robot is absolutely still. Don't forget to close port view before you run the EV3 Basic program because EV3 Basic programs cannot run when any kind of program or dialog box is already running or open on the brick.
'WriterBot v3 by Nigel Ward
'See the 'Going Further' section of EV3Basic.com
'attach gyro sensor to port 2
Sensor.SetMode(2,0) 'measure angle
'*******************NUMBER ENTRY CODE STARTS HERE******************
height=10 'desired character height in cm
LCD.Clear()
LCD.Text(1,0, 0,2,"Use left &")
LCD.Text(1,0, 20,2,"right keys")
LCD.Text(1,0, 40,2,"to select")
LCD.Text(1,0, 60,2,"the text")
LCD.Text(1,0, 80,2,"height (cm)")
LCD.Text(1,70,100,2,height)
Button="R" 'must assign a value to a variable before using it
While Button<>"E"
Buttons.Wait()
Button=Buttons.GetClicks()
If Button="L" And height > 5 Then
height=height-1
ElseIf Button="R" and height <20 then 'max height = 20cm
height=height+1
EndIf
LCD.Text(1,70,100,2,height+" ") 'think about why I added a space
EndWhile
LCD.Clear()
LCD.Text(1,0, 0,2,"Next enter")
LCD.Text(1,0,20,2,"the desired")
LCD.Text(1,0,40,2,"text.")
LCD.Text(1,0,60,2,"Press any")
LCD.Text(1,0,80,2,"button to")
LCD.Text(1,0,100,2,"continue.")
Buttons.Wait()
'Buttons.Flush () 'get rid of button input
Button=Buttons.GetClicks() ' flush does not make click sound
scale=height 'for compatibility with robot drawing code
'*******************TEXT ENTRY CODE STARTS HERE******************
'Allows a string of up to 11 characters to be typed
'This program is compatible with brick mode (it can be compiled to brick)
'Each character has dimensions 16x22 on screen
'Columns are numbered 1-11
mystring=""
LCD.Clear()
LCD.Text(1,0,3,1," <=Backspace >=Enter")
line[1]="0123456789."
line[2]="ABCDEFGHIJK"
line[3]="LMNOPQRSTUV"
line[4]="WXYZ -!?$<>"
LCD.Text(1,0,16,2,line[1])
LCD.Text(1,0,38,2,line[2])
LCD.Text(1,0,60,2,line[3])
LCD.Text(1,0,82,2,line[4])
LCD.Rect(1,0,103,178,25) 'draw rect around typing area
col=6 'current column
row=2 'current row
x=81 'x coordinate of highlighted char
y=35 'y coordinate of highlighted char
LCD.InverseRect(x,y,16,22)
loop="True" 'loop control variable
While loop
Buttons.Wait()
Button=Buttons.GetClicks()
LCD.InverseRect(x,y,16,22) 'clear inverse
If Button="R" Then
col=1+Math.Remainder(col,11) 'allow jump from col 11 to col 1
ElseIf Button="L" Then
col=11-Math.Remainder(12-col,11) 'allow jump from col 1 to col 11
ElseIf Button="U" Then
row=4-Math.Remainder(5-row,4) 'allow jump from row 4 to row 1
ElseIf Button="D" Then
row=1+Math.Remainder(row,4) 'allow jump from row 1 to row 4
ElseIf Button="E" Then
If row=4 and col>9 Then
If col =10 then 'backspace
If mystring<>"" Then
'remove last character from mystring
mystring=Text.GetSubText(mystring,1,text.GetLength(mystring)-1)
Else 'nothing to delete
Speaker.Tone(100,1000,1000)
endif
ElseIf col = 11 Then 'Enter
Loop="False" ' do not repeat loop
EndIf
Else 'middle button was pressed over a normal character
If text.GetLength(mystring)<>11 Then
mystring=mystring+Text.GetSubText(line[row],col,1)
Else 'already have the maximum 11 characters
Speaker.Tone(100,1000,1000)
EndIf
EndIf
EndIf
x=-15+col*16 'calculate x coordinate of character to be highlighted
y=-9+row*22 'calculate y coordinate of character to be highlighted
LCD.InverseRect(x,y,16,22)
LCD.text(1,1,107,2," ") 'clear text
LCD.text(1,1,107,2,mystring)
EndWhile
LCD.Clear()
LCD.text(1,0,0,2,"Your text:")
LCD.text(1,0,20,2,mystring)
LCD.text(1,0,40,2,"Press Enter")
LCD.text(1,0,60,2,"when the")
LCD.text(1,0,80,2,"robot is")
LCD.text(1,0,100,2,"ready...")
Buttons.Wait()
Button=Buttons.GetClicks() ' just to get the 'click'
Program.Delay(2000)
'*******************ROBOT WRITER CODE STARTS HERE******************
ports="BC"
penport="A"
s = scale * 0.054 'adjust scale factor
sf = 1 'speed factor 0.6-3 (recommended value =1)
angle=Sensor.ReadRawValue(2, 0) 'angle is total angle turned by robot
'must assign values to variables before they are used...
movesign=0 '1 means forwards, -1 means backwards
turnsign=0 '1 means clockwise, -1 means ccw
turnangle=0
d=0 'distance to move (in mm if scale = 1)
Sub Move
startb=Motor.GetCount ("B")
startc=Motor.GetCount ("C")
If movesign>0 Then 'moving forwards
While (Motor.GetCount ("B")< startb+(d*s)) And (Motor.GetCount ("C")< startc+(d*s))
Angle_error=Sensor.ReadRawValue(2, 0)-angle 'Gyro on port 2
Motor.StartSync(ports, sf*(10-angle_error), sf*(10+angle_error))
EndWhile
Else
While (Motor.GetCount ("B")> startb-(d*s)) And (Motor.GetCount ("C")> startc-(d*s))
Angle_error=Sensor.ReadRawValue(2, 0)-angle
Motor.StartSync(ports, sf*(-10-angle_error), sf*(-10+angle_error))
EndWhile
EndIf
Motor.Stop(ports,"True")
EndSub
Sub Turn
Motor.StartSync(ports,turnsign*10*sf,turnsign*-10*sf)
If turnsign=1 Then 'turn clockwise
turnangle=turnangle*0.975 'compensate for gyro sensor bias
While Sensor.ReadRawValue(2, 0) < angle+turnangle
EndWhile
angle=angle+turnangle
Else 'turn counter-clockwise
While Sensor.ReadRawValue(2, 0) > angle-turnangle
EndWhile
angle=angle-turnangle
EndIf
Motor.Stop(ports,"True")
EndSub
'For compatibility with the EV3 Explorer compiler,
'use array indices that are the Unicode
'code numbers of the corresponding characters.
'For example, the code number for "A" is 65
sequence[48]= "0A 1P 2c cA Cc 6c Ca 5p 1C 10" '0
sequence[49]= "0A 30 0c 0P cc 0a 4p 0A 5P 2p" '1
sequence[50]= "0A 3c 0P ca ca dC 1A 2p" '2
sequence[51]= "0A 3c 0P ca ca cA cA Ca Cp cC 10" '3
sequence[52]= "1A 0P 2p 2A 0H 0P gG 0A 2p 0a 1A" '4
sequence[53]= "0A 1C 0P Ca cA cC 1a 2a 2p 0a 4A" '5
sequence[54]= "0N mM 0P Qr 20 Qs 1p 0A 0C dC" '6
sequence[55]= "1K 0P jk 6p 0e fE" '7
sequence[56]= "1C 0P cA da ca ca dA cp 0C 10" '8
sequence[57]= "0a 5C 0P cA cC 2C ca Ca cC 1p 0a 2A" '9
sequence[65]= "0A 0P 2p 0a 2P 6F ef 0f ee 2p 0A" 'A
sequence[66]= "0A 2a 0P 1C cA cC 1A 4a 5C Ca cp Cc 5a" 'B
sequence[67]= "2A 1c 0P CA cc 2c ca cp 0c 3A" 'C
sequence[68]= "0P 1C cC 2C cC 1a 8p 0a 20" 'D
sequence[69]= "0A 2a 0P 1p 0F ef 0P 6a 4A 2p" 'E
sequence[70]= "0A 2a 0P 1F 0p ef 0P 6a 4p 0A 20" 'F
sequence[71]= "0C cP 0c 1C CA cc 2c ca cp 0c 3A" 'G
sequence[72]= "0A 2a 0P 2p 0C DC 0P 4p 0a 2a 0P 4p 0A" 'H
sequence[73]= "0A 4a 0P 2p 5a 0P 4p 0A 5P 2p" 'I
sequence[74]= "0A 4a 0P 2a 3c cA Cp cC 10" 'J
sequence[75]= "0A 0P 4p 0a 2C 0P Da dp 0C" 'K
sequence[76]= "0A 4P 8a 2p" 'L
sequence[77]= "0A 0P 4C Ca cC 8p 0a" 'M
sequence[78]= "0A 0P 4E Fe 4p 8a" 'N
sequence[79]= "1C 0P cC 2C cA cC 2C cp 0C 10" 'O
sequence[80]= "0A 0P 4a 1c cA Cc 5p 0c dC" 'P
sequence[81]= "0a 5C 0P cA cC 2C ca 5C 6p 0a 1c 0P cp 0C" 'Q
sequence[82]= "0A 0P 4a 1c cA Cc 5c dp 0C" 'R
sequence[83]= "0a 5C 0P cA cA da ca cp 0c 3A" 'S
sequence[84]= "0A 4a 0P 20 5a 4p 0A 10" 'T
sequence[85]= "0a 8P 3C cA cC 3p 7P 5p 0a" 'U
sequence[86]= "0A 4J 0P Jj 0j jp 0J 8a" 'V
sequence[87]= "0A 4P 8e eE 0E Ee 4p 8a" 'W
sequence[88]= "0F 0P fp 0f 6f 0P fp 0F" 'X
sequence[89]= "1A 0P 2E ep Ee 0e 0P ep 0E 8a" 'Y
sequence[90]= "0A 4a 0P 2F Ff 2p" 'Z
sequence[32]= "20" 'space
sequence[45]= "0A 1a 0P 2p 0a 2A" 'hyphen
sequence[46]= "0P 0b 0b 0p 0B 0B 10" 'period
sequence[33]= "0K jJ 0P 7p 5P 0a 0p 10" ' !
sequence[63]= "0A 3P qs 0A 1p 1P 0A 0p 10" ' ?
sequence[36]= "0P 40 8p" '$ for testing only
For j=1 To Text.GetLength(mystring)
char=text.GetSubText(mystring,j,1) 'extracts jth character from mystring
unicode_for_char=text.GetCharacterCode(char)
For n=1 to (1+Text.GetLength(sequence[unicode_for_char]))/3
movechar=text.GetSubText(sequence[unicode_for_char],n*3-2,1)
move_unicode=Text.GetCharacterCode(movechar)
If move_unicode>64 And move_unicode<91 Then 'capital letter
movesign=-1
movechar=text.ConvertToLowerCase(movechar)
Else
movesign=1
EndIf
turnchar=text.GetSubText(sequence[unicode_for_char],n*3-1,1)
turn_unicode=Text.GetCharacterCode(turnchar)
If turn_unicode>64 And turn_unicode<91 Then 'capital letter
turnsign=-1
turnchar=text.ConvertToLowerCase(turnchar)
Else
turnsign=1
EndIf
If movechar="1" Then
d=100
Move()
ElseIf movechar="2" Then
d=200
Move()
ElseIf movechar="3" Then
d=300
Move()
ElseIf movechar="4" Then
d=400
Move()
ElseIf movechar="5" Then
d=100
movesign=-1 'go backwards
Move()
ElseIf movechar="6" Then
d=200
movesign=-1
Move()
ElseIf movechar="7" Then
d=300
movesign=-1
Move()
ElseIf movechar="8" Then
d=400
movesign=-1
Move()
ElseIf movechar="c" Then 'move sqr(2)*100*s
d=141.4 'sqr(2)
Move()
ElseIf movechar="d" Then
d=282.8 'sqr(8)
Move()
ElseIf movechar="e" Then
d=223.6 'sqr(5)
Move()
ElseIf movechar="f" Then
d=447.2 'sqr(20)
Move()
ElseIf movechar="g" Then
d=316.2 'sqr(10)
Move()
ElseIf movechar="j" Then
d=412.3 'sqr(17)
Move()
ElseIf movechar="m" Then
d=360.6 'sqr(13)
Move()
EndIf
If turnchar="a" Then
turnangle=90
Turn()
ElseIf turnchar="b" Then
turnangle=180
Turn()
ElseIf turnchar="c" Then
turnangle=45
Turn()
ElseIf turnchar="e" Then
turnangle=26.565
Turn()
ElseIf turnchar="f" Then
turnangle=63.434
Turn()
ElseIf turnchar="g" Then
turnangle=18.435
Turn()
ElseIf turnchar="h" Then
turnangle=71.565
Turn()
ElseIf turnchar="j" Then
turnangle=14.036
Turn()
ElseIf turnchar="k" Then
turnangle=75.963
Turn()
ElseIf turnchar="m" Then
turnangle=33.69
Turn()
ElseIf turnchar="n" Then
turnangle=56.31
Turn()
ElseIf turnchar="p" Then
Motor.Move(penport,-20*turnsign,200,"True") 'lowers pen if turnsign positive
EndIf
EndFor
Motor.Move(ports,10*sf,85*s,"True") 'space between characters
EndFor
Speaker.Tone(30,1000,300)
Speaker.Wait()
Speaker.Tone(30,1500,300)
Speaker.Wait()
After adjusting by trial and error the sensor bias compensation value in the line above that is highlighted in orange, I was able to obtain the result below:
So, does the robot work better now that we are using the gyro sensor to control the turn angles rather than simply telling the motors to turn through specific angles? I think so. It's not perfect (and never will be) - why is the final 'C' rather small for example? But it's good enough to show that the gyro sensor can be used to good effect. Here for comparison is the version made with the previous code that did not use the gyro sensor:
So was this WriterBot project worth the many hours (of which a significant fraction can only be described as frustration) that I spent developing this program? Maybe, for I feel I have learnt new things about the gyro sensor and about error feedback, as well as having the satisfaction of having made an EV3 program with a unique combination of features that gives a satisfactory (and satisfying) result. And I like to think it might help other people. People like you!