diff --git a/robot/distance.h b/robot/distance.h new file mode 100644 index 0000000..bbfd025 --- /dev/null +++ b/robot/distance.h @@ -0,0 +1,19 @@ +#include "Adafruit_VL53L0X.h" +Adafruit_VL53L0X lox = Adafruit_VL53L0X(); + +void initDistance(){ + + if (!lox.begin()) { + Serial.println(F("Failed to boot VL53L0X")); + while(1); + } + lox.startRangeContinuous(); +} + + +void updateDistance(){ + if (lox.isRangeComplete()) { + Serial.print("Distance in mm: "); + Serial.println(lox.readRange()); + } +} diff --git a/robot/encoder.h b/robot/encoder.h new file mode 100644 index 0000000..139fccc --- /dev/null +++ b/robot/encoder.h @@ -0,0 +1,50 @@ + + +struct Encoder{ + int rotations; + int currentAngle; + int estAngle; + bool threashhold; + + float position(){ + return rotations + (estAngle / 360.0); + } +}; + +Encoder encoder; + + +void initEncoder(){ + pinMode(A0, INPUT); + encoder.currentAngle = 0; + encoder.rotations = 0; + encoder.estAngle=0; + encoder.threashhold=false; +} + +void updateEncoder(){ + int rawIn = analogRead(A0); +// static long lastRan = 0; +// if(50>millis()-lastRan) return; +// lastRan = millis(); + + + int rotation = map(rawIn, 4, 348, 0, 360); + int diff = rotation-encoder.currentAngle; + if(!encoder.threashhold && (abs(diff)>=50)){ + encoder.threashhold=true; + if(diff<0){ + encoder.rotations++; + encoder.estAngle = 0; + } + if(diff>0){ + encoder.rotations--; + encoder.estAngle = 360; + } + } + if(abs(diff)<30){ + encoder.threashhold=false; + encoder.estAngle = rotation; + } + encoder.currentAngle = rotation; +} diff --git a/robot/gyro.h b/robot/gyro.h new file mode 100644 index 0000000..3f35628 --- /dev/null +++ b/robot/gyro.h @@ -0,0 +1,120 @@ +#include "MPU6050_6Axis_MotionApps612.h" +//#include "MPU6050.h" // not necessary if using MotionApps include file + + +#if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE +#include "Wire.h" +#endif + +MPU6050 mpu; + +// MPU control/status vars +bool dmpReady = false; // set true if DMP init was successful +uint8_t mpuIntStatus; // holds actual interrupt status byte from MPU +uint8_t devStatus; // return status after each device operation (0 = success, !0 = error) +uint16_t packetSize; // expected DMP packet size (default is 42 bytes) +uint16_t fifoCount; // count of all bytes currently in FIFO +uint8_t fifoBuffer[64]; // FIFO storage buffer + +// orientation/motion vars +Quaternion q; // [w, x, y, z] quaternion container +VectorInt16 aa; // [x, y, z] accel sensor measurements +VectorInt16 gy; // [x, y, z] gyro sensor measurements +VectorInt16 aaReal; // [x, y, z] gravity-free accel sensor measurements +VectorInt16 aaRealLast; // [x, y, z] gravity-free accel sensor measurements +VectorInt16 aaWorld; // [x, y, z] world-frame accel sensor measurements +VectorFloat gravity; // [x, y, z] gravity vector +float euler[3]; // [psi, theta, phi] Euler angle container +float ypr[3]; // [yaw, pitch, roll] yaw/pitch/roll container and gravity vector + +volatile bool mpuInterrupt = false; // indicates whether MPU interrupt pin has gone high +void ICACHE_RAM_ATTR dmpDataReady() { + mpuInterrupt = true; +} + +#define INTERRUPT_PIN D7 + +void initGyro(){ + // initialize device + Serial.println(F("Initializing I2C devices...")); + mpu.initialize(); + pinMode(INTERRUPT_PIN, INPUT); + + // verify connection + Serial.println(F("Testing device connections...")); + Serial.println(mpu.testConnection() ? F("MPU6050 connection successful") : F("MPU6050 connection failed")); + + // wait for ready +// Serial.println(F("\nSend any character to begin DMP programming and demo: ")); +// while (Serial.available() && Serial.read()); // empty buffer +// while (!Serial.available()); // wait for data +// while (Serial.available() && Serial.read()); // empty buffer again + + // load and configure the DMP + Serial.println(F("Initializing DMP...")); + devStatus = mpu.dmpInitialize(); + + // supply your own gyro offsets here, scaled for min sensitivity + + mpu.setXAccelOffset(-6009); + mpu.setYAccelOffset(-5577); + mpu.setZAccelOffset(7961); + mpu.setXGyroOffset(98); + mpu.setYGyroOffset(85); + mpu.setZGyroOffset(81); + // make sure it worked (returns 0 if so) + if (devStatus == 0) { + // Calibration Time: generate offsets and calibrate our MPU6050 +// mpu.CalibrateAccel(6); +// mpu.CalibrateGyro(6); + Serial.println(); + mpu.PrintActiveOffsets(); + // turn on the DMP, now that it's ready + Serial.println(F("Enabling DMP...")); + mpu.setDMPEnabled(true); + + // enable Arduino interrupt detection + Serial.print(F("Enabling interrupt detection (Arduino external interrupt ")); + Serial.print(digitalPinToInterrupt(INTERRUPT_PIN)); + Serial.println(F(")...")); + attachInterrupt(digitalPinToInterrupt(INTERRUPT_PIN), dmpDataReady, RISING); + mpuIntStatus = mpu.getIntStatus(); + + // set our DMP Ready flag so the main loop() function knows it's okay to use it + Serial.println(F("DMP ready! Waiting for first interrupt...")); + dmpReady = true; + + // get expected DMP packet size for later comparison + packetSize = mpu.dmpGetFIFOPacketSize(); + } else { + // ERROR! + // 1 = initial memory load failed + // 2 = DMP configuration updates failed + // (if it's going to break, usually the code will be 1) + Serial.print(F("DMP Initialization failed (code ")); + Serial.print(devStatus); + Serial.println(F(")")); + } + + delay(5000); +} + +bool updateGyro(){ + if (!dmpReady) return false; + if (mpu.dmpGetCurrentFIFOPacket(fifoBuffer)) { +// Xf = 1/4*(aaRealLast.x + aaReal.x)t^2 + Vot + Xo +// aaRealLast = aaReal; + + mpu.dmpGetQuaternion(&q, fifoBuffer); + mpu.dmpGetAccel(&aa, fifoBuffer); + mpu.dmpGetGyro(&gy, fifoBuffer); + mpu.dmpGetGravity(&gravity, &q); + mpu.dmpGetYawPitchRoll(ypr, &q, &gravity); + mpu.dmpGetEuler(euler, &q); + mpu.dmpGetLinearAccel(&aaReal, &aa, &gravity); + mpu.dmpGetLinearAccelInWorld(&aaWorld, &aaReal, &q); + + return true; + } + return false; +} diff --git a/robot/page_html.h b/robot/page_html.h new file mode 100644 index 0000000..3d483be --- /dev/null +++ b/robot/page_html.h @@ -0,0 +1,162 @@ + +const char index_html[] PROGMEM = R"rawliteral( + + + + + + PID Controller + + + +
Loading...
+
+ + +

PID

+ + + + + + +
+ + + +
+ + + +
+ + + +)rawliteral"; diff --git a/robot/pid.h b/robot/pid.h new file mode 100644 index 0000000..2b026d0 --- /dev/null +++ b/robot/pid.h @@ -0,0 +1,66 @@ +#include + +//double angKp=3.5, angKi=80, angKd=0.042; +//double posKp=20, posKi=0.0, posKd=0.0; +//double turnKp=2, turnKi=0.0, turnKd=0.0; + + +double angKp=4.0, angKi=106.75, angKd=0.0472; +double posKp=0.96, posKi=1.28, posKd=0.5; +double turnKp=1, turnKi=5.0, turnKd=0.17; + + +double angleInput, angleOutput, angleSetpoint; +PID anglePID(&angleInput, &angleOutput, &angleSetpoint, angKp, angKi, angKd, AUTOMATIC); + +double posInput, posOutput, posSetpoint; +PID posPID(&posInput, &posOutput, &posSetpoint, posKp, posKi, posKd, AUTOMATIC); + +double turnInput, turnOutput, turnSetpoint; +PID turnPID(&turnInput, &turnOutput, &turnSetpoint, turnKp, turnKi, turnKd, AUTOMATIC); + +PID* pids[] = {&anglePID, &posPID, &turnPID}; + +void initPID(){ + angleSetpoint = 0; + anglePID.SetOutputLimits(-180, 180); + anglePID.SetMode(AUTOMATIC); + anglePID.SetSampleTime(5); + + posSetpoint = 0.5; + posPID.SetOutputLimits(-1, 1); + posPID.SetMode(AUTOMATIC); + posPID.SetSampleTime(5); + posPID.SetControllerDirection(DIRECT); + + turnSetpoint = 0; + turnPID.SetOutputLimits(-15, 15); + turnPID.SetMode(AUTOMATIC); + turnPID.SetSampleTime(5); +} + +struct Speeds{ + float left; + float right; +}; + +Speeds updatePID(){ + posPID.Compute(); + angleSetpoint = posOutput; + anglePID.Compute(); + + float maxTurn = max(0.0f, 15.0f-abs((float)angleOutput)); + turnPID.SetOutputLimits(-maxTurn, maxTurn); + turnSetpoint = desiredYaw; + turnInput = currentYaw; + turnPID.Compute(); + + Speeds speeds; + speeds.left = (float)(angleOutput + turnOutput); + speeds.right = (float)(-angleOutput + turnOutput); + if(angleInput>20 || angleInput<-20){ + speeds.left = 0; + speeds.right = 0; + } + return speeds; +} diff --git a/robot/robot.ino b/robot/robot.ino new file mode 100644 index 0000000..51850a6 --- /dev/null +++ b/robot/robot.ino @@ -0,0 +1,81 @@ + +#include "I2Cdev.h" + + +#include +Servo left; +Servo right; + +float angleOffset = -2.4241745; +float desiredYaw = 0.0; +float currentYaw = 0.0; + +struct DebugState{ + int motorTargetAngle; +}; + +DebugState dbgState; + +#include "distance.h" +#include "encoder.h" +#include "gyro.h" +#include "pid.h" +#include "webserver.h" + + +void initSerial(){ + Serial.begin(115200); + while (!Serial); + delay(300); + Serial.print("\n\n\n\n\n\n\n\n\n\n\n"); +} + +void initMotors(){ + left.attach(D3); + right.attach(D4); +} + +void initI2C(){ + digitalWrite(D2, LOW); + digitalWrite(D1, LOW); + delay(100); + Wire.begin(); + Wire.setClock(400000); +} + +void setup() { + initSerial(); + initWifi(false); + initServer(); + initMotors(); + initPID(); + initI2C(); + initEncoder(); + initDistance(); + initGyro(); +} + +void loop() { + if (updateGyro()) { //gyro data + currentYaw=ypr[0]*180/M_PI; + double angle = ypr[1] * 180 / M_PI; + if(angle>180) + angle -= 180; + angleInput = angle + angleOffset; + } + {// encoder data + updateEncoder(); + posInput = encoder.position(); + } + updateDistance(); + + + Speeds speeds = updatePID(); + + speeds.left = min(90.0f-10, max(-90.0f+10, speeds.left)); + speeds.right = min(90.0f-10, max(-90.0f+10, speeds.right)); + left.write(90+(int)speeds.left); + right.write(90+(int)speeds.right); + + delay(5); +} diff --git a/robot/webserver.h b/robot/webserver.h new file mode 100644 index 0000000..c08a4c6 --- /dev/null +++ b/robot/webserver.h @@ -0,0 +1,157 @@ +#include +#include +#include +#include +#include +#include + +#include "page_html.h" + +AsyncWebServer server(80); + +void initWifi(bool host){ + if(host){ + WiFi.softAP("MEOW"); + IPAddress IP = WiFi.softAPIP(); + Serial.print("AP IP address: "); + Serial.println(IP); + }else{ + Serial.print("Connecting to "); + Serial.println("Michael Loves CP"); + WiFi.enableInsecureWEP(false); + WiFi.mode(WIFI_STA); + WiFi.setPhyMode(WIFI_PHY_MODE_11G); + WiFi.begin("Michael Loves CP", "cockandpavly20"); + while(WiFi.status() != WL_CONNECTED){ + delay(500); + Serial.print('.'); + } + Serial.println(); + } + Serial.println(WiFi.localIP()); +} + +struct FVec3{ + float x,y,z; +}; +struct FVec4{ + float x,y,z; +}; + +struct Everything{ + float motorTargetAngle; + float position; + FVec3 anglePID; + FVec3 posPID; + FVec3 ypr; + FVec3 euler; + FVec3 gravity; + FVec4 q; + FVec3 aa; + FVec3 gy; + FVec3 aaReal; + FVec3 aaWorld; +}; + +void initServer(){ + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ + request->send_P(200, "text/html", index_html); + }); + server.on("/get_stuff_bin", HTTP_GET, [](AsyncWebServerRequest *request){ + float arr[] = { + dbgState.motorTargetAngle, + encoder.position(), + angleSetpoint, angleInput, angleOutput, + posSetpoint, posInput, posOutput, + ypr[0]*180/M_PI, ypr[1]*180/M_PI, ypr[2]*180/M_PI, + euler[0]*180/M_PI, euler[1]*180/M_PI, euler[2]*180/M_PI, + gravity.x, gravity.y, gravity.z, + q.w, q.x, q.y, q.z, + aa.x, aa.y, aa.z, + gy.x, gy.y, gy.z, + aaReal.x, aaReal.y, aaReal.z, + aaWorld.x, aaWorld.y, aaWorld.z + }; + request->send(200, "application/text", (const uint8_t*)arr, sizeof(arr)*4); + }); + server.on("/get_stuff", HTTP_GET, [](AsyncWebServerRequest *request){ + char buff[1024]; + int ret = snprintf(buff, sizeof(buff), + R"({ + "motorTargetAngle": %d, + "position": %f, + "anglePID": {"setpoint": %lf, "input": %lf, "output": %lf}, + "posPID": {"setpoint": %lf, "input": %lf, "output": %lf}, + "turnPID": {"setpoint": %lf, "input": %lf, "output": %lf}, + "ypr": {"yaw": %f, "pitch": %f, "roll": %f}, + "euler": {"psi": %f, "theta": %f, "phi": %f}, + "gravity": {"x": %f, "y": %f, "z": %f}, + "q": {"w": %f, "x": %f, "y": %f, "z": %f}, + "aa": {"x": %hd, "y": %hd, "z": %hd}, + "gy": {"x": %hd, "y": %hd, "z": %hd}, + "aaReal": {"x": %hd, "y": %hd, "z": %hd}, + "aaWorld": {"x": %hd, "y": %hd, "z": %hd} + })", + dbgState.motorTargetAngle, + encoder.position(), + angleSetpoint, angleInput, angleOutput, + posSetpoint, posInput, posOutput, + turnSetpoint, turnInput, turnOutput, + ypr[0]*180/M_PI, ypr[1]*180/M_PI, ypr[2]*180/M_PI, + euler[0]*180/M_PI, euler[1]*180/M_PI, euler[2]*180/M_PI, + gravity.x, gravity.y, gravity.z, + q.w, q.x, q.y, q.z, + aa.x, aa.y, aa.z, + gy.x, gy.y, gy.z, + aaReal.x, aaReal.y, aaReal.z, + aaWorld.x, aaWorld.y, aaWorld.z + ); + request->send(200, "application/json", buff); + }); + server.on("/get_pid", HTTP_POST, [](AsyncWebServerRequest *request){}, NULL, + [](AsyncWebServerRequest *request, uint8_t *bodyData, size_t bodyLen, size_t index, size_t total) { + StaticJsonDocument<256> json; + deserializeJson(json, &bodyData[index], bodyLen); + int idx = json["index"]; + if(idx>3){ + request->send(400); + return; + } + PID& pid = *pids[idx]; + char buff[256]; + int ret = snprintf(buff, sizeof(buff), + R"({"kp": %lf, "ki": %lf, "kd": %lf, "direction": %d})", + pid.GetKp(), pid.GetKi(), pid.GetKd(), pid.GetDirection() + ); + request->send(200, "application/json", buff); + }); + + server.on("/set_pid", HTTP_POST, [](AsyncWebServerRequest *request){}, NULL, + [](AsyncWebServerRequest *request, uint8_t *bodyData, size_t bodyLen, size_t index, size_t total) { + StaticJsonDocument<256> json; + deserializeJson(json, &bodyData[index], bodyLen); + int idx = json["index"]; + if(idx>3){ + request->send(400); + return; + } + + PID& pid = *pids[idx]; + if(json.containsKey("kp")) + pid.SetTunings(json["kp"], json["ki"], json["kd"]); + if(json.containsKey("direction")) + pid.SetControllerDirection(json["direction"]); + + request->send(200); + }); + + server.on("/set_desired_yaw", HTTP_POST, [](AsyncWebServerRequest *request){}, NULL, + [](AsyncWebServerRequest *request, uint8_t *bodyData, size_t bodyLen, size_t index, size_t total) { + StaticJsonDocument<256> json; + deserializeJson(json, &bodyData[index], bodyLen); + desiredYaw = json["yaw"]; + Serial.print(desiredYaw); + request->send(200); + }); + server.begin(); +}