owned this note
owned this note
Published
Linked with GitHub
---
title: G-건(총 모양 마우스) 만들기 프로젝트
description: 센서 및 버튼 입력을 이용해 마우스의 기능을 하는 총 모양 장치를 개발하는 프로젝트
image: https://i.imgur.com/uIefbOj.png
---
# G-건(총 모양 마우스) 만들기 프로젝트
![title](https://i.imgur.com/uIefbOj.png "베인인이(출처: 똘게더)")
센서 및 버튼 입력을 이용해 마우스의 기능을 하는 총 모양 장치를 개발하는 프로젝트.
**현재 이 프로젝트의 내용은 최신화가 되어 있지 않습니다.
따라서 프로젝트 내용 그대로 만들시 작동이 보장되지 않습니다.
기본적인 설계의 흐름만 참조하는 용도로 사용하시기 바랍니다.**
[Github](https://github.com/Taejunejung/Ggun)
### 타임라인
> [color=#467faa] 1. 똘삼이 본격적으로 프로젝트를 시작함. [time=2021, July 3rd, Saturday]
> 2. 똘삼이 프로젝트명으로 G-건(GTA Gun)을 제안함. [time=2021, July 4th, Sunday 11:32 PM]
> 3. 처음으로 버튼 입력 기능이 동작하는 수신기 프로토타입을 제작함. [time=2021, July 5th, Monday 1:39 AM]
> 4. 수신기 프로토타입이 아날로그 입력 기능이 동작하도록 개선함. [time=2021, July 5th, Monday 10:54 PM]
> 5. Orientation 센서에서 지역 가속도를 전역 가속도로 변환하는 방법을 찾는 과정에서 난관에 봉착함. [time=2021, July 6th, Tuesday 12:28 AM]
> 6. 방향을 전환해, 리얼 임팩트 건과 같이 지역 가속도만을 이용해 마우스를 움직이는 방법을 먼저 시도해 보기로 함. [time=2021, July 6th, Tuesday 6:40 PM]
> 7. 가속도 방식을 철저히 포기하고, 각속도를 이용하기로 함. [time=2021, July 6th, Tuesday 10:37 PM]
> 8. 기적적으로, 시청자가 가져온 코드가 자이로를 이용하여 기울어진 상태에서도 상당히 잘 작동하는 것을 확인함. [time=2021, July 6th, Tuesday 11:59 PM]
> 9. 이동 평균 필터를 적용하는 것을 마지막으로, 마우스 커서 변위값을 도출해내는 방식을 확정함. [time=2021, July 7th, Thursday]
> 10. 블루투스 통신을 통해 센서 및 장치 등의 값을 전송하여 컴퓨터를 조작하는 시도를 시작함. [time=2021, July 11th, Sunday]
> 11. 두 장치를 직접 연결하여 조작하는 데에는 성공하였으나, 블루투스 모듈을 이용하였을때에는 레이턴시가 크게 나타남. [time=2021, July 11th, Sunday]
> 12. 블루투스 버전 4.0을 지원하는 다른 모듈을 이용해서 낮은 레이턴시로 작동시키는 데에 성공함. [time=2021, July 12th, Monday 2:02 AM]
> 13. 프로토타입을 장난감 총 내부에 집어넣기 위한 작업에 돌입함. [time=2021, July 12th, Monday]
> 14. 프로토타입을 총 내부에 집어넣은 제품을 완성하였으나, 기울기를 반영한 동작이 제대로 동작하지 않음. 이 부분을 보완하려면 돌아가야 할 것들이 많아, 프로젝트를 여기서 마무리지으려고 함. [time=2021, July 18th, Sunday]
> 15. 인줄 알았으나, 센서 모듈을 짭 모듈로 변경하여 생긴 차이에 의한 오류였던 것으로 판명됨. 즉, 기울기를 정상적으로 반영하는 장난감 총을 완성하는 데에 성공한 것이었고, 프로젝트는 성공적이었음. [time=2021, July 18th, Sunday 1:49 AM]
> *프로젝트 완결* (휴식 기간 포함 약 15일 소요됨)
## 요구 사항
- [x] 기울기 반영
> 아두이노를 사용한 기존의 에어마우스형 컨트롤러는 기울인 뒤 이동시 움직임이 부정확해지는 부분이 있음. 이를 LG의 매직 리모컨과 유사한 형태로 어떤 그립상태에서도 사용자가 원하는 움직임이 가능하도록 제작한다.
- [x] 저지연 무선
> 블루투스 or wifi등을 이용하여 최소의 레이턴시로 움직임을 반영하도록 한다.
- [x] 플러그 앤 플레이
> 별도 프로그램 및 드라이버 설치 없이 연결하는 순간 바로 작동하도록
- [x] 총 상호작용 구현
> 진동 모터를 이용한 격발 체감 효과, 일반 모터 혹은 리니어 모터를 이용한 상부 블로우백 효과 등 가능한 한 실총과 유사한 경험을 제공하도록
# 구상 및 자료 조사
## 구상도
### 초기
```mermaid
graph TD
subgraph "장치 내부"
%% nodes
ARD[(아두이노 나노)]
BTN1[[버튼 1: 좌클릭]]
BTN2[[버튼 2: 우클릭]]
BTN3[[버튼 3: 재장전]]
BTN4[[버튼 4: Reset]]
ANALOG((아날로그 스틱))
BNO[다중 기능 센서]
BLT[블루투스 모듈]
%%
BTN1 --- ARD
BTN2 --- ARD
BTN3 --- ARD
BTN4 --- ARD
ARD --- ANALOG
ARD --- BNO
ARD --- BLT
end
subgraph PC[컴퓨터]
BLT -.-|블루투스 시리얼 통신| PROGRAM[대충 드라이버 프로그램]
end
%% classDef global font-size:15px
%% class ARD,BTN1,BTN2,BTN3,BTN4,BNO,BLT, global
```
### 개선안 1. 수신기 추가(채택)
```mermaid
graph TD
subgraph gun[장치]
%% nodes
ARD1[(아두이노 나노)]
BTN1[[버튼 1: 좌클릭]]
BTN2[[버튼 2: 우클릭]]
BTN3[[버튼 3: 재장전]]
BTN4[[버튼 4: Reset]]
ANALOG((아날로그 스틱))
BNO[다중 기능 센서]
BLT1[블루투스 모듈]
%% links
BTN1 --- ARD1
BTN2 --- ARD1
BTN3 --- ARD1
BTN4 --- ARD1
ARD1 --- ANALOG
ARD1 --- BNO
ARD1 --- BLT1
end
subgraph receiver[수신기]
%% nodes
BLT2[블루투스 모듈]
ARD2[(아두이노 레오나르도)]
%% links
BLT1 -.-|블루투스 마스터/슬레이브 통신| BLT2
BLT2 --- ARD2
end
PC[컴퓨터] ----|USB| ARD2
%% classDef global fill:#fffffff0,stroke:#a0a0a0,stroke-width:1.5px
%% class ARD,BTN1,BTN2,BTN3,BTN4,BNO,BLT, global
```
> 블루투스로 PC와 시리얼 통신해 PC에서 파이썬 등으로 컨트롤할 수 있으나 게임에 따라 사용이 불가능할 수 있어(핵으로 인식) 키보드 마우스로 작동하는 아두이노를 추가하는 방식으로 구상도를 그려봤습니다.
> [name=sko153]
> [출처](https://tgd.kr/s/jungtaejune/55583245#comment-55584694)
> 경험상 블루투스 모듈 연결이 문제를 일으키는 경우가 많으니 좀더 간단하고 싼? RF 모듈을 쓰는 것도 좋을 것 같습니다. [name=Gang V]
> [구매처](https://www.aliexpress.com/item/4000018571977.html)
### 개선안 2. ESP32 보드 사용
```mermaid
graph TD
subgraph "장치 내부"
%% nodes
ARD[(ESP32)]
BTN1[[버튼 1: 좌클릭]]
BTN2[[버튼 2: 우클릭]]
BTN3[[버튼 3: 재장전]]
BTN4[[버튼 4: Reset]]
ANALOG((아날로그 스틱))
BNO[다중 기능 센서]
%%
BTN1 --- ARD
BTN2 --- ARD
BTN3 --- ARD
BTN4 --- ARD
ARD --- ANALOG
ARD --- BNO
end
PC[컴퓨터]
ARD -..-|"블루투스(입력 장치로 인식)"| PC
```
> ESP32(ESP32 D1)라는 보드를 사용하면 내장 블루투스를 사용해 블루투스 키보드, 마우스 장치로 인식이 가능합니다.
(일반적인 아두이노 보드가 아니어서 어려울 수 있습니다.)
> [name=sko153]
> [의견 출처](https://tgd.kr/s/jungtaejune/55583245#comment-55584694), [관련 자료](https://cdn-learn.adafruit.com/assets/assets/000/092/545/medium800/sensors_BNO055_STEMMA_fab_print.png?1593120289)
## 자료
<details><summary>Arduino Nano</summary>
<blockquote>
<p><img src="https://www.etechnophiles.com/wp-content/uploads/2021/05/Arduino-Every-Pinout-in-detail.png" alt="" title="Arduino Nano pinout"></p>
<ul>
<li>Arduino Nano pinout</li>
</ul>
</blockquote>
</details>
<details><summary>Arduino Micro</summary>
<blockquote>
<p><img src="https://i.imgur.com/gHb5fHS.png" alt="" title="Arduino Micro pinout"></p>
<ul>
<li>Arduino Micro pinout</li>
</ul>
</blockquote>
</details>
<details><summary>Arduino Pro Micro</summary>
<blockquote>
<p><img src="https://cdn.sparkfun.com/assets/9/c/3/c/4/523a1765757b7f5c6e8b4567.png" alt="" title="Arduino Pro Micro"></p>
<ul>
<li>Arduino Pro Micro pinout</li>
</ul>
</blockquote>
</details>
<details><summary>BNO055</summary>
<blockquote>
<p><img src="https://cdn-learn.adafruit.com/assets/assets/000/092/545/medium800/sensors_BNO055_STEMMA_fab_print.png?1593120289" alt="bno055_pinout"></p>
<ul>
<li><p><a href="https://learn.adafruit.com/adafruit-bno055-absolute-orientation-sensor/pinouts">BNO055 Pinout</a> </p>
</li>
<li><p><a href="https://cdn-shop.adafruit.com/datasheets/BST_BNO055_DS000_12.pdf">BNO055 9DOF 센서 데이터시트</a></p>
</li>
<li><a href="https://learn.adafruit.com/adafruit-bno055-absolute-orientation-sensor/device-calibration">캘리브레이션</a> <a href="https://www.mathworks.com/help/supportpkg/arduinoio/ug/calibrate-sensors.html">추가</a><ul>
<li><a href="https://learn.adafruit.com/adafruit-bno055-absolute-orientation-sensor/arduino-code">예제</a>, <a href="https://github.com/adafruit/Adafruit_BNO055/blob/master/examples/rawdata/rawdata.ino">예시 코드 1</a></li>
</ul>
</li>
</ul>
</blockquote>
</details>
<details><summary>HC-05 블루투스 모듈</summary>
<blockquote>
<ul>
<li><a href="https://components101.com/wireless/hc-05-bluetooth-module">정보</a></li>
</ul>
</blockquote>
</details>1593120289
### 코드공유소
*성황리에 폐쇄*
# 회로 설계
## 부분 동작 시연용 회로
![circuit_prototype_1](https://i.imgur.com/gnfQoyh.png)
아두이노 프로 마이크로에 버튼, BNO055, 아날로그 스틱을 연결한 모습.
> [회로 참고용 영상](https://clips.twitch.tv/TangibleJoyousMilkBleedPurple-KDsa6fwgXdHk1IJM)
## 최종 회로
![circuit_fritzing](https://i.imgur.com/gONjCeQ.jpeg)
# 프로그램 설계 및 개발
## 수신기 부분 동작 시연 코드
```c=
#include <Keyboard.h>
#include <Mouse.h>
const byte Pin_Mouse_Left = 7;
const byte Pin_Mouse_Right = 8;
const byte Pin_Reload = 9;
const byte Pin_Recenter = 10;
const byte Pin_Joybutton = 4;
boolean state = true;
void setup() {
Serial.begin(9600);
Keyboard.begin();
Mouse.begin();
pinMode(Pin_Mouse_Left, INPUT_PULLUP);
pinMode(Pin_Mouse_Right, INPUT_PULLUP);
pinMode(Pin_Reload, INPUT_PULLUP);
pinMode(Pin_Recenter, INPUT_PULLUP);
pinMode(Pin_Joybutton, INPUT_PULLUP);
}
void loop() {
int sensorValueY = analogRead(A1);
int sensorValueX = analogRead(A0);
if (digitalRead(Pin_Mouse_Left) == LOW){
Mouse.press(MOUSE_LEFT);
Serial.println("press mouse left");
} else {
Mouse.release(MOUSE_LEFT);
}
if (digitalRead(Pin_Mouse_Right) == LOW){
Mouse.press(MOUSE_RIGHT);
Serial.println("press mouse right");
} else {
Mouse.release(MOUSE_RIGHT);
}
if (digitalRead(Pin_Reload) == LOW){
Keyboard.press('r');
Serial.println("press Reload");
} else {
Keyboard.release('r');
}
if (digitalRead(Pin_Recenter) == LOW){
Keyboard.press(KEY_BACKSPACE);
Serial.println("press Recenter");
} else {
Keyboard.release(KEY_BACKSPACE);
}
if (digitalRead(Pin_Joybutton) == LOW){
Keyboard.press('p');
Serial.println("press Joybutton");
} else {
Keyboard.release('p');
}
// Y+
if (sensorValueY < 300) {
Keyboard.press('w');
} else {
Keyboard.release('w');
}
//Y-
if (sensorValueY > 700) {
Keyboard.press('s');
} else {
Keyboard.release('s');
}
//X
if (sensorValueX < 300) {
Keyboard.press('a');
} else {
Keyboard.release('a');
}
//X-
if (sensorValueX > 700) {
Keyboard.press('d');
} else {
Keyboard.release('d');
}
delay (20);
}
```
수신기가 키보드 및 마우스로서 동작하는 모습을 체크하고 디버깅 하기 위한 코드.
버튼 및 아날로그 스틱을 수신기에 연결하여 독립적으로 테스트함.
## 센서 동작 시연 코드
```c=
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BNO055.h>
#include <utility/imumaths.h>
// 마우스 제어용 헤더
#include <Mouse.h>
#include <Keyboard.h>
/* This driver reads raw data from the BNO055
Connections
===========
Connect SCL to analog 5
Connect SDA to analog 4
Connect VDD to 3.3V DC
Connect GROUND to common ground
History
=======
2015/MAR/03 - First release (KTOWN)
*/
/* Set the delay between fresh samples */
#define BNO055_SAMPLERATE_DELAY_MS (10)
// ===================== 코드에서 사용할 전역 변수 값들 =====
// 가속도의 무시를 위한 데드존
const float DEADZONE = 0.01f;
// ========================================================
// Check I2C device address and correct line below (by default address is 0x29 or 0x28)
// id, address
Adafruit_BNO055 bno = Adafruit_BNO055(-1, 0x29);
void setup(void)
{
Serial.begin(115200);
Serial.println("Orientation Sensor Raw Data Test"); Serial.println("");
/* Initialise the sensor */
if(!bno.begin())
{
/* There was a problem detecting the BNO055 ... check your connections */
Serial.print("Ooops, no BNO055 detected ... Check your wiring or I2C ADDR!");
while(1);
}
delay(1000);
/* Display the current temperature */
int8_t temp = bno.getTemp();
Serial.print("Current Temperature: ");
Serial.print(temp);
Serial.println(" C");
Serial.println("");
bno.setExtCrystalUse(true);
Serial.println("Calibration status values: 0=uncalibrated, 3=fully calibrated");
// 마우스 초기화
Mouse.begin();
}
void loop(void)
{
// Possible vector values can be:
// - VECTOR_ACCELEROMETER - m/s^2
// - VECTOR_MAGNETOMETER - uT
// - VECTOR_GYROSCOPE - rad/s
// - VECTOR_EULER - degrees
// - VECTOR_LINEARACCEL - m/s^2
// - VECTOR_GRAVITY - m/s^2
imu::Vector<3> euler = bno.getVector(Adafruit_BNO055::VECTOR_EULER);
/* Display calibration status for each sensor. */
uint8_t system, gyro, accel, mag = 0;
bno.getCalibration(&system, &gyro, &accel, &mag);
// 각속도 얻어오는 부분
imu::Vector<3> Gyro = bno.getVector(Adafruit_BNO055::VECTOR_GYROSCOPE);
float gyroX = -Gyro.x();
float gyroY = Gyro.y();
float c = cosf(euler.z() / 180.0f * 3.141592f);
float s = sinf(euler.z() / 180.0f * 3.141592f);
float newGyroX = gyroX * c + gyroY * s;
float newGyroY = gyroX * s * -1 + gyroY * c;
// 각속도 테스트용 출력 부분
Serial.print("newGyroX:");
Serial.print(newGyroX);
Serial.print(" newGyroY:");
Serial.print(newGyroY);
Mouse.move(newGyroX, newGyroY, 0);
delay(BNO055_SAMPLERATE_DELAY_MS);
}
```
자이로 측정값과 오일러 각도를 이용하여, 장치의 기울기에 대응하여 마우스 커서가 적절한 방향으로 이동하게 만듦. 완벽하지는 않지만 상당히 높은 수준의 정확도를 보임.
> [name=햙쨕]
[두 시연 코드를 합쳐놓은 코드](https://tgd.kr/s/jungtaejune/55655687)
## 최종 코드
수신기와 송신기 기능을 각각 구현한 최종 코드다. 송신기에는 센서와 각종 입력 핀들이 구성되어 있고, 수신기에는 간단하게 블루투스 모듈만 구성되어 있다.
### transmitter.ino
```c=
//*************************************************************************************
// G-gun 송신기용 코드 (총)
//*************************************************************************************
#include <Wire.h>
//IMU 센서 제어용 헤더
#include <Adafruit_Sensor.h>
#include <Adafruit_BNO055.h>
#include <utility/imumaths.h>
#include <math.h>
// boolean 타입 사용을 위한 헤더
#include <stdbool.h>
//폴링 딜레이 설정.
#define BNO055_SAMPLERATE_DELAY_MS (1)
#define STARTUP_DELAY_MS (1000)
#define RAD_TO_DEG 57.295779513082320876798154814105
#define DEG_TO_RAD 0.01745329251994329576923690768489
//필요
#define FRAME_START 0xAA
#define FRAME_END 0xDD
#define FRAME_MAX_SIZE 13
//필요
typedef struct _data
{
byte pin;
uint8_t joystick_x;
uint8_t joystick_y;
float mouse_x;
float mouse_y;
} DATA;
// ===================== 코드에서 사용할 전역 변수 값들 =====
// 가속도의 무시를 위한 데드존
const float DEADZONE = 0.01f;
bool bHold = false;
// recenter 용도
const byte Pin_Joybutton = 4;
// 전송될 핀들
const byte Pin_Mouse_Left = 7;
const byte Pin_Mouse_Right = 8;
const byte Pin_Reload = 9;
const byte Pin_Jump = 10;
const byte Pin_Use = A3;
const byte Pin_Skill = A2;
const byte Pin_Weapon = 5;
const byte Pin_JoyUp = A0;
const byte Pin_JoyDown = A1;
const byte Pin_JoyLeft = 15;
const byte Pin_JoyRight = 14;
double filteredX, filteredY;
// set alpha 0.0f to turn off ema filter
const float alpha = 0.8f;
// 인코딩 및 전송 관련
DATA data;
byte buffer[FRAME_MAX_SIZE];
void write_bytes(byte *buffer, DATA *data)
{
buffer[0] = FRAME_START;
buffer[1] = (byte)(data->pin);
buffer[2] = (byte)(data->joystick_x);
buffer[3] = (byte)(data->joystick_y);
// mouse_x
memcpy(buffer + 4, &data->mouse_x, 4);
// *((float *)(buffer + 4)) = data->mouse_x;
// mouse_y
memcpy(buffer + 8, &data->mouse_y, 4);
// *((float *)(buffer + 8)) = data->mouse_y;
buffer[12] = FRAME_END;
}
// 핀 데이터를 읽어서 각 bit에 저장함
byte get_pin_data()
{
byte pin_data = 0;
pin_data |= (byte)(digitalRead(Pin_Mouse_Left) == LOW) << 0;
pin_data |= (byte)(digitalRead(Pin_Mouse_Right) == LOW) << 1;
pin_data |= (byte)(digitalRead(Pin_Reload) == LOW) << 2;
pin_data |= (byte)(digitalRead(Pin_Jump) == LOW) << 3;
pin_data |= (byte)(digitalRead(Pin_Use) == LOW) << 4;
pin_data |= (byte)(digitalRead(Pin_Skill) == LOW) << 5;
pin_data |= (byte)(digitalRead(Pin_Weapon) == LOW) << 6;
return pin_data;
}
// 조이스틱 핀 데이터를 읽어서 상태를 -1, 0, 1로 저장함.
uint8_t get_joystick_data_x()
{
bool xPlus = digitalRead(Pin_JoyRight) == LOW;
bool xMinus = digitalRead(Pin_JoyLeft) == LOW;
uint8_t status;
// will be translated to -1, 1, 0
if (xMinus)
{
status = 0;
}
else if (xPlus)
{
status = 2;
}
else
{
status = 1;
}
return status;
}
// 조이스틱 핀 데이터를 읽어서 상태를 -1, 0, 1로 저장함.
uint8_t get_joystick_data_y()
{
bool yPlus = digitalRead(Pin_JoyDown) == LOW;
bool yMinus = digitalRead(Pin_JoyUp) == LOW;
uint8_t status;
// will be translated to -1, 1, 0
if (yMinus)
{
status = 0;
}
else if (yPlus)
{
status = 2;
}
else
{
status = 1;
}
return status;
}
// ========================================================
// IMU 센서 I2C 주소 설정 (-1, 0x29)
Adafruit_BNO055 bno = Adafruit_BNO055(-1, 0x29);
/**************************************************************************/
/*
Arduino setup function (automatically called at startup)
*/
/**************************************************************************/
void setup(void)
{
Serial.begin(9600);
Serial1.begin(9600); //HC 05 블루투스 시리얼
Serial.println("Orientation Sensor Raw Data Test\n");
/* 센서 연결 */
if (!bno.begin())
{
/* 센서가 연결되지 않을 경우 오류 메시지 출력 */
Serial.print("Ooops, no BNO055 detected ... Check your wiring or I2C ADDR!");
while (1)
;
}
bno.setExtCrystalUse(true);
// 핀 설정
pinMode(A0, INPUT); //아날로그 핀을 디지털 핀으로 교체
pinMode(A1, INPUT); //아날로그 핀을 디지털 핀으로 교체
pinMode(A2, INPUT); //아날로그 핀을 디지털 핀으로 교체
pinMode(A3, INPUT); //아날로그 핀을 디지털 핀으로 교체
pinMode(Pin_Mouse_Left, INPUT_PULLUP);
pinMode(Pin_Mouse_Right, INPUT_PULLUP);
pinMode(Pin_Reload, INPUT_PULLUP);
pinMode(Pin_Jump, INPUT_PULLUP);
pinMode(Pin_Joybutton, INPUT_PULLUP);
pinMode(Pin_JoyUp, INPUT_PULLUP);
pinMode(Pin_JoyDown, INPUT_PULLUP);
pinMode(Pin_JoyLeft, INPUT_PULLUP);
pinMode(Pin_JoyRight, INPUT_PULLUP);
pinMode(Pin_Use, INPUT_PULLUP);
pinMode(Pin_Skill, INPUT_PULLUP);
pinMode(Pin_Weapon, INPUT_PULLUP);
delay(STARTUP_DELAY_MS);
}
void loop(void)
{
bHold = digitalRead(Pin_Joybutton) == LOW; // 조이스틱 중앙버튼
if (!bHold) // 리센터 버튼을 누를경우 마우스 움직임 처리를 무시함.
{
imu::Vector<3> eulerVector = bno.getVector(Adafruit_BNO055::VECTOR_EULER);
imu::Vector<3> gyroVector = bno.getVector(Adafruit_BNO055::VECTOR_GYROSCOPE);
/* Display calibration status for each sensor. */
uint8_t system, gyro, accel, mag = 0;
bno.getCalibration(&system, &gyro, &accel, &mag);
// z축 회전값으로 코사인 사인 함수값 계산
double c = cos(eulerVector.z() * DEG_TO_RAD);
double s = sin(eulerVector.z() * DEG_TO_RAD);
// DEADZONE 이상의 수치를 보여주는 각속도 값 읽어오기
double gyroX = fabs(gyroVector.x()) < DEADZONE ? 0 : -gyroVector.x();
double gyroY = fabs(gyroVector.y()) < DEADZONE ? 0 : gyroVector.y();
// 해당 각속도를 회전변환을 통해 중력과 평행하는 각속도로 변환
double moveX = gyroX * c + gyroY * s;
double moveY = gyroX * -s + gyroY * c;
//전역으로 안되어있길래 수정함
// 이동 평균 필터링으로 각속도를 수정
filteredX = filteredX * alpha + moveX * (1 - alpha);
filteredY = filteredY * alpha + moveY * (1 - alpha);
data.mouse_x = filteredX;
data.mouse_y = filteredY;
}
else
{
// recenter 상태인 경우 0.0f을 전송
data.mouse_x = 0.0f;
data.mouse_y = 0.0f;
}
data.pin = get_pin_data();
data.joystick_x = get_joystick_data_x();
data.joystick_y = get_joystick_data_y();
// data를 인코드해서 buffer에 저장함
write_bytes(buffer, &data);
// buffer를 블루투스 시리얼로 전송함
Serial1.write(buffer, FRAME_MAX_SIZE);
Serial1.flush();
// 디버그 용 시리얼 출력
// Serial.print(",mouse_x:");
// Serial.print(data.mouse_x);
// Serial.print(",mouse_y:");
// Serial.println(data.mouse_y);
// Serial.print("pin:");
// Serial.print(data.pin, BIN);
// Serial.print(",joy_x:");
// Serial.print(data.joystick_x);
// Serial.print(",joy_y:");
// Serial.println(data.joystick_y);
delay(BNO055_SAMPLERATE_DELAY_MS);
}
```
### transmitter_final.ino
```c=
//*************************************************************************************
// G-gun 송신기용 코드 (총)
//*************************************************************************************
#include <Wire.h>
//IMU 센서 제어용 헤더
#include <Adafruit_Sensor.h>
#include <Adafruit_BNO055.h>
#include <utility/imumaths.h>
#include <math.h>
// boolean 타입 사용을 위한 헤더
#include <stdbool.h>
//폴링 딜레이 설정.
#define BNO055_SAMPLERATE_DELAY_MS (1)
#define STARTUP_DELAY_MS (1000)
#define RAD_TO_DEG 57.295779513082320876798154814105
#define DEG_TO_RAD 0.01745329251994329576923690768489
//필요
#define FRAME_START 0xAA
#define FRAME_END 0xDD
#define FRAME_MAX_SIZE 13
//필요
typedef struct _data
{
byte pin;
uint8_t joystick_x;
uint8_t joystick_y;
float mouse_x;
float mouse_y;
} DATA;
// ===================== 코드에서 사용할 전역 변수 값들 =====
// 가속도의 무시를 위한 데드존
const float DEADZONE = 0.01f;
bool bHold = false;
// recenter 용도
const byte Pin_Joybutton = 6;
// 전송될 핀들 (7월 18일 핀배열 전부 새로함)
const byte Pin_Mouse_Left = 16;
const byte Pin_Mouse_Right = 14;
const byte Pin_Reload = 15;
const byte Pin_Jump = A0;
const byte Pin_Use = 4;
const byte Pin_Skill = A2;
const byte Pin_Weapon = 5;
const byte Pin_JoyUp = 8;
const byte Pin_JoyDown = 7;
const byte Pin_JoyLeft = 9;
const byte Pin_JoyRight = 10;
double filteredX, filteredY;
const float alpha = 0.0f;
// 인코딩 및 전송 관련
DATA data;
byte buffer[FRAME_MAX_SIZE];
void write_bytes(byte *buffer, DATA *data)
{
buffer[0] = FRAME_START;
buffer[1] = (byte)(data->pin);
buffer[2] = (byte)(data->joystick_x);
buffer[3] = (byte)(data->joystick_y);
// mouse_x
memcpy(buffer + 4, &data->mouse_x, 4);
// *((float *)(buffer + 4)) = data->mouse_x;
// mouse_y
memcpy(buffer + 8, &data->mouse_y, 4);
// *((float *)(buffer + 8)) = data->mouse_y;
buffer[12] = FRAME_END;
}
// 핀 데이터를 읽어서 각 bit에 저장함
byte get_pin_data()
{
byte pin_data = 0;
pin_data |= (byte)(digitalRead(Pin_Mouse_Left) == LOW) << 0;
pin_data |= (byte)(digitalRead(Pin_Mouse_Right) == LOW) << 1;
pin_data |= (byte)(digitalRead(Pin_Reload) == LOW) << 2;
pin_data |= (byte)(digitalRead(Pin_Jump) == LOW) << 3;
pin_data |= (byte)(digitalRead(Pin_Use) == LOW) << 4;
pin_data |= (byte)(digitalRead(Pin_Skill) == LOW) << 5;
pin_data |= (byte)(digitalRead(Pin_Weapon) == LOW) << 6;
return pin_data;
}
// 조이스틱 핀 데이터를 읽어서 상태를 -1, 0, 1로 저장함.
uint8_t get_joystick_data_x()
{
bool xPlus = digitalRead(Pin_JoyRight) == LOW;
bool xMinus = digitalRead(Pin_JoyLeft) == LOW;
uint8_t status;
// will be translated to -1, 1, 0
if (xMinus)
{
status = 0;
}
else if (xPlus)
{
status = 2;
}
else
{
status = 1;
}
return status;
}
// 조이스틱 핀 데이터를 읽어서 상태를 -1, 0, 1로 저장함.
uint8_t get_joystick_data_y()
{
bool yPlus = digitalRead(Pin_JoyDown) == LOW;
bool yMinus = digitalRead(Pin_JoyUp) == LOW;
uint8_t status;
// will be translated to -1, 1, 0
if (yMinus)
{
status = 0;
}
else if (yPlus)
{
status = 2;
}
else
{
status = 1;
}
return status;
}
// ========================================================
// IMU 센서 I2C 주소 설정 (-1, 0x29)
Adafruit_BNO055 bno = Adafruit_BNO055(-1, 0x28); //짭센서 0x29 정품센서 0x28
/**************************************************************************/
/*
Arduino setup function (automatically called at startup)
*/
/**************************************************************************/
void setup(void)
{
Serial.begin(9600);
Serial1.begin(9600); //HC 05 블루투스 시리얼
Serial.println("Orientation Sensor Raw Data Test\n");
/* 센서 연결 */
if (!bno.begin())
{
/* 센서가 연결되지 않을 경우 오류 메시지 출력 */
Serial.print("Ooops, no BNO055 detected ... Check your wiring or I2C ADDR!");
while (1)
;
}
bno.setExtCrystalUse(true);
// 핀 설정
pinMode(A0, INPUT); //아날로그 핀을 디지털 핀으로 교체
pinMode(A1, INPUT); //아날로그 핀을 디지털 핀으로 교체
pinMode(A2, INPUT); //아날로그 핀을 디지털 핀으로 교체
pinMode(A3, INPUT); //아날로그 핀을 디지털 핀으로 교체
pinMode(Pin_Mouse_Left, INPUT_PULLUP);
pinMode(Pin_Mouse_Right, INPUT_PULLUP);
pinMode(Pin_Reload, INPUT_PULLUP);
pinMode(Pin_Jump, INPUT_PULLUP);
pinMode(Pin_Joybutton, INPUT_PULLUP);
pinMode(Pin_JoyUp, INPUT_PULLUP);
pinMode(Pin_JoyDown, INPUT_PULLUP);
pinMode(Pin_JoyLeft, INPUT_PULLUP);
pinMode(Pin_JoyRight, INPUT_PULLUP);
pinMode(Pin_Use, INPUT_PULLUP);
pinMode(Pin_Skill, INPUT_PULLUP);
pinMode(Pin_Weapon, INPUT_PULLUP);
delay(STARTUP_DELAY_MS);
}
void loop(void)
{
bHold = digitalRead(Pin_Joybutton) == LOW; // 조이스틱 중앙버튼
if (!bHold) // 리센터 버튼을 누를경우 마우스 움직임 처리를 무시함.
{
imu::Vector<3> eulerVector = bno.getVector(Adafruit_BNO055::VECTOR_EULER);
imu::Vector<3> gyroVector = bno.getVector(Adafruit_BNO055::VECTOR_GYROSCOPE);
/* Display calibration status for each sensor. */
uint8_t system, gyro, accel, mag = 0;
bno.getCalibration(&system, &gyro, &accel, &mag);
// z축 회전값으로 코사인 사인 함수값 계산
double c = cosf(eulerVector.z() * DEG_TO_RAD);
double s = sinf(eulerVector.z() * DEG_TO_RAD);
// DEADZONE 이상의 수치를 보여주는 각속도 값 읽어오기
double gyroX = fabs(gyroVector.z()) < DEADZONE ? 0 : -gyroVector.z(); //정품센서와 짭센서가 X와 Z 오리엔테이션이 달라서 변경함.
double gyroY = fabs(gyroVector.y()) < DEADZONE ? 0 : gyroVector.y();
// 해당 각속도를 회전변환을 통해 중력과 평행하는 각속도로 변환
double moveX = gyroX * c + gyroY * s;
double moveY = gyroX * -s + gyroY * c;
//전역으로 안되어있길래 수정함
// 이동 평균 필터링으로 각속도를 수정
// filteredX = filteredX * alpha + moveX * (1 - alpha);
// filteredY = filteredY * alpha + moveY * (1 - alpha);
data.mouse_x = moveX;
data.mouse_y = moveY;
}
else
{
// recenter 상태인 경우 0.0f을 전송
data.mouse_x = 0.0f;
data.mouse_y = 0.0f;
}
data.pin = get_pin_data();
data.joystick_x = get_joystick_data_x();
data.joystick_y = get_joystick_data_y();
// data를 인코드해서 buffer에 저장함
write_bytes(buffer, &data);
// buffer를 블루투스 시리얼로 전송함
Serial1.write(buffer, FRAME_MAX_SIZE);
Serial1.flush();
// 디버그 용 시리얼 출력
// Serial.print(",mouse_x:");
// Serial.print(data.mouse_x);
// Serial.print(",mouse_y:");
// Serial.println(data.mouse_y);
// Serial.print("pin:");
// Serial.print(data.pin, BIN);
// Serial.print(",joy_x:");
// Serial.print(data.joystick_x);
// Serial.print(",joy_y:");
// Serial.println(data.joystick_y);
delay(BNO055_SAMPLERATE_DELAY_MS);
}
```
입력 핀 배열 및 BNO055 센서 축 방향에 관해서 변경이 있었음. 정품 BNO055 센서를 이용했다면 마우스 변위 계산 부분에 이쪽 코드를 이용해야 할 것.
### receiver.ino
```c=
//*************************************************************************************
// G-gun 수신기용 코드 (동글)
//*************************************************************************************
#include <Wire.h>
#include <Mouse.h>
#include <stdbool.h>
#include <Keyboard.h>
#define FRAME_START 0xAA
#define FRAME_END 0xDD
#define FRAME_MAX_SIZE 13
bool mouse_left, mouse_right, reload, joybutton, jump, use, skill, weapon;
short joystick_x, joystick_y;
float mouse_x, mouse_y;
const float MouseXScale = 0.3f; //마우스 감도 설정
const float MouseYScale = 0.2f; //마우스 감도 설정
byte buffer[FRAME_MAX_SIZE];
void setup()
{
Serial.begin(9600);
Serial1.begin(9600); //HC 05 블루투스 시리얼 시작.
Serial1.setTimeout(20);
while (!Serial1)
{
}
}
void loop()
{
memset(buffer, 0, sizeof(buffer));
//FRAME을 한번에 읽는 버전
// 이후의 Serial을 Bluetooth를 이용하는 Serial로 바꿔주세요
if (Serial1.available() >= FRAME_MAX_SIZE)
{
Serial1.readBytes(buffer, 1);
// Serial.print(buffer[0], HEX);
// 프레임이 미리 지정된 FRAME_START로 시작하지 않으면 건너뜀
if (buffer[0] != FRAME_START) {
// Serial.println("");
return;
}
Serial1.readBytes(buffer, FRAME_MAX_SIZE - 1);
// for(int i=0; i< FRAME_MAX_SIZE - 1; i++) {
// Serial.print(":");
// Serial.print(buffer[i], HEX);
// }
// Serial.println("");
// boolean values
mouse_left = buffer[0] & 0x01;
mouse_right = buffer[0] >> 1 & 0x01;
reload = buffer[0] >> 2 & 0x01;
jump = buffer[0] >> 3 & 0x01;
use = buffer[0] >> 4 & 0x01;
skill = buffer[0] >> 5 & 0x01;
weapon = buffer[0] >> 6 & 0x01;
// joystick values
// -1 means negative, 0 means neutral, and 1 means positive
joystick_x = (short)buffer[1] - 1;
joystick_y = (short)buffer[2] - 1;
// joystick_x = (signed short)(uint8_t)buffer[1] - 1;
// joystick_y = (signed short)(uint8_t)buffer[2] - 1;
// read mouse_x as float
memcpy(&mouse_x, buffer + 3, 4);
// read mouse_y as float
memcpy(&mouse_y, buffer + 7, 4);
// 역시 프레임이 FRAME_END로 끝나지 않으면 건너뜀
if (buffer[11] != FRAME_END)
return;
} else {
return;
}
// 처리
if (mouse_left) //마우스 왼클릭
{
Mouse.press(MOUSE_LEFT);
// Serial.println("press mouse left");
}
else
{
Mouse.release(MOUSE_LEFT);
}
if (mouse_right) //마우스 오른클릭
{
Mouse.press(MOUSE_RIGHT);
// Serial.println("press mouse right");
}
else
{
Mouse.release(MOUSE_RIGHT);
}
if (reload) //리로드 버튼 R
{
Keyboard.press('r');
// Serial.println("press Reload");
}
else
{
Keyboard.release('r');
}
if (jump)
{
Keyboard.press(0x20);
// Serial.println("press Jump");
}
else
{
Keyboard.release(0x20);
}
if (use)
{
Keyboard.press('f');
// Serial.println("press use");
}
else
{
Keyboard.release('f');
}
if (skill)
{
Keyboard.press(KEY_CAPS_LOCK);
// Serial.println("press skill");
}
else
{
Keyboard.release(KEY_CAPS_LOCK);
}
if (weapon)
{
Keyboard.press(KEY_TAB);
// Serial.println("press weapon");
}
else
{
Keyboard.release(KEY_TAB);
}
// ============= 5방향 스틱 (중앙 버튼은 송신기에서 리센터 버튼으로 사용) ============
// X+ : right
// X- : left
// Y+ : down
// Y- : up
// Y-
if (joystick_y == -1)
{
Keyboard.press('w');
}
else
{
Keyboard.release('w');
}
//Y+
if (joystick_y == 1)
{
Keyboard.press('s');
}
else
{
Keyboard.release('s');
}
//X-
if (joystick_x == -1)
{
Keyboard.press('a');
}
else
{
Keyboard.release('a');
}
//X+
if (joystick_x == 1)
{
Keyboard.press('d');
}
else
{
Keyboard.release('d');
}
//마우스 움직임
Mouse.move(mouse_x * MouseXScale, mouse_y * MouseYScale, 0);
}
```
# 프로토타입 제작 및 테스트
## 제작 과정
*>>> 똘튜브 영상 희망 <<<*
## 결과물
<iframe src="https://clips.twitch.tv/embed?clip=BumblingSuspiciousPuddingBabyRage-hkKhCtyJF6QSTtk7&parent=hackmd.io" frameborder="0" allowfullscreen="true" scrolling="no" height="378" width="620"></iframe>
# 완성품 설계 및 제작
## 제작 과정
*>>> 똘튜브 영상 희망 <<<*
## 결과물
![g-gun_final](https://i.imgur.com/omPbekC.jpg)
> G-건을 이용해서 Grand Theft Auto를 조작하고 있는 모습
<!-- <iframe src="https://clips.twitch.tv/embed?clip=NaiveArtisticManateeHassaanChop--uPVUImf80v8Ohxj&parent=hackmd.io" frameborder="0" allowfullscreen="true" scrolling="no" height="378" width="620"></iframe> --타사 제품에 대한 비방처럼 보일수 있어 주석 처리함(똘삼)>
# 최종 결과물
*>>> 똘튜브 영상 희망 <<<*
> [color=#ff1f1a] 영상 첨부하는 법:
> 아래와 같이 적은 후 `-EGT7Mehn98` 부분을 원하는 영상의 ID로 교체
> [![IMAGE ALT TEXT HERE](https://img.youtube.com/vi/-EGT7Mehn98/0.jpg)](https://www.youtube.com/watch?v=-EGT7Mehn98)
[^1]: 출처: https://duino4projects.com/arduino-circuit-design-program/