HTMAA 2024 - Jonathan Cohen
For our group assignment we met in the ee cba lab and we powered Marcello's legged robot from the bench power supply. We first measured the current draw at 6v for the board by itself then we measured the draw for each motor and when both are running.
We then measured the same values with the multimeter in line.
This week I want to build on my last board by expanding it. I'll be reusing the XIAO ESP32-C6 and BNO085. I am inspired by single purpose devices so I will make a little game device. I need to add the SSD1306 128 x 64 OLED for the output device. SSD1306 Data Sheet
The schematic here is very simple, just following the i2c wiring. All we need are 4 connections for each board pwr, gnd, sda, and scl. (CAD Link )
In order to reuse the XIAO ESP32-C6 (selected since it would be cool to control the project wirelessly) and the BNO085 across projects. I will use female headers on the PCB board so that I can just solder male headers on xiao and bno085, and for the SSD1306.
Mill and then solder time! I just soldered the headers onto the boards and then populated the board onto the back of the pcb.
First need to figure out what angles make sense for gameplay and how the sensor is oriented releative to the user.
#include Wire.h
#include Adafruit_GFX.h
#include Adafruit_SSD1306.h
#include Adafruit_BNO08x.h
#include math.h
#include vector
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define SNAKE_SIZE 4
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
Adafruit_BNO08x bno08x;
sh2_SensorValue_t sensorValue;
struct euler_t {
float yaw;
float pitch;
float roll;
} initialYPR, currentYPR;
struct Point {
int x;
int y;
};
std::vector snake = {{SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2}};
Point apple = {random(SCREEN_WIDTH / SNAKE_SIZE) * SNAKE_SIZE, random(SCREEN_HEIGHT / SNAKE_SIZE) * SNAKE_SIZE};
int snakeSpeed = SNAKE_SIZE; // Snake moves by one block size
int directionX = 0;
int directionY = -snakeSpeed; // Start moving up
void setup() {
Serial.begin(115200);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("SSD1306 allocation failed");
while (1);
}
display.setRotation(2);
if (!bno08x.begin_I2C()) {
Serial.println("BNO08x not detected");
while (1);
}
bno08x.enableReport(SH2_ARVR_STABILIZED_RV);
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("Initializing...");
display.display();
unsigned long startTime = millis();
while (millis() - startTime < 2000) {
if (bno08x.getSensorEvent(&sensorValue) && sensorValue.sensorId == SH2_ARVR_STABILIZED_RV) {
quaternionToEuler(&sensorValue.un.arvrStabilizedRV, &initialYPR, true);
}
}
display.clearDisplay();
display.setCursor(0, 0);
display.println("IMU Ready!");
display.display();
delay(1000);
}
void loop() {
if (bno08x.getSensorEvent(&sensorValue) && sensorValue.sensorId == SH2_ARVR_STABILIZED_RV) {
quaternionToEuler(&sensorValue.un.arvrStabilizedRV, ¤tYPR, true);
float adjustedPitch = currentYPR.pitch - initialYPR.pitch;
float adjustedRoll = currentYPR.roll - initialYPR.roll;
// Map pitch and roll to snake direction
if (adjustedPitch > 15) { // Threshold to move left
directionX = -snakeSpeed;
directionY = 0;
} else if (adjustedPitch < -15) { // Threshold to move right
directionX = snakeSpeed;
directionY = 0;
} else if (adjustedRoll > 15) { // Threshold to move down
directionX = 0;
directionY = snakeSpeed;
} else if (adjustedRoll < -15) { // Threshold to move up
directionX = 0;
directionY = -snakeSpeed;
}
// Update snake position
Point newHead = {snake.front().x + directionX, snake.front().y + directionY};
// Boundary wrapping
if (newHead.x >= SCREEN_WIDTH) newHead.x = 0;
else if (newHead.x < 0) newHead.x = SCREEN_WIDTH - SNAKE_SIZE;
if (newHead.y >= SCREEN_HEIGHT) newHead.y = 0;
else if (newHead.y < 0) newHead.y = SCREEN_HEIGHT - SNAKE_SIZE;
snake.insert(snake.begin(), newHead); // Add new head position
// Check for apple collision
if (newHead.x == apple.x && newHead.y == apple.y) {
// Generate new apple
apple.x = (random(SCREEN_WIDTH / SNAKE_SIZE)) * SNAKE_SIZE;
apple.y = (random(SCREEN_HEIGHT / SNAKE_SIZE)) * SNAKE_SIZE;
} else {
// Remove the last segment of the snake to maintain length
snake.pop_back();
}
// Check for self-collision
for (size_t i = 1; i < snake.size(); i++) {
if (snake[i].x == newHead.x && snake[i].y == newHead.y) {
resetGame();
return;
}
}
// Draw snake and apple
display.clearDisplay();
display.fillRect(apple.x, apple.y, SNAKE_SIZE, SNAKE_SIZE, SSD1306_WHITE);
for (const Point& segment : snake) {
display.fillRect(segment.x, segment.y, SNAKE_SIZE, SNAKE_SIZE, SSD1306_WHITE);
}
display.display();
}
delay(100); // Game speed control
}
void resetGame() {
snake = {{SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2}};
apple = {random(SCREEN_WIDTH / SNAKE_SIZE) * SNAKE_SIZE, random(SCREEN_HEIGHT / SNAKE_SIZE) * SNAKE_SIZE};
directionX = 0;
directionY = -snakeSpeed;
display.clearDisplay();
display.setCursor(0, 0);
display.println("Game Over!");
display.display();
delay(2000);
}
void quaternionToEuler(sh2_RotationVectorWAcc_t* rotational_vector, euler_t* ypr, bool degrees) {
float q0 = rotational_vector->real;
float q1 = rotational_vector->i;
float q2 = rotational_vector->j;
float q3 = rotational_vector->k;
ypr->yaw = atan2(2.0 * (q1 * q2 + q3 * q0), 1 - 2 * (q2 * q2 + q3 * q3));
ypr->pitch = asin(-2.0 * (q1 * q3 - q2 * q0));
ypr->roll = atan2(2.0 * (q2 * q3 + q1 * q0), 1 - 2 * (q3 * q3 + q1 * q1));
if (degrees) {
ypr->yaw *= RAD_TO_DEG;
ypr->pitch *= RAD_TO_DEG;
ypr->roll *= RAD_TO_DEG;
}
}
Next we made a basic snake game controlled by the IMU! Chatgpt 4o was also used for coding help
Just 3D printing a case to make it look a bit nicer Case CAD File
The snake works! Small dot is food and the other line is the snake. Very hard to play with the accels but also a lot of fun.
I put together a case for my little game board to make it look slightly more polished. Notice that there are no buttons since we don't need any :) Once side is the exposed PCB because it is cool just don't put it on a conductive surface...