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!