1. Introduction
Welcome to the documentation for the Cubexell CubeSolver. This project is a complete Java-based hardware and software pipeline designed to autonomously scan, calculate, and physically solve a 3x3 Rubik's Cube via Raspberry Pi.
System Overview
- Camera & Vision: A Raspberry Pi camera captures the cube's faces from below. OpenCV (via JavaCPP) processes the images to extract sticker colors.
- State Generation: The color data is mapped into a mathematical 3D array representation.
- Solver Engine: A selected algorithm calculates the most efficient series of moves.
- Motor Control: The solution is translated into electronic signals sent via Pi4J to RoHS stepper motors.
2. Installation
Here is a link to the repository: https://github.com/randomcuber/cubesolverCampFull
Ensure your Raspberry Pi is running a recent OS version with Java installed and the PiCamera enabled.
# Clone the repository
git clone https://github.com/randomcuber/cubesolverCampFull.git
cd cubesolverCampFull
# Compile using your standard Java build tool (e.g., Maven/Gradle)
# mvn clean install
3. Reference
This section documents the core Java architecture.
Hardware: Robot & Motors
The Robot abstract class acts as the central hardware manager, maintaining references to six Motor interfaces (Up, Down, Right, Left, Front, Back). Its implementation is RaspberryPiRobot.
RohsStepperMotor.java implements Motor using Pi4J to control RoHS steppers. It uses a half-stepping 8-step magnet firing sequence to move the driver pins (in1, in2, in3, in4).
public void doTurn(double stepsToTurn, boolean direction) {
// ... loop iterates stepsToTurn times ...
if (stepSequence[motorStepCounter][0] == 1) {
in1.high();
} else {
in1.low();
}
// ... (repeats for in2, in3, in4) ...
if (direction) {
motorStepCounter = ((motorStepCounter-1) + stepSequence.length) % stepSequence.length;
} else {
motorStepCounter = (motorStepCounter+1) % stepSequence.length;
}
LockSupport.parkNanos(1000000); // wait 1 millisecond
}
public void reset() {
in1.low(); in2.low(); in3.low(); in4.low();
}
How it works: The doTurn() method fires the electromagnets inside the stepper motor in a specific sequence (stored in the stepSequence 2D array). To step backward or forward without throwing an ArrayIndexOutOfBoundsException, it uses modulo arithmetic (% stepSequence.length) to loop the counter perfectly from 7 back to 0. The LockSupport.parkNanos() call forces the thread to sleep for 1 millisecond between steps, granting the motor the physical time needed to move the gear before the next magnet fires. Finally, reset() cuts power entirely so the cube can physically "corner cut" and snap into alignment.
Vision: Camera & Inspector
The implementation, OpenCvRaspberryPiCamera, uses JavaCPP and OpenCV. It captures frames and reads reference colors from a dynamically updated labValues.txt file to determine sticker colors.
public static void readLabReferenceValues() {
try (BufferedReader reader = new BufferedReader(new FileReader("labValues.txt"))) {
String line;
while((line = reader.readLine()) != null) {
String[] parts = line.trim().split(" ");
int i = Integer.parseInt(parts[0]);
int j = Integer.parseInt(parts[1]);
int k = Integer.parseInt(parts[2]);
realReferenceColors[i][j][k][0] = Integer.parseInt(parts[3]);
realReferenceColors[i][j][k][1] = Integer.parseInt(parts[4]);
realReferenceColors[i][j][k][2] = Integer.parseInt(parts[5]);
}
} catch (IOException e) {
e.printStackTrace();
}
}
How it works: Because lighting changes the RGB value of a sticker significantly, the camera relies on the LAB color space. This method reads a locally cached text file containing the tuned LAB thresholds for your specific room. It splits each line by spaces, assigning the coordinates (i, j, k) to the massive 4-dimensional realReferenceColors array, mapping the exact L, A, and B integers required to identify White, Yellow, Green, etc.
Data: Cube & Move Optimization
The puzzle state is stored efficiently as a 3D character array: char[6][3][3]. The Cube.java class also handles condensing messy, redundant move lists.
if (moveLayer.equalsIgnoreCase(nextMoveLayer)) {
int newRotationNum = (moveRotations + nextMoveRotations) % 4;
String newMove;
if (newRotationNum > 0) {
if (newRotationNum == 1) {
newMove = moveLayer;
} else if (newRotationNum == 2) {
newMove = moveLayer + "2";
} else {
newMove = moveLayer + "i";
}
newSolution.add(newMove);
}
i++;
}
How it works: Algorithms often generate inefficient sequences, like turning the Right face clockwise (R), then clockwise again (R). This optimizer assigns a numerical value to turns (e.g., R = 1, R2 = 2, Ri = 3). If two adjacent moves share the same face (moveLayer), it adds their values together and uses modulo 4. If the result is 0 (like R + Ri = 1 + 3 = 4 % 4 = 0), the move cancels out completely and is skipped. If the result is 2 (R + R = 2), it outputs R2.
Solver Algorithms
Classes must implement CubeSolvingMethod. The project contains multiple approaches, from beginner methods to advanced Kociemba algorithms.
BeginnerMethod.java
public void solveWG() {
char[] position = cube.findEdgePosition('W', 'G');
if (position[0] == 'U' && position[1] == 'F') {
return;
} else if (position[0] == 'F' && position[1] == 'U') {
cube.recordMoves(solution, new String[]{"F", "R", "U"});
} else if (position[0] == 'U' && position[1] == 'R') {
cube.recordMoves(solution, new String[]{"U"});
}
// ... extended logic for all 24 possible orientations ...
}
How it works: This deterministic, state-matching approach locates exactly where a specific piece is using findEdgePosition (returning its two face coordinates, like Up and Front). It then checks a massive if-else block. If the piece is in the exact right spot but flipped (F and U), it blindly injects the hardcoded correction macro {"F", "R", "U"} into the master solution array.
OldPochmannMethod.java
public void solveEdges() {
HashSet<String> unsolvedEdges = createUnsolvedEdges();
while (unsolvedEdges.size() > 0 && !cube.areEdgesSolved()) {
String faceColor = getEdgeToBeSolved();
while (!"WR".equalsIgnoreCase(faceColor) &&
!"RW".equalsIgnoreCase(faceColor)) {
executeEdgeSwap(faceColor, solution);
if (unsolvedEdges.contains(faceColor)) {
unsolvedEdges.remove(faceColor);
unsolvedEdges.remove(cube.getReverseOfString(faceColor));
}
faceColor = getEdgeToBeSolved();
}
if (unsolvedEdges.size() > 0) {
// Cycle break: swap with a random remaining unsolved edge
executeEdgeSwap(unsolvedEdges.iterator().next(), solution);
}
}
}
How it works: This method simulates the classic blindfolded solving technique by swapping pieces one at a time from a specific "buffer" position to their correct target locations. The solveEdges() loop tracks all remaining unsolved pieces in a HashSet. The inner while loop continuously identifies the piece currently sitting in the buffer (getEdgeToBeSolved()) and executes an algorithm to shoot it to its true home (executeEdgeSwap). It repeats this sequence until the buffer accidentally gets filled with its own solved piece ("WR" or "RW"). If the buffer piece gets trapped but there are still unsolved edges left in the puzzle, the outer if statement forces a "cycle break" by swapping the buffer piece with an arbitrary remaining unsolved edge to keep the solve going.
KociembaAlgorithm.java
public String[] solve(char[][][] cubeColors) {
StringBuilder sb = new StringBuilder();
// ... looping and standardizing color chars ...
appendColorsOnFace(cubeColors[UP_FACE_INDEX], sb);
appendColorsOnFace(cubeColors[RIGHT_FACE_INDEX], sb);
appendColorsOnFace(cubeColors[FRONT_FACE_INDEX], sb);
appendColorsOnFace(cubeColors[DOWN_FACE_INDEX], sb);
appendColorsOnFace(cubeColors[LEFT_FACE_INDEX], sb);
appendColorsOnFace(cubeColors[BACK_FACE_INDEX], sb);
String facelets = sb.toString();
String solution = Search.solution(facelets, maxDepth, maxTime, useSeparator);
return convertSolution(solution);
}
How it works: The Kociemba mathematical solver requires a very specific 54-character string format (U1-U9, R1-R9, F1-F9, D1-D9, L1-L9, B1-B9). This code flattens the 3-dimensional char[6][3][3] array by appending each face sequentially to a StringBuilder. That formatted string is then passed to the external org.kociemba.twophase.Search.solution() engine to calculate the absolute mathematical minimum number of moves.
Utilities (Scrambler)
The CubeScrambler.java class generates randomized sequences of valid moves to shuffle the puzzle. It features checks to ensure moves are not immediately redundant.
public String[] getScramble(int numMoves) {
String[] scramble = new String[numMoves];
scramble[0] = randomScrambleMove();
for(int i=1; i<numMoves; i++){
char prevMoveFace = getFace(scramble[i-1]);
String move = randomScrambleMove();
char moveFace = getFace(move);
while(moveFace == prevMoveFace) {
move = randomScrambleMove();
moveFace = getFace(move);
}
scramble[i] = move;
}
return scramble;
}
How it works: When generating an array of strings representing scramble moves, this algorithm keeps track of the previous face turned using the prevMoveFace variable. The while(moveFace == prevMoveFace) loop acts as a safety guard; if the randomly selected move turns the exact same face as the step before it (like turning Right, then turning Right again), it continuously re-rolls the random selection until it gets a completely different face.
4. Usage
Interaction happens via Main.java. First, you must navigate on the terminal to the cubeSovlerCampFull directory.
There are 3 arguments when we run a program:
./gradlew run --args="ARG1 ARG2 ARG3"
- ARG1: which method is used (beginner, OP, or kociemba),
B,O,K - ARG2: scramble or use vision? (yes or no),
Y,N - ARG3: Run autotune? (yes or no),
Y,N
Example:
./gradlew run --args="O Y N"
5. Tuning
Vision AutoTune
If lighting changes, you can calibrate the camera by passing Y as the 3rd command line argument. This triggers the autoTune logic in OpenCvRaspberryPiCamera, which scans the cube and writes new reference values to labValues.txt.
Tuning Motor Timing
If your RoHS steppers skip steps, adjust the nanosecond delay in RohsStepperMotor.java:
// In RohsStepperMotor.java -> doTurn() method
LockSupport.parkNanos(1000000); // 1 millisecond. Increase for torque.
6. Contributing
Adding a New Solving Method
public class CustomSolver implements CubeSolvingMethod {
public String[] solve(char[][][] cubeColors) {
// Implement algorithm logic here
return new String[]{"[list of moves to solve cube]"};
}
}
Hardware Support
To support different motor drivers, create a class implementing the Motor interface (turn() and reset()) and inject it into the Robot constructor in Main.java.