// ########################################################################
//
//  16 Ch Controller UTC v0.0
//
//  Released:  19/11/2025    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
  
*/

// declare libraries
import processing.serial.*;
Serial usbPort;

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

String Released$ = "19/11/2025";  // latest update

// declare variables
// AppMode == 2  PWM mode 300 - 2700 µs
int AppMode = 2;        // 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
boolean Loaded = false; // == true after setup()
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 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 = 20;       // 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

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

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

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
    }
  }
}

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

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

if (keyCode == LEFT) {decActiveSliders();}
if (keyCode == RIGHT) {incActiveSliders();}
}

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

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

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

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);}
}

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

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();}
  }
}

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

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

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

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();}
}

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

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
}

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

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 = int(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] = int(data.substring(0,zI));
          memVLL[zS]= servoVLL[zS];
          data = data.substring(zI + 1);
        }
        servoVLL[15] = int(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] = int(data.substring(0,zI));
          memVal[zS] = servoVal[zS];
          data = data.substring(zI + 1);
        }
        servoVal[15] = int(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] = int(data.substring(0,zI));
          memVUL[zS] = servoVUL[zS];
          data = data.substring(zI + 1);
        }
        servoVUL[15] = int(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;
  }
}

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