双手不够,脑袋来凑–体感探头吃鸡头盔

五一节的时候做了一个伪的体感头盔,当你偏头的时候,他可以识别你是向左偏头,还是向右偏头,然后再控制游戏人物进行探头。为什么做这个呢?因为我手比较笨,之前本来是打算做一个脚踏板的,但是妹子觉得做成体感的更好玩,所以才做了这么一个头盔,这里我就来分享一下我的思路。

怎么知道是否偏头了呢?
首先,最重要的是,我们怎么让单片机知道我们是否偏头了?向哪边偏头的?这里我想到了以前做的一个空中飞鼠,使用的MPU-6050传感器,通过它返回的数据,单片机就可以知道我们是否偏头了,向哪边偏,甚至偏了多少度都可以计算出来。
MPU-6050包含3轴的陀螺仪和3轴的加速度计,其实呢这次我们只需要使用Y轴的加速度计就可以了,有点大材小用,不过正好手里有啊。那么我为什么取Y轴的加速度计的值呢?静止情况下,在Z轴上会有向下的重力,当我们偏头的时候,重力会在Y轴或者X轴上有一个分量,具体是哪个轴要具体去看实际使用中是哪个轴,我使用的就是Y轴,这个分量越大,则说明偏的角度越大,当然最大情况下就是等于重力。这个就是初中物理的知识,即使我讲得不清楚我想大家都会明白。

选择什么单片机呢?
数据怎么传输呢?首先,有线连接肯定不方便,所以什么arduino啊,STM32啊,51啊就先排除了。那么我们常用的无线连接方式有什么呢?1.wifi,2.蓝牙。
在这个项目中,其实最佳选择就是蓝牙,因为我的主板是自带了蓝牙的,如果你的电脑没有蓝牙,买个适配器就行了。再用蓝牙芯片模拟HID设备,做QE的按键操作,就完成了。其实我也想过使用蓝牙,因为以前也用过CC2541,但是现在手里没有开发板,买一块的话估计以后也要吃灰,所以就不考虑蓝牙了。
其实这个体感头盔只是自己玩玩的东西,也不是专业去做,所以我尽量选择开发简单,手里有的东西来做,所以最终选择的是ESP8266,它可以使用arduino进行开发,很简单,蛋似呢需要额外的上位机软件配合,不过这并不是问题。

信息传输
整体大概就是这样,很简单,PC的上位机程序在指定端口启动一个ServerSocket,ESP8266启动时首先连接到无线路由器,再通过socket连接到PC的上位机程序。运行时,ESP866采集MPU6050的运动数据,判断是否偏头,如果偏头则发送指令到上位机,上位机接受到了指令之后,对应的按下Q或者E键,实现游戏中人物的偏头。当然,我们PC的IP地址可能是会变的,我是自己设置了静态获取IP的,这是我的方法,大家也可以用其他方法。

电路连接图
使用I2C连接。

ESP8266程序
我们对数据的处理非常简单,因为我们只需要判断是否偏头,所以只需根据数据Y轴上加速度的值与阈值对比,判断我们是向左偏头,还是向右偏头,这个阈值是我自己偏头得来的,大家可以根据自己偏头的习惯来修改。不过这里要注意,I2C读取的数据是两个8位的值,低八位和高八位最后合并为一个16位的值,这个16位的值其实是有符号的,它是以补码的形式表示,但是arduino中我不知道应该怎么处理,打印出来看始终是无符号整形,所以这里我就直接这么处理了,大家有好的方法可以分享出来。

#include <Wire.h>
#include <ESP8266WiFi.h>

const char* ssid     = "**********";
const char* password = "**********";
const char* host = "192.168.1.5";
const uint16_t port = 8123;

const int MPU_ADDRESS = 0x68; //MPU-6050的I2C地址

struct RAWDATA
{
    int YAccel;
};

RAWDATA rawData;
WiFiClient client;

char cmd = 'c';
boolean sendFlag = false;

void setup() {
  Serial.begin(115200);
  // 连接wifi
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());

  // 连接socket 
  Serial.println("");
  Serial.println("socket client conn...");
  while (!client.connect(host, port)) {
    Serial.println("connection failed, wati 5 second.");
    delay(5000);
  }
  Serial.println("socket client connected successful.");
  Serial.println("");

  // IIC
  Serial.println("start I2C...");
  Wire.begin();
  Wire.beginTransmission(MPU_ADDRESS);
  Wire.write(0x6B);
  Wire.write(0);
  Wire.endTransmission(true);
  Wire.beginTransmission(MPU_ADDRESS);
  Wire.write(0x1C);
  Wire.write(0);
  Wire.endTransmission(true);
  Serial.println("");
}

void loop() {
  ReadData();
//  printRawData();
  checkRowData();
  sendCmd();
  delay(50);
}

// 从MPU的寄存器中读取数据到数组
void ReadData() {
  Wire.beginTransmission(MPU_ADDRESS);
  Wire.write(0x3D);
  Wire.requestFrom(MPU_ADDRESS, 2, true);
  Wire.endTransmission(true);
  rawData.YAccel = (Wire.read() << 8) | Wire.read();
}

void printRawData() {
//  Serial.print(rawData.XAccel_H, HEX);Serial.print(" ");Serial.print(rawData.XAccel_L, HEX);Serial.print("\t\t");
//  Serial.print(rawData.YAccel_H, HEX);Serial.print(" ");Serial.print(rawData.YAccel_L, HEX);Serial.print("\t\t");
//  Serial.print(rawData.XAccel_H << 8 | rawData.XAccel_L);Serial.print("\t\t");
//  Serial.print(rawData.YAccel_H << 8 | rawData.YAccel_L);Serial.print("\t\t");
//  Serial.print(rawData.XGyro_H << 8 | rawData.XGyro_L);Serial.print("\t\t");
//  Serial.print(rawData.YGyro_H << 8 | rawDqqata.YGyro_L);Serial.print("\t\t");
  Serial.print(rawData.YAccel);Serial.print("\t"); Serial.println(rawData.YAccel, HEX);
  Serial.println("");
}

void sendCmd() {
  if (sendFlag) {
    if (client.connected()) {
      client.print(cmd);
      client.flush();
      sendFlag = false;
    } else {
      while (!client.connect(host, port)) {
      }
      client.print(cmd);
      client.flush();
      sendFlag = false;
    }
  }
}

void checkRowData() {
  if (rawData.YAccel > 5000 && rawData.YAccel < 32768) {
    if (cmd != 'l') {
      cmd = 'l';
      sendFlag = true;
    }
  } else if (rawData.YAccel < 59000 && rawData.YAccel > 32768) {
    if (cmd != 'r') {
      cmd = 'r';
      sendFlag = true;
    }
  } else {
    if (cmd != 'c') {
      cmd = 'c';
      sendFlag = true;
    }
  }
}

上位机程序
电脑上需要写一个上位机程序,使用ServerSocket处理ESP8266的连接,接受发送过来的指令,同时根据指令,做对应的按键操作。
程序非常简单,这里我是使用的NIO,但其实这种情况单线程的BIO其实就可以了,不过现在已经习惯了NIO,这一点就不要纠结了。


import java.awt.*; import java.awt.event.KeyEvent; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; import java.util.Set; public class ChickenDinner { private ServerSocketChannel serverSocketChannel; private Selector selector; private Robot robot; private int lastPress; public static void main(String[] args) { ChickenDinner chickenDinner = new ChickenDinner(); } public ChickenDinner() { this.init(); this.run(); } private void init() { try { serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.configureBlocking(false); serverSocketChannel.socket().bind(new InetSocketAddress(8123)); selector = Selector.open(); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); robot = new Robot(); lastPress = 0; } catch (IOException e) { e.printStackTrace(); } catch (AWTException e) { e.printStackTrace(); } } private void run() { try { while (true) { selector.select(); Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iter = selectionKeys.iterator(); while (iter.hasNext()) { SelectionKey selectionKey = iter.next(); if (selectionKey.isAcceptable()) { SocketChannel socketChannel = ((ServerSocketChannel) selectionKey.channel()).accept(); socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); } else if (selectionKey.isReadable()) { SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); ByteBuffer readBuf = ByteBuffer.allocate(1); int ret = socketChannel.read(readBuf); if (ret == -1) { continue; } char cmd = (char) readBuf.get(0); if ('l' == cmd) { System.out.println("left"); lastPress = KeyEvent.VK_Q; robot.keyPress(lastPress); } else if ('r' == cmd) { System.out.println("right"); lastPress = KeyEvent.VK_E; robot.keyPress(lastPress); } else if ('c' == cmd) { System.out.println("center"); if (lastPress != 0) { robot.keyRelease(lastPress); lastPress = 0; } } } iter.remove(); } } } catch (IOException e) { e.printStackTrace(); } } }

放到头盔里就这个样子

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据