Attribute VB_Name = "PogoMain"
'PogoMain
'14 June 2005
'Copyright
'C Lausted, Institute for Systems Biology
'
'Main module for Pogo project.
'Handle startup and shutdown.
'Copy values between the various modules, classes, and forms.
'Handle the actual synthesis program.
'
'Properties:
'  ProgRun, Down, Downs, Line, Lines
'
'Dependencies:  MainForm (& its ctlPiezo192), ConfigForm,
'               PogoMIO, PogoDIO, ServoForm, PlaybookForm,
'               InkjetForm, ReagentsForm, SlideForm,
'               PogoMisc, ErrorMessage, CameraForm.
'//////////////////////////////////////////////////////////////////


'Constants.
Public Const CVERSION As String = "v0.6.0"

Public Const CSLIDEROWS As Integer = 3          'Slides are arranged in three rows.
Public Const CSLIDECOLS As Integer = 9          'Slides are arranged in nine columns.
Public Const CSLIDES As Integer = 27            'There are 3*9=27 slides.
Public Const CTARGETS  As Integer = 1249        'Max number of feature columns on whole slide holder.
Public Const CCOLS As Integer = 70              'Columns of spots on one array.
Public Const CROWS As Integer = 140             'Rows of spots on one array.
Public Const CBASES As Integer = 100            'Maximum; may be less.
Public Const CPITCH As Integer = 222            '2000 counts per inch / 90 dots per inch.
Public Const CMICRONSPERCOUNT As Double = 1.27  'There are 25400um in 20000 counts.
Public Const CBANKS As Integer = 6              'Number of reagent banks.
Public Const CNOZZLES As Integer = 32           'Number of inkjets per bank.
Public Const CTOTALNOZ As Integer = 192         'CBANKS * CNOZZLES
Public Const CSOLENOIDS As Integer = 7          'Zero to seven.  Zero unused.
Public Const CMAXSOLDUR As Double = 30          'Solenoids may not be open > 30 seconds.
Public Const CMAXPURGEDROPS As Integer = 1000   'Can purge by jetting up to 1000*1000 drops.
Public Const CCAMERAPIXW As Integer = 1024      'Camera width in pixels.
Public Const CCAMERAPIXH As Integer = 768       'Camera height in pixels.
Public Const CDROPLETDETECTION As Integer = 1
Public Const CNOZZLEPURGINGAREA As Integer = 2
Public Const CSOLENOIDPURGING As Integer = 3
Public Const CNEARTOGLOVES As Integer = 4
Public Const CPARK As Integer = 5

'Globals.
Public sw As Stopwatch
Dim oErrGoto As Hashtable      'Use this hash to implement synth prog branching.
Dim iBank(6) As Integer        'E.g. 1:X/88 2:Y/89 3:A/65 4:C/67 5:G/71 6:T/84.

'Variables, Module-wide, accessed only as properties.
Private bProgRun As Boolean        'Is the synthesis program running? Access with ProgRun.
Private iDown As Integer           'Access with Down.
Private iDowns As Integer          'Access with Downs.
Private iLine As Integer           'Access with Line.
Private iLines As Integer          'Access with Lines.
Private iPurgeDrops As Integer     'Access with PurgeDrops.
Private iDrops As Integer          'Access with Drops.
Private iSolDur(CSOLENOIDS) As Double  'Access with SolDur.
 
'Kernel32 dll function declarations.
Private Declare Function WaitForSingleObject Lib "Kernel32" _
   (ByVal hHandle As Long, ByVal dwMilliseconds As Long) As Long
Private Declare Function CloseHandle Lib "Kernel32" _
   (ByVal hObject As Long) As Long
Private Declare Function OpenProcess Lib "Kernel32" _
   (ByVal dwDesiredAccess As Long, ByVal bInheritHandle As Long, _
    ByVal dwProcessId As Long) As Long
Private Declare Function TerminateProcess Lib "Kernel32" _
   (ByVal pHandle As Long, _
    ByVal code As Long) As Long
    


'-------------------------------------------------------------------
'Subroutines and functions.
'-------------------------------------------------------------------

Private Sub Main()
    '24 July 2004 CGL.
    'Startup Pogo.
    Dim bTestMode As Boolean
    Dim sErr As String
    Dim iErr As Integer
    
    'Load but don't show one instance of these forms (ErrorMessage first).
    Load ErrorMessage
    Load ConfigForm
    Load PlaybookForm
    Load PogoDIO
    Load PogoMIO
    Load ServoForm
    Load InkjetForm
    Load CameraForm
    Load MainForm
    
    'Initialize these classes.
    Set sw = New Stopwatch
    Set oErrGoto = New Hashtable
    
    'Check if module ServoForm, PogoMIO, or PogoDIO is in test mode.
    'If so, put all three modules in test mode.
    'We're in test mode if MainForm.itemTestMode.Checked = TRUE.
    If ServoForm.bTest Then sErr = "Unable to connect to the 6K4 controller." & vbCrLf
    If PogoMIO.bTest Then sErr = sErr & "Unable to access the MIO board." & vbCrLf
    If PogoDIO.bTest Then sErr = sErr & "Unable to access the DIO board." & vbCrLf
    bTestMode = (Len(sErr) > 0)
    If bTestMode Then
        'Allow the user to end execution here.
        sErr = sErr & "Would you like to continue in test mode?"
        If (MsgBox(sErr, vbYesNo) = vbNo) Then Unload MainForm
    End If
    MainForm.lblTestMode.Visible = bTestMode
    MainForm.itemTestMode.Checked = bTestMode
    ServoForm.bTest = bTestMode
    PogoMIO.bTest = bTestMode
    PogoDIO.bTest = bTestMode
    
    'Check if camera was detected.
    If (CameraForm.bTest = False) Then
        MainForm.lblCamera.Caption = "Camera connected"
        MainForm.lblCamera.ForeColor = vbBlue
    End If
    
    'Other setup.
    ApplyConfiguration  'Initialize modules with data from ConfigForm.
    CheckIfHomed        'We want the positioning soft limits to be meaningful.
    ProgRun = False
    'Default parameters for the synthesis program.
    '(none yet)
    MainForm.Show
End Sub


Private Sub CheckIfHomed()
    '6 July 2004 CGL.
    'If servo not homed, do it now.
    Dim txt As String, i As Integer
    Dim bX As Boolean, bY As Boolean, bZ As Boolean
    bX = Not ServoForm.bHomed(1)
    bY = Not ServoForm.bHomed(2)
    bZ = Not ServoForm.bHomed(3)
    If bX Then txt = txt & "X axis is not homed." & vbCrLf
    If bY Then txt = txt & "Y axis is not homed." & vbCrLf
    If bZ Then txt = txt & "Z axis is not homed." & vbCrLf
    If (Len(txt) > 0) Then
        txt = txt & "Would you like to home the machine now?" & vbCrLf
        txt = txt & "To avoid damaging the machine, it is recommended to click Yes."
        If (MsgBox(txt, vbYesNo) = vbNo) Then Exit Sub
        i = ServoForm.Home(bX, bY, bZ)
        If (i <> 0) Then ErrorMessage.Display "Error homing.", 0
    End If
End Sub


Public Sub ShutdownPogo()
    '23 June 2004 CGL.
    'This is called by MainForm.Form_Unload().
    Set sw = Nothing
    Unload PogoMIO
    Unload PogoDIO
    Unload PlaybookForm
    Unload SlideForm
    Unload InkjetForm
    Unload PlaybookForm
    Unload ServoForm
    Unload ConfigForm
End Sub


Public Sub ApplyConfiguration()
    '27 Aug 2004 CGL.
    'This is run when ConfigForm is updated.
    Dim i As Integer, j As Integer, sld As Integer
    
    'Get bank symbol values from ConfigForm.
    'Initialize iBank(6) with ASCII value of inkjet reagent symbols.
    For i = 1 To 6
        iBank(i) = Asc(ConfigForm.GetParam("ReagentBank" & i))
    Next i
    
    'Values are transferred from ConfigForm to PogoMain (here).
    For i = 1 To 6
        SolDur(i) = Val(ConfigForm.GetParam("SolDur" & i))
    Next i
    Drops = Val(ConfigForm.GetParam("JetDrops"))
    PurgeDrops = Val(ConfigForm.GetParam("PurgeDrops"))
    
    'Values are transfered from ConfigForm to ServoForm.
    'Servo limits.
    ServoForm.MinX = Val(ConfigForm.GetParam("MinX"))
    ServoForm.MaxX = Val(ConfigForm.GetParam("MaxX"))
    ServoForm.MinY = Val(ConfigForm.GetParam("MinY"))
    ServoForm.MaxY = Val(ConfigForm.GetParam("MaxY"))
    ServoForm.MinZ = Val(ConfigForm.GetParam("MinZ"))
    ServoForm.MaxZ = Val(ConfigForm.GetParam("MaxZ"))
    'Positions of eight landmarks.
    ServoForm.btnImportant(1).Tag = Concatenate(" ", ConfigForm.GetParam("LaserX"), _
        ConfigForm.GetParam("LaserY"), ConfigForm.GetParam("LaserZ"))
    ServoForm.btnImportant(2).Tag = Concatenate(" ", ConfigForm.GetParam("InkjetPurgingX"), _
        ConfigForm.GetParam("InkjetPurgingY"), ConfigForm.GetParam("InkjetPurgingZ"))
    ServoForm.btnImportant(3).Tag = Concatenate(" ", ConfigForm.GetParam("SolenoidPurgingX"), _
        ConfigForm.GetParam("SolenoidPurgingY"), ConfigForm.GetParam("SolenoidPurgingZ"))
    ServoForm.btnImportant(4).Tag = Concatenate(" ", ConfigForm.GetParam("GlovesX"), _
        ConfigForm.GetParam("GlovesY"), ConfigForm.GetParam("GlovesZ"))
    ServoForm.btnImportant(5).Tag = Concatenate(" ", ConfigForm.GetParam("ParkX"), _
        ConfigForm.GetParam("ParkY"), ConfigForm.GetParam("ParkZ"))
    ServoForm.btnImportant(6).Tag = Concatenate(" ", ConfigForm.GetParam("User6X"), _
        ConfigForm.GetParam("User6Y"), ConfigForm.GetParam("User6Z"))
    ServoForm.btnImportant(7).Tag = Concatenate(" ", ConfigForm.GetParam("User7X"), _
        ConfigForm.GetParam("User7Y"), ConfigForm.GetParam("User7Z"))
    ServoForm.btnImportant(8).Tag = Concatenate(" ", ConfigForm.GetParam("User8X"), _
        ConfigForm.GetParam("User8Y"), ConfigForm.GetParam("User8Z"))
        
    'Values are transferred from ConfigForm to ctlPiezo192.
    MainForm.ctlPiezo192.x = Val(ConfigForm.GetParam("LaserX"))
    MainForm.ctlPiezo192.y = Val(ConfigForm.GetParam("LaserY"))
    MainForm.ctlPiezo192.z = Val(ConfigForm.GetParam("LaserZ"))
    For i = 1 To CBANKS
        MainForm.ctlPiezo192.BankOffset(i) = Val(ConfigForm.GetParam("BankOffset" & i))
    Next i
    
    'Values are transferred from ConfigForm to InkjetForm.
    For i = 1 To CSLIDECOLS
        InkjetForm.SlideX(i) = Val(ConfigForm.GetParam("SlideX" & i))
    Next i
    For i = 1 To CSLIDEROWS
        InkjetForm.SlideY(i) = Val(ConfigForm.GetParam("SlideY" & i))
    Next i
    InkjetForm.SlideZ = Val(ConfigForm.GetParam("SlideZ"))
    For i = 1 To CBANKS
        InkjetForm.BankOffset(i) = Val(ConfigForm.GetParam("BankOffset" & i))
    Next i
    InkjetForm.ApplyProperties
    
    'Values are transferred from ConfigForm to CameraForm.
    'Note that camera coordinates are used by PogoMain rather than CameraForm.
    CameraForm.TColumns = Val(ConfigForm.GetParam("NumTilesX"))
    CameraForm.TRows = Val(ConfigForm.GetParam("NumTilesY"))
    CameraForm.MinR = Val(ConfigForm.GetParam("MinRadius"))
    CameraForm.MaxR = Val(ConfigForm.GetParam("MaxRadius"))
    CameraForm.Gamma = Val(ConfigForm.GetParam("Gamma"))
    CameraForm.Zoom = Val(ConfigForm.GetParam("Zoom"))
End Sub


Public Function Sym2Bank(iSym As Integer) As Integer
    '18 Oct 2004 CGL.
    'Given a symbol (A=65,C=67,G=71,T=84,etc) return a bank number (1..6).
    'Given a symbol ("_"=95, "-"=45) return bank number zero.
    'Quick access with global array variable iBank().
    'This variable is updated from ConfigForm by ApplyConfiguration().
    'If no match, return error code of -1.
    Dim i As Integer
    Sym2Bank = -1
    For i = 6 To 1 Step -1
        If (iSym = iBank(i)) Then Sym2Bank = i
        If (iSym = 0 Or iSym = 95 Or iSym = 45) Then Sym2Bank = 0
    Next i
End Function


Public Sub PrintNow()
    '28 July 2004 CGL
    'Print all loaded slides, down one.
    Const ALLSLIDES  As Integer = 0
    Dim sErr As String, iErr As Integer
    Dim lX As Long, lY As Long, lZ As Long
    'Determine which down (base position) to print.
    If (Down < 1) Or (Down > CBASES) Then
        If (MsgBox("Print down #1?", vbYesNo) = vbNo) Then Exit Sub
        Down = 1
    End If
    'Get current coordinates.
    lX = ServoForm.cmdPos(1)
    lY = ServoForm.cmdPos(2)
    lZ = ServoForm.cmdPos(3)
    'Print and return to previous position.
    sErr = InkjetForm.InkjetPrint(ALLSLIDES, Down, "Default", Drops, MainForm.bTestMode)
    If (Len(sErr) > 0) Then ErrorMessage.Display sErr, 5
    iErr = ServoForm.MoveAbsolute(lX, lY, lZ)
    ReagentsForm.Show  'Not sure why this line is needed.
End Sub


Public Property Get WasteSol() As Integer
    '11 Aug 2004 CGL.
    WasteSol = Int(ConfigForm.GetParam("Waste"))
End Property



'-------------------------------------------------------------------
'HIGH LEVEL MOTION CONTROL ROUTINES.
'-------------------------------------------------------------------

Public Function MoveAbsolute(ByVal x As Long, ByVal y As Long, ByVal z As Long) As String
    '20 Aug 2004 CGL.
    MoveAbsolute = ServoForm.MoveAbsolute(x, y, z)
End Function


Public Function MoveToSlide(ByVal iSlide As Integer, ByVal xo As Long, _
    ByVal yo As Long, ByVal zo As Long) As String
    '11 Aug 2004 CGL.
    'Move to a position offset xo,yo,zo from slide number 1 to 27.
    Dim sErr As String
    Dim i As Long, j As Long
    Dim x As Long, y As Long, z As Long
    'Parameter error check.
    If (iSlide < 1 Or iSlide > CSLIDES) Then
        sErr = "Error in PogoMain.MoveToSlide: Invalid slide number"
        ErrorMessage.Display sErr, 5
        MoveToSlide = sErr
        Exit Function
    End If
    'Get coordinates of slide.
    i = ((iSlide - 1) Mod CSLIDECOLS) + 1  'Which slide column it is in.
    j = Int((iSlide - 1) / CSLIDECOLS) + 1 'Which slide row it is in.
    x = ConfigForm.GetParam("SlideX" & i)
    y = ConfigForm.GetParam("SlideY" & j)
    z = ConfigForm.GetParam("SlideZ")
    'Move.
    sErr = MoveAbsolute((x + xo), (y + yo), (z + zo))
    If (Val(sErr) <> 0) Then MoveToSlide = "Error returned to PogoMain.MoveToSlide by ServoForm.MoveAbsolute."
End Function


Public Function MoveWashOverSlide(ByVal iSlide As Integer, ByVal iWash As Integer) As String
    '11 Aug 2004 CGL.
    'Place wash line over top specified slide.  If iSlide=0, then use purging area.
    'If iWash is the waste line, no motion is necessary.
    Dim sErr As String
    Dim xo As Long, x1 As Long, y1 As Long, z1 As Long
    'Parameter error checking.
    If (iWash = WasteSol) Then Exit Function
    If (iSlide < 0 Or iSlide > CSLIDES) Then sErr = sErr & "Invalid slide number. "
    If (iWash < 1 Or iWash > 6) Then sErr = sErr & "Invalid wash line number. "
    If (Len(sErr) > 0) Then
        sErr = "Error in PogoMain.MoveWashOverSlide. " & sErr
        ErrorMessage.Display sErr, 5
        MoveWashOverSlide = sErr
        Exit Function
    End If
    'Get coordinates and move.
    xo = ConfigForm.GetParam("Wash" & iWash)
    If (iSlide = 0) Then
        'Handle purging case.  Absolute coordinates for x1, y1, z1.
        x1 = ConfigForm.GetParam("SolenoidPurgingX")
        y1 = ConfigForm.GetParam("SolenoidPurgingY")
        z1 = ConfigForm.GetParam("SolenoidPurgingZ")
        sErr = MoveAbsolute((x1 + xo), y1, z1)
    Else
        'Handle slide case.  Relative coordinates for x1, y1, z1.
        x1 = ConfigForm.GetParam("WashRelPosX")
        y1 = ConfigForm.GetParam("WashRelPosY")
        z1 = ConfigForm.GetParam("WashRelPosZ")
        sErr = MoveToSlide(iSlide, (x1 + xo), y1, z1)
    End If
End Function


Public Function MoveCameraOverSlide(ByVal iSlide As Integer, ByVal iXOffset As Integer, _
    ByVal iYOffset As Integer) As String
    '27 Aug 2004 CGL.
    'Place camera over top specified slide.  Use iXOffset to offset horizontally by one image
    'width, use iYOffset to offset vertically by one image height.
    Dim sErr As String
    Dim xo As Long, x1 As Long, y1 As Long, z1 As Long
    'Parameter error check.
    If (iSlide < 1 Or iSlide > CSLIDES) Then
        sErr = "Error in PogoMain.MoveToSlide: Invalid slide number"
        ErrorMessage.Display sErr, 5
        MoveCameraOverSlide = sErr
        Exit Function
    End If
    'Get coordinates and move.
    x1 = ConfigForm.GetParam("CameraX")
    y1 = ConfigForm.GetParam("CameraY")
    z1 = ConfigForm.GetParam("CameraZ")
    x1 = x1 + (iXOffset * Val(ConfigForm.GetParam("ImagePitchX")))
    y1 = y1 + (iYOffset * Val(ConfigForm.GetParam("ImagePitchY")))
    sErr = MoveToSlide(iSlide, x1, y1, z1)
End Function


Public Function MoveCameraTile(ByVal iXOffset As Integer, ByVal iYOffset As Integer) As String
    '27 Aug 2004 CGL.
    'Move using relative coordinates where iXOffset is the number of camera widths
    'and iYOffset is the number of camera heights.  This depends on the field of view.
    Dim dHFOV As Double   'Horizontal field of view in encoder counts.
    Dim dVFOV As Double   'Vertical field of view in encoder counts.
    dHFOV = iXOffset * Val(ConfigForm.GetParam("ImagePitchX"))
    dVFOV = iYOffset * Val(ConfigForm.GetParam("ImagePitchY"))
    MoveCameraTile = ServoForm.MoveRelative(CLng(dHFOV), CLng(dVFOV), 0)
End Function



'-------------------------------------------------------------------
'SYNTHESIS PLAYBOOK PROPERTIES AND ROUTINES.
'-------------------------------------------------------------------

Public Property Get ProgRun() As Boolean
    '21 Sept 2004 CGL.
    'Report if the synthesis program is running.
    'First check if the user cancelled it from the error dialog box.
    If (Not ErrorMessage.btnStopProgram.Enabled) Then
        bProgRun = False
        ActivateButtonsAndMenus (Not bProgRun)
    End If
    ProgRun = bProgRun
End Property
Property Let ProgRun(ByVal bRunning As Boolean)
    '21 Sept 2004 CGL.
    Dim obj As Object
    'This indicates if a synthesis program is running.
    bProgRun = bRunning
    'Enable/disable the Stop Synthesis button on error dialog.
    ErrorMessage.btnStopProgram.Enabled = bProgRun
    'Activate or deactivate buttons & menus on MainForm and ctlPiezo192.
    ActivateButtonsAndMenus (Not bProgRun)
End Property

Private Sub ActivateButtonsAndMenus(ByVal bActive As Boolean)
    '17 Nov 2004 CGL.
    'Activate or deactivate buttons & menus on MainForm and ctlPiezo192.
    For Each obj In MainForm
        If (left(obj.Name, 3) = "btn") Then obj.Enabled = bActive
    Next
    MainForm.ctlPiezo192.ButtonsEnabled = bActive
    MainForm.btnStop.Enabled = Not bActive
    MainForm.Refresh
    'Activate or deactived buttons on other forms.
    CameraForm.ButtonsEnabled = bActive
    ServoForm.ButtonsEnabled = bActive
    ReagentsForm.ButtonsEnabled = bActive
End Sub


Public Property Get Down() As Integer
    '27 July 2004 CGL.
    'What down (or base position in the oligo) we are currently on.
    'Reflect value in iDown and MainForm.tbxDown.  It is either
    '"Startup script", an integer between 1..100, or "Finishing script"
    Down = iDown
End Property
Public Property Let Down(ByVal newvalue As Integer)
    'Check bounds and update value.
    iDown = newvalue
    If (iDown < -1) Then iDown = Downs  'Wrap around.
    If (iDown > Downs) Then iDown = -1  'Wrap around.
    'Update textbox on MainForm.
    If (iDown = -1) Then
        MainForm.tbxDown.Text = "Finishing script"
    ElseIf (iDown = 0) Then
        MainForm.tbxDown.Text = "Startup script"
    Else
        MainForm.tbxDown.Text = iDown
    End If
    MainForm.tbxDown.Refresh
End Property

Public Property Get Downs() As Integer
    '27 July 2004 CGL.
    'This property gives the length of the longest oligo.
    'Reflect value in iDown and MainForm.tbxDowns.  Be sure its an integer.
    Downs = iDowns
End Property
Public Property Let Downs(ByVal newvalue As Integer)
    'Check bounds and update value.
    iDowns = newvalue
    If (iDowns > CBASES) Then iDown = CBASES
    If (iDowns < 1) Then iDowns = 1
    'Update textbox on MainForm.
    MainForm.tbxDowns.Text = iDowns
    MainForm.tbxDowns.Refresh
End Property

Public Property Get Line() As Integer
    '27 July 2004 CGL.
    'Which script line (synthesis operation) we are on.
    'Reflect value in iLine and Mainform.tbxLine.  Be sure its an integer.
    Line = iLine
End Property
Public Property Let Line(ByVal newvalue As Integer)
    '27 July 2004 CGL.
    'Check bounds and update value.
    iLine = newvalue
    If (iLine < 1) Then iLine = Lines  'Wrap around.
    If (iLine > Lines) Then iLine = 1  'Wrap around.
    'Update textbox on MainForm.
    MainForm.tbxLine.Text = iLine
    MainForm.tbxLine.Refresh
End Property

Public Property Get Lines() As Integer
    '27 July 2004 CGL.
    'How many script lines (operations) in one cycle of synthesis.
    'Reflect value in iLines and MainForm.tbxLines.  Be sure its an integer.
    Lines = iLines
End Property
Public Property Let Lines(ByVal newvalue As Integer)
    'Check bounds and update value.
    If (newvalue < 1) Then iLines = 1 Else iLines = newvalue
    'Update textbox on MainForm.
    MainForm.tbxLines.Text = iLines
    MainForm.tbxLines.Refresh
End Property

Public Property Get Drops() As Integer
    '28 July 2004 CGL.
    'Number of drops to dispense with FOTF printing.
    Drops = iDrops
End Property
Public Property Let Drops(ByVal newvalue As Integer)
    'Check bounds and update value.
    iDrops = newvalue
    If (iDrops < 1) Then iDrops = 1
    If (iDrops > PogoDIO.MAXDROPS) Then iDrops = PogoDIO.MAXDROPS
    'Update textbox on ReagentsForm.
    ReagentsForm.tbxDrops = iDrops
    ReagentsForm.tbxDrops.Refresh
End Property

Public Property Get SolDur(ByVal i As Integer) As Double
    '28 July 2004 CGL
    'Default period of time to dispense reagent from solenoid i.
    If (i < 1) Or (i > CSOLENOIDS) Then
        SolDur = 0  'No such solenoid.
    Else
        SolDur = iSolDur(i)
    End If
End Property
Public Property Let SolDur(ByVal i As Integer, ByVal newvalue As Double)
    'Check bounds and update value.
    If (i < 1) Or (i > CSOLENOIDS) Then Exit Property
    If (newvalue < 0) Then newvalue = 0
    If (newvalue > CMAXSOLDUR) Then newvalue = CMAXSOLDUR
    iSolDur(i) = newvalue
    'Update textbox on ReagentsForm.
    ReagentsForm.tbxSolDur(i) = newvalue
    ReagentsForm.tbxSolDur(i).Refresh
End Property

Public Property Get PurgeDrops() As Integer
    '28 July 2004 CGL.
    'Number of drops to dispense when purging inkjet nozzles.
    PurgeDrops = MainForm.ctlPiezo192.PurgeDrops
End Property
Public Property Let PurgeDrops(ByVal newvalue As Integer)
    'Check bounds and update value.
    iPurgeDrops = newvalue
    If (iPurgeDrops < 1) Then iPurgeDrops = 1
    If (iPurgeDrops > CMAXPURGEDROPS) Then iPurgeDrops = CMAXPURGEDROPS
    'Update textbox on ReagentsForm.
    MainForm.ctlPiezo192.PurgeDrops = iPurgeDrops
End Property


Public Sub CountDowns()
    '14 June 2005 CGL.
    Downs = LongestOligo
End Sub

Public Function LongestOligo() As Integer
    '14 June 2005 CGL.
    'Determine which lineup contains longest oligo.  Minimum is 1.
    Dim i As Integer, cnt As Integer
    cnt = 1
    For i = 1 To 27
        If MainForm.btnSlide(i).Tag = "used" Then
            If MainForm.Slide(i).iDowns > cnt Then
                cnt = MainForm.Slide(i).iDowns
            End If
        End If
    Next i
    LongestOligo = cnt
End Function


Public Sub SetLineAndDown()
    '27 July 2004 CGL.
    'Base this on which playbook and which line were last viewed by the user.
    'Requires globals iDown, iLine, and iLines.
    Select Case PlaybookForm.sstProgram.Tab
        Case 0  'Startup script tab.
            Down = 0
            Line = PlaybookForm.lbxInitProg.ListIndex + 1
            Lines = PlaybookForm.lbxInitProg.ListCount
        Case 1  'Stepwise script tab.
            Down = 1
            Line = PlaybookForm.lbxStepProg.ListIndex + 1
            Lines = PlaybookForm.lbxStepProg.ListCount
        Case 2  'Finishing script tab.
            Down = -1
            Line = PlaybookForm.lbxFinishProg.ListIndex + 1
            Lines = PlaybookForm.lbxFinishProg.ListCount
        Case Else
            ErrorMessage.Display "Error in Mainform.SetLineAndDown", 0
    End Select
End Sub


Public Sub DisplayCurrentLine(ByVal bRefresh As Boolean)
    '27 July 2004 CGL.
    'Put previous, current, and next playbook line on panel.
    'Requires globals iDown and iLine.
    'This is called after PlaybookForm is closed.
    'Also copy current script command into lbxPlaybook.Tag.
    Dim lbx As ListBox
    Dim i As Integer
    Dim iThisTime As Integer        'What script to display.
    Static iLastTime As Integer     'What script is currently displayed.
    
    'First find which playbook we're using.
    Set lbx = PlaybookForm.lbxStepProg: iThisTime = 1
    If (Down = 0) Then Set lbx = PlaybookForm.lbxInitProg: iThisTime = 0
    If (Down > Downs) Then Down = -1
    If (Down = -1) Then Set lbx = PlaybookForm.lbxFinishProg: iThisTime = -1
    
    'Copy a whole script from PlaybookForm.lbx? to MainForm.lbxPlaybook.
    If ((iThisTime <> iLastTime) Or bRefresh) Then
        MainForm.lbxPlaybook.BackColor = lbx.BackColor
        MainForm.lbxPlaybook.Clear
        For i = 1 To lbx.ListCount
            MainForm.lbxPlaybook.AddItem (Format(i, "000  ") & lbx.List(i - 1))
        Next i
        Lines = lbx.ListCount
    End If
    iLastTime = iThisTime
    
    'Highlight the current line (script command).
    If (Line > MainForm.lbxPlaybook.ListCount) Then
        Line = MainForm.lbxPlaybook.ListCount  'Avoid bounds error.
    End If
    MainForm.lbxPlaybook.ListIndex = Line - 1
    MainForm.lbxPlaybook.Tag = lbx.List(Line - 1)  'Keep a copy of command in this Tag.
End Sub


Public Sub RunSynthProg()
    '18 Oct 2004 CGL.
    'Synthesize the microarrays using PlaybookForm.
    'Use properties Down, Line, Downs, Lines, ProgRun.
    Dim i As Integer
    Dim txt As String
    Static iLastLine As Integer, iLastDown As Integer 'Keep track of where we left off.
    
    'Setup. Let user know what line and down we start at.
    ProgRun = True
    txt = "You are now starting synthesis program from:" & vbCrLf
    txt = txt & "Down " & Down & vbCrLf & "Line " & Line & vbCrLf
    If ((Line <> iLastLine) Or (Down <> iLastDown)) And (iLastLine <> 0) And (iLastDown <> 0) Then
        txt = txt & "Be aware that during your last program, you left off at:" & vbCrLf
        txt = txt & "Down " & iLastDown & vbCrLf & "Line " & iLastLine
    End If
    ErrorMessage.Display txt, 60
    If (ProgRun = False) Then Exit Sub
    oErrGoto.DeleteAll
    
    'The Periodic Jet helps keep the nozzles from plugging.
    'If it is on, temporarily deactivate it during synthesis.
    If (ReagentsForm.PeriodicJet = vbChecked) Then ReagentsForm.PeriodicJet = vbGrayed
    
    'Main loop.
    Do
        'Loop through each down (synthesis cycle).
        Do
            'Loop through each line of the script (step of the cycle).
            Screen.MousePointer = vbHourglass
            DisplayCurrentLine (False)
            istat = ExecuteLine(MainForm.lbxPlaybook.Tag)
            Line = Line + 1: iLastLine = Line
            DoEvents
            If Not ProgRun Then Exit Do
        Loop Until (Line = 1)  'Because Line wraps back to 1 when Line>Lines.
        If Not ProgRun Then Exit Do
        Down = Down + 1: iLastDown = Down
        Line = 1: iLastLine = Line
    Loop Until (Down = 0)  'Because Down wraps back to -1 when Down>Downs.
    'If we finished the synthesis completely, reset some stuff.
    Rem If (Down = -1) Then Down = 0: iLastLine = 0: iLastDown = 0
    
    'Finish up.
    DisplayCurrentLine (False)
    ProgRun = False
    StatusCaption "Idle."
    If (ReagentsForm.PeriodicJet = vbGrayed) Then ReagentsForm.PeriodicJet = vbChecked
    Screen.MousePointer = vbDefault
End Sub


Function ExecuteLine(ByVal sline As String) As Integer
    '17 Nov 2004 CGL.
    'Execute one line of the program.
    Dim sArgs               'Array of arguments in sLine.
    Static sErr As String   'Error codes returned by functions.*********
    Dim iErr As Integer     'Error codes returned by functions.  REMOVE.
    Dim i As Integer, j As Integer
    Dim cnt As Integer
    Dim iSld As Variant     'Array of integers of slide indexes.
    Dim x As Variant        'Multipurpose arrays.
    
    'Break out of here if user clicked Stop
    
    If Not ProgRun Then Exit Function
    
    'Turn line into array of arguments.
    If (Len(sline) < 1) Then Exit Function
    sline = Replace(sline, "#", "# ") 'Be sure comment is recognized,
    sArgs = MyParse(sline)
    
    'Check all possible commands.
    Select Case UCase(sArgs(0))
        Case "#", "REM"
            'Nothing to do.
            sErr = ""
        Case "PRINT"
            'Print on one or all slides.
            StatusCaption "Printing..."
            sErr = ProgPrint(sline)
            If (sErr <> "") Then ErrorMessage.Display sErr, 10
        Case "WASH"
            'Dispense reagent from one solenoid onto slides.
            StatusCaption "Washing.."
            sErr = ProgWash(sline)
        Case "DRY"
            'Use a manifold for drying.
            StatusCaption "Drying..."
            sErr = ProgDry(sline)
            Rem sErr = OldProgDry(sline)
        Case "WAIT"
            'sArgs(1) is the time in seconds from 1 to 300.
            sErr = ProgWait(sline)
        Case "TESTNOZZLES"
            'Test the inkjet nozzles.
            StatusCaption "Nozzle test..."
            MainForm.ctlPiezo192.TestNozzles (CBANKS)
            sErr = ""
        Case "SETSOL"
            'Set default solenoid duration in seconds..
            StatusCaption "Setting solenoid dispense time..."
            SolDur(Int(sArgs(1))) = Val(sArgs(2))
            sErr = ""
        Case "SETDROPS"
            'Set the default number of drops to jet.
            StatusCaption "Setting default drop number..."
            Drops = Int(sArgs(1))
            sErr = ""
        Case "MESSAGE"
            'Show a message to the user and log it.
            x = right(sline, (Len(sline) - Len("MESSAGE ")))
            ErrorMessage.Display CStr(x), 0
            sErr = ""
            'Use this secret code to test the ErrGoto command.
            If (MyTrim(UCase(x)) = "ERROR") Then sErr = "Error"
        Case "HOME"
            'Home one or all axes.
            x = sArgs(1)
            If (x = "X") Then iErr = ServoForm.Home(True, False, False)
            If (x = "Y") Then iErr = ServoForm.Home(False, True, False)
            If (x = "Z") Then iErr = ServoForm.Home(False, False, True)
            If (x = "ALL") Then iErr = ServoForm.Home(True, True, True)
            sErr = ""
        Case "MOVEREL"
            'Move in relative coordinates.
            iErr = ServoForm.MoveRelative(sArgs(1), sArgs(2), sArgs(3))
            sErr = ""
        Case "MOVEABS"
            'Move in absolute coordinates.
            StatusCaption "Moving to absolute coordinates..."
            iErr = ServoForm.MoveAbsolute(sArgs(1), sArgs(2), sArgs(3))
            If (iErr = 0) Then sErr = "" Else sErr = "MoveAbs error"
        Case "MOVETO"
            'Move to useful position.
            x = Int(sArgs(1))
            StatusCaption ("Moving to position " & x)
            sErr = ""
            If (x >= 1 And x <= 8) Then sErr = ServoForm.MoveToImportantPos(x)
        Case "BOXFLUSH"
            'Turn box inert gas solenoid on/off.
            StatusCaption "Set the gas valve on Relay5."
            If (sArgs(1) = "ON") Then ReagentsForm.BoxDryerOn = True
            If (sArgs(1) = "OFF") Then ReagentsForm.BoxDryerOn = False
            sErr = ""
        Case "PUMPWASTE"
            StatusCaption "Removing waste..."
            j = ConfigForm.GetParam("Waste")  'Waste is this solenoid number.
            'Count the number of slides.
            cnt = 0
            For i = 1 To 27
                If (MainForm.btnSlide(i).Tag = "used") Then cnt = cnt + 1
            Next i
            'Calculate how long to pump the waste.
            x = CSng(sArgs(1))          'Duration.
            If (sArgs(2) = "TRUE") Then x = x * cnt
            'Pump the waste.
            iErr = PogoDIO.Pump(j, x)
            sErr = ""
        Case "FTPLOG"
            StatusCaption "Sending ftp..."
            sErr = ""
            'TODO: Implement this!!!
        Case "IMAGE"
            StatusCaption "Imaging..."
            sErr = ProgImage(sline)
        Case "ERRGOTO"
            'Check sErr to see if the last command executed successfully.
            If (Len(sErr) > 0) Then
                StatusCaption "Error encountered on the previous command."
                ProgErrGoto Int(Val(sArgs(1))), Int(Val(sArgs(2)))
            End If
            sErr = ""
        Case "PURGENOZZLES"
            'Shoot from 1k..10k drops from every nozzle.
            StatusCaption "Jetting from all nozzles."
            x = Int(Val(sArgs(1)))
            PurgeNozzles x
            Rem ServoForm.MoveToImportantPos (CNOZZLEPURGINGAREA)
            Rem MainForm.ctlPiezo192.PurgeAllNozzles (x)
        Case "AILOG"
            'Record voltages from all 8 analog inputs.
            x = ""
            For i = 0 To 7
                x = x & i & ":" & Format(PogoMIO.AIVoltage(i), "0.000") & "V "
            Next i
            ErrorMessage.Log CStr(x)
        Case "SHELL"
            'Send a command to the shell.
            'Strip out the "SHELL"
            x = sline
            x = Replace(x, sArgs(0), "", 1, 1, vbTextCompare)
            'Strip out the time.
            x = Replace(x, sArgs(1), "", 1, 1, vbTextCompare)
            x = LTrim(x)
            sErr = ShellCommand(x, Val(sArgs(1)))
            If (sErr <> "") Then ErrorMessage.Log ("SHELL error: " & sErr)
        Case Else
            ErrorMessage.Display ("Unrecognized instruction: " & sArgs(0)), 2
            sErr = ""
    End Select
    StatusCaption "Running synthesis script"
    
    'Slow things down if test mode.
    If MainForm.bTestMode Then sw.Wait (1)
    
    'Done.
    ExecuteLine = 0  'Return no error code.
End Function

Private Sub StatusCaption(ByVal sMsg As String)
    '27 Aug 2004 CGL
    MainForm.lblStatus.Caption = sMsg
    MainForm.lblStatus.Refresh
End Sub


Private Function ProgPrint(sline As String) As String
    '7 Sept 2004 CGL.
    'This function supports the synthesis program script.
    'Print on one or more slides.
    'Parameter 1 : Lineup or all-one-reagent.
    '          2 : Drops
    '          3+: Slide list
    Dim sArgs As Variant 'List of arguments.
    Dim i As Integer, j As Integer
    Dim sErr As String          'Error report.
    Dim iPrintDrops As Integer  'How many drops to print
    Dim iPrintDown As Integer   'Which down to print.
    Dim sReagent As String      'Whether print tetrazole or base.
    Dim iSld As Variant         'Slide list.
        
    'If we are in presynth script, use down 1; postsynth use last down.
    iPrintDown = Down
    If (iPrintDown < 1) Then iPrintDown = 1
    If (iPrintDown > Downs) Then iPrintDown = Downs
    
    'Parameters.
    sArgs = MyParse(sline)
    If (sArgs(1) = "DEFAULT") Then sReagent = "Default" Else sReagent = sArgs(1)
    If (sArgs(2) = "DEFAULT") Then iPrintDrops = Drops Else iPrintDrops = Int(sArgs(2))
    
    'Decide if printing all-at-once or one-at-a-time.
    If (sArgs(3) = "DEFAULT") Then
        'Print all at once.
        sErr = InkjetForm.InkjetPrint(0, iPrintDown, sReagent, iPrintDrops, bTestMode)
    Else
        'Check slide list.  WhichSlides() provides error message to user so just exit function.
        iSld = WhichSlides(3, sline)
        If (iSld(1) = 0) Then ProgPrint = "No slide specified.": Exit Function
        'Print one slide at a time.
        For i = LBound(iSld) To UBound(iSld)
            sErr = sErr & InkjetForm.InkjetPrint(Int(iSld(i)), iPrintDown, sReagent, iPrintDrops, bTestMode)
        Next i
    End If
    
    ProgPrint = sErr
End Function


Public Function ProgWash(sline As String) As String
    '27 July 2004 CGL.
    'This function supports the synthesis program script.
    'Dispense reagent from one solenoid onto slides.
    'Parameter 1 : Solenoid number
    '          2 : Duration
    '          3+: Slide list
    Dim iErr As Integer, sErr As String
    Dim sArgs As Variant 'List of arguments.
    Dim i As Integer
    Dim iSol As Integer  'Solenoid.
    Dim sDur As Single   'Duration.
    Dim iSld As Variant  'Slide list.
    
    'Parameters.
    sArgs = MyParse(sline)
    iSol = Int(sArgs(1))
    If (sArgs(2) = "DEFAULT") Then sDur = SolDur(iSol) Else sDur = Val(sArgs(2))
    'Get an array of slide numbers to work on.  WhichSlides() handles error messages.
    iSld = WhichSlides(3, sline)
    If (iSld(1) = 0) Then ProgWash = "No slide specified.": Exit Function
    
    'For each slide...
    For i = LBound(iSld) To UBound(iSld)
        'Move in to position and dispense.
        sErr = MoveWashOverSlide(iSld(i), iSol)
        iErr = PogoDIO.Pump(iSol, sDur)
    Next i
    
    ProgWash = ""  'Return no error code.
End Function


Private Function ProgWait(sline As String) As String
    '7 Nov 2004 CGL.
    'This function supports the synthesis program script.
    'Wait a given number of seconds.  Show progress on screen.  Allow breaking out.
    Dim sArgs As Variant
    Dim dur As Double
    Dim remaining As Long
    sw.Reset
    sArgs = MyParse(sline)
    dur = Val(sArgs(1)) 'sArgs(1) is the time in seconds from 1 to 300.
    If (dur > 300) Then ErrorMessage.Log "Warning: Duration > 300s in PogoMain.ProgWait."
    Do While (Not sw.Elapsed(dur)) And ProgRun
        'Update the display each second.
        If (remaining <> Int(dur - sw.SecondsElapsed)) Then
            remaining = Int(dur - sw.SecondsElapsed)
            StatusCaption ("Wait " & remaining & " seconds.")
        End If
        DoEvents
    Loop
    ProgWait = ""  'Return no error code.
End Function


Private Function ProgDry(sline As String) As String
    '14 Jan 2005 CGL.
    'This function supports the synthesis program script.
    'Dry slides using the blower manifold.
    Dim sErr As String
    Dim iSld As Variant
    Dim i As Integer, j As Integer
    Dim iLeft As Integer, iRight As Integer
    Dim xo As Long, yo As Long, zo As Long  'Relative location of manifold.
    
    'Use the manifold for drying one or more slides.
    'Get an array of slide numbers to work on.  WhichSlides() provides error messages.
    iSld = WhichSlides(1, sline)
    If (iSld(1) = 0) Then ProgDry = "No slide specified.": Exit Function
    
    'Where is the manifold?
    'TODO: get this information from ConfigForm.
    xo = 6# * 20000: yo = -0.9 * 20000: zo = 6000
    
    'For each row of slides.
    For i = 1 To 3
        sErr = LeftAndRightInSlideRow(i, iSld, iLeft, iRight)
        If (sErr = "") Then
            'Caclulate number of slides in row.
            j = (iRight - iLeft) + 1
            'Move to rightmost slide and dry whole row.
            sErr = MoveToSlide(iRight, xo, yo, zo)
            PogoMIO.N2JetOn = True
            sErr = ServoForm.MoveZigzag(8333, 35000, (3 * j + 2))
            Rem sErr = ServoForm.MoveZigzag(12500, 32000, (2 * j))
            PogoMIO.N2JetOn = False
        End If
    Next i
    
    ProgDry = ""
End Function

Private Function LeftAndRightInSlideRow(ByVal iRow As Integer, iSld, _
    ByRef iLeft As Integer, ByRef iRight As Integer) As String
    '18 Jan 2005 CGL.
    'Given a row iRow in list of slides iSld, return the left and right ones.
    'If no slide in row, return "No slide."
    'Assume three rows and nine columns of slides.
    'TODO: use this function in InkjetForm.InkjetPrint.
    Dim j As Integer
    Dim iLo As Integer
    Dim iHi As Integer
    LeftAndRightInSlideRow = "No slide."
    iLo = (iRow - 1) * 9 + 1
    iHi = (iRow - 1) * 9 + 9
    iLeft = 0
    iRight = 0
    For j = LBound(iSld) To UBound(iSld)
        If (iSld(j) >= iLo) And (iSld(j) <= iHi) Then
            LeftAndRightInSlideRow = ""
            If (iLeft = 0) Then iLeft = iSld(j)
            If (iRight = 0) Then iRight = iSld(j)
            If (iSld(j) < iLeft) Then iLeft = iSld(j)
            If (iSld(j) > iRight) Then iRight = iSld(j)
        End If
    Next j
    j = 0 'Delete this line.
End Function

Private Function OldProgDry(sline As String) As String
    '30 Sept 2004 CGL.  This function is no longer used.
    'This function supports the synthesis program script.
    'Dry slides using the blower manifold.
    Dim sErr As String
    Dim iSld As Variant
    Dim i As Integer, j As Integer
    Dim x As Long, y As Long, z As Long     'Coordinates of slide.
    Dim xo As Long, yo As Long, zo As Long  'Relative location of manifold.
    'Use the manifold for drying one or more slides.
    'Get an array of slide numbers to work on.  WhichSlides() provides error messages.
    iSld = WhichSlides(1, sline)
    If (iSld(1) = 0) Then OldProgDry = "No slide specified.": Exit Function
    'Where is the manifold?
    xo = 5.7 * 20000: yo = -0.8 * 20000: zo = 6000
    'Move to first slide and turn on jet.
    i = LBound(iSld)
    sErr = MoveToSlide(Int(iSld(i)), xo, yo, zo)
    'ServoForm.SetVelocityX (20)
    PogoMIO.N2JetOn = True
    'Reciprocating motion past all the slides, then shut off jet.
    For i = LBound(iSld) To UBound(iSld)
        sErr = MoveToSlide(Int(iSld(i)), xo, yo, zo)
        For j = 1 To 5
            iErr = ServoForm.MoveRelative(-2500, 34000, 0)
            iErr = ServoForm.MoveRelative(-2500, -34000, 0)
        Next j
    Next i
    PogoMIO.N2JetOn = False
    OldProgDry = ""
End Function


Public Function ProgImage(sline As String) As String
    '29 Sept 2004 CGL.
    'This function supports the synthesis program script.
    'Photograph, analyze (or not), and save (or not).
    'Parameter 1 : Analyze (0=no, 1=after phosphoramidite, 2=after tetrzole)
    '          2 : Filename (NONE=do not save png)
    '          3+: Slide list
    Dim sErr As String
    Dim sArgs As Variant 'List of arguments.
    Dim i As Integer
    Dim iAnalyze As Integer                   'How to analyze.
    Dim bAnalyze As Boolean                   'Whether to analyze at all.
    Dim sFile As String, sFilename As String  'Filename.
    Dim bSave As Boolean                      'Whether to save the file at all.
    Dim iSld As Variant                       'Slide list.
    
    'Parameters.
    sArgs = MyParse(sline)
    iAnalyze = Int(sArgs(1))
    bAnalyze = (iAnalyze = 1 Or iAnalyze = 2)
    sFile = sArgs(2)
    bSave = Not (UCase(sFile) = "NONE")
    'Get an array of slide numbers to work on. WhichSlides() provides error messages.
    iSld = WhichSlides(3, sline)
    If (iSld(1) = 0) Then ProgImage = "No slide specified.": Exit Function
    
    'For each slide...
    For i = LBound(iSld) To UBound(iSld)
        'Construct full filename.
        sFilename = "s" & Format(iSld(i), "00") & ".d" & Format(Down, "000") & "." & sFile & ".png"
        'Move in to position and photograph.
        sErr = MoveCameraOverSlide(iSld(i), 0, 0)
        sErr = CameraForm.PhotoMultipleTiles(iRows, iCols, bAnalyze, bSave, sFilename)
        If bAnalyze Then sErr = AnalyzeDropletArray(iAnalyze)
    Next i
    
    ProgImage = sErr
    ProgImage = ""  'Return no error code.  Delete this line when AnalyzeDropletArray is working.
End Function


Private Function AnalyzeDropletArray(ByVal iMode As Integer) As String
    '27 Aug 2004 CGL.
    'TODO: Write this function!!!
    'Check droplet diameters and positions to see if printing was successful.
    'After phosphoramidites iMode=1, after tetrazole iMode=2.
    Static prevArea() As Integer   'How big drops were after printing just phosphoramidite.
    Dim x As Integer, y As Integer, xm As Integer, ym As Integer
    xm = CameraForm.FeaturesUboundX
    ym = CameraForm.FeaturesUboundY
    If (iMode = 1) Then
        ReDim prevArea(xm, ym)
    End If
End Function


Private Sub ProgErrGoto(ByVal iLineJump As Integer, ByVal iAttempts As Integer)
    '7 Sept 2004 CGL.
    'An error was encountered, probably in ProgPrint or ProgImage, so we need to back up a few lines.
    'Typically, ProgImage will show a misprint so we need to re-do the last wash and print.
    'For example, to re-do the last two lines, set iLineJump = -2.
    'If the error occurs iAttempts times, throw up an message dialog box.
    'We use hash table oErrGoto to keep track of iAttempts for this line.
    Dim newAttempt As Integer
    Dim newLine As Integer
    Dim txt As String
    
    newAttempt = Int(Val(oErrGoto.Retrieve(Line)))
    newAttempt = newAttempt + 1
    
    If (newAttempt >= iAttempts) Then
        'Too many attemps.
        'Shut off N2 gas.
        ReagentsForm.BoxDryerOn = False
        'Warn the user and wait for response.
        txt = "Unable to complete line " & Line & " in " & iAttempts & " attempts."
        ErrorMessage.Display txt, 0
        DoEvents
        'Reset counter value in hash table to zero.
        oErrGoto.Define Line, 0
    Else
        'Increment counter value in hash table, then jump back a few lines.
        oErrGoto.Define Line, newAttempt
        newLine = Line + iLineJump - 1
        If (newLine < 1) Then newLine = 1
        If (newLine > Lines) Then newLine = Lines
        Line = newLine
    End If
End Sub


Private Function WhichSlides(iKey As Integer, sline As String) As Variant
    '30 Sept 2004 CGL.
    'This function supports the synthesis program script.
    'Return an array of integers representing slides that are to be processed.
    'The sLine is a list of slides and iKey is the first element in it.
    'If element iKey is "DEFAULT" then every active slide is to be processed.
    'If not, slides numbers are listed explicitly.
    'Example of sLine are "DRY DEFAULT" and "DRY 27, 26, 25, 24"
    Dim sArgs As Variant  'Argument array.
    Dim iSld As Variant   'Return array.
    Dim cnt As Integer
    Dim i As Integer, j As Integer
    Dim txt As String
    ReDim iSld(1 To 27)
    
    cnt = 0
    sArgs = MyParse(sline)
    If (sArgs(iKey) = "DEFAULT") Then
        'Use all active slides loaded on the form.
        For i = 27 To 1 Step -1
            If MainForm.btnSlide(i).Tag = "used" Then
                'This slide is used; add it to iSld.
                cnt = cnt + 1
                iSld(cnt) = i
            End If
        Next i
    Else
        'Use the list of slides from sLine/sArgs.
        For i = iKey To UBound(sArgs)
            j = Int(sArgs(i))
            'Check that slide number j is valid.
            If (j >= 1 And j <= 27) Then
                cnt = cnt + 1
                iSld(cnt) = j
            Else
                ErrorMessage.Display ("Slide number " & j & " is invalid"), 0
            End If
        Next i
    End If
    
    'Shrink temporary return array. If no slide is found, array has single element = 0.
    If (cnt < 1) Then
        txt = "No slide has been selected. "
        If ProgRun Then txt = txt & "Your synthesis program will now be halted. "
        ProgRun = False
        ErrorMessage.Display txt, 0
        ReDim iSlide(1 To 1)
    Else
        ReDim Preserve iSld(1 To cnt)
    End If
    WhichSlides = iSld
End Function


Public Sub PurgeNozzles(ByVal i1000Drops As Integer)
    '7 Nov 2004 CGL.
    'Move to inkjet purging area and jet some drops.
    If (i1000Drops = 0) Then i1000Drops = PurgeDrops  'Use the default number.
    ServoForm.MoveToImportantPos (CNOZZLEPURGINGAREA)
    MainForm.ctlPiezo192.PurgeAllNozzles (i1000Drops)
End Sub


Public Function ShellCommand(ByVal sCommand As String, ByVal timeLimit As Long) As String
    '17 Nov 2004 CGL.
    'Send the command sCommand to the shell.
    'If the process doesn't finish in the allotted time, it is terminated. Specify seconds.
    '
    'For example, >FTP -s:C:\MyFtpScript.txt
    'Where MyFtpScript.txt is
    '  open mangrove.umd.edu
    '  lausted
    '  password
    '  cd public_html
    '  put c:\ErrorLog.txt
    '  bye
    '
    'Another example
    '>NCONVERT -out jpeg -q 10 *.png && ECHO The png files have been converted to jpeg. & PAUSE
    '
    Const PROCESS_ALL_ACCESS = &H1F0FFF
    Const INHERIT_HANDLE As Boolean = False
    Dim iTask As Long
    Dim ret As Long
    Dim pHandle As Long
    Dim sErr As String
    
    'Limit checking.
    If (timeLimit < 1 Or timeLimit > 3600) Then
        sErr = "Time limit out of range"
        ShellCommand = sErr
        Exit Function
    End If
    
    iTask = Shell("cmd /c " & sCommand, vbNormalFocus)  'Or vbMinimizedNoFocus.
    pHandle = OpenProcess(PROCESS_ALL_ACCESS, INHERIT_HANDLE, iTask)
    timeLimit = timeLimit * 1000 'Convert seconds to milliseconds.
    ret = WaitForSingleObject(pHandle, timeLimit)
    If (ret <> 0) Then
        'Didn't complete in the given time period so kill the process.
        sErr = "Could not execute the shell command."
        ret = TerminateProcess(pHandle, 0)
        If (ret <> 1) Then sErr = sErr & " Also could not kill the process."
    End If
    ret = CloseHandle(pHandle)
    ShellCommand = sErr
End Function

