Tutorial 2 - Converting Arachnid tasks to Whisker

Top  Previous  Next

Source code for this project is supplied in the Tutorials folder (by default, within \Program Files\WhiskerControl).

 

Requirements

 

Computers:

Acorn RiscOS machine.
Windows PC, with Microsoft Word, Visual Basic and the Whisker SDK installed.

 

Know-how:

A basic familiarity of opening and saving files in both Windows and RiscOS (opening files in Applications, etc.).
Basics of VB, including using the SDK Control (see Tutorial 1).

 

 

Overview

 

A plan to show how one can go about transferring a BASIC file in !Arachnid to a Whisker task in VB, by example. The first two steps will be required for anyone wishing to convert their programs.

 

The subsequent steps show how to approach the problem for a specific task, which should be helpful to those wishing to convert their own software.

 

 

Step 1: Save the !Arachnid program to a DOS 1.44MB floppy disk in TEXT format.

 

Most disks are already in DOS format – any disk that the Windows PC reads will do.

 

Using the Acorn computer

If the file is a BASIC file, open it in !Edit (shift-double-click). Set the file type to TEXT (middle button to get the menu, miscfile type).

Save the file to the floppy disk (as e.g. 0:mytask).

 

 

Step 2: Open the text file on the Windows machine

 

Copy the file from the floppy to a sensible place on your computer, e.g. a folder called 'My Whisker Tasks'.

 

Run Microsoft Word. Select Open (Ctrl-O), and select the "mytask" file.

 

Microsoft Word should show a listing, with line breaks in the right place, etc.

 

Note: I recommend Word because it will deal with the Acorn line-break symbols. Other text editors might complain, or try to put the whole program on one line of text.

 

Save this file. You could print the file to look at while you work, to stop jumping between windows.

 

For this example, we will use a simple task "Licker" (used by Angela Roberts's lab in Cambridge). The code for this can be seen in full in the appendix of this document.

 

It is not without its flaws as a BASIC program; many people would object to the use of GOTOs; the structure is poorly commented, and some bits have been REMed out, and other bits seem to be a hang-over from a previous version.

 

This program was chosen for the tutorial partly because it was not perfect to begin with (and, to be honest, because it is short, and I was converting it anyway!). A tutorial which translated a 'perfect' BASIC program would not be that useful however - most of the programs that need converting will themselves have some idiosyncracies.

 

 

Step 3: Examine code structure

 

Assuming that the !Arachnid task has been written fairly well, you should be able to look at the code and divide it into bits: general structure will usually be a main section of the code -  concerned with initialisation, getting settings, etc., then starting the task, PROCwait-ing, and then dealing with the data, some subroutines, and service functions. It's best to deal with each bit separately – some bits will translate very simply into VB, and others may require substantial reworking.

 

Note: I would not recommend 'cutting and pasting' !Arachnid code into VB, and then trying to remove the problems. The chances of obtaining a good Whisker Task at the end are almost zero!

 

What follows is my overview of the licker task, divided into bits.

 

Lines 10 – 40 Initialisation (freeing lines, zeroing variables)

       

  10 REM LICKER 2

  12 PROCinit:PROCkill_all:MODE0: BREAK=0: SESSION%=0

  15 ON ERROR IF ERR=17 RUN ELSE REPORT: PRINT" at line"; ERL: PROCbreak: END

  30 @%=&20107

  35 PROCfree_switch(0,E%):PROCfree_switch(6,E%)

  40 FOR Z%=1 TO 5:PROCgovn_switch(Z%,E%):NEXT

 

Lines 42 – 49: Getting Session Settings

 

  42 PRINT TAB(2,2)"LICKING";

  43 PRINT TAB(2,4)"MONKEY NAME";:INPUT TAB(36,4),MONKEY$

  44 PRINT TAB(2,6)"SESSION LENGTH";:INPUT TAB(36,6),A%:A%=A%*6000

  45 IF A%=0 GOTO 44

  46 PRINT TAB(2,8)"FREE JUICE";:INPUT TAB(36,8),B$

  47 IF B$="Y" OR B$="N" GOTO 48 ELSE 46

  48 PRINT TAB(2,10)"FRONT (0) OR BACK (6)"::INPUT TAB(36,10),LOC%

  49 IF LOC%=6 PUMP%=4 ELSE PUMP%=2

 

Lines 60 – 96 Starting the program

 

  60 PROCpipe_timer(6,A%,0,"FNend(",0,E%)

  61 CLS:PRINT TAB(2,3)"NOS OF LICKS";

  62 PROCswitch_on(PUMP%,E%)

  65 PROCpipe_switch(LOC%,On,1,"FNfree(",0,E%)

  90 PROCwait(E%):*AE

  92 IF C$="R" RUN

  94 IF C$="M" CHAIN ":0.MENU"

  96 END

 

Service functions

 

 100 DEFFNfree(P%,R%)

 105  IF R%=0 =0

 110  IF B$="Y":PROCswitch_on(PUMP%,E%):X=X+1:PRINT TAB(15,3)X;

 115  IF B$="N" GOTO 300

 120 =0

 130 DEFFNend(P%,R%)

 140  IF R%=0 =0

 145  PROCkill_all

 150  PROCswitch_off(PUMP%,E%):PROCswitch_off(1,E%)

 160  CLS:PRINT TAB(2,1)"LICKING"

 170  PRINT TAB(2,3)"MONKEY";:PRINT TAB(30,3)MONKEY$

 180  PRINT TAB(2,5)"TOTAL NOS OF LICKS";:PRINT TAB(30,5)X: IF BREAK=1 GOTO 480

 190  PRINT TAB(2,18)"PRESS 'R' WHEN READY TO CONTINUE";:INPUT TAB(36,18)C$

 200  IF C$<>"R" GOTO 190

 210  PRINT TAB(2,20)"RE-RUN 'R' RETURN TO MENU 'M' OR EXIT'E'";:INPUT TAB(45,20)C$

 220  IF C$="M" GOTO 260

 230  IF C$="R" GOTO 260

 240  IF C$="E" GOTO 260

 250  GOTO 210

 260 =0

 300  PROCkill_switch(LOC%,E%)

 310  PROCswitch_off(PUMP%,E%)

330PROCpipe_switch(LOC%,On,1,"FNban(",0,E%)

340 =0

 360 DEFFNban(P%,R%)

 370  IF R%=0 =0

 380  PROCswitch_on(PUMP%,E%):X=X+1:PRINT TAB(15,3)X;

 385  PROCpipe_timer(2,100,0,"FNbanoff(",0,E%)

 390 =0

 410 DEFFNbanoff(P%,R%)

 420  IF R%=0 =0

 430  PROCswitch_off(PUMP%,E%)

 440 =0

 

Error & Exception handling

 

 450  DEFPROCbreak

 460  BREAK=1

 470  GOTO 150

 480  SESSION%=FNtimer(6,E%)

 490  PRINT TAB(2,7)"SESSION LENGTH";:PRINT TAB(30,7)((A%-SESSION%)/6000)

 500 ENDPROC

 

 

Step 4. Set up a VB project and initialisation code

 

This is a nice simple task, so it is suitable to put all the code into a single form. Set up a new project in VB, for a standard Exe. Set the form's caption to be something sensible ("Licker for Whisker"),and its Border size to be 'Fixed Single'.  Put a SDK control on the form (call it Whisker). Open the code editor and put 'Option Explicit' at the top of the declarations.

 

Converting the initialisation:

       

  10 REM LICKER 2

  12 PROCinit:PROCkill_all:MODE0: BREAK=0: SESSION%=0

  15 ON ERROR IF ERR=17 RUN ELSE REPORT: PRINT" at line"; ERL: PROCbreak: END

  30 @%=&20107

  35 PROCfree_switch(0,E%):PROCfree_switch(6,E%)

  40 FOR Z%=1 TO 5:PROCgovn_switch(Z%,E%):NEXT

 

Error handling is different in VB (but less necessary because of the compiler and debugger) – we'll come back to this in Step 8. Neither MODE nor @%  are needed. Line numbers are similarly unnecessary.

 

VB & Whisker handle things slightly differently to !Arachnid. Initialisation now means connecting to the server: So PROCinit: PROCkill_all becomes Whisker.ConnectToServer.

 

The server can also use names, as well as numbers, to refer to lines. (This is most useful, as it means that only the SERVER has to worry about any re-wiring. You don't have to rewrite all your clients!).  So instead of PROCgovn_switch / PROCfree_switch we just use Whisker.ClaimLine, specifying input or output with the fourth parameter using one of the two constants, wsInput or wsOutput [Note: the server knows which lines are input, and which are output in its settings, and will send an error message if it doesn't match!].

 

Because Whisker uses names for devices, FrontPump, Houselight, etc. we should declare string constants that contain the names of the devices we are going to use. Put the following at the top of the program (declarations section).

 

Option Explicit

 

' LICKER 2 conversion to VB

 

'Whisker device names (defined by the server)

'Put these as constants, rather than typing them in every time.

'That way the compiler will notice if we misspell one of them.

 

'Group name

Const Box = "Box1"

 

'Input Devices

Const FrontLicker = "FrontLicker"

Const RearLicker = "RearLicker"

 

'Output Devices

Const FrontPump = "FrontPump"

Const RearPump = "RearPump"

Const Houselight = "Houselight"

 

Where do we want to put the Whisker initialisation? We're in VB, so we don't just have a first line to put it on. We could put it in Form_Load, so it goes as soon as we start, but that might be but it might make more sense to do it when the user is ready to go – i.e. give them a button to click to start. Add a button to the form, call it cmdGo, and set the caption to Go. Double click on the button to jump to it's _click handler function.

 

Private Sub cmdGo_Click()

'Initialise

  If Not (Whisker.ConnectToServer) Then

      Call Break("Could not connect to server!")

  End If

  'Claim all lines for box

  Call Whisker.ClaimGroup(Box, "")

  'Check output lines

  Call Whisker.LineClaim(Box, FrontPump,  FrontPump, wsOutput, wsResetOff)

  Call Whisker.LineClaim(Box, RearPump,  RearPump, wsOutput, wsResetOff)

  Call Whisker.LineClaim(Box, Houselight,  Houselight, wsOutput, wsResetOff)

  'Check input lines

  Call Whisker.LineClaim(Box, FrontLicker,  FrontLicker, wsInput, wsResetInput)

  Call Whisker.LineClaim(Box, RearLicker,  RearLicker, wsInput, wsResetInput)

End Sub

 

The initialisation code in Whisker is a little longer, but seemingly clearer. The only differences are that we now check for whether we find the server (not needed in !Arachnid), the ClaimGroup function, and  the addition of comments.

 

I've put in 'Call Break("Could not connect to server!") - later on we will make our version of the break subroutine able to report problems to the user.

 

ClaimGroup requests control of all lines that go to a single box. This is handy because it gives us control over devices (say a second tone generator that is added after we wrote the program, that could affect us – even if we don't need them – and thus stops other programs from controlling them by accident.

 

ClaimLine is, as we noted above, similar to PROCgovn / PROCfree. The names are repeated because I have chosen to use the same name as the server's names – however, we need not do this. We could use a different name in the second parameter, which might (for example) represent the line's use. In this way we can give two or more output lines the same name – and control them as if they were a single output line. The final parameter is a Reset parameter, which governs how the server behaves to output lines when the client ends: you may wish the houselight to remain on after the client has finished, for example. This value is ignored for input lines (for clarity, we can pass 'wsResetInputLine').

 

Comments are added because they are a Good Thing. Unlike in BBC Basic they don't make it into the final program, so they have no cost whatsoever. Use them.

 

There's one more thing that we could do while we consider initialisation. What if the user wants to change the server name, or the connection port for any reason, or the name that the client reports to whisker? It's unlikely they will, in this instance, but it is possible. I've added it here as a demonstration because it's easy to do.

 

The obvious solution is to use the Whisker.ChangeSettings method (see Tutorial 1). Put another command button on the form, with a caption "Whisker Settings…". Inside it's click_handler, put the command:

 

Call Whisker.ChangeSettings

 

 

Step 5: Getting information from the user

 

In !Arachnid:

 

  42 PRINT TAB(2,2)"LICKING";

  43 PRINT TAB(2,4)"MONKEY NAME";:INPUT TAB(36,4),MONKEY$

  44 PRINT TAB(2,6)"SESSION LENGTH";:INPUT TAB(36,6),A%:A%=A%*6000

  45 IF A%=0 GOTO 44

  46 PRINT TAB(2,8)"FREE JUICE";:INPUT TAB(36,8),B$

  47 IF B$="Y" OR B$="N" GOTO 48 ELSE 46

  48 PRINT TAB(2,10)"FRONT (0) OR BACK (6)"::INPUT TAB(36,10),LOC%

  49 IF LOC%=6 PUMP%=4 ELSE PUMP%=2

 

This code gets from the user a string, a number, a choice and a yes/no option. [The 'monkey name' is never actually used, but let's not worry about that.]  All of this is done using simple keyboard input at the prompt. This is not the way VB works – VB makes pure Windows programs, and they don't have command line prompts in the same way.

 

So – what do we do? The answer is that we put controls (textboxes, checkboxes,  and options) on our form, and let the user fill them in when they run the task. Open the form designer (click on View Object button), and add two options (for front and rear), a checkbox (for free juice Y/N), and two edit boxes (for monkey name & session length). Put labels by the edit boxes to confirm their use, and fill in the captions for the options and checkbox.

 

The layout I chose is shown below.

 

Tutorial2_Licker_UI

 

Give them all sensible names: txtName, txtLength, optFront, optRear, and chkJuice. In order to make sure that txtName and txtLength are labelled 'the right way round', it's a good idea to put the name of value in a each control's .Text property, so that it is shown inside the boxes while you are laying the controls out.

 

Note: I use the standard naming convention of a 3 letter prefix for controls showing their type. It is a good idea to use this system, and get used to, as most other VB programmers use it, so you will find their code is easy to read if you do it too!

 

Now – when the program starts we can read the options that the user has chosen from the controls, rather than from the command line. The options are a choice between 2, a yes/no, a string, and a number (which we'll want to convert to milliseconds). We need some variables to hold these settings, so we have to declare them.

 

Put the following into the declarations section at the top of the module (below the const declarations we put in earlier:

 

'Settings from form

Private bFreeJuice As Boolean

Private bUseFront As Boolean

Private strMonkey As String

Private lSessionLength As Long

 

Private strLicker As String

Private strPump As String

 

What this code does is 'create' the variables that will hold our settings: Three are Strings (familiar), one is a Long (this is just a kind of integer [such as I% in BBC BASIC]), and two are Boolean variables. Booleans are variables that can only hold 2 values (True or False). This may sound like a very limited variable, but actually they are very useful! We'll use the Licker and Pump variables to hold the name of whichever set (front or rear) that we are actually using.

 

This syntax, and the whole notion of declaring variables, may be unfamiliar to you if you are used to BBC BASIC. Leave it for now, and I'll explain all after we finish adding code…

 

Put the following into the cmdGo_click subroutine (above the initialisation code we wrote earlier):

 

Private Sub cmdGo_Click()

'Get Settings

   bFreeJuice = chkJuice.Value

   bUseFront = optFront.Value

   strMonkey = txtMonkey.Text

   lSessionLength = 1000 * (Val(txtLength.Text) / 60)

'check settings

  If sSessionLength <= 0 Or strMonkey = "" Then

      Call Error("Invalid Settings")

  End If

 

  If bUseFront Then

       Pump = FrontPump

       Licker = FrontLicker

  Else

       Pump = RearPump

       Licker = RearLicker

  End If

 

'Initialise

...

 

This code reads the values that the user has entered from the controls and puts them into our variables. The SessionLength variable is converted from minutes to milliseconds in the process.

 

A brief check on the settings is performed, then we decide which pump and licker to use (compare with LOC% and PUMP% in the !Arachnid code).

 

Note: It seems a lot more long winded to program using windows rather than text inputs (and messing around with user interfaces is always tedious), but a good interface makes a program much easier & quicker to use.

 

Just think: you only write a program once, but you run it many times (hopefully) so – even though it feels like a drag to add a easy to use interface, you do generally save time in the long run.

 

An aside on declaring variables and scope

 

In BBC BASIC, you only generally have to declare (& reserve space for) array variables (using the DIM command), and you cannot use arrays you have not declared in that way.  Ordinary variables, on the other hand, you do not usually declare, you just use them when you want.

 

This is a disaster (waiting to happen). I am sure that anyone who has programmed in !Arachnid has had a bug caused by a misspelling of a variable: the trouble is that these bugs often go unnoticed in BBC BASIC. Setting a 'non-existent' variable to 3 is not an error – BASIC just makes a new variable (with the misspelled name).

 

VB can operate in exactly the same way, but we type 'Option Explicit' at the top of a module to stop it doing so. If you are writing a very quick program (like in Tutorial 1) then you might not bother with Option Explicit. But don't leave it out for anything more complicated than a few lines, unless you like code that doesn't work, and is difficult to debug!

 

"Scope" of variables

 

Where you declare variables is crucial. In BBC BASIC you only ever declare normal variables (using the LOCAL keyword) inside subroutines – these variables then only "exist" inside the PROC or FN in which they are declared. Elsewhere in the program you cannot look at, or change, the value of these variables – this 'locale' in which the variable works is known as its scope.

 

Variables in VB are local by default – when declared with 'Dim' they only have meaning within the subroutine where they are declared and used. But of course, in VB, all the code is within one subroutine or another. So how do you declare variables that can be used from any subroutine? The answer, of course, is in the Declarations section – at the top of the module before the first Sub is declared.

 

Module variables can be declared by three keywords: Dim, Public or Private. Always use Private (at least, until you know when not to!).

 

Why? Later on, you'll start writing programs that use more than one module – Form modules, Basic modules & Class modules. If you declare your variables as Public in one module and then you decide to use a variable of the same name in another module, you can cause bugs by accidentally  changing the variable from 'outside'. If you make your variables Private, you never have to worry about using the same variable names in different places. This saves a lot of pain when writing big programs, and means that you can re-use your code without it not working.

 

Very occasionally you might want to use a variable that can be seen by all modules. Only these variables should be declared public.

 

 

Step 6: Setting and responding to events.

 

The task starts in !Arachnid as follows:

 

  50 X=0:PROCswitch_on(1,E%)

  60 PROCpipe_timer(6,A%,0,"FNend(",0,E%)

  61 CLS:PRINT TAB(2,3)"NOS OF LICKS";

  62 PROCswitch_on(PUMP%,E%)

  65 PROCpipe_switch(LOC%,On,1,"FNfree(",0,E%)

  90 PROCwait(E%):*AE

  92 IF C$="R" RUN

  94 IF C$="M" CHAIN ":0.MENU"

  96 END

 

This zeros the X variable, turns on line 1 (houselight) and pipes a timer to a service function, formats a display for the number of licks, and turns on whichever pump was chosen. It then pipes a 'lick' response to a service function and enters the wait state.

 

The conversion to Whisker in VB is trivial. Add the following under the initialisation code (at the bottom of cmdGo_click):

 

'Start

   iLicks = 0

  Call Whisker.LineSetState(Houselight, wsOn)

  Call Whisker.TimerSetEvent(EndSession, sSessionLength * 1000, 0)

  Call Whisker.LineSetState(Pump, wsOn)

  Call Whisker.LineSetEvent(Licker, Free, wsLineToOn)

 

The constants wsOn and wsLineToOn should be fairly self explanatory, they're use being similar to On in !Arachnid.

 

Looking at the rest of the code I saw that X is used to count licks. I had to read the whole code, to work out what X is for – it might have been much easier if it had a more informative name. So I have replaced the name X with a more sensible name, iLicks (I because it is an integer).

 

I have asked for events called EndSession and Free. This will turn up when the timer finishes, or the licker is detected. I could have used literal strings, e.g. "EndSession", but it is better to use constants, and avoid the chance of bugs because of misspellings.

 

We want to use these variables throughout the module, so we need to declare them at the top. Add the following line to the declarations section:

 

Private iLicks As Integer

 

'EventNames

Const Free = "Free"

Const EndSession = "EndSession"

 

This is not quite the end. Whisker does not call service functions in the same way that

!Arachnid does. Rather, it sends Event messages, which tell you what has happened. When one of these messages arrives, VB will call the Whisker_Event subroutine.

 

In order to convert !Arachnid "Pipes" into Whisker, the simplest thing to do is just to check which message we got, and call the corresponding subroutine. Visual Basic has just the command for this: the Select Case statement.

 

Insert the following in the Whisker_Event subroutine:

 

Private Sub Whisker_Event(ByVal EventMessage As String, ByVal Time As Long)

  Select Case EventMessage

  Case Free: Call Free

  Case EndSession: Call EndSession

  End Select

End Sub

 

When you come to write Whisker Clients from scratch you can continue to do this – add a Case: line for each pipe, and call a procedure. The Whisker system gives you a bit more flexibility than !Arachnid, however, and you can do 'what you like' with your event messages.

 

There is no equivalent to entering the 'wait' state in Whisker for Visual Basic. VB is an event-driven language and is therefore always in the equivalent of the wait state. But there is one more thing we have to do – we don't want the user to be able to start the whole session off again, or change the settings on the form, now we have started.

 

Add the following at the bottom of cmdGo_click:

 

'Disable the forms buttons

   Me.optFront.Enabled = False

   Me.optRear.Enabled = False

   Me.chkJuice.Enabled = False

   Me.txtLength.Enabled = False

   Me.txtName.Enabled = False

   Me.cmdGo.Enabled = False

 

We don't really need to disable anything apart from cmdGo – but by disabling the others, it is a bit more clear to the user that he/she cannot change them while it is running.

 

Note: why is there a "Me." in front of all the control names? Me is a special object in VB, which can be used to refer to the form from within the form's code. It isn't needed here at all – but typing it means that VB will pop up one of the menus, from which you can select the control's name. This saves you typing them in…

 

 

Step 7: Service functions

 

These should be fairly easy to transfer to whisker. We can continue to call them by their !Arachnid names (i.e. beginning with FN), but they can be subroutines now.

 

 100 DEFFNfree(P%,R%)

 105  IF R%=0 =0

 110  IF B$="Y":PROCswitch_on(PUMP%,E%):X=X+1:PRINT TAB(15,3)X;

 115  IF B$="N" GOTO 300

 120 =0

 130 DEFFNend(P%,R%)

 140  IF R%=0 =0

 145  PROCkill_all

 150  PROCswitch_off(PUMP%,E%):PROCswitch_off(1,E%)

 160  CLS:PRINT TAB(2,1)"LICKING"

 170  PRINT TAB(2,3)"MONKEY";:PRINT TAB(30,3)MONKEY$

 180  PRINT TAB(2,5)"TOTAL NOS OF LICKS";:PRINT TAB(30,5)X: IF BREAK=1 GOTO 480

 190  PRINT TAB(2,18)"PRESS 'R' WHEN READY TO CONTINUE";:INPUT TAB(36,18)C$

 200  IF C$<>"R" GOTO 190

 210  PRINT TAB(2,20)"RE-RUN 'R' RETURN TO MENU 'M' OR EXIT'E'";:INPUT TAB(45,20)C$

 220  IF C$="M" GOTO 260

 230  IF C$="R" GOTO 260

 240  IF C$="E" GOTO 260

 250  GOTO 210

 260 =0

 300  PROCkill_switch(LOC%,E%)

 310  PROCswitch_off(PUMP%,E%)

 330  PROCpipe_switch(LOC%,On,1,"FNban(",0,E%)

 340 =0

 360 DEFFNban(P%,R%)

 370  IF R%=0 =0

 380  PROCswitch_on(PUMP%,E%):X=X+1:PRINT TAB(15,3)X;

 385  PROCpipe_timer(2,100,0,"FNbanoff(",0,E%)

 390 =0

 410 DEFFNbanoff(P%,R%)

 420  IF R%=0 =0

 430  PROCswitch_off(PUMP%,E%)

 440 =0

 

The FNban and FNbanoff functions are trivial. FNend function stops the program, and asks the user if they wish to re-run the session. FNfree is a bit odd. Careful investigation of the code reveals that lines 300-340 are really part of FNfree – called by the GOTO on line 115.

 

This is one of those times when renumbering would be good – and putting lines 300-340 in after line 115. Even better – don't use GOTO at all! (VisualBasic would not let us do this kind of thing – jumping 'out' of one function's code, before returning. And that's just as well. It's EVIL. It works, but is very difficult to follow & understand).

 

So we can translate the code. Put the following into the code editor, at the bottom of the code: You can ignore the comments – they just illustrate which lines are replaced.

 

Private Sub FNfree()

  If bFreeJuice Then                             'IF B$ = 'Y' THEN

      Call Whisker.LineSetState(Pump, wsOn)     'PROCswitch_on

       iLicks = iLicks + 1                       'X=X+1

  Else                                           'IF B$ = 'N' THEN GOTO...

      Call Whisker.LineClearEventsByLine(Licker, wsLineToOn) 'PROCkill_switch

      Call Whisker.LineSetState(Pump, wsOff)                 'PROCswitch_off

      Call Whisker.LineSetEvent(Licker, ban, wsLineToOn)     'PROCpipe_switch

  End If

End Sub

 

Private Sub FNend()

  Call Whisker.LineSetState(Pump, wsOff)         'PROCswitch_off

  Call Whisker.LineSetState(Houselight, wsOff)   'PROCswitch_off

  Call Whisker.Disconnect                       'PROCkill_all

End Sub

 

Private Sub FNban()

  Call Whisker.LineSetState(Pump, wsOn)             'PROCswitch_on

   iLicks = iLicks + 1                                 'X=X+1

  Call Whisker.TimerSetEvent(banoff, 1000, 0)     'PROCpipe_timer

End Sub

 

Private Sub FNbanoff()

  Call Whisker.LineSetState(Pump, wsOff) 'PROCswitch_off

End Sub

 

I've ignored the printing of the data on screen when iLicks (X) is incremented, and the communication with the user at the end of FNend. This is only for clarity – we'll come back onto these points in the next step. In the meantime, we can note in passing how similar the code is: the translation of the 'innards' of an !Arachnid task to a Whisker Client is very simple indeed (Note that you have to convert timer durations from cs to ms however!).

 

I have chosen the direct equivalent of 'PROCkill_switch' to translate that function, by using ClearEventsByLine(). This clears the events set for a particular transition on a particular line. That could be replaced by LineClearEvent(free) – which would stop any line events from seding the event 'free', or KillEvent(free) – which would mean that the event 'free' would be ignored (Whisker_Event would not be called). This flexibility can be useful in complex designs in which several alternative actions could produce the same outcome.  However – I have used the nearest equivalent command to the !Arachnid line code.

 

The observant reader will already know what is missing before we have finished, however – there are two 'pipes' in the code, which we have replaced with 'SetEvent' instructions. So we need to

 

1. declare the constant strings for our event names in the declarations section:

 

Const banoff = "BanOff"

Const ban = "Ban"

 

2. add Case: lines to the code in Whisker_Event, giving:

 

Private Sub Whisker_Event(ByVal EventMessage As String, ByVal Time As Long)

  Select Case EventMessage

  Case Free: Call FNFree

  Case EndSession: Call FNEndSession

  Case ban: Call FNban

  Case banoff: Call FNbanoff

  End Select

End Sub

 

Now we're done – apart from 3 things: Showing the results, PROCbreak, and allowing a re-run. All of these are basically involve exchanging information with the user. As we saw in Step 5 – that is done differently in Visual Basic.

 

So that's it as far as the original code is concerned – the final step is just adding Visual Basic – style finishing touches to our translated client.

 

 

Step 8: Communicating with the user

 

Displaying session info

 

We have a form window on screen – we can easily write data to this by means of Label controls. I decided to put the data output onto a highlighted bit of the window as shown below:

 

Tutorial2_Licker_UI_2

 

Open the form designer, and put a Frame on the window where the output will appear – call it "fraOutput". Set its Border property to none, clear its caption, and set its background colour to something bright (say, yellow).

 

Put four labels in this frame. Set the background colours to be the same as that of the frame. Set the captions to be "Session length(min):", "(time)", "Licks:", and "(licks)". Set the Names of the (time) and (licks) labels to be lblTime and lblLicks. We will use these to show the data.

 

After each iLicks = iLicks + 1 line, add the following (again, the Me isn't needed):

 

Me.lblLicks.Caption = Str(iLicks)

 

This will then display an uptodate licks count to the user.

 

How about the session length? The !Arachnid version didn't display that until the end, but we are actually receiving timing information from the server every time it contacts us – the Time parameter of Whisker_Event contains the time in ms since we connected, or reset our clock on the server. I suggest we use this to display the time to the user.

 

So just add this to the bottom of the Whisker_Event subroutine:

 

Me.lblTime.Caption = Str(Int(Time / 60000))

 

This is an example of the extra flexibility of the Event system over the !Arachnid Pipe system – what we are doing in this example is trivial, but we could be doing some more interesting post-processing on some or all of our events in this way.

 

Re-runnning the session

 

Rather than use the command line (and all those ugly GOTOs), we can allow the user to re-run the program once it has finished, simply by re-Enabling the cmdGo button.

 

Note that the FNend routine will disconnect us from the server – all we need to do is let the user re-enter the data, and click cmdGo to reconnect, and off we go again.

 

Add the following to the bottom of the FNend routine:

 

'Re-Enable the forms buttons

   Me.optFront.Enabled = True

   Me.optRear.Enabled = True

   Me.chkJuice.Enabled = True

   Me.txtLength.Enabled = True

   Me.txtName.Enabled = True

   Me.cmdGo.Enabled = True

 

Note: we need to make sure our variables are not affected by this – in more complex designs it might not be so simple! But here, we are okay - note that iLicks will be zeroed when we restart.

 

Handling errors

 

Full error-handling is tricky in VB, but not really needed here (refer to the online help & any VB tutorials you have to find out more). But there are only 3 kinds of 'exceptional' situation we need to take notice of in this task:

 

       Problems starting – all we need to do is tell the user what the problem is and wait.

 

Server Problems are signified by Whisker_Error messages. They mean that the server can't do what we asked – which is a Bad Thing. So we should report these to the user and take the appropriate action (disconnect, in this fairly simple case).

 

User wants to Quit (this is the only error handling that happened in the !Arachnid version of "Licker" =>  escape jumped to  PROCbreak)

 

Telling a windows program to Quit is usually done by clicking the close button – and VB will then ask the form if it wants to quit by calling the QueryUnload subroutine. It is a handy skill to ask the user if they really want to quit from that subroutine, and pass the answer back to VB. I'll give an example here.

 

Add the following code to the module to complete the conversion:

 

Private Sub break(ByVal message As String)

Whisker.AlertOperator (message)                 'PASS THE MESSAGE ON

End Sub

 

Private Sub Whisker_Error(ByVal ErrorMessage As String, ByVal Time As Long)

Whisker.Disconnect

Whisker.AlertOperator (ErrorMessage)         'PASS THE MESSAGE ON

End Sub

 

Private Sub Whisker_Disconnected()

Whisker.AlertOperator("Session over - Disconnected", True, False)

End Sub

 

Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)

'This will be called when VB wants to unload. See QueryUnload in vb help

'If it is because of a User clicking 'X', UnloadMode will be vbFormControlMenu

'(It could be for some other reason, e.g. because the computer is shutting down)

If UnloadMode = vbFormControlMenu Then

 Cancel = (MsgBox("Are you sure you want to Quit?", vbOKCancel, "Confirm Quit") = vbCancel)

End If

End Sub

 

This code shows how to use a VB's MsgBox function to ask a question of the user. Note that the MsgBox function is Modal – that is, it stops the subroutine while the messagebox is displayed (it has to, in order to return the value saying what the user chose).  This is not as bad as it sounds:

 

Modal messageboxes will stop Whisker messages if you are running from VisualBasic (e.g. by pressing F5 VB environment). So, if you ran this program by pressing F5, then tried to quit, our client would ignore all events (licks, end of session, etc) while the message box was being shown. This makes it seem like using messageboxes is a complete disaster!

 

However, if you compile this program into an Exe file (using File->Make exe), then try the same thing, you'll see that Whisker responds perfectly well to events while the message box is displayed (Phew!).

 

What if you want to display a message to the user but not stop the flow of the program at all (i.e. you don't care about the answer)? For this purpose, Whisker supplies the "Alert Operator" method. This allows the client to display a messagebox on the monitor, without waiting for a reply. The second parameter of the AlertOperator function allows you to display a messagebox on any connected WhiskerStatusClients (this is done by means of a special clientmessage broadcast to all clients). This is provided to allow you to inform a remote machine (e.g. your workstation) that something has occurred without waiting for that machine to check.

 

So – that shows how to convert a simple digital io task from !Arachnid to Whisker.

 

But: please see the SDK documentation for the difference between KillEvent() and ClearEvent() in Whisker, when converting Arachnid programs.

 

A final quick question to check that you are still awake: why do I  not try to let listeners know that I had finished (note the second parameter is 'False')?  Would the following not have worked in Whisker_Disconnected?

 

Private Sub Whisker_Disconnected()

Whisker.AlertOperator("Session over - Disconnected", True, True)

End Sub

 

If not, why not? (answer is in Tutorial 2 files folder, along with the source code)

 

 

APPENDIX – Original Licker task code

 

  10 REM LICKER 2

  12 PROCinit:PROCkill_all:MODE0: BREAK=0: SESSION%=0

  15 ON ERROR IF ERR=17 RUN ELSE REPORT: PRINT" at line"; ERL: PROCbreak: END

  30 @%=&20107

  35 PROCfree_switch(0,E%):PROCfree_switch(6,E%)

  40 FOR Z%=1 TO 5:PROCgovn_switch(Z%,E%):NEXT

  42 PRINT TAB(2,2)"LICKING";

  43 PRINT TAB(2,4)"MONKEY NAME";:INPUT TAB(36,4),MONKEY$

  44 PRINT TAB(2,6)"SESSION LENGTH";:INPUT TAB(36,6),A%:A%=A%*6000

  45 IF A%=0 GOTO 44

  46 PRINT TAB(2,8)"FREE JUICE";:INPUT TAB(36,8),B$

  47 IF B$="Y" OR B$="N" GOTO 48 ELSE 46

  48 PRINT TAB(2,10)"FRONT (0) OR BACK (6)"::INPUT TAB(36,10),LOC%

  49 IF LOC%=6 PUMP%=4 ELSE PUMP%=2

  50 X=0:PROCswitch_on(1,E%)

  60 PROCpipe_timer(6,A%,0,"FNend(",0,E%)

  61 CLS:PRINT TAB(2,3)"NOS OF LICKS";

  62 PROCswitch_on(PUMP%,E%)

  65 PROCpipe_switch(LOC%,On,1,"FNfree(",0,E%)

  90 PROCwait(E%):*AE

  92 IF C$="R" RUN

  94 IF C$="M" CHAIN ":0.MENU"

  96 END

 100 DEFFNfree(P%,R%)

 105  IF R%=0 =0

 110  IF B$="Y":PROCswitch_on(PUMP%,E%):X=X+1:PRINT TAB(15,3)X;

 115  IF B$="N" GOTO 300

 120 =0

 130 DEFFNend(P%,R%)

 140  IF R%=0 =0

 145  PROCkill_all

 150  PROCswitch_off(PUMP%,E%):PROCswitch_off(1,E%)

 160  CLS:PRINT TAB(2,1)"LICKING"

 170  PRINT TAB(2,3)"MONKEY";:PRINT TAB(30,3)MONKEY$

 180  PRINT TAB(2,5)"TOTAL NOS OF LICKS";:PRINT TAB(30,5)X: IF BREAK=1 GOTO 480

 190  PRINT TAB(2,18)"PRESS 'R' WHEN READY TO CONTINUE";:INPUT TAB(36,18)C$

 200  IF C$<>"R" GOTO 190

 210  PRINT TAB(2,20)"RE-RUN 'R' RETURN TO MENU 'M' OR EXIT'E'";:INPUT TAB(45,20)C$

 220  IF C$="M" GOTO 260

 230  IF C$="R" GOTO 260

 240  IF C$="E" GOTO 260

 250  GOTO 210

 260 =0

 300  PROCkill_switch(LOC%,E%)

 310  PROCswitch_off(PUMP%,E%)

330PROCpipe_switch(LOC%,On,1,"FNban(",0,E%)

340 =0

 360 DEFFNban(P%,R%)

 370  IF R%=0 =0

 380  PROCswitch_on(PUMP%,E%):X=X+1:PRINT TAB(15,3)X;

 385  PROCpipe_timer(2,100,0,"FNbanoff(",0,E%)

 390 =0

 410 DEFFNbanoff(P%,R%)

 420  IF R%=0 =0

 430  PROCswitch_off(PUMP%,E%)

 440 =0

 450 DEFPROCbreak

 460  BREAK=1

 470  GOTO 150

 480  SESSION%=FNtimer(6,E%)

 490  PRINT TAB(2,7)"SESSION LENGTH";:PRINT TAB(30,7)((A%-SESSION%)/6000)

 500 ENDPROC