Skip to content

Week 9

Assignment

  • Control circuit over wireless network
  • Send or get data from the internet

Project

For my final project, I am doing a Rubik's cube solver so I decided to control the solver via ESP8266.

The idea was to create API which would be able to find a solution to scrambled Rubik's cube and this solution would be requested by the ESP. But ESP does not have that many pins, so I decided to send via serial the solution to Arduino and control the robot from there.

Sequence diagram

Schema

Wiring

Schema

Source codes

Arduino
#include <AccelStepper.h>

#define MOTOR_INTERFACE_TYPE 1
#define ENABLE 12
const int stepsPerRevolution = 200;
const int motorSpeed = 16000;

const int stepperCount = 6;
const int potPin = A6;

AccelStepper steppers[stepperCount] = {
  AccelStepper(MOTOR_INTERFACE_TYPE, 2, 3),
  AccelStepper(MOTOR_INTERFACE_TYPE, 4, 5),
  AccelStepper(MOTOR_INTERFACE_TYPE, 6, 7),
  AccelStepper(MOTOR_INTERFACE_TYPE, 8, 9),
  AccelStepper(MOTOR_INTERFACE_TYPE, A0, A1),
  AccelStepper(MOTOR_INTERFACE_TYPE, A2, A3)
};

void setup() {
  pinMode(ENABLE, OUTPUT);
  digitalWrite(ENABLE, HIGH);
  Serial.begin(115200);

  for (int i = 0; i < stepperCount; i++) {
    steppers[i].setMaxSpeed(motorSpeed);
    steppers[i].setAcceleration(motorSpeed);
  }
  delay(1000);
  digitalWrite(ENABLE, LOW);
}

int getDelayFromPot() {
  int potValue = analogRead(potPin);
  return map(potValue, 0, 1023, 10, 200);
}

void moveAndWait(AccelStepper &stepper, int steps) {
  stepper.move(steps);
  while (stepper.distanceToGo() != 0) {
    stepper.run();
  }
  delay(getDelayFromPot());
}

int processNextMove(int index, String data) {
  AccelStepper motor;
  switch (data.charAt(index)) {
    case 'F':
      motor = steppers[0];
      break;
    case 'R':
      motor = steppers[1];
      break;
    case 'B':
      motor = steppers[2];
      break;
    case 'U':
      motor = steppers[3];
      break;
    case 'D':
      motor = steppers[4];
      break;
    case 'L':
      motor = steppers[5];
      break;
    default:
      return data.length();
  }
  int dirMultiplier = -1;
  int moveMultiplier = 1;
  int nextIndex = index + 3;
  switch(data.charAt(index + 1)) {
    case '\'':
      dirMultiplier = 1;
      break;
    case '2':
      moveMultiplier = 2;
      break;
    case ' ':
      nextIndex--;
      break;
  }
  for (int i = 0; i < moveMultiplier; i++) {
    moveAndWait(motor, stepsPerRevolution/4*dirMultiplier);
  }
  return nextIndex;
}

void solveCube(String data) {
  int index = 0;

  while (index < data.length()) {
    index = processNextMove(index, data);
  }
}

void loop() {
  if (Serial.available() > 0) {
    String receivedData = Serial.readStringUntil('\n');

    solveCube(receivedData);
  }
  delay(100);
}
ESP8266
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266HTTPClient.h>

const char* ssid = "ssid";
const char* password = "pswd";

// solver API
const char* flask_host = "192.168.0.109";
const int flask_port = 8080;

ESP8266WebServer server(8081);

void handleSolve() {
  String cube = server.uri();
  cube.replace("/solve/", "");

  if (cube.length() != 54) {
    server.send(400, "text/plain", "Cube string must be 54 characters.");
    return;
  }

  WiFiClient client;
  HTTPClient http;

  String url = "http://" + String(flask_host) + ":" + String(flask_port) + "/solve/" + cube;

  http.begin(client, url);
  int httpCode = http.GET();

  if (httpCode > 0) {
    String payload = http.getString();
    Serial.println(payload);
    server.send(200, "application/json", payload);
  } else {
    server.send(500, "text/plain", "Failed to contact solver");
  }

  http.end();
}

void setup() {
  Serial.begin(115200);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }

  server.enableCORS(true);
  server.on("/solve/", HTTP_GET, handleSolve);
  server.onNotFound([]() {
    if (server.uri().startsWith("/solve/")) {
      handleSolve();
    } else {
      server.send(404, "text/plain", "Not found");
    }
  });

  server.begin();
}

void loop() {
  server.handleClient();
}
Rubik's cube solver
from enum import Enum

import twophase.solver as sv

'''URFDLB'''

face_indices = {
    'U': [0, 1, 2, 3, 5, 6, 7, 8],
    'R': [9, 10, 11, 12, 14, 15, 16, 17],
    'F': [18, 19, 20, 21, 23, 24, 25, 26],
    'D': [27, 28, 29, 30, 32, 33, 34, 35],
    'L': [36, 37, 38, 39, 41, 42, 43, 44],
    'B': [45, 46, 47, 48, 50, 51, 52, 53]
}

class TryEnum(Enum):
    FIRST_TRY = 1
    SECOND_TRY = 2
    FAIL = 3


def rotate_face_around_center(cube, face, clockwise=True):
    cube = list(cube)
    idx = face_indices[face]
    rotate_map = [0, 1, 2, 3, 4, 5, 6, 7]

    if clockwise:
        rotated_order = [5, 3, 0, 6, 1, 7, 4, 2]
    else:
        rotated_order = [2, 4, 7, 1, 6, 0, 3, 5]

    original = [cube[idx[i]] for i in rotate_map]
    for i, j in enumerate(rotated_order):
        cube[idx[rotate_map[i]]] = original[j]

    return cube


def rotate_faces_sideways(cube):
    cube = list(cube)
    sides = ['L', 'F', 'R', 'B']

    saved = [ [cube[i] for i in face_indices[face]] for face in sides ]
    for i, face in enumerate(sides):
        prev_face = sides[i - 1]
        for idx, val in zip(face_indices[face], saved[sides.index(prev_face)]):
            cube[idx] = val

    cube = rotate_face_around_center(cube, 'U', clockwise=False)
    cube = rotate_face_around_center(cube, 'D', clockwise=True)

    return ''.join(cube)

def format_result(result):
    sp = result.split('(')[0]
    return sp.replace('1', '').replace('3', '\'')

def rotate_turns_back(result):
    cp = list(result)
    for i in range(len(cp)):
        if cp[i] == 'B':
            cp[i] = 'R'
        elif cp[i] == 'R':
            cp[i] = 'F'
        elif cp[i] == 'F':
            cp[i] = 'L'
        elif cp[i] == 'L':
            cp[i] = 'B'
    return ''.join(cp)

def solve(cube: str, max_length: int, timeout: int):
    res = sv.solve(cube, max_length, timeout)
    if 'Error' not in res:
        return TryEnum.FIRST_TRY, format_result(res)
    cube = rotate_faces_sideways(cube)
    res = sv.solve(cube, max_length, timeout)
    if 'Error' not in res:
        res = rotate_turns_back(res)
        return TryEnum.SECOND_TRY, format_result(res)
    return TryEnum.FAIL, res

if __name__ == '__main__':
    '''URFDLB'''

    cubestring = 'LRDFUDUFRURRLRRFBRRDBLFDBLULFLRDUFBDBUFLLBLFDFUUBBUBDD'

    tries, solution = solve(cubestring, 20, 1)
    print()
    print(tries.name, solution)
Flask API
from flask import Flask, request, jsonify
from flask_cors import CORS
from rubiks_solver import solve

app = Flask(__name__)
CORS(app)

@app.route("/solve/<cube>")
def solve_cube(cube):
max_length = int(request.args.get("max_length", 20))
timeout = int(request.args.get("timeout", 1))

    if len(cube) != 54:
        return jsonify({"error": "Cube string must be exactly 54 characters."}), 400

    result_type, solution = solve(cube, max_length, timeout)
    print(solution)
    return solution

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

Result