Skip to content

14. Interface and Application Programming

processing

Overview of week 14 assignment

  1. Group assignment
    1. compare as many tool options as possible
  2. Individual assignment
    1. write an application that interfaces a user with an input &/or output device that you made

1. Group assignment

For more information, see the Week 14: Group assignment page.

2. Individual assignment

A. Stepper Motor Control with Dial in Processing

This week, I experimented with controlling a stepper motor from a Processing application using G-code-ish commands. I started with a simple setup using one stepper motor and tried rotating it with a dial in Processing.

a. Wiring and components

I reused the setup from week 10, which includes a Nema17 stepper motor, A4988 driver, and an AC adaptor. Please see week 10 for more details.

A4988 RP2040
STEP 27
DIR 26
MS1 28
MS2 29
MS3 6

wiring

b. Arduino and Processing code

In this setup, Processing reads the mouse movement and clicks, then sends a message (like G-code) to the Arduino via serial, which turns the stepper motor.

Although I used Processing a long time ago, I'm not familiar with Java, so I relied heavily on ChatGPT, especially for the Processing part.

Prompt:

  • Create a simple circular UI in Processing to send G-code commands using serial communication between Processing and RP2040
  • Program RP2040 using the Arduino IDE to control one stepper motor via A4988 driver

References:


G-code (-ish) formatting in Processing:

1
2
3
String gcode = "ROTATE X" + int(angle);  // Updated command format
println("Sending: " + gcode);
port.write(gcode + "\n");
  • "ROTATE": A custom command to move the motor to a specific angle. For example, "ROTATE X90" means "rotate to 90 degrees".
  • "X": The target angle.
  • int(angle): Converts the angle to an integer.
  • port.write(... + "\n"): Sends the command to Arduino, ending with a newline.
Processing code (Expand here!)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import processing.serial.*;

Serial port;
float angle = 0;
int radius = 100;
PVector center;

void setup() {
  size(400, 400);
  center = new PVector(width / 2, height / 2);

  println(Serial.list());
  port = new Serial(this, "/dev/tty.usbmodem14101", 115200); // Update port as needed
}

void draw() {
  background(10);

  // Draw dial
  stroke(255);
  fill(0);
  ellipse(center.x, center.y, radius * 2, radius * 2);

  // Draw knob
  float knobX = center.x + cos(radians(angle)) * radius;
  float knobY = center.y + sin(radians(angle)) * radius;
  strokeWeight(8);
  stroke(255);
  line(center.x, center.y, knobX, knobY);

  // Show angle text
  fill(255);
  textAlign(CENTER);
  textSize(30);
  text("Angle: " + int(angle) + "°", width / 2, height - 50);
}

void mousePressed() {
  float dx = mouseX - center.x;
  float dy = mouseY - center.y;
  float newAngle = degrees(atan2(dy, dx));
  if (newAngle < 0) newAngle += 360;

  angle = newAngle;

  String gcode = "ROTATE X" + int(angle);  // Updated command format
  println("Sending: " + gcode);
  port.write(gcode + "\n");
}

G-code decoding in Arduino:

String gcode = Serial.readStringUntil('\n');
if (gcode.startsWith("ROTATE")) {
  int xIndex = gcode.indexOf('X');
  float angle = gcode.substring(xIndex + 1).toFloat();
}
  • Serial.readStringUntil('\n'): Reads the G-code command sent from Processing.
  • gcode.startsWith("ROTATE"): Checks if the command is "ROTATE".
  • gcode.indexOf('X'): Finds the position of X in the command.
  • gcode.substring(xIndex + 1).toFloat(): Extracts and converts the angle value to a float.
  • moveTo(targetSteps): Moves the motor to the desired position based on the angle.
Arduino code (Expand here!)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
const int STEP_PIN = 27;
const int DIR_PIN = 26;
const int STEPS_PER_ROTATION = 3200;

const int MS1 = 28;
const int MS2 = 29;
const int MS3 = 6;

long currentPos = 0;

void setup() {
  Serial.begin(115200);

  pinMode(STEP_PIN, OUTPUT);
  pinMode(DIR_PIN, OUTPUT);

  pinMode(MS1, OUTPUT);
  pinMode(MS2, OUTPUT);
  pinMode(MS3, OUTPUT);

  // Set 1/16 microstepping
  digitalWrite(MS1, HIGH);
  digitalWrite(MS2, HIGH);
  digitalWrite(MS3, HIGH);
}

void loop() {
  if (Serial.available()) {
    String gcode = Serial.readStringUntil('\n');
    gcode.trim();

    if (gcode.startsWith("ROTATE")) {  // Updated command keyword
      int xIndex = gcode.indexOf('X');
      if (xIndex != -1) {
        float angle = gcode.substring(xIndex + 1).toFloat();
        long targetSteps = angle / 360.0 * STEPS_PER_ROTATION;
        moveTo(targetSteps);
      }
    }
  }
}

void moveTo(long targetSteps) {
  long delta = targetSteps - currentPos;
  bool dir = delta >= 0;
  digitalWrite(DIR_PIN, dir ? LOW : HIGH);

  for (long i = 0; i < abs(delta); i++) {
    digitalWrite(STEP_PIN, HIGH);
    delayMicroseconds(500);
    digitalWrite(STEP_PIN, LOW);
    delayMicroseconds(500);
  }

  currentPos = targetSteps;
}

c. Outcome


B. XY Motion Control with Processing and G-code

Next, I added another motor to simulate the XY motion.

a. Wiring and components

Basically, I added one more set of A4988 and Nema 17 motor, while keeping the other components from the previous setup. For the new A4988 driver, I wired it according to the table below, and the MS1, MS2, and MS3 pins are shared with both of A4988 drivers.

A4988 1st (X) RP2040
STEP 27
DIR 26
A4988 2nd (Y)
STEP 29
DIR 28
Microstep mode (shared with two A4988s)
MS1 3
MS2 4
MS3 2

I also added an OLED display to show the current G-code command.

OLED (SH1106) RP2040
GND GND
VCC 3.3V
SCL 7
SDA 6

And here is the wiring...

wiring_2

First, I did a quick test to make sure both motors were working properly, and surprisingly it worked.

b. Arduino and Processing code

Prompt:

  • Develop a simple rectangular UI in Processing to send G-code commands via serial communication to an RP2040. The interface tracks mouse position and clicks, converts the XY coordinates into G-code, and transmits the commands to the RP2040 through serial communication.
  • Program the RP2040 using the Arduino IDE to control two stepper motors via A4988 drivers, with one motor assigned to X-axis movement and the other to Y-axis. Additionally, display the current G-code command on an OLED display.

G-code sender in Processing:

Processing code (Expand here!)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import processing.serial.*; // Import serial communication library

Serial port;
PVector pos;          // Current position of the tool head
int canvasSize = 400; // Size of the canvas
int squareSize = 300; // Size of the XY control square
PVector origin;       // Top-left of the square

void settings() {
  size(canvasSize, canvasSize); // Must be in settings() in some Processing modes
}

void setup() {
  pos = new PVector(width / 2, height / 2);
  origin = new PVector((width - squareSize) / 2, (height - squareSize) / 2);

  println(Serial.list()); // List available serial ports

  // Replace with your actual port name
  port = new Serial(this, "/dev/tty.usbmodem14101", 115200);
}

void draw() {
  background(255);

  // Draw control square
  stroke(0);
  noFill();
  rect(origin.x, origin.y, squareSize, squareSize);

  // Draw crosshairs
  stroke(200);
  line(origin.x + squareSize / 2, origin.y, origin.x + squareSize / 2, origin.y + squareSize);
  line(origin.x, origin.y + squareSize / 2, origin.x + squareSize, origin.y + squareSize / 2);

  // Draw current position dot
  fill(0, 100, 255);
  noStroke();
  ellipse(pos.x, pos.y, 10, 10);

  // Display current position as G-code units (0–300)
  fill(0);
  textAlign(LEFT);
  text("X: " + int(pos.x - origin.x) + ", Y: " + int(pos.y - origin.y), 10, height - 10);
}

void mousePressed() {
  if (overSquare(mouseX, mouseY)) {
    sendGcode(mouseX, mouseY);
  }
}

void mouseDragged() {
  if (overSquare(mouseX, mouseY)) {
    sendGcode(mouseX, mouseY);
  }
}

boolean overSquare(float x, float y) {
  return (x >= origin.x && x <= origin.x + squareSize &&
          y >= origin.y && y <= origin.y + squareSize);
}

void sendGcode(float mx, float my) {
  // Constrain inside square
  float x = constrain(mx, origin.x, origin.x + squareSize);
  float y = constrain(my, origin.y, origin.y + squareSize);

  pos.set(x, y);

  // Convert to 0–300 workspace units
  float xG = x - origin.x;
  float yG = y - origin.y;

  String gcode = "G1 X" + int(xG) + " Y" + int(yG);
  println("Sending: " + gcode);
  port.write(gcode + "\n");
}

G-code decoding in Arduino:

  1. setup()
    • Initializes serial, motors, and OLED display.
  2. loop()
    • Waits for G-code (e.g., G1 X100 Y50).
    • Displays it and moves motors.
  3. displayGcode(gcode)
    • Shows G-code on OLED.
  4. processGcode(gcode)
    if (gcode.startsWith("G1")) {
      int xIndex = gcode.indexOf('X');
      int yIndex = gcode.indexOf('Y');
    
      // Extract and convert X and Y values
      if (xIndex != -1) {
        String xStr = gcode.substring(xIndex + 1, nextSpace);
        float xVal = xStr.toFloat();
        targetX = xVal / MAX_POS * STEPS_PER_REV;
      }
    
      moveToXY(targetX, targetY);  // Move motors to new position
    }
    
    • Parses G1 command and extracts X/Y.
    • Calls moveToXY().
  5. moveToXY(x, y): Moves motors to target X/Y position.

    • Direction Setup:

      • Sets the direction of motors based on the target position relative to the current position.
      • dirX and dirY control the movement direction for the X and Y axes.
      digitalWrite(DIR_PIN_X, dirX ? HIGH : LOW);
      digitalWrite(DIR_PIN_Y, dirY ? HIGH : LOW);
      
    • Loop to Move Motors:

      • This loop moves both motors for the required steps to reach the target positions.
      • maxSteps is the greater of stepsX and stepsY, ensuring both motors move together.
      • Each iteration sends a step pulse to both motors and includes a delay to control the speed.
      for (long i = 0; i < maxSteps; i++) {
          if (i * stepsX / maxSteps < stepsX)
              digitalWrite(STEP_PIN_X, HIGH);
      
          if (i * stepsY / maxSteps < stepsY)
              digitalWrite(STEP_PIN_Y, HIGH);
      
          delayMicroseconds(500);
      
          if (i * stepsX / maxSteps < stepsX)
              digitalWrite(STEP_PIN_X, LOW);
      
          if (i * stepsY / maxSteps < stepsY)
              digitalWrite(STEP_PIN_Y, LOW);
      
          delayMicroseconds(500);
      }
      
  6. Position Update:

    • Updates posX and posY to the target position after movement.

      posX = targetX;
      posY = targetY;
      
Arduino code (Expand here!)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#include <Wire.h>
#include <U8g2lib.h>

// Stepper Motor 1 (X axis)
const int STEP_PIN_X = 27;
const int DIR_PIN_X = 26;

// Stepper Motor 2 (Y axis)
const int STEP_PIN_Y = 29;
const int DIR_PIN_Y = 28;

// Motion settings
const int STEPS_PER_REV = 3200; // 200 * 16 microsteps
const int MAX_POS = 300;

// Microstepping control pins (shared)
const int MS1 = 3;
const int MS2 = 4;
const int MS3 = 2;

// Position
long posX = 0;
long posY = 0;

// OLED display
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

String lastGcode = "";

void setup() {
  Serial.begin(115200);

  // Motor setup
  pinMode(STEP_PIN_X, OUTPUT);
  pinMode(DIR_PIN_X, OUTPUT);
  pinMode(STEP_PIN_Y, OUTPUT);
  pinMode(DIR_PIN_Y, OUTPUT);

  pinMode(MS1, OUTPUT);
  pinMode(MS2, OUTPUT);
  pinMode(MS3, OUTPUT);

  // Set 1/16 microstepping
  digitalWrite(MS1, HIGH);
  digitalWrite(MS2, HIGH);
  digitalWrite(MS3, HIGH);

  // OLED setup
  u8g2.begin();
  u8g2.setFont(u8g2_font_10x20_tr);
}

void loop() {
  if (Serial.available()) {
    String gcode = Serial.readStringUntil('\n');
    gcode.trim();
    if (gcode.length() > 0) {
      lastGcode = gcode;
      displayGcode(lastGcode);  // Show on OLED
    }

    if (gcode.startsWith("G1")) {
      processGcode(gcode);
    }
  }
}

void displayGcode(String gcodeText) {
  u8g2.clearBuffer();
  u8g2.drawStr(0, 12, gcodeText.c_str());
  u8g2.sendBuffer();
}

void processGcode(String gcode) {
  int xIndex = gcode.indexOf('X');
  int yIndex = gcode.indexOf('Y');

  long targetX = posX;
  long targetY = posY;

  if (xIndex != -1) {
    int nextSpace = gcode.indexOf(' ', xIndex);
    String xStr = gcode.substring(xIndex + 1, nextSpace == -1 ? gcode.length() : nextSpace);
    float xVal = xStr.toFloat();
    targetX = xVal / MAX_POS * STEPS_PER_REV;
  }

  if (yIndex != -1) {
    int nextSpace = gcode.indexOf(' ', yIndex);
    String yStr = gcode.substring(yIndex + 1, nextSpace == -1 ? gcode.length() : nextSpace);
    float yVal = yStr.toFloat();
    targetY = yVal / MAX_POS * STEPS_PER_REV;
  }

  moveToXY(targetX, targetY);
}

void moveToXY(long targetX, long targetY) {
  long dx = targetX - posX;
  long dy = targetY - posY;

  bool dirX = dx >= 0;
  bool dirY = dy >= 0;

  digitalWrite(DIR_PIN_X, dirX ? HIGH : LOW);
  digitalWrite(DIR_PIN_Y, dirY ? HIGH : LOW);

  long stepsX = abs(dx);
  long stepsY = abs(dy);
  long maxSteps = max(stepsX, stepsY);

  for (long i = 0; i < maxSteps; i++) {
    if (i * stepsX / maxSteps < stepsX) {
      digitalWrite(STEP_PIN_X, HIGH);
    }
    if (i * stepsY / maxSteps < stepsY) {
      digitalWrite(STEP_PIN_Y, HIGH);
    }

    delayMicroseconds(500);

    if (i * stepsX / maxSteps < stepsX) {
      digitalWrite(STEP_PIN_X, LOW);
    }
    if (i * stepsY / maxSteps < stepsY) {
      digitalWrite(STEP_PIN_Y, LOW);
    }

    delayMicroseconds(500);
  }

  posX = targetX;
  posY = targetY;
}

3. Files

No files this week, code in the text.

Afterthoughts

  • G28