How to control multiple servos with a PS4 Controller using Processing Game Control Plus

822 Views Asked by At

I passed the last 3 days trying to figure this out with my basic programming knowledge and this is what I achieved: Collect data from PS4 controller with a Processing program and send it to Arduino through Serial port being able to control one servo using serial and Game Control Plus libraries.

Okay, then I shouldn't have more problems? Right? No. I don't have any idea of how I'm going to pass the other 3 analogical axes of the PS4 control and make Arduino get them and split into variables to control the other servos.

I'm going to share my 2 codes:

//Processing Code

import processing.serial.*;

import org.gamecontrolplus.gui.*;
import org.gamecontrolplus.*;
import net.java.games.input.*;

Serial myPort;

ControlIO control;
ControlDevice stick;
float px, py, pz, pw;


int[] lista;


public void setup() {
  lista = new int[4];
  String portName = Serial.list()[2]; 
  myPort = new Serial(this, portName, 9600);
  surface.setTitle("GCP Joystick example");

  control = ControlIO.getInstance(this);


  stick = control.filter(GCP.STICK).getMatchedDevice("joystick");
  if (stick == null) {
    println("No suitable device configured");
    System.exit(-1); 
  }

}


public void getUserInput() {
  px = map(stick.getSlider("X").getValue(), -1, 1, 0, width);
  py = map(stick.getSlider("Y").getValue(), -1, 1, 0, height);
  pz = map(stick.getSlider("Z").getValue(), -1, 1, 0, width);
  pw = map(stick.getSlider("W").getValue(), -1, 1, 0, height);
}

// Event handler for the SHADOW button


public void draw() {
  getUserInput(); // Polling the input device
  background(255, 255, 240);

  fill(0, 0, 255, 32);
  noStroke();
  println(int(px)); 
  myPort.write(int(px));

// I tried to pass this way, but I still don't know how make Arduino recognize and split them
/**  lista[0] = int(px); 
  lista[1] = int(py);
  lista[2] = int(pz);
  lista[3] = int(pw);

for (int i = 0; i < 4) {
  println(lista[i]); 
  myPort.write(lista[i]);
  if (myPort.readStringUntil('\n') == "k"){ 
    i += 1
}
  else{}
println("---------"); 
  */
}

Now, the Arduino code:

#include <Servo.h>

char val;
Servo servo1;
int x_pos;
int servo1_pin = 9;
int initial_position = 90;
int x_pos_pas = 50;

void setup() {
  Serial.begin(9600);
  servo1.attach(servo1_pin); 
  servo1.write (initial_position);
}

void loop() {
  if (Serial.available()) 
   { 
     val = Serial.read();
     x_pos = int(int(val) * 1.8);
   }
  if(x_pos != x_pos_pas){
    servo1.write(x_pos);    
  }
  else{}
  x_pos_pas = x_pos;
  delay(10);
}
1

There are 1 best solutions below

3
On BEST ANSWER

It's a bit confusing what you're trying to do, but from what I can gather the confusion is around serial communication.

here's how I understand how the communication works with your current code and a few issues with that:

1. Truncation

You're writing px which is mapped to the sketch dimensions (which I can't see anywhere, so guessing it's the default 100x100 ?).

If the dimensions are bigger than 255 this may be a problem.

Even through Processing Serial write(int) takes an int, behind the scenes it's using jssc.Serial.writeInt() which takes a value from 0 to 255. (Essentially it writes a single byte, but within 0-255 range, where as the byte type in Processing java is from -127 to 128).

If you want to send values greater than 255 you would need to split them into individual bytes. (see the ARGB value split example using bit shifting and AND masking and using OR to put the individual bytes together into a multiple byte integer)

2. Repetition

px is sent again in the for loop: myPort.write(lista[i]); The code is commented anyway, but it's something to be mindful of. Personally I make new simple test sketches solving individual simple problems instead of large unused blocks which make the code harder to read/navigate and ultimately debug. I encourage you to split the bigger problem into multiple simpler single problems, and solving one at a time.

3. Data termination

The commented code attempts to send 4 integers, but there is no way for Arduino to know in which order the data arrives. If anything goes wrong it will be very hard to tell what's the order.

There are multiple ways to do this. Ideally you would write a communication protocol. (and there are many resources on this topic.)

It sounds like a fun project to learn about working with bits/bytes/byte arrays and putting together a communication protocol from scratch with some form of data verification (checksum / CRC / etc.)

If you're tight on time and you simply want to drive a servo without worrying too much about reliably sending multiple (potentially) large values over serial I recommend trying Firmata Processing

You would need to flash the Firmata firmware to your Arduino then use the Firmata Processing library to connect to the serial port (with the correct baud rate) and call arduino.servoWrite() as required. See the library's arduino_servo example as well.

Based on your comment on mapped values between 0 -> 100, you could use repurpose the SerialEvent example using any other character > 100 to differentiate data from the string terminator char. Bare in mind you wil be loosing precision:

  1. getValue() returns a float which has 32bit precision
  2. You could in theory use the same technique with 0-254 (using 255 as the terminator character). 0-100 range uses 6 bits

Here is a rough example of what might look like in code, taking into account the code has not been tested with actual devices:

Processing:

import processing.serial.*;

import org.gamecontrolplus.gui.*;
import org.gamecontrolplus.*;
import net.java.games.input.*;

Serial myPort;

ControlIO control;
ControlDevice stick;
float px, py, pz, pw;

// px,py,pz,pw remapped to 0-100 range
int[] lista = new int[4];
// px,py,pz as bytes + terminator character (255)
byte[] toArduino = {0,0,0,0,(byte)255};

public void setup() {
  size(100,100);
  lista = new int[4];
  String portName = Serial.list()[2]; 
  try{
    myPort = new Serial(this, portName, 9600);
  }catch(Exception e){
    println("error connecting to serial port: " + portName);
    e.printStackTrace();
  }
  surface.setTitle("GCP Joystick example");

  control = ControlIO.getInstance(this);

  try{
    stick = control.filter(GCP.STICK).getMatchedDevice("joystick");
  }catch(Exception e){
    e.printStackTrace();
  }
  if (stick == null) {
    println("No suitable device configured");
  }

}


public void getUserInput() {
  if(stick == null){
    return;
  }
  // map values
  px = map(stick.getSlider("X").getValue(), -1, 1, 0, width);
  py = map(stick.getSlider("Y").getValue(), -1, 1, 0, height);
  pz = map(stick.getSlider("Z").getValue(), -1, 1, 0, width);
  pw = map(stick.getSlider("W").getValue(), -1, 1, 0, height);
  // cast to int
  lista[0] = (int)px;
  lista[1] = (int)py;
  lista[2] = (int)pz;
  lista[3] = (int)pw;
  // update bytearray
  toArduino[0] = (byte)lista[0];
  toArduino[1] = (byte)lista[1];
  toArduino[2] = (byte)lista[2];
  toArduino[3] = (byte)lista[3];
}

// Event handler for the SHADOW button


public void draw() {
  getUserInput(); // Polling the input device
  background(255, 255, 240);

  fill(0, 0, 255, 32);
  noStroke();

  text("px: " + px
    +"\npy: " + py
    +"\npz: " + pz
    +"\npw: " + pw
    ,5,15);

  if(myPort != null){
    myPort.write(toArduino);
  }
}

Arduino:

/*
  Serial Event example

  When new serial data arrives, this sketch adds it to a String.
  When a newline is received, the loop prints the string and clears it.

  A good test for this is to try it with a GPS receiver that sends out
  NMEA 0183 sentences.

  NOTE: The serialEvent() feature is not available on the Leonardo, Micro, or
  other ATmega32U4 based boards.

  created 9 May 2011
  by Tom Igoe

  This example code is in the public domain.

  http://www.arduino.cc/en/Tutorial/SerialEvent
*/
String inputString = "";         // a String to hold incoming data
bool stringComplete = false;  // whether the string is complete

const char TERMINATOR = (char)255;

void setup() {
  // initialize serial:
  Serial.begin(9600);
  // reserve 200 bytes for the inputString:
  inputString.reserve(200);
}

void loop() {
  // in case serialEvent() isn't called automatically
  serialEvent();
  // print the string when a newline arrives:
  if (stringComplete) {
    // process string
    uint8_t px = inputString.charAt(0);
    uint8_t py = inputString.charAt(1);
    uint8_t pz = inputString.charAt(2);
    uint8_t pw = inputString.charAt(3);
    // TODO: pass this to servos
    // debug print
    Serial.print("px:");
    Serial.print(px);
    Serial.print("\tpy:");
    Serial.print(py);
    Serial.print("\tpz:");
    Serial.print(pz);
    Serial.print("\tpw:");
    Serial.println(pw);
    // clear the string:
    inputString = "";
    stringComplete = false;
  }
}

/*
  SerialEvent occurs whenever a new data comes in the hardware serial RX. This
  routine is run between each time loop() runs, so using delay inside loop can
  delay response. Multiple bytes of data may be available.
*/
void serialEvent() {
  while (Serial.available()) {
    // get the new byte:
    char inChar = (char)Serial.read();
    // add it to the inputString:
    inputString += inChar;
    // if the incoming character is a newline, set a flag so the main loop can
    // do something about it:
    if (inChar == TERMINATOR) {
      stringComplete = true;
    }
  }
}