Week 9: Output Devices

HTMAA 2024 - Jonathan Cohen

Group Assignment

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.

CMU sketch

We then measured the same values with the multimeter in line.

CMU sketch

Individual Assignment

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

Design!

CMU sketch CMU sketch

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.

Fabricate!

CMU sketch CMU sketch

Mill and then solder time! I just soldered the headers onto the boards and then populated the board onto the back of the pcb.

Code!

CMU sketch

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

Make it look cool

CMU sketch

Just 3D printing a case to make it look a bit nicer Case CAD File

Reflections

CMU sketch

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.

CMU sketch

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...

CMU sketch