Files
balancing-cube/software/cube-controller/cube-controller.ino
Willem Pennings 606301d83e Publish files
2024-02-10 16:26:00 +01:00

245 lines
6.9 KiB
C++

// Toggle over-the-air (WiFi) programming functionality (0 = disabled, 1 = enabled)
#define OTA 1
// Toggle between use of nonlinear or linear controller (0 = linear, 1 = nonlinear)
#define USE_NONLINEAR_CONTROLLER 1
// TickTwo provides ESP32-compatible timers
#include <TickTwo.h>
#if OTA
// Libraries related to wireless functionality
#include <WiFi.h>
#include <ESPmDNS.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
#endif
// Cube controller library
#include "src/cube-controller.h"
#if OTA
// WiFi settings
const char* wifi_ssid = "YOUR_SSID";
const char* wifi_password = "YOUR_PASSWORD";
#endif
// Instantiation of objects
Motor motor1(M1_ENABLE, M1_CURRENT), motor2(M2_ENABLE, M2_CURRENT), motor3(M3_ENABLE, M3_CURRENT);
WheelEstimator whe_est_1(M1_SPEED), whe_est_2(M2_SPEED), whe_est_3(M3_SPEED);
AttitudeEstimator att_est(IMU_SDA, IMU_SCL);
AttitudeWheelController cont(USE_NONLINEAR_CONTROLLER);
AttitudeTrajectory att_tra;
// Run cube controller at the frequency as specified in parameter file
void controller();
TickTwo timer(controller, dt_us, 0, MICROS_MICROS);
// Status LED control: LED is either solid or blinking at 2.5 Hz
void control_led();
TickTwo timer_led(control_led, 200, 0, MILLIS);
#if OTA
// Handle OTA flashing requests every 5 seconds
void handle_ota();
TickTwo timer_ota(handle_ota, 5000, 0, MILLIS);
#endif
// Quaternion and angle error
float qe0, qe1, qe2, qe3;
float phi;
float phi_lim = phi_min;
// Trajectory initialization flag
bool flag_tra = false;
// LED status when blinking
bool led_status = false;
// Security flags
bool flag_arm = false;
bool flag_terminate = false;
// Torques
float tau_1 = 0, tau_2 = 0, tau_3 = 0;
void setup() {
// Open serial connection
Serial.begin(921600);
Serial.println("This is the ESP32 cube controller.");
#if OTA
// Set up WiFi connection
WiFi.mode(WIFI_STA);
WiFi.begin(wifi_ssid, wifi_password);
unsigned int nAttempts = 0;
while (WiFi.waitForConnectResult() != WL_CONNECTED && nAttempts < 3) {
delay(1000);
nAttempts++;
}
// If WiFi connection failed, indicate this using six yellow flashes
if(WiFi.waitForConnectResult() != WL_CONNECTED) {
for(int i = 0; i < 5; i++) {
neopixelWrite(RGB_BUILTIN, 255, 255, 0);
delay(250);
neopixelWrite(RGB_BUILTIN, 0, 0, 0);
delay(250);
}
}
// Set hostname
ArduinoOTA.setHostname("Cube ESP32");
// Print WiFi status
Serial.print("Connected to WiFi (local IP is ");
Serial.print(WiFi.localIP());
Serial.println(").");
// Set up OTA flashing using ArduinoOTA
ArduinoOTA.begin();
#endif
// Delay for one second to allow Maxon ESCON drivers to fully initialize
delay(1000);
// Motor setup (also spins each motor in positive direction very briefly)
motor1.init();
motor2.init();
motor3.init();
// Wheel estimator initialization (also calibrates the hall sensor for each wheel)
whe_est_1.init();
whe_est_2.init();
whe_est_3.init();
// Attitude estimator initialization (also calibrates the gyroscope)
att_est.init();
// Start controller and LED timers
timer.start();
timer_led.start();
#if OTA
// OTA handler timer
timer_ota.start();
#endif
}
void loop() {
// Update controller timer
timer.update();
timer_led.update();
#if OTA
// Handle OTA updates
timer_ota.update();
#endif
}
#if OTA
void handle_ota(){
ArduinoOTA.handle();
}
#endif
void controller() {
// Estimate wheel velocities
whe_est_1.estimate(tau_1);
whe_est_2.estimate(tau_2);
whe_est_3.estimate(tau_3);
// Estimate cube attitude
att_est.estimate();
// Calculate rotation quaternion error
qe0 = att_est.q0 * att_tra.qr0 + att_est.q1 * att_tra.qr1 + att_est.q2 * att_tra.qr2 + att_est.q3 * att_tra.qr3;
qe1 = att_est.q0 * att_tra.qr1 - att_est.q1 * att_tra.qr0 - att_est.q2 * att_tra.qr3 + att_est.q3 * att_tra.qr2;
qe2 = att_est.q0 * att_tra.qr2 - att_est.q2 * att_tra.qr0 + att_est.q1 * att_tra.qr3 - att_est.q3 * att_tra.qr1;
qe3 = att_est.q0 * att_tra.qr3 - att_est.q1 * att_tra.qr2 + att_est.q2 * att_tra.qr1 - att_est.q3 * att_tra.qr0;
// Normalize rotation quaternion error (real part only since we don't need the rest)
qe0 /= sqrt(qe0 * qe0 + qe1 * qe1 + qe2 * qe2 + qe3 * qe3);
// Calculate error angle
phi = 2.0 * acos(qe0);
// Controller enable and disable logic. The controller only activates once its orientation is such that the error with
// respect to the desired orientation is smaller than phi_min. As soon as this is reached, the error tolerance phi_lim
// is increased to phi_max. If this error is exceeded, the controller is disabled until the chip is reset.
if(abs(phi) <= phi_lim && !flag_terminate) {
// Controller is active
flag_arm = true;
// Increase error range to the maximum limit
phi_lim = phi_max;
// Generate trajectory
// Comment out the generate() method if the cube is balancing on a side instead of a corner
if(!flag_tra) {
flag_tra = true;
att_tra.init();
}
att_tra.generate();
// Controller calculates motor torques based on cube and wheel states
cont.control(att_tra.qr0, att_tra.qr1, att_tra.qr2, att_tra.qr3, att_est.q0, att_est.q1, att_est.q2, att_est.q3,
att_tra.omega_r_x, att_tra.omega_r_y, att_tra.omega_r_z, att_est.omega_x, att_est.omega_y, att_est.omega_z,
att_tra.alpha_r_x, att_tra.alpha_r_y, att_tra.alpha_r_z, whe_est_1.theta_w, whe_est_2.theta_w,
whe_est_3.theta_w, whe_est_1.omega_w, whe_est_2.omega_w, whe_est_3.omega_w);
// Get motor torques from controller
tau_1 = cont.tau_1;
tau_2 = cont.tau_2;
tau_3 = cont.tau_3;
} else {
// Outside safe limits: reset motor torques to zero
tau_1 = 0.0;
tau_2 = 0.0;
tau_3 = 0.0;
// Disarm mechanism
if(flag_arm) {
flag_arm = false;
flag_terminate = true;
}
}
// Apply torques to motors
// When balancing on the x side, comment out motor 2 and 3. When balancing on the y side, comment out 1 and 3.
motor1.set_torque(tau_1);
motor2.set_torque(tau_2);
motor3.set_torque(tau_3);
}
void control_led() {
if(flag_arm) {
if(abs(phi) <= phi_lim - 10 * pi / 180) {
// Cube is not close to error limit: solid green LED
neopixelWrite(RGB_BUILTIN, 0, 255, 0);
} else {
// Cube is close to error limit: alternate between red and green. Cube should be manually put to rest.
if(led_status) {
neopixelWrite(RGB_BUILTIN, 255, 0, 0);
} else {
neopixelWrite(RGB_BUILTIN, 0, 255, 0);
}
led_status = !led_status;
}
} else {
if(flag_tra) {
// A trajectory was already generated so the cube must now be halted due to a limit error
if(led_status) {
neopixelWrite(RGB_BUILTIN, 0, 0, 0);
} else {
neopixelWrite(RGB_BUILTIN, 255, 0, 0);
}
led_status = !led_status;
} else {
// Show a solid red LED to indicate readiness to start
neopixelWrite(RGB_BUILTIN, 255, 0, 0);
}
}
}