14. Interface and Application Programming

Overview of week 14 assignment
- Group assignment
- compare as many tool options as possible
- Individual assignment
- 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 |

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:
| 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...

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:
setup()
- Initializes serial, motors, and OLED display.
loop()
- Waits for G-code (e.g.,
G1 X100 Y50).
- Displays it and moves motors.
displayGcode(gcode)
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().
-
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);
}
-
Position Update:
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