import processing.core.*; 
import processing.data.*; 
import processing.event.*; 
import processing.opengl.*; 

import processing.serial.*; 
import java.awt.*; 
import java.awt.event.*; 
import java.awt.datatransfer.*; 
import javax.swing.*; 
import java.io.*; 

import java.util.HashMap; 
import java.util.ArrayList; 
import java.io.File; 
import java.io.BufferedReader; 
import java.io.PrintWriter; 
import java.io.InputStream; 
import java.io.OutputStream; 
import java.io.IOException; 

public class Proc_16Ch_Controller_05 extends PApplet {

// ########################################################################
//
//  16 Ch Controller v0.06
//
//  Released:  09/01/2021    By: TechKnowTone
//
// ########################################################################
/*
  This code communicates with an Arduino micro via the USB serial interface
  to control up to 16 servo motors connected to a PCA9685 over I2C.
  
  It can also be used in PWM and Angle mode as a generic servo controller,
  and has been adapted for use with LX-16A digital servos too.
  
  This version includes:
  * 16 sliders with lower and upper limits
  * set PWM frequency in range 40 - 100 Hz
  * control OE pin
  * switch channels ON/OFF
  * copy values to the clipboard
  * switch other control modes when user clicks on 'Processing' logo
  * in LX-16A motor mode display a timer/rpm window
  
*/

// declare libraries

Serial usbPort;








// declare variables
// AppMode == 0  PCA9685 mode
// AppMode == 1  Angle mode 0 - 180°
// AppMode == 2  PWM mode 500 - 2400 µs
// AppMode == 3  LX-16A mode -1000 - +1000
int AppMode = 0;        // defines the mode of this application, 0 = default
int BB = 255;           // text background Blue colour component
int BG = 255;           // text background Green colour component
int BR = 255;           // text background Red colour component
int Ch_Cnt = 0;         // number of channels selected
int comFlag = 0;        // = 1 if COM serial available
int comListLength = 0;  // number of items in COM connection list
String comName = "";    // serial port COM name
int comPnt = 0;         // COM port list pointer
String CopyData = "";   // data to be copied to the clipboard
String CR = "\r\n";     // carriage return for strings
String data = "";
boolean data_loaded = false;  // = true once data is loaded from Arduino
boolean DFU = false;    // == true to invoke a redraw on mouse release
boolean Dither = false; // dither enable
int DitherCnt = 25;     // dither timer counter; clocked every 20ms
int DitherOffset = 0;   // the value applied to PWQM values when dither is active
int DitherPhase = 0;    // phase of applying dither offset value
int DitherVal = 1;      // dither offset value
int drawFlag = 1;       // = 1 to force screen redraw
int Freq = 50;          // PWM frequency
String Help$ = "";      // message generated by help system
String HelpLast$ = "";  // previous message generated by help system
PImage img;             // image loaded as background
int index = 0;
int interval = 20;      // main loop interval in milliseconds
int JoyX0 = 259;        // x-coordinate of joystick slider left end
int JoyX1 = 656;        // x-coordinate of joystick slider right end
int keyDwn = 0;         // tracks keyboard up/down activity, 0 = up, 1 = down
boolean keySHIFT = false;  // true if SHIFT key is held down
int LastVal;            // any previous value
int LF = 10;            // ASCII line feed
int mDwn = 0;           // state of mouse button down timer
int mB, mON, mX, mXD, mY; // mouse flags,x,y and down x
int memState;           // temp record of memLamp[n] state
String msgCh = "0";     // channel message sent to Arduino
String msgCOM = "None"; // COM port name
String msgRx = "";      // message received from Arduino
String msgTx = "";      // message sent to Arduino
boolean msRun = false;  // start/stop timer flag
int msTimer = 0;        // ms timer used in rpm measurement
int mXL,mYL;            // previous x,y values
int nextWiFims = 0;     // WiFi ms timer
int nextMillis = 0;     // loop end time in millis
String Released$ = "24/08/2020";  // latest update
String Rx$ = "";        // single character received from usb port
int servoLast = 0;      // previous value sent to servo
int servoLL = 50;       // absolute servo lower limit
int servoOE = 1;        // servo board Output Enable; default = 1  OFF
int servoTgt = 0;       // target/pointer servo for manual setting
int servoUL = 600;      // absolute servo upper limit
int sliderDwn = 0;      // =1 when mouse is clicked on slider
int speedTQ = 0;        // speed flag, 0 = tortoise, 1 = quick
int TB = 0;             // text Blue colour component
int TG = 0;             // text Green colour component
int TR = 0;             // text Red colour component
//int upDwnFlg = 0;       // up/down state flag, 0 = down, 1 = up
String usbBuff = "";    // buffer string used to prevent WiFi overload
int usbLen = 0;         // length of usbMsg
String usbMsg = "";     // latest message sent to the usb port
int WiFiInt = 90;       // WiFi interval in ms
int WiFiPW = 64;        // WiFi packet width, must be longer than the max command length

// declare arrays
boolean [] Ch_En = new boolean [16]; // channel enabled flags
int [] JoyXval = new int [16];  // joystick slider position values
int [] memLamp = new int [16]; // record memory status
int [] memVal = new int [16]; // record of servo value
int [] memVLL = new int [16]; // record of servo value LL
int [] memVUL = new int [16]; // record of servo value UL
int [] servoVal = new int [16]; // target servo value
int [] servoVLL = new int [16]; // target servo value LL
int [] servoVUL = new int [16]; // target servo value UL

public void setup() {
  // first code to run
  mON = 0; // mouse X,Y is off initially
  // allow Arduino time to boot up
  //delay(1500);
  LoadDefArrays();
  ClearChEn();
    // screen size must match background bitmap
  img = loadImage("16-Channel Controller.png");
  textSize(13);
  getCOMPort();
  println("Ready!");
  sendExpRq();
}

// ----------------------------------------------------------------------

public void draw() {
  // main loop. Comes here about 60 times a second
  if (msRun) {drawFlag = 1;}
  if (drawFlag > 0) {
    background(img); // load background image
    if (AppMode > 0) {
      drawBlankouts();
    } else {
      drawFreqMsg(); drawDither(); drawOElamp(); drawMemlamps();
    }
    drawServoVal();
    drawLLMsg(); drawULMsg(); drawJoyX();
    drawCOMMsg(); drawRxMsg(); drawTxMsg();
    drawHelp(); drawFlag = 0;
  }
  if (mON > 0) {drawMouseXY();}
  
  // perform usb buffer task
  if (millis() >= nextWiFims) {
    nextWiFims = nextWiFims + WiFiInt;
    // the following should never be true unless significant time is lost somewhere
    if (nextWiFims < millis()) {nextWiFims = millis() + WiFiInt;}
    // only send data to the usb port if it is active
    if (comFlag > 0) {usbPortOp();} else {usbBuff = "";}
  }
  
  // perform 20ms tasks
  if (millis() >= nextMillis) {
    nextMillis = nextMillis + interval;
    // the following should never be true unless significant time is lost somewhere
    if (nextMillis < millis()) {nextMillis = millis() + interval;}
    
    // perform auto-repeat on some buttons
    if (mDwn > 0) {
      // perform auto repeat functions
      mDwn--;
      if (mDwn < 1) {mDwn = 5; mouseDown();}
    } else {
      // perform dither action every 500ms if enabled
      if ((Dither) && (sliderDwn == 0)) {
        DitherCnt--;
        if (DitherCnt < 1) {
          DitherCnt = 25;
          switch (DitherPhase) {
            case 0:
              // apply +ve dither
              DitherOffset = DitherVal;
              break;
            case 1:
              // apply no dither
              DitherOffset = 0; break;
            case 2:
              // apply -ve dither
              DitherOffset = - DitherVal; break;
            case 3:
              // apply no dither
              DitherOffset = 0; DitherPhase = -1; break;
          } DitherPhase++; drawFlag = 1;
          appendOffset();
        }
      } else {DitherCnt = 25; DitherPhase = 0;} // set dither period to 500ms
    }
  }
}

// ----------------------------------------------------------------------

public void keyPressed() {
  // used to receive keyboard keys
  keyDwn = 1;
  if (keyCode == SHIFT) {keySHIFT = true;}
}

// ----------------------------------------------------------------------

public void keyReleased() {
  keyDwn = 0; keySHIFT = false;
}

// ----------------------------------------------------------------------

public void mouseDragged() {
  // mouse has been clicked and dragged on window
  mX = mouseX; mY = mouseY;
  if (mX < 780) {mDwn = 0;}  // clear the down flag if not in memory lamp area
  if (sliderDwn > 0) {readXSlider(servoTgt);}
}

// ----------------------------------------------------------------------

public void mouseDown() {
  // called from main loop to repeat certain events after a pause delay
  if (sliderDwn == 1) {return;}  // ignore mouse down mDwn flag when in slider adjust mode
  
  if ((mY >= 12) && (mY <= 37)) {
    // Freq field  
    if ((mX > 454) && (mX < 469)) {decFreq();}
    if ((mX > 511) && (mX < 525)) {incFreq();}

    // dither field
    if ((mX > 552) && (mX < 567)) {decDither();}
    if ((mX > 610) && (mX < 623)) {incDither();}
  }

  if ((mY >= 51) && (mY <= 652)) {
    // in channel region
    // channel buttons multi-select
    if (mX < 61) {SetChEn(); mDwn = 0;}  // only do this once

    // slider end points  
    if ((mX >=226) && (mX <243)) {decXSlider();}
    if ((mX >672) && (mX <=687)) {incXSlider();}

    // limits  
    if ((mX > 139) && (mX < 155)) {setLLValDwn();}
    if ((mX > 207) && (mX < 223)) {setLLValUp();}
    if ((mX > 693) && (mX < 709)) {setULValDwn();}
    if ((mX > 761) && (mX < 777)) {setULValUp();}

    // held on Mem lamp?
    if ((mX > 784) && (mX < 810)) {MemHeld();}
  }
}

// ----------------------------------------------------------------------

public void mouseMoved() {
  // used primarily with the help system
  mX = mouseX; mY = mouseY;
  Help_Message();
  if (mON > 0) {drawFlag = 1;}
}

// ----------------------------------------------------------------------

public void mousePressed() {
  // mouse has been clicked on window
  mX = mouseX; mY = mouseY; mB = mouseButton; mDwn = 25;
  cursor(HAND);
  if ((mX < 50) && (mY < 50)) {
    // toggle mouse X,Y display ON/OFF
    if (mON < 1) {mON = 1;} else {mON = 0; drawFlag = 1;}
  }

  // top row
  if ((mY >= 12) && (mY <= 37)) {
    // frequency field
    if ((mX > 454) && (mX < 469)) {decFreq();}
    if ((mX > 469) && (mX < 511)) {RstFreq();}
    if ((mX > 511) && (mX < 525)) {incFreq();}

    // dither field
    if ((mX > 552) && (mX < 567)) {decDither();}
    if ((mX > 567) && (mX < 610)) {toggleDither();}
    if ((mX > 610) && (mX < 623)) {incDither();}
  }
  if ((mY >= 8) && (mY <= 42)) {
    // OE and RESERT buttons
    if ((mX > 631) && (mX < 677)) {toggleOE();}
    if ((mX > 701) && (mX < 772)) {sendRESET();}
  }

  // channel region?
  if ((mY >= 46) && (mY <= 658)) {
    // clicked somewhere in the channel region, so determine the channel
    servoTgt = ((mY-46) * 16)/(658-46);
    if (servoTgt > 15) {servoTgt = 15;}
    //msgTx = "servoTgt = " + str(servoTgt); drawTxMsg();
    
    // channel buttons
    if (mX < 61) {toggleChEn(); mDwn = 50;}

    // adjust slider lower limits
    if ((mX > 139) && (mX < 155)) {setLLValDwn();}
    if ((mX > 207) && (mX < 223)) {setLLValUp();}

    // slider region
    if ((mX >=226) && (mX <243)) {decXSlider();}
    if ((mX >672) && (mX <=687)) {incXSlider();}
    if ((mX >=243) && (mX <=672)) {
      // mouse in X-slider region
      sliderDwn = 1; mXD = mX - JoyXval[servoTgt];
      readXSlider(servoTgt);
    }

    // adjust slider upper limits
    if ((mX > 693) && (mX < 709)) {setULValDwn();}
    if ((mX > 761) && (mX < 777)) {setULValUp();}

    // store upper and lower limits
    if ((mX > 154) && (mX < 208)) {setLL();}
    if ((mX > 708) && (mX < 762)) {setUL();}
    
    // clicked on Mem lamp?
    if ((mX > 784) && (mX < 810)) {MemPressed(); mDwn = 50;}

  }

  // clicked on COM port?
  if ((mX > 104) && (mX < 187) && (mY > 666) && (mY < 691)) {
    if (mouseButton == LEFT) {getCOMPort();}
    else if (mouseButton == RIGHT) {closeCOMPort();}
  }

  if (AppMode == 0) {
    if ((mX > 701) && (mX < 772) && (mY >= 662) && (mY <= 689)) {copyToClipboard();}
  }  
  if ((mX >= 13) && (mX <= 50) && (mY >= 682) && (mY <= 722)) {toggleMode();}
  if (AppMode == 3) {
    if ((mX >= 696) && (mX <= 779) && (mY >= 665) && (mY <= 691)) {toggleMsTimer();}
  }  
  if ((mX >= 710) && (mX <= 806) && (mY >= 696) && (mY <= 722)) {sendExpRq();}
}

// ----------------------------------------------------------------------

public void mouseReleased() {
  // mouse released from click and drag operation
  mX = mouseX; mY = mouseY;
  cursor(ARROW);
  if (mDwn > 0) {
    // look for click events
    // clicked on Mem lamp
    if ((mX > 784) && (mX < 810)) {MemRecall();}
  }
  mDwn = 0;
  sliderDwn = 0; // clear slider flag
  if (DFU) {drawFlag = 1; Help_Message(); DFU = false;}  // redraw on button release
}

// ----------------------------------------------------------------------

public void serialEvent(Serial usbPort) {
  // respond to a serial data received event
  nextWiFims = millis() + 10;  // delay sending whilst receiving
  Rx$ = usbPort.readString(); print(Rx$);
  data += Rx$; int RxLF = data.indexOf(LF);
  if (RxLF >= 0) {
    // LF/CR detected in string so evaluate
    if (data.indexOf("#") == 0) {
      // received string starts with "#" so assume it is an array
      int zS; int zI;  // temporary pointer
      data = trim(data);  // remove LF/CR
      if (data.indexOf("F") == 1) {
        // receiving pwmFreq value from Arduino
        data = data.substring(3);
        Freq = PApplet.parseInt(data);
        data = "";  // clear string buffer
        println("pwmFreq value loaded");
        msgRx = ""; data_loaded = false;
      } else if (data.indexOf("L") == 1) {
        // receiving SLL values from Arduino
        //println(data);
        data = data.substring(3);
        for (zS = 0; zS < 15; zS++) {
          zI = data.indexOf(",");
          servoVLL[zS] = PApplet.parseInt(data.substring(0,zI));
          memVLL[zS]= servoVLL[zS];
          data = data.substring(zI + 1);
        }
        servoVLL[15] = PApplet.parseInt(data); memVLL[15]= servoVLL[15];
        data = "";  // clear string buffer
        println("LL[] array data loaded");
        msgRx = "";
      } else if (data.indexOf("R") == 1) {
        // receiving SRV values from Arduino
        //println(data);
        data = data.substring(3);
        for (zS = 0; zS < 15; zS++) {
          zI = data.indexOf(",");
          servoVal[zS] = PApplet.parseInt(data.substring(0,zI));
          memVal[zS] = servoVal[zS];
          data = data.substring(zI + 1);
        }
        servoVal[15] = PApplet.parseInt(data); memVal[15] = servoVal[15];
        data = "";  // clear string buffer
        println("RV[] array data loaded");
        msgRx = "";
      } else if (data.indexOf("U") == 1) {
        // receiving SUL values from Arduino
        //println(data);
        data = data.substring(3);
        for (zS = 0; zS < 15; zS++) {
          zI = data.indexOf(",");
          servoVUL[zS] = PApplet.parseInt(data.substring(0,zI));
          memVUL[zS] = servoVUL[zS];
          data = data.substring(zI + 1);
        }
        servoVUL[15] = PApplet.parseInt(data); memVUL[15] = servoVUL[15];
        data = "";  // clear string buffer
        println("UL[] array data loaded");
        data_loaded = true;
        ClearChEn(); servoOE = 1; setXSlider(); ClearMemLamps();
        msgRx = "Ready";
      } else {
        msgRx = "ERROR - loading data"; 
      }
    } else {
      // data does not contain a '#' character
      msgRx = data;
      if (data.length() > 20) {msgRx = data.substring(0,20);}
    }
    //println(msgRx); 
    data = "";
    drawFlag = 1;
  }
}

// ----------------------------------------------------------------------

// ----------------------------------------------------------------------

public void appendOffset() {
  // this is called when Dither is active to append the offset to the
  // current servo tyarget
  if (comFlag < 1) {return;}  // don't do anything if not connected

  int zSV;
  setSliderX();

  zSV = servoVal[servoTgt] + DitherOffset;
  zSV = max(zSV,servoVLL[servoTgt]); zSV = min(zSV,servoVUL[servoTgt]);
  msgTx = "ST" + str(servoTgt) + ".SV" + str(zSV) + ".";
  usbPortWrite(msgTx); drawFlag = 1;
}

// ----------------------------------------------------------------------

public void centreJoyX() {
  // centre the joystick based on the current limits, LL, UL
  servoVal[servoTgt] = servoVLL[servoTgt] + ((servoVUL[servoTgt] - servoVLL[servoTgt])/2);
  setSliderX(); drawFlag = 1;
}

// ----------------------------------------------------------------------

public void centreServo() {
  // centre the servo based on the current limits, LL, UL
  centreJoyX(); servoLast = servoVal[servoTgt];
  msgTx = "SV" + str(servoVal[servoTgt]) + ".";
  usbPortWrite(msgTx);
}

// ----------------------------------------------------------------------

public void ClearChEn() {
  // clear the channel enable flags
  for (int zC = 0; zC < 16; zC++) {Ch_En[zC] = false;}
  Ch_Cnt = 0;
}

// ----------------------------------------------------------------------

public void ClearMemLamps() {
  // clear the memory state flags
  for (int zC = 0; zC < 16; zC++) {memLamp[zC] = 1;}
}

// ----------------------------------------------------------------------

public void closeCOMPort() {
  // Close the COM port and empty the serial buffer
  if (comFlag > 0) {
    usbPort.clear(); usbPort.stop();
    comFlag = 0; msgCOM = "----"; drawFlag = 1;
    msgTx = "COM Port Closed"; println(msgTx);
    servoOE = 1;  // turn OFF OE enabled lamp
    ClearChEn();  // clear all of the channel ON flags
    Dither = false;
  }
}

// ----------------------------------------------------------------------

public void copyToClipboard() {
  // called when user clicks on 'Copy' button
  // the code either copies a declaration of arrays or a list to the clipboard
  if (comFlag < 1) {return;}  // don't do anything if not connected
  if (AppMode > 0) {return;}  // don't copy if not in default mode

  int zS; String zH = ""; String zN = "";
  if (mB == LEFT) {
    // left mouse click so copy a declaration of arrays
    CopyData = "// Define PWM freq and servo calibration arrays" + CR;
    CopyData = CopyData + "#define Freq " + str(Freq) + "  // MG996R servos run at ~50 Hz updates" + CR;
    CopyData = CopyData + "// Servo:   { 0   1   2   3   4   5   6   7   8   9   A   B   C   D   E   F }" + CR;
    CopyData = CopyData + "int SLL[] = {";
    for (zS = 0; zS < 16; zS++) {
      CopyData = CopyData + str(memVLL[zS]);
      if (zS < 15) {CopyData = CopyData + ",";}
    }
    CopyData = CopyData + "};  // servo lower limit values" + CR;
    CopyData = CopyData + "int SRV[] = {";
    for (zS = 0; zS < 16; zS++) {
      CopyData = CopyData + str(memVal[zS]);
      if (zS < 15) {CopyData = CopyData + ",";}
    }
    CopyData = CopyData + "};  // reference values used to set a servo" + CR;
    CopyData = CopyData + "int SUL[] = {";
    for (zS = 0; zS < 16; zS++) {
      CopyData = CopyData + str(memVUL[zS]);
      if (zS < 15) {CopyData = CopyData + ",";}
    }
    CopyData = CopyData + "};  // servo upper limit values" + CR;
    msgTx = "Arrays Copied"; drawTxMsg();
  } else {
    // right mouse click so copy a definition list
    CopyData = CR + "// Define servo Cal. constants using Hex references" + CR;
    CopyData = CopyData + "#define Freq " + str(Freq) + "  // MG996R servos run at ~50 Hz updates" + CR;
    // create lower limit list
    for (zS = 0; zS < 16; zS++) {
      zH = str(zS);
      if (zS == 10) {zH = "A";}
      if (zS == 11) {zH = "B";}
      if (zS == 12) {zH = "C";}
      if (zS == 13) {zH = "D";}
      if (zS == 14) {zH = "E";}
      if (zS == 15) {zH = "F";}
      zN = str(zS); if (zS < 10) {zN = "0" + zN;} 
      CopyData = CopyData + "#define SLL" + zH + " " + str(memVLL[zS]);
      CopyData = CopyData + "\t// servo " + zN + " lower limit value" + CR;
    }
    // create centre value list
    for (zS = 0; zS < 16; zS++) {
      zH = str(zS);
      if (zS == 10) {zH = "A";}
      if (zS == 11) {zH = "B";}
      if (zS == 12) {zH = "C";}
      if (zS == 13) {zH = "D";}
      if (zS == 14) {zH = "E";}
      if (zS == 15) {zH = "F";}
      zN = str(zS); if (zS < 10) {zN = "0" + zN;} 
      CopyData = CopyData + "#define SRV" + zH + " " + str(memVal[zS]);
      CopyData = CopyData + "\t// servo " + zN + " reset value" + CR;
    }
    // create upper limit list
    for (zS = 0; zS < 16; zS++) {
      zH = str(zS);
      if (zS == 10) {zH = "A";}
      if (zS == 11) {zH = "B";}
      if (zS == 12) {zH = "C";}
      if (zS == 13) {zH = "D";}
      if (zS == 14) {zH = "E";}
      if (zS == 15) {zH = "F";}
      zN = str(zS); if (zS < 10) {zN = "0" + zN;} 
      CopyData = CopyData + "#define SUL" + zH + " " + str(memVUL[zS]);
      CopyData = CopyData + "\t// servo " + zN + " upper limit value" + CR;
    }
    msgTx = "List Copied"; drawTxMsg();
  }
  // now copy it
  StringSelection data = new StringSelection(CopyData);
  Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
  clipboard.setContents(data, data);
  drawFlag = 1;
}

// ----------------------------------------------------------------------

public void countChannels() {
  // called to get a handle on the number of active channels
  Ch_Cnt = 0;
  for (int zC = 0; zC < 16; zC++) {
    if (Ch_En[zC] == true) {Ch_Cnt++;}
  }
}

// ----------------------------------------------------------------------

public void decDither() {
  // reduce the dither value and send it to the Arduino
  if (comFlag < 1) {return;}  // don't do anything if not connected
  if (AppMode > 0) {return;}  // don't apply dither if not in default mode
  
  if (DitherVal > 1) {
    DitherVal--; drawFlag = 1;
    //msgTx = "SF" + str(Freq) + ".";
    //usbPortWrite(msgTx);
  }
  if (Dither) {DitherCnt = 50;  DitherPhase = 0;}  // display the change
}

// ----------------------------------------------------------------------

public void decFreq() {
  // reduce the PWM frequency value and send it to the Arduino
  if (comFlag < 1) {return;}  // don't do anything if not connected
  if (AppMode > 0) {return;}  // don't dec freq. if not in default mode

  if (Freq > 40) {
    Freq--; drawFlag = 1;
    msgTx = "SF" + str(Freq) + ".";
    usbPortWrite(msgTx);
  }
}

// ----------------------------------------------------------------------

public void decXSlider() {
  // decrement target slider value
  if (comFlag < 1) {return;}  // don't do anything if not connected
  if (servoVal[servoTgt] > servoVLL[servoTgt]) {
    servoLast = servoVal[servoTgt];
    if (keySHIFT) {servoVal[servoTgt] -= 5;} else {servoVal[servoTgt]--;}
    if (servoVal[servoTgt] < servoVLL[servoTgt]) {servoVal[servoTgt] = servoVLL[servoTgt];}
    setXSlider();
    if (AppMode == 0) {
      if (Ch_En[servoTgt]) {
        msgTx = "ST" + str(servoTgt) + "." +"SV" + str(servoVal[servoTgt]) + ".";
      } else {
        msgTx = "ST" + str(servoTgt) + "." +"SV0.";
      }
      usbPortWrite(msgTx);
    }
    if (AppMode == 1) {
      msgTx = "ST" + str(servoTgt) + "." +"SA" + str(servoVal[servoTgt]) + ".";
      if (Ch_En[servoTgt]) {usbPortWrite(msgTx);}
    }
    if (AppMode == 2) {
      msgTx = "ST" + str(servoTgt) + "." +"SV" + str(servoVal[servoTgt]) + ".";
      if (Ch_En[servoTgt]) {usbPortWrite(msgTx);}
    }
    if (AppMode == 3) {
      msgTx = "LT" + str(servoTgt) + "." +"LV" + str(servoVal[servoTgt]) + ".";
      if (Ch_En[servoTgt]) {usbPortWrite(msgTx);}
    }
    valueChanged();
    drawFlag = 1;
  }
}

// ----------------------------------------------------------------------

public void drawBlankouts() {
  // in angle mode so blank out certain form functions
  stroke(255,255,255); strokeWeight(1);
  fill(255,255,255);    // set fill colour
  rect(276,7,423,38);   // hide top controls
  rect(781,31,32,636);  // hide side lamps
  rect(700,658,78,36);  // Copy button
  
  TR=0; TG=0; TB=0;
  if (AppMode == 1) {drawTxtField("- Angle Mode",278,17);}
  if (AppMode == 2) {drawTxtField("- PWM Mode",278,17);}
  if (AppMode == 3) {
    drawTxtField("- LX-16A Servo Mode",278,17);
    TB=255;  // revert to blue text
    // draw timer field
    stroke(0,0,0); strokeWeight(1);
    fill(255,255,255);    // set fill colour white
    rect(696,665,83,26);   // draw field window
    if (msRun) {
      // msTimer is running so display current time difference
      drawTxtCtrField(str(millis() - msTimer),737, 670);
    } else {
      // msTimer n ot running so display timer value
      drawTxtCtrField(str(msTimer),737, 670);
    }
  } TB=255;  // revert to blue text
}

// ----------------------------------------------------------------------

public void drawCOMMsg() {
  // put text in COM value field
  if (comFlag < 1) {TR = 255;}  // show text in red if disconnected
  drawTxtCtrField(msgCOM, 147, 672); TR = 0;
}

// ----------------------------------------------------------------------

public void drawDither() {
  // put text in the dither field
  if (AppMode > 0) {return;}  // don't draw if not in default mode

  if (comFlag < 1) {TR = 255; TG = 0; TB = 0;}
  else {
    if (Dither){TR = 0; TG = 0; TB = 255; BR = 255; BG = 255; BB = 0;}
    else {TR = 150; TG = 150; TB = 150; BR = 255; BG = 255; BB = 255;}
  }
  if ((comFlag > 0) && Dither) {
    // dither is active so display the changing offset value
    String zTx$ = "  ";
    if (DitherCnt > 25) {
      zTx$ += str(DitherVal);
    } else {
      if (DitherOffset > 0) {zTx$ = " +";}
      zTx$ += str(DitherOffset);
    }
    zTx$ += " "; drawTxtCtrField(zTx$, 588, 18);
  } else {
    // either no COM or Dither active so display value
    drawTxtCtrField("  " + str(DitherVal) +  "  ", 588, 18);
  } TR = 0; TG = 0; TB = 255; BR = 255; BG = 255; BB = 255;
}

// ----------------------------------------------------------------------

public void drawFreqMsg() {
  // put text in the Frequency field
  if (AppMode > 0) {return;}  // don't draw if not in default mode

  if (comFlag < 1) {TR = 255; TG = 0; TB = 0;}
  drawTxtCtrField(str(Freq), 490, 18); TR = 0;
}

// ----------------------------------------------------------------------

public void drawHelp() {
  // display help messages
  TR = 0; TG = 0; TB = 255;
  drawTxtCtrField(Help$, 408, 702);
}

// ----------------------------------------------------------------------

public void drawJoyX() {
  // draw the X joystick slider
  if (comFlag < 1) {return;}  // don't draw if not connected
  if (!data_loaded && (AppMode == 0)) {return;}  // don't draw if no data loaded
  
  int zXw = 16; stroke(0,0,0); 
  strokeWeight(2);
  if (AppMode < 3) {  
    if (Ch_En[0]) {fill(255,100,100);} else {fill(255,255,255);}
    rect(JoyXval[0]-zXw,49,zXw+zXw,30);
  }
  
  if (Ch_En[1]) {fill(255,100,100);} else {fill(255,255,255);}
  rect(JoyXval[1]-zXw,87,zXw+zXw,30);
  if (Ch_En[2]) {fill(255,100,100);} else {fill(255,255,255);}
  rect(JoyXval[2]-zXw,125,zXw+zXw,31);
  if (Ch_En[3]) {fill(255,100,100);} else {fill(255,255,255);}
  rect(JoyXval[3]-zXw,164,zXw+zXw,31);
  
  if (Ch_En[4]) {fill(255,100,100);} else {fill(255,255,255);}
  rect(JoyXval[4]-zXw,202,zXw+zXw,31);
  if (Ch_En[5]) {fill(255,100,100);} else {fill(255,255,255);}
  rect(JoyXval[5]-zXw,240,zXw+zXw,31);
  if (Ch_En[6]) {fill(255,100,100);} else {fill(255,255,255);}
  rect(JoyXval[6]-zXw,279,zXw+zXw,31);
  if (Ch_En[7]) {fill(255,100,100);} else {fill(255,255,255);}
  rect(JoyXval[7]-zXw,317,zXw+zXw,31);
  
  if (Ch_En[8]) {fill(255,100,100);} else {fill(255,255,255);}
  rect(JoyXval[8]-zXw,356,zXw+zXw,31);
  if (Ch_En[9]) {fill(255,100,100);} else {fill(255,255,255);}
  rect(JoyXval[9]-zXw,394,zXw+zXw,31);
  if (Ch_En[10]) {fill(255,100,100);} else {fill(255,255,255);}
  rect(JoyXval[10]-zXw,432,zXw+zXw,31);
  if (Ch_En[11]) {fill(255,100,100);} else {fill(255,255,255);}
  rect(JoyXval[11]-zXw,471,zXw+zXw,31);
  
  if (Ch_En[12]) {fill(255,100,100);} else {fill(255,255,255);}
  rect(JoyXval[12]-zXw,509,zXw+zXw,31);
  if (Ch_En[13]) {fill(255,100,100);} else {fill(255,255,255);}
  rect(JoyXval[13]-zXw,547,zXw+zXw,31);
  if (Ch_En[14]) {fill(255,100,100);} else {fill(255,255,255);}
  rect(JoyXval[14]-zXw,586,zXw+zXw,31);
  if (Ch_En[15]) {fill(255,100,100);} else {fill(255,255,255);}
  rect(JoyXval[15]-zXw,625,zXw+zXw,31);
}

// ----------------------------------------------------------------------

public void drawLLMsg() {
  // put text in LL value field
  if (comFlag < 1) {return;}  // don't draw if not connected
  int zX = 182;
  if (AppMode < 3) {drawTxtCtrField("       ", zX, 57); drawTxtCtrField(str(servoVLL[0]), zX, 57);}    // Ch0
  drawTxtCtrField("       ", zX, 95); drawTxtCtrField(str(servoVLL[1]), zX, 95);    // Ch1
  drawTxtCtrField("       ", zX, 134); drawTxtCtrField(str(servoVLL[2]), zX, 134);  // Ch2
  drawTxtCtrField("       ", zX, 172); drawTxtCtrField(str(servoVLL[3]), zX, 172);  // Ch3

  drawTxtCtrField("       ", zX, 211); drawTxtCtrField(str(servoVLL[4]), zX, 211);  // Ch4
  drawTxtCtrField("       ", zX, 249); drawTxtCtrField(str(servoVLL[5]), zX, 249);  // Ch5
  drawTxtCtrField("       ", zX, 288); drawTxtCtrField(str(servoVLL[6]), zX, 288);  // Ch6
  drawTxtCtrField("       ", zX, 326); drawTxtCtrField(str(servoVLL[7]), zX, 326);  // Ch7

  drawTxtCtrField("       ", zX, 364); drawTxtCtrField(str(servoVLL[8]), zX, 364);  // Ch8
  drawTxtCtrField("       ", zX, 402); drawTxtCtrField(str(servoVLL[9]), zX, 402);  // Ch9
  drawTxtCtrField("       ", zX, 440); drawTxtCtrField(str(servoVLL[10]), zX, 440); // Ch10
  drawTxtCtrField("       ", zX, 479); drawTxtCtrField(str(servoVLL[11]), zX, 479); // Ch11

  drawTxtCtrField("       ", zX, 518); drawTxtCtrField(str(servoVLL[12]), zX, 518);  // Ch12
  drawTxtCtrField("       ", zX, 556); drawTxtCtrField(str(servoVLL[13]), zX, 556);  // Ch13
  drawTxtCtrField("       ", zX, 594); drawTxtCtrField(str(servoVLL[14]), zX, 594);  // Ch14
  drawTxtCtrField("       ", zX, 632); drawTxtCtrField(str(servoVLL[15]), zX, 632);  // Ch15
}

// ----------------------------------------------------------------------

public void drawMemlamps() {
  // draw Memory status lamps
  if (comFlag < 1) {return;}   // don't draw if not connected
  if (AppMode > 0) {return;}       // don't draw if in angle mode
  
  stroke(255,255,255); strokeWeight(1);
  for (int zS = 0; zS < 16; zS++) {
    switch(memLamp[zS]) {
      case 0: fill(128,128,128); break; // grey
      case 1: fill(40,210,0); break;    // green - reset value
      case 2: fill(220,220,40); break;  // yellow - modified from reset
      case 3: fill(64,64,255); break;   // blue - stored value
      case 4: fill(210,64,210); break;   // purple - modified from stored value
    }
    switch(zS) {
      case 0: rect(787,53,20,20); break;
      case 1: rect(787,92,20,20); break;
      case 2: rect(787,130,20,20); break;
      case 3: rect(787,168,20,20); break;
      case 4: rect(787,207,20,20); break;
      case 5: rect(787,246,20,20); break;
      case 6: rect(787,284,20,20); break;
      case 7: rect(787,322,20,20); break;
      case 8: rect(787,360,20,20); break;
      case 9: rect(787,399,20,20); break;
      case 10: rect(787,437,20,20); break;
      case 11: rect(787,475,20,20); break;
      case 12: rect(787,514,20,20); break;
      case 13: rect(787,552,20,20); break;
      case 14: rect(787,591,20,20); break;
      case 15: rect(787,629,20,20); break;
    }
  }
}

// ----------------------------------------------------------------------

public void drawMouseXY() {
  // draws the value of mouse x,y top left
  // draws a full X/Y axis on the form
  if ((mX != mXL) || (mY != mYL)) {
    // draw full X/Y axis
    stroke(255,0,0,128); strokeWeight(1);
    line(0,mY,823,mY); line(mX,0,mX,731);

    int zX,zY;
    if (mX > 412) {zX = mX - 58;} else {zX = mX + 8;}
    if (mY > 380) {zY = mY - 38;} else {zY = mY + 8;}
    stroke(0,0,0); fill(255,255,255); rect(zX,zY,50,30);
    fill(0,0,0); text("X=" + mX, zX + 4, zY + 13); text("Y=" + mY, zX + 4, zY + 27);
    mXL = mX; mYL = mY;
  }
}

// ----------------------------------------------------------------------

public void drawOElamp() {
  // draw OE lamp if active
  if (comFlag < 1) {return;}  // don't draw if not connected
  if (AppMode > 0) {return;}  // don't draw if in angle mode

  if (servoOE > 0) {
    // GREEN off
    fill(20,210,0); 
  } else {
    // RED active
    fill(255,20,0); 
  } stroke(255,255,255); strokeWeight(1); rect(682,10,11,30);
}

// ----------------------------------------------------------------------

public void drawRxMsg() {
  // put text in the Rx field
  drawTxtField("                                             ", 242, 672);
  drawTxtField(msgRx, 242, 672);
}

// ----------------------------------------------------------------------

public void drawServoVal() {
  // put text in Val field
  if (comFlag < 1) {return;}  // don't draw if not connected
  
  int zX = 82; String zSV$ = ""; int zSD;
  if (mDwn > 0) {DitherOffset = 0;}
  
  if (AppMode < 3) {
    zSV$ = str(servoVal[0]); if (servoTgt == 0) {if (Dither && Ch_En[0]) {
        zSD = servoVal[0] + DitherOffset;
        zSD = max(zSD,servoVLL[0]); zSD = min(zSD,servoVUL[0]);
        zSV$ = str(zSD);
      }
    }
    drawTxtField("       ", zX, 57); drawTxtField(zSV$, zX, 57);    // Ch0
  }
  
  zSV$ = str(servoVal[1]); if (servoTgt == 1) {if (Dither && Ch_En[1]) {
      zSD = servoVal[1] + DitherOffset;
      zSD = max(zSD,servoVLL[1]); zSD = min(zSD,servoVUL[1]);
      zSV$ = str(zSD);
    }
  }
  drawTxtField("       ", zX, 95); drawTxtField(zSV$, zX, 95);    // Ch1

  zSV$ = str(servoVal[2]); if (servoTgt == 2) {if (Dither && Ch_En[2]) {
      zSD = servoVal[2] + DitherOffset;
      zSD = max(zSD,servoVLL[2]); zSD = min(zSD,servoVUL[2]);
      zSV$ = str(zSD);
    }
  }
  drawTxtField("       ", zX, 134); drawTxtField(zSV$, zX, 134);  // Ch2

  zSV$ = str(servoVal[3]); if (servoTgt == 3) {if (Dither && Ch_En[3]) {
      zSD = servoVal[3] + DitherOffset;
      zSD = max(zSD,servoVLL[3]); zSD = min(zSD,servoVUL[3]);
      zSV$ = str(zSD);
    }
  }
  drawTxtField("       ", zX, 172); drawTxtField(zSV$, zX, 172);  // Ch3

  zSV$ = str(servoVal[4]); if (servoTgt == 4) {if (Dither && Ch_En[4]) {
      zSD = servoVal[4] + DitherOffset;
      zSD = max(zSD,servoVLL[4]); zSD = min(zSD,servoVUL[4]);
      zSV$ = str(zSD);
    }
  }
  drawTxtField("       ", zX, 211); drawTxtField(zSV$, zX, 211);  // Ch4

  zSV$ = str(servoVal[5]); if (servoTgt == 5) {if (Dither && Ch_En[5]) {
      zSD = servoVal[0] + DitherOffset;
      zSD = max(zSD,servoVLL[0]); zSD = min(zSD,servoVUL[0]);
      zSV$ = str(zSD);
    }
  }
  drawTxtField("       ", zX, 249); drawTxtField(zSV$, zX, 249);  // Ch5

  zSV$ = str(servoVal[6]); if (servoTgt == 6) {if (Dither && Ch_En[6]) {
      zSD = servoVal[6] + DitherOffset;
      zSD = max(zSD,servoVLL[6]); zSD = min(zSD,servoVUL[6]);
      zSV$ = str(zSD);
    }
  }
  drawTxtField("       ", zX, 288); drawTxtField(zSV$, zX, 288);  // Ch6

  zSV$ = str(servoVal[7]); if (servoTgt == 7) {if (Dither && Ch_En[7]) {
      zSD = servoVal[7] + DitherOffset;
      zSD = max(zSD,servoVLL[7]); zSD = min(zSD,servoVUL[7]);
      zSV$ = str(zSD);
    }
  }
  drawTxtField("       ", zX, 326); drawTxtField(zSV$, zX, 326);  // Ch7

  zSV$ = str(servoVal[8]); if (servoTgt == 8) {if (Dither && Ch_En[8]) {
      zSD = servoVal[8] + DitherOffset;
      zSD = max(zSD,servoVLL[8]); zSD = min(zSD,servoVUL[8]);
      zSV$ = str(zSD);
    }
  }
  drawTxtField("       ", zX, 364); drawTxtField(zSV$, zX, 364);  // Ch8

  zSV$ = str(servoVal[9]); if (servoTgt == 9) {if (Dither && Ch_En[9]) {
      zSD = servoVal[9] + DitherOffset;
      zSD = max(zSD,servoVLL[9]); zSD = min(zSD,servoVUL[9]);
      zSV$ = str(zSD);
    }
  }
  drawTxtField("       ", zX, 402); drawTxtField(zSV$, zX, 402);  // Ch9

  zSV$ = str(servoVal[10]); if (servoTgt == 10) {if (Dither && Ch_En[10]) {
      zSD = servoVal[10] + DitherOffset;
      zSD = max(zSD,servoVLL[10]); zSD = min(zSD,servoVUL[10]);
      zSV$ = str(zSD);
    }
  }
  drawTxtField("       ", zX, 440); drawTxtField(zSV$, zX, 440); // Ch10

  zSV$ = str(servoVal[11]); if (servoTgt == 11) {if (Dither && Ch_En[11]) {
      zSD = servoVal[11] + DitherOffset;
      zSD = max(zSD,servoVLL[11]); zSD = min(zSD,servoVUL[11]);
      zSV$ = str(zSD);
    }
  }
  drawTxtField("       ", zX, 479); drawTxtField(zSV$, zX, 479); // Ch11

  zSV$ = str(servoVal[12]); if (servoTgt == 12) {if (Dither && Ch_En[12]) {
      zSD = servoVal[12] + DitherOffset;
      zSD = max(zSD,servoVLL[12]); zSD = min(zSD,servoVUL[12]);
      zSV$ = str(zSD);
    }
  }
  drawTxtField("       ", zX, 518); drawTxtField(zSV$, zX, 518); // Ch12

  zSV$ = str(servoVal[13]); if (servoTgt == 13) {if (Dither && Ch_En[13]) {
      zSD = servoVal[13] + DitherOffset;
      zSD = max(zSD,servoVLL[13]); zSD = min(zSD,servoVUL[13]);
      zSV$ = str(zSD);
    }
  }
  drawTxtField("       ", zX, 556); drawTxtField(zSV$, zX, 556); // Ch13

  zSV$ = str(servoVal[14]); if (servoTgt == 14) {if (Dither && Ch_En[14]) {
      zSD = servoVal[14] + DitherOffset;
      zSD = max(zSD,servoVLL[14]); zSD = min(zSD,servoVUL[14]);
      zSV$ = str(zSD);
    }
  }
  drawTxtField("       ", zX, 594); drawTxtField(zSV$, zX, 594); // Ch14

  zSV$ = str(servoVal[15]); if (servoTgt == 15) {if (Dither && Ch_En[15]) {
      zSD = servoVal[15] + DitherOffset;
      zSD = max(zSD,servoVLL[15]); zSD = min(zSD,servoVUL[15]);
      zSV$ = str(zSD);
    }
  }
  drawTxtField("       ", zX, 632); drawTxtField(zSV$, zX, 632); // Ch15
}

// ----------------------------------------------------------------------

public void drawTxtCtrField(String zM,int zX, int zY) {
  // called by other draw text functions. This centres the text around zX
  zX = zX - (int)(textWidth(zM)/2.0f);
  stroke(BR,BG,BB); fill(BR,BG,BB); 
  rect(zX, zY, textWidth(zM), 15);
  fill(TR,TG,TB); text(zM, zX, zY+12);
}

// ----------------------------------------------------------------------

public void drawTxMsg() {
  // put text in Tx field
  drawTxtField("                                       ", 498, 672);
  drawTxtField(msgTx, 498, 672);
}

// ----------------------------------------------------------------------

public void drawTxtField(String zM,int zX, int zY) {
  // called by other draw text functions
  stroke(242,242,242); fill(242,242,242); 
  rect(zX, zY, 8+textWidth(zM), 14);
  fill(TR,TG,TB); text(zM, zX+4, zY+12);
}

// ----------------------------------------------------------------------

public void drawULMsg() {
  // put text in UL value field
  if (comFlag < 1) {return;}  // don't draw if not connected
  int zX = 738;
  if (AppMode < 3) {drawTxtCtrField("       ", zX, 57); drawTxtCtrField(str(servoVUL[0]), zX, 57);}    // Ch0
  drawTxtCtrField("       ", zX, 95); drawTxtCtrField(str(servoVUL[1]), zX, 95);    // Ch1
  drawTxtCtrField("       ", zX, 134); drawTxtCtrField(str(servoVUL[2]), zX, 134);  // Ch2
  drawTxtCtrField("       ", zX, 172); drawTxtCtrField(str(servoVUL[3]), zX, 172);  // Ch3

  drawTxtCtrField("       ", zX, 211); drawTxtCtrField(str(servoVUL[4]), zX, 211);  // Ch4
  drawTxtCtrField("       ", zX, 249); drawTxtCtrField(str(servoVUL[5]), zX, 249);  // Ch5
  drawTxtCtrField("       ", zX, 288); drawTxtCtrField(str(servoVUL[6]), zX, 288);  // Ch6
  drawTxtCtrField("       ", zX, 326); drawTxtCtrField(str(servoVUL[7]), zX, 326);  // Ch7

  drawTxtCtrField("       ", zX, 364); drawTxtCtrField(str(servoVUL[8]), zX, 364);  // Ch8
  drawTxtCtrField("       ", zX, 402); drawTxtCtrField(str(servoVUL[9]), zX, 402);  // Ch9
  drawTxtCtrField("       ", zX, 440); drawTxtCtrField(str(servoVUL[10]), zX, 440); // Ch10
  drawTxtCtrField("       ", zX, 479); drawTxtCtrField(str(servoVUL[11]), zX, 479); // Ch11

  drawTxtCtrField("       ", zX, 518); drawTxtCtrField(str(servoVUL[12]), zX, 518); // Ch12
  drawTxtCtrField("       ", zX, 556); drawTxtCtrField(str(servoVUL[13]), zX, 556); // Ch13
  drawTxtCtrField("       ", zX, 594); drawTxtCtrField(str(servoVUL[14]), zX, 594); // Ch14
  drawTxtCtrField("       ", zX, 632); drawTxtCtrField(str(servoVUL[15]), zX, 632); // Ch15
}

// ----------------------------------------------------------------------

public void exit() {
  // program is closing so reset Arduino...
  usbPortWrite("XX.");
  delay(20); // allow 20ms for data to be sent
  super.exit();
}

// ----------------------------------------------------------------------

public void getCOMPort() {
  // initialise serial comms if a port exists
  if (comFlag > 0) {
    // stops an existing connection if already made
    closeCOMPort(); delay(100);
  }
  comListLength = Serial.list().length;
  if (comListLength > 0) {
    comPnt++; if (comPnt >= comListLength) {comPnt = 0;}
    // test available serial port
    comName = Serial.list()[comPnt];
    println(comName);
    try {
      usbPort = new Serial(this, comName, 115200);
      //usbPort.bufferUntil(LF);
      comFlag = 1; msgCOM = comName;
      msgTx = "USB Connected"; println(msgTx);
    } catch(Exception e) {
      comFlag = 0; msgCOM = "-Error-";
    }
  } else {
    // no serial port available
    comFlag = 0; msgCOM = "- NA -";
  } drawFlag = 1;
}

// ----------------------------------------------------------------------

public void incDither() {
  // increase the dither value and send it to the Arduino
  if (comFlag < 1) {return;}  // don't do anything if not connected
  if (AppMode > 0) {return;}  // don't inc if in angle mode

  if (DitherVal < 20) {
    DitherVal++; drawFlag = 1;
    //msgTx = "SF" + str(Freq) + ".";
    //usbPortWrite(msgTx);
  }
  if (Dither) {DitherCnt = 50;  DitherPhase = 0;}  // display the change
}

// ----------------------------------------------------------------------

public void incFreq() {
  // increase the PWM frequency value and send it to the Arduino
  if (comFlag < 1) {return;}  // don't do anything if not connected
  if (AppMode > 0) {return;}  // don't inc if in angle mode

  if (Freq < 100) {
    Freq++; drawFlag = 1;
    msgTx = "SF" + str(Freq) + ".";
    usbPortWrite(msgTx);
  }
}

// ----------------------------------------------------------------------

public void incXSlider() {
  // increment slider value
  if (comFlag < 1) {return;}  // don't do anything if not connected
  if (servoVal[servoTgt] < servoVUL[servoTgt]) {
    servoLast = servoVal[servoTgt];
    if (keySHIFT) {servoVal[servoTgt] += 5;} else {servoVal[servoTgt]++;}
    if (servoVal[servoTgt] > servoVUL[servoTgt]) {servoVal[servoTgt] = servoVUL[servoTgt];}
    setXSlider();
    if (AppMode == 0) {
      if (Ch_En[servoTgt]) {
        msgTx = "ST" + str(servoTgt) + "." +"SV" + str(servoVal[servoTgt]) + ".";
      } else {
        msgTx = "ST" + str(servoTgt) + "." +"SV0.";
      }
      usbPortWrite(msgTx);
    }
    if (AppMode == 1) {
      msgTx = "ST" + str(servoTgt) + "." +"SA" + str(servoVal[servoTgt]) + ".";
      if (Ch_En[servoTgt]) {usbPortWrite(msgTx);}
    }
    if (AppMode == 2) {
      msgTx = "ST" + str(servoTgt) + "." +"SV" + str(servoVal[servoTgt]) + ".";
      if (Ch_En[servoTgt]) {usbPortWrite(msgTx);}
    }
    if (AppMode == 3) {
      msgTx = "LT" + str(servoTgt) + "." +"LV" + str(servoVal[servoTgt]) + ".";
      if (Ch_En[servoTgt]) {usbPortWrite(msgTx);}
    }
    valueChanged();
    drawFlag = 1;
  }
}

// ----------------------------------------------------------------------

public void LoadDefArrays() {
  // load the calibration values into the arrays
  // if you fit a new servo you need to relax values to determine limits
  // relax value limits are LL=120, UL=590
  int zVal = 350; int zVLL = 100; int zVUL = 600;
  if (AppMode == 1) {zVal = 90; zVLL = 0; zVUL = 180;}
  if (AppMode == 2) {zVal = 1500; zVLL = 500; zVUL = 2400;}
  if (AppMode == 3) {zVal = 0; zVLL = -1000; zVUL = 1000;}
  for (int zS = 0; zS < 16; zS++) {
    // Set default values
    servoVal[zS]  = zVal; servoVLL[zS]  = zVLL; servoVUL[zS]  = zVUL;
    memVal[zS]  = zVal; memVLL[zS]  = zVLL; memVUL[zS]  = zVUL;
    memLamp[zS] = 1;
  }
}

// ----------------------------------------------------------------------

public void MemHeld() {
  // user clicked on mem lamp for more than 1 second so store values
  if (comFlag < 1) {return;}  // don't do anything if not connected
  if (AppMode > 0) {return;}       // don't draw if in angle mode

  cursor(WAIT);
  memVLL[servoTgt] = servoVLL[servoTgt];
  memVal[servoTgt] = servoVal[servoTgt];
  memVUL[servoTgt] = servoVUL[servoTgt];
  memLamp[servoTgt] = 3; // set lamp blue
  mDwn = 0;              // reset the mouse down flag
  drawFlag = 1;
}

// ----------------------------------------------------------------------

public void MemPressed() {
  // user clicked on a memory lamp
  if (comFlag < 1) {return;}  // don't do anything if not connected
  if (AppMode > 0) {return;}  // don't action if in angle mode

  memState = memLamp[servoTgt];   // record the current lamp setting
  memLamp[servoTgt] = 0;          // switch OFF lamp whilst pressed
  drawFlag = 1;
}

// ----------------------------------------------------------------------

public void MemRecall() {
  // recall the channel settings from memory
  if (comFlag < 1) {return;}  // don't do anything if not connected
  if (AppMode > 0) {return;}  // don't action if in angle mode

  servoVLL[servoTgt] = memVLL[servoTgt];
  servoVal[servoTgt] = memVal[servoTgt];
  servoVUL[servoTgt] = memVUL[servoTgt];
  setSliderX();            // reposition X slider
  // reset the lamp field
  switch(memState) {
    case 1: memLamp[servoTgt] = 1; break;   // reset lamp
    case 2: memLamp[servoTgt] = 1; break;   // reset lamp
    case 3: memLamp[servoTgt] = 3; break;   // reset lamp
    case 4: memLamp[servoTgt] = 3; break;   // reset lamp
  }
  // send the recalled settings to the Arduino
  msgTx = "ST" + str(servoTgt) + ".";
  if (Ch_En[servoTgt]) {
    msgTx = msgTx +"SV" + str(servoVal[servoTgt]) + ".";
  } else {
    msgTx = msgTx +"SV0.";
    }
  usbPortWrite(msgTx); 
  msgTx = "ST" + str(servoTgt) + ".";
  msgTx = msgTx + "SL" + str(servoVLL[servoTgt]) + ".";
  usbPortWrite(msgTx); 
  msgTx = "ST" + str(servoTgt) + ".";
  msgTx = msgTx + "SU" + str(servoVUL[servoTgt]) + ".";
  usbPortWrite(msgTx); 
  drawFlag = 1;
}

// ----------------------------------------------------------------------

public void readXSlider(int zS) {
  // determine X-slider position and send value
  if (comFlag < 1) {return;}  // don't do anything if not connected
  if ((AppMode == 3) && (servoTgt == 0)) {return;}
  
  servoLast = servoVal[zS];  // remember the current value
  JoyXval[zS] = mX - mXD;
  JoyXval[zS] = max(JoyXval[zS], JoyX0);
  JoyXval[zS] = min(JoyXval[zS], JoyX1);
  // normal single channel mode
  servoVal[zS] = servoVLL[zS] + (((JoyXval[zS] - JoyX0) * (servoVUL[servoTgt] - servoVLL[servoTgt])) / (JoyX1 - JoyX0));
  servoVal[zS] = max(servoVal[zS], servoVLL[zS]);
  servoVal[zS] = min(servoVal[zS], servoVUL[zS]);
  if (servoLast != servoVal[zS]) {
    servoLast = servoVal[zS];
    valueChanged();
    if (AppMode == 0) {
      // in normal 16-ch PWM mode
      if (Ch_En[zS]) {
        msgTx = "ST" + str(zS) + "." +"SV" + str(servoVal[zS]) + ".";
      } else {
        msgTx = "ST" + str(zS) + "." +"SV0.";
      }
      usbPortWrite(msgTx);
    }
    if (AppMode == 1) {
      // in 16-Ch angle mode
      msgTx = "ST" + str(zS) + "." +"SA" + str(servoVal[zS]) + ".";
      if (Ch_En[zS]) {usbPortWrite(msgTx);}
    }
    if (AppMode == 2) {
      // in PWM mode
      msgTx = "ST" + str(zS) + "." +"SV" + str(servoVal[zS]) + ".";
      if (Ch_En[zS]) {usbPortWrite(msgTx);}
    }
    if (AppMode == 3) {
      // in LX-16A mode
      msgTx = "LT" + str(zS) + "." +"LV" + str(servoVal[zS]) + ".";
      if (Ch_En[zS]) {usbPortWrite(msgTx);}
    }
    drawFlag = 1;
  }
}

// ----------------------------------------------------------------------

public void RstFreq() {
  // reset the PWM frequency to 50Hz or 60Hz depending on mouse click
  if (comFlag < 1) {return;}  // don't do anything if not connected
  if (AppMode > 0) {return;}  // don't action if in angle mode

if (mB == LEFT) {Freq = 50;}
  else {Freq = 60;} drawFlag = 1;
  msgTx = "SF" + str(Freq) + ".";
  usbPortWrite(msgTx);
}

// ----------------------------------------------------------------------

public void sendButton(String zS) {
  // called when a button is clicked on the scrn
  if (comFlag < 1) {return;}  // don't do anything if not connected
  msgTx = zS; usbPortWrite(msgTx);
  drawFlag = 1;
}

// ----------------------------------------------------------------------

public void sendExpRq() {
  // send an EXPORT request to the Arduino
  msgTx = "SE."; usbPortWrite(msgTx);
  drawFlag = 1;
}

// ----------------------------------------------------------------------

public void sendRESET() {
  // reset defaults and send RESET msg to arduino
  servoOE = 1;      // clear OE flag as Arduino will do the same
  ClearChEn();
  ClearMemLamps();
  // the Arduino should RESET and export the data values
  msgTx = "!"; usbPortWrite(msgTx);
  drawFlag = 1;
}

// ----------------------------------------------------------------------

public void sendServoVal() {
  // send the current channels servo value
  servoLast = servoVal[servoTgt];
  if (AppMode == 0) {
    if (Ch_En[servoTgt]) {
      msgTx = "ST" + str(servoTgt) + "." +"SV" + str(servoVal[servoTgt]) + ".";
    } else {
      msgTx = "ST" + str(servoTgt) + "." +"SV0.";
    } usbPortWrite(msgTx);
  }
  if (AppMode == 1) {
    msgTx = "ST" + str(servoTgt) + "." +"SA" + str(servoVal[servoTgt]) + ".";
    if (Ch_En[servoTgt]) {usbPortWrite(msgTx);}
  }
  if (AppMode == 2) {
    msgTx = "ST" + str(servoTgt) + "." +"SV" + str(servoVal[servoTgt]) + ".";
    if (Ch_En[servoTgt]) {usbPortWrite(msgTx);}
  }
  if (AppMode == 3) {
    msgTx = "LT" + str(servoTgt) + "." +"LV" + str(servoVal[servoTgt]) + ".";
    if (Ch_En[servoTgt]) {usbPortWrite(msgTx);}
  }
  drawFlag = 1;
}

// ----------------------------------------------------------------------

public void SetChEn() {
  // set all channel enables the same and send data to the Arduino
  if (comFlag < 1) {return;}  // don't do anything if not connected
  if ((AppMode == 3) && (servoTgt == 0)) {return;}
  
  int zS = servoTgt;
  for (servoTgt = 0; servoTgt < 16; servoTgt++) {
    Ch_En[servoTgt] = Ch_En[zS];
    sendServoVal();
  } servoTgt = zS; drawFlag = 1;
  
  countChannels();
  if ((Ch_Cnt < 1) && (Dither)) {turnDitherOFF();}
}

// ----------------------------------------------------------------------

public void setLL() {
  // set lower limit from slider position
  if (comFlag < 1) {return;}  // don't do anything if not connected
  if ((AppMode == 3) && (servoTgt == 0)) {return;}
  
  LastVal = servoVLL[servoTgt];
  if (mB == LEFT) {
    servoVLL[servoTgt] = servoVal[servoTgt];
    if (servoVLL[servoTgt] >= servoVUL[servoTgt]) {servoVLL[servoTgt]--;}
  } else {
    servoVLL[servoTgt] = servoLL;
  }
  setXSlider(); drawFlag = 1;
  msgTx = "SL" + str(servoVLL[servoTgt]) + ".";
  usbPortWrite(msgTx);
  if (LastVal != servoVLL[servoTgt]) {valueChanged();}
}

// ----------------------------------------------------------------------

public void setLLValDwn() {
  // reduce the slider lower limit
  if (comFlag < 1) {return;}  // don't do anything if not connected
  if ((AppMode == 3) && (servoTgt == 0)) {return;}
  
  if (servoVLL[servoTgt] > servoLL) {
    servoVLL[servoTgt]--;
    if (keySHIFT) {servoVLL[servoTgt] -= 5;} else {servoVLL[servoTgt]--;}
    if (servoVLL[servoTgt] < servoLL) {servoVLL[servoTgt] = servoLL;}
    setXSlider(); drawFlag = 1;
    msgTx = "SL" + str(servoVLL[servoTgt]) + ".";
    usbPortWrite(msgTx);
    valueChanged();
  }
}

// ----------------------------------------------------------------------

public void setLLValUp() {
  // increase the slider upper limit
  if (comFlag < 1) {return;}  // don't do anything if not connected
  if ((AppMode == 3) && (servoTgt == 0)) {return;}

  if (servoVLL[servoTgt] < servoUL) {
    if (keySHIFT) {servoVLL[servoTgt] += 5;} else {servoVLL[servoTgt]++;}
    if (servoVLL[servoTgt] > servoUL) {servoVLL[servoTgt] = servoUL;}
    if (servoVLL[servoTgt] >= servoVUL[servoTgt]) {servoVUL[servoTgt]++;}
    if (servoVal[servoTgt] < servoVLL[servoTgt]) {servoVal[servoTgt] = servoVLL[servoTgt];}
    setXSlider(); drawFlag = 1;
    msgTx = "SL" + str(servoVLL[servoTgt]) + ".";
    usbPortWrite(msgTx);
    valueChanged();
  }
}

// ----------------------------------------------------------------------

public void setSliderX() {
  // set the X-slider position for current servoVal[servoTgt] value
  if (comFlag < 1) {return;}  // don't do anything if not connected
  if ((AppMode == 3) && (servoTgt == 0)) {return;}
  
  int zX0 = 259; int zX1 = 655; int zSV = servoVal[servoTgt];
  if (Dither) {
    zSV += DitherOffset;
    zSV = max(zSV,servoVLL[servoTgt]); zSV = min(zSV,servoVUL[servoTgt]);
  }
  JoyXval[servoTgt] = zX0 + ((zSV - servoVLL[servoTgt]) * (zX1 - zX0))/(servoVUL[servoTgt] - servoVLL[servoTgt]);
  drawFlag = 1;
}

// ----------------------------------------------------------------------

public void setUL() {
  // set upper limit from slider position if left mouse click
  // otherwise use absolute limit
  if (comFlag < 1) {return;}  // don't do anything if not connected
  if ((AppMode == 3) && (servoTgt == 0)) {return;}
  
  LastVal = servoVUL[servoTgt];
  if (mB == LEFT) {
    servoVUL[servoTgt] = servoVal[servoTgt];
    if (servoVUL[servoTgt] <= servoVLL[servoTgt]) {servoVUL[servoTgt]++;}
  } else {
    servoVUL[servoTgt] = servoUL;
  }
  setXSlider(); drawFlag = 1;
  msgTx = "SU" + str(servoVUL[servoTgt]) + ".";
  usbPortWrite(msgTx);
  if (LastVal != servoVUL[servoTgt]) {valueChanged();}
}

// ----------------------------------------------------------------------

public void setULValDwn() {
  // reduce the slider upper limit
  if (comFlag < 1) {return;}  // don't do anything if not connected
  if ((AppMode == 3) && (servoTgt == 0)) {return;}
  
  if (servoVUL[servoTgt] > servoLL) {
    if (keySHIFT) {servoVUL[servoTgt] -= 5;} else {servoVUL[servoTgt]--;}
    if (servoVUL[servoTgt] < servoLL) {servoVLL[servoTgt] = servoLL;}
    if (servoVUL[servoTgt] <= servoVLL[servoTgt]) {servoVLL[servoTgt]--;}
    if (servoVal[servoTgt] > servoVUL[servoTgt]) {servoVal[servoTgt] = servoVUL[servoTgt];}
    setXSlider(); drawFlag = 1;
    msgTx = "SU" + str(servoVUL[servoTgt]) + ".";
    usbPortWrite(msgTx);
    valueChanged();
  }
}

// ----------------------------------------------------------------------

public void setULValUp() {
  // increase the slider upper limit
  if (comFlag < 1) {return;}  // don't do anything if not connected
  if ((AppMode == 3) && (servoTgt == 0)) {return;}
  
  if (servoVUL[servoTgt] < servoUL) {
    if (keySHIFT) {servoVUL[servoTgt] += 5;} else {servoVUL[servoTgt]++;}
    if (servoVUL[servoTgt] > servoUL) {servoVUL[servoTgt] = servoUL;}
    setXSlider(); drawFlag = 1;
    msgTx = "SU" + str(servoVUL[servoTgt]) + ".";
    usbPortWrite(msgTx);
    valueChanged();
  }
}

// ----------------------------------------------------------------------

public void setXSlider() {
  // set the X-slider position for all servoVal[n] values
  if (comFlag < 1) {return;}  // don't do anything if not connected
  if ((AppMode == 3) && (servoTgt == 0)) {return;}
  
  int zX0 = 259; int zX1 = 655;
  for (int zS = 0; zS < 16; zS++) {
    JoyXval[zS] = zX0 + ((servoVal[zS] - servoVLL[zS]) * (zX1 - zX0))/(servoVUL[zS] - servoVLL[zS]);
  }
  drawFlag = 1;
}

// ----------------------------------------------------------------------

public void toggleChEn() {
  // toggles the channel enable and sends data if channel is turned ON
  if (comFlag < 1) {return;}  // don't do anything if not connected
  if ((AppMode == 3) && (servoTgt == 0)) {return;}
  
  Ch_En[servoTgt] = !Ch_En[servoTgt];
  if (AppMode > 0) {
    if (AppMode == 3) {  
      // LX-16A serisal mode
     if (!Ch_En[servoTgt]) {msgTx = "LT" + str(servoTgt) + ".LV0.";}
      else {msgTx = "LT" + str(servoTgt) + "." +"LV" + str(servoVal[servoTgt]) + ".";}
      usbPortWrite(msgTx);
    } else if (AppMode == 2) {  
      if (Ch_En[servoTgt]) {msgTx = "ST" + str(servoTgt) + ".SV" + str(servoVal[servoTgt]) + ".";}
      else {msgTx = "SD.";}  // disable the channel
      usbPortWrite(msgTx);
    } else {
      if (Ch_En[servoTgt]) {msgTx = "SO0.";} else {msgTx = "SO1.";} 
      usbPortWrite(msgTx);
    }
  } else {
    sendServoVal();
    countChannels();
    if ((Ch_Cnt < 1) && (Dither)) {turnDitherOFF();}
  }
  drawFlag = 1;
}

// ----------------------------------------------------------------------

public void toggleDither() {
  // called to switch Dither ON/OFF
  if (AppMode > 0) {return;} // don't action if in angle mode

  drawFlag = 1;
  if (Ch_Cnt < 1)  {turnDitherOFF(); return;}
  
  if (Dither) {turnDitherOFF();}
  else {Dither = true;}  
  Help_Message();
}

// ----------------------------------------------------------------------

public void toggleMode() {
  // change the mode of the application
  AppMode++; if (AppMode > 3) {AppMode = 0;}
  
  switch(AppMode) {
    case 0:
      // switch form to default mode
      servoLL = 50; servoUL = 600;
      LoadDefArrays(); setXSlider(); break;
    case 1:
      // switch form into angle mode
      servoLL = 0; servoUL = 180;
      LoadDefArrays(); setXSlider(); break;
    case 2:
      // switch form into PWM mode
      servoLL = 500; servoUL = 2400;
      LoadDefArrays(); setXSlider(); break;
    case 3:
      // switch form into LX-16A Serial mode
      servoLL = -1000; servoUL = 1000;
      msTimer = 0; msRun = false;  // start with timer reset
      LoadDefArrays(); setXSlider(); break;
  }
  drawFlag = 1;
}

// ----------------------------------------------------------------------

public void toggleMsTimer() {
  // user has clicked on msTimer field so start/stop timer
  if (msRun) {
    // timer is running, so stop it
    msRun = false; msTimer = millis() - msTimer;
  } else {
    // timer is not running, so start it
    msRun = true; msTimer = millis();
  }
  DFU = true;  // redraw on mouse release
}

// ----------------------------------------------------------------------

public void toggleOE() {
  // toggle the OE flag and update
  if (comFlag < 1) {return;}  // don't do anything if not connected
  if (AppMode > 0) {return;}  // don't action if in angle mode

  if (servoOE > 0) {
    // turning OE ON
    servoOE = 0; sendButton("SO0.");
  }
  else {
    // turning OE OFF
    servoOE = 1; sendButton("SO1.");
    ClearChEn();  // clear channels to indicate servo pulses are OFF
    turnDitherOFF();  // remove dither if it was ON
  } drawFlag = 1;
}

// ----------------------------------------------------------------------

public void turnDitherOFF() {
  // called when Dither is active to turn it OFF
  Dither = false; DitherPhase = 0; DitherOffset = 0;
  appendOffset();
}

// ----------------------------------------------------------------------

public void usbPortOp() {
  // called from a timer to empty the usb buffer
  usbLen = usbBuff.length();
  if (usbLen < 1) {return;}  // nothing in buffer
  
  // we are going to send something from usbBuff
  if (usbLen <= WiFiPW) {
    // it should only take 2.8ms to send 32 bytes
    usbPort.write(usbBuff);  // send small packet
    usbBuff = ""; usbLen = 0; // send small packet and clear buffer
  } else {
    usbPort.write(usbBuff.substring(0,WiFiPW)); // send WiFiPW length packet
    usbBuff = usbBuff.substring(WiFiPW,usbBuff.length());  // shift buffer by 16 char
    usbLen = usbBuff.length();
  }
}

// ----------------------------------------------------------------------

public void usbPortWrite(String zmsg) {
  // called to send text over the serial port
  // nothing is sent if comFlag or usbEn are false
  if (comFlag > 0) {
    usbBuff = usbBuff + zmsg;  // add new message to buffer
    //println(zmsg);
  } else {usbBuff = "";}  // flush the buffer if not sending
}

// ----------------------------------------------------------------------

public void valueChanged() {
  // called whenever a value changes to set lamps
  // if a lamp was green it will change to yellow
  // if a lamp was blue it will change to purple
  if (comFlag < 1) {return;}  // don't do anything if not connected
  switch(memLamp[servoTgt]) {
    case 1: memLamp[servoTgt] = 2; break;
    case 3: memLamp[servoTgt] = 4; break;
  }
}

// Called from a mouse move this presents help messages for areas under
// the mouse pointer.

public void Help_Message() {
  Help$ = "-";
  // top row
  if ((mY >= 12) && (mY <= 37) && (AppMode == 0)) {
    // frequency field
    if ((mX > 454) && (mX < 469)) {Help$ = "Click to decrement PWM frequency.";}
    if ((mX > 469) && (mX < 511)) {Help$ = "PCA9685 PWM frequency (Hz).  Click to reset.";}
    if ((mX > 511) && (mX < 525)) {Help$ = "Click to increment PWM frequency.";}

    // dither field
    if ((mX > 552) && (mX < 567)) {Help$ = "Click to decrement dither offset.";}
    if ((mX > 567) && (mX < 610)) {
      Help$ = "Dither offset. ";  
      if (Dither) {Help$ += "Click to DISABLE dither signal.";}
      else {Help$ += "Click to ENABLE dither signal.";}
    }
    if ((mX > 610) && (mX < 623)) {Help$ = "Click to increment dither offset.";}
  }
  if ((mY >= 8) && (mY <= 42)) {
    // OE and RESET buttons
    if  (AppMode == 0) {
      if ((mX > 631) && (mX < 677)) {
        if (servoOE > 0) {
          Help$ = "PCA9685 OE = HIGH disabled. Click to enable.";
        } else {
          Help$ = "PCA9685 OE = LOW enabled. Click to disable.";
        }
      }
    }
    if ((mX > 701) && (mX < 772)) {Help$ = "Click to send RESET to Arduino.";}
  }

  // channel region?
  if ((mY >= 46) && (mY <= 658)) {
    // clicked somewhere in the channel region, so determine the channel
    int zCh = ((mY-46) * 16)/(658-46); if (zCh > 15) {zCh = 15;}
    
    if ((zCh > 0) || (AppMode < 3)) {
      // in AppMode 3 we do not display help for channel 0
      // channel buttons
      if (mX < 61) {Help$ = "Click to toggle channel " + str(zCh) + " enable.";}
  
      // channel value field
      if ((mX >= 65) && (mX <= 132)) {Help$ = "Channel " + str(zCh) + " slider value.";}
  
      // adjust slider lower limits
      if ((mX > 139) && (mX < 155)) {Help$ = "Click to reduce channel " + str(zCh) + " lower limit.";}
      if ((mX > 207) && (mX < 223)) {Help$ = "Click to increase channel " + str(zCh) + " lower limit.";}
  
      // slider region
      if ((mX >=226) && (mX <243)) {Help$ = "Click to reduce channel " + str(zCh) + " value.";}
      if ((mX >672) && (mX <=687)) {Help$ = "Click to increase channel " + str(zCh) + " value.";}
      if ((mX >=243) && (mX <=672)) {
        // mouse in X-slider region
        Help$ = "Click and drag slider to adjust channel " + str(zCh) + " value.";
      }
  
      // adjust slider upper limits
      if ((mX > 693) && (mX < 709)) {Help$ = "Click to reduce channel " + str(zCh) + " upper limit.";}
      if ((mX > 761) && (mX < 777)) {Help$ = "Click to increase channel " + str(zCh) + " upper limit.";}
  
      // store upper and lower limits
      if ((mX > 154) && (mX < 208)) {Help$ = "Channel " + str(zCh) + " lower limit. Click to set from slider.";}
      if ((mX > 708) && (mX < 762)) {Help$ = "Channel " + str(zCh) + " upper limit. Click to set from slider.";}
      
      // clicked on Mem lamp?
      if ((mX > 784) && (mX < 810) && (AppMode == 0)) {Help$ = "Channel " + str(zCh) + " memory lamp. Click to recall. Hold to store.";}
    }  
  }

  if ((mY >= 666) && (mY <= 691)) {
    // Com, RX, Tx fields
    if ((mX >= 105) && (mX <= 180)) {
      if (comFlag < 1) {Help$ = "USB port is disconnected. Click-left to connect.";}
      else {Help$ = "USB port is connected. Click-right to disconnect.";}
    }
    if ((mX >= 235) && (mX <= 439)) {Help$ = "Messages received from USB port.";}
    if ((mX >= 494) && (mX <= 682)) {Help$ = "Messages sent to the USB port.";}
    if ((mX >= 704) && (mX <= 773) && (AppMode == 0)) {Help$ = "Click to copy stored values to clipboard.";}
    if ((mX >= 696) && (mX <= 779) && (AppMode == 3)) {
      if (msRun) {
        Help$ = "Click to Stop millisecond timer.";
      } else {
        if (msTimer == 0) {
          // timer has not yet run
          Help$ = "Click to Start millisecond timer.";
        } else {
          // display rpm result
          Help$ = str(msTimer) + "ms = " + nf(60000.0f/PApplet.parseFloat(msTimer),0,2) + "rpm. Click to restart timer.";
        }
      }
    }
  }

  if ((mY >= 696) && (mY <= 722)) {
    // Help field & TechKnowTone
    if ((mX >= 105) && (mX <= 705)) {Help$ = "Hello";}
    if ((mX >= 710) && (mX <= 806)) {Help$ = "Author: TechKnowTone     Released: " + Released$;}
  }
  
  if ((mX >= 12) && (mX <= 51) && (mY >= 683) && (mY <= 722)) {Help$ = "Click to change App mode.";}
  
  if (!Help$.equals(HelpLast$)) {
    // string is new so display it and remember it
    HelpLast$ = Help$; drawFlag = 1;
  }
  
}
// ----------------------------------------------------------------------
  public void settings() {  size(824,732); }
  static public void main(String[] passedArgs) {
    String[] appletArgs = new String[] { "Proc_16Ch_Controller_05" };
    if (passedArgs != null) {
      PApplet.main(concat(appletArgs, passedArgs));
    } else {
      PApplet.main(appletArgs);
    }
  }
}
