아두이노 프로젝트 6. 배구 토스 연습 보조 장치

아두위키 : Arduwiki


개요

배구 토스 연습 보조 장치

배구 토스 연습 시에 배구 공이 팔의 정확한 부위에 맞았는지 여부와 횟수를 측정하는 장치입니다. 센서가 삽입된 팔 토시를 끼고 어플리케이션의 연습시작 버튼을 누르면 공이 닿을 때마다 정확한 토스 여부에 따라 빨간불, 파란불이 들어오며 횟수가 저장됩니다.

고려사항

  • 가속도 센서를 부착해 해당 데이터를 바탕으로 사람 고개 방향을 측정합니다.
  • 헬멧의 좌우에는 네오픽셀 스트랩이 부착되어 고개 방향에 따라 밝아져 방향등의 역할을 합니다.
  • 전원은 9V 배터리를 사용하며 쉽게 교체할 수 있으며 on/off 기능을 포함합니다.

사용 하드웨어

회로 구성

기능 구현

#include "Adafruit_NeoPixel.h"
#include <Wire.h>

#define neoPIN 12
#define NUMPIXELS 8
#define bright 255
#define dly 1000

Adafruit_NeoPixel neo(NUMPIXELS, neoPIN, NEO_GRB + NEO_KHZ800);

////값을 변경해도 되는 변수//////////////////////////////////////////////////////////////////////////
//네오픽셀 점등 주기
const int blinkTIme = 500;

//아래 두 값은 좌측, 우측으로 얼마나 기울여야 불이 점등하는지 정하는 값이며 절대값이 클수록 많이 기울여야 합니다.
const int left_limint = 20;
const int right_limint = -20;
////////////////////////////////////////////////////////////////////////////////////////////////////

const int MPU_addr = 0x68;
int16_t AcX, AcY, AcZ, Tmp, GyX, GyY, GyZ;
unsigned long past = 0;
float dt;
float accel_angel_x, accel_angel_y, accel_angel_z;
float gyro_angel_x, gyro_angel_y, gyro_angel_z;
float filtered_angel_x, filtered_angel_y, filtered_angel_z;
float baseAcX, baseAcY, baseAcZ;
float baseGyX, baseGyY, baseGyZ;
unsigned long t_now;
unsigned long t_prev;
bool neo_state[2] = { false, false };  //{우측, 좌측}

void setup() {
  neo.begin();
  neo.setBrightness(bright);
  neo.clear();
  neo.show();
  Serial.begin(9600);
  Serial.println("Seirla start");
  initMPU6050();
  calibAccelGyro();
  initDT();
  bool state2 = false;
  for (int i = 0; i < 6; i++) {
    if (!state2) {
      for (int i = 0; i < NUMPIXELS; i++) {
        neo.setPixelColor(i, 0, 255, 0);
      }
    } else {
      for (int i = 0; i < NUMPIXELS; i++) {
        neo.setPixelColor(i, 0, 0, 0);
      }
    }
    neo.show();
    state2 = !state2;
    delay(100);
  }
}

void loop() {
  readAccelGyro();
  calcDT();
  calcAccelYPR();
  if (millis() - past > blinkTIme) {
    past = millis();
    Serial.println(accel_angel_y, 2);
    if (accel_angel_y > right_limint) {  //우측
      if (neo_state[1]) {
        neo_state[1] = false;
        neo.clear();
      }
      if (!neo_state[0]) {
        for (int i = 0; i < NUMPIXELS / 2; i++) {
          neo.setPixelColor(i, bright, bright, 0);
        }
        neo_state[0] = !neo_state[0];
      } else if (neo_state[0]) {
        neo.clear();
        neo_state[0] = !neo_state[0];
      }
    } else if (accel_angel_y < left_limint) {  //좌측
      if (neo_state[0]) {
        neo_state[0] = false;
      }
      if (!neo_state[1]) {
        for (int i = 4; i < NUMPIXELS; i++) {
          neo.setPixelColor(i, bright, bright, 0);
        }
        neo_state[1] = !neo_state[1];
      } else if (neo_state[1]) {
        neo.clear();
        neo_state[1] = !neo_state[1];
      }
    } else {
      neo.clear();
    }
    neo.show();
  }
}

void initMPU6050() {
  Wire.begin();
  Wire.beginTransmission(MPU_addr);
  Wire.write(0x6B);
  Wire.write(0);
  Wire.endTransmission(true);
}

void readAccelGyro() {
  Wire.beginTransmission(MPU_addr);
  Wire.write(0x3B);
  Wire.endTransmission(false);
  Wire.requestFrom(MPU_addr, 14, true);
  AcX = Wire.read() << 8 | Wire.read();
  AcY = Wire.read() << 8 | Wire.read();
  AcZ = Wire.read() << 8 | Wire.read();
  Tmp = Wire.read() << 8 | Wire.read();
  GyX = Wire.read() << 8 | Wire.read();
  GyY = Wire.read() << 8 | Wire.read();
  GyZ = Wire.read() << 8 | Wire.read();
}

void calibAccelGyro() {
  float sumAcX = 0, sumAcY = 0, sumAcZ = 0;
  float sumGyX = 0, sumGyY = 0, sumGyZ = 0;

  readAccelGyro();

  for (int i = 0; i < 10; i++) {
    readAccelGyro();
    sumAcX += AcX;
    sumAcY += AcY;
    sumAcZ += AcZ;
    sumGyX += GyX;
    sumGyY += GyY;
    sumGyZ += GyZ;
    delay(100);
  }
  baseAcX = sumAcX / 10;
  baseAcY = sumAcY / 10;
  baseAcZ = sumAcZ / 10;

  baseGyX = sumGyX / 10;
  baseGyY = sumGyY / 10;
  baseGyZ = sumGyZ / 10;
}

void initDT() {
  t_prev = millis();
}

void calcDT() {
  t_now = millis();
  dt = (t_now - t_prev) / 1000.0;
  t_prev = t_now;
}

void calcAccelYPR() {
  float accel_x, accel_y, accel_z;
  float accel_xz, accel_yz;
  const float RADIANS_TO_DEGREES = 180 / 3.14159;

  accel_x = AcX - baseAcX;
  accel_y = AcY - baseAcY;
  accel_z = AcZ + (16384 - baseAcZ);

  accel_yz = sqrt(pow(accel_y, 2) + pow(accel_z, 2));
  accel_angel_y = atan(-accel_x / accel_yz) * RADIANS_TO_DEGREES;

  accel_xz = sqrt(pow(accel_x, 2) + pow(accel_z, 2));
  accel_angel_x = atan(accel_y / accel_xz) * RADIANS_TO_DEGREES;

  accel_angel_z = 0;
}

네오픽셀

헬멧의 양 옆에는 네오픽셀 스트랩이 부착되어있습니다.

처음에는 정면을 바라본 상태로 전원을 켠 후 가속도 센서가 정상적으로 연결된다면 네오픽셀이 초록색으로 3번 깜빡거립니다.

만약 네오픽셀이 붉은 색으로 연속해서 점등한다면 에러가 발생한 상황으로 연결 확인 및 아두이노 리셋이 필요합니다.

이후에는 고개를 기울여 MPU-6050을 통해 수집된 데이터가 조건에 부합할 때 점등하며, blinkTime 변수에 선언된 값을 주기(ms 단위)로 깜빡거립니다.

고개를 다시 똑바로 세우면 네오픽셀은 off 상태가 됩니다.

MPU-6050 가속도 센서 데이터 수집

헬멧에 부착된 MPU-6050 가속도 센서를 통해 x, y, z축 가속도 데이터를 수집합니다.

수집된 데이터를 원하는 값으로 가공한 후 left_limint, right_limint에서 미리 설정해 둔 임계값과 비교해 네오픽셀 점등 여부를 결정합니다.

left_limint, right_limint 값이 커질수록 고개를 더 많이 기울여야 작동합니다.

결과

전원 On 후 네오픽셀 초록색 점등

좌우 기울임

Comment

사진 상으로 깜빡임이 표현되지 않지만 전원이 켜진 후 가속도 센서 연결이 확인되면 네오픽셀이 초록색으로 빠르게 3번 깜빡입니다.

이후 좌우측 기울임에 따라 양쪽에 부착된 네오픽셀이 0.5초 간격으로 점등하는 것을 확인할 수 있습니다.

추신

공집사의 아두이노 프로젝트 결과물은 판매되는 제품이 아니며, 프로젝트 수준에서 간단하게 진행되었습니다.

문의 및 의뢰는 크몽 공집사로 연락주시면 감사하겠습니다.