Click here to read Part 1 of this article >
We left off last time in UCLA “AirMouse” using ROHM Sensor Evaluation Kit – Part 1 having finished the hardware portion of the project. The transmitter portion involved assembling a shield with the AirMouse’s buttons and the interface between the Uno’s GPIO pins and the RF module. We chose to use the ROHM SensorShield with the accelerometer module because of its convenience in prototyping and DIY projects such as this. As you’ll see in this tutorial, the ROHM shield packages the I2C communications between the Arduino and its peripherals nicely so that with some simple code, the user can receive accelerometer data without having to write any of the underlying I2C functions to send and receive data from device addresses. The receiver portion of the project involved assembling similar breakout board for a Teensy microcontroller to interface it with an RF module as well.
The code we’ll introduce and describe in this tutorial will help you complete the project by connecting the two modules to each other. We’ll show you our basic code to send data between the two modules and process the accelerometer data to move a cursor on the computer monitor, and hopefully you’ll be able to come up with some cool additions and improvements of your own!
We’ve all used computer mice before, but they are limited to only working on top of flat surfaces like tables. We’ve made 3D AirMouse, a computer mouse that functions in 3D space. The user can tilt the mouse to move the cursor on the screen, allowing for a wide range of customizable motions. We’ve already covered the basics of building the AirMouse in Part 1. There are two main pieces that make up the AirMouse: the handheld mouse transmitter, and the receiver connected to the user’s computer. The transmitter gathers information about the mouse’s orientation and the states of its buttons, while the receiver translates this information to perform corresponding actions on the computer screen. The AirMouse was built using Arduino Uno and nRF24L01 RF modules, combined with an accelerometer on the ROHM sensor shield.
Transmitter:
Receiver:
Once you have the hardware wired up as specified in Part 1, you can use the following program to run the transmitter and receiver.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
#include <SPI.h> #include "RF24.h" #define byte uint8_t #include <Wire.h> #include <KX022.h> KX022 accelerometer(KX022_DEVICE_ADDRESS_1E); RF24 radio(9,10); uint64_t pipes[2] = {0xF0F0F0F0F0, 0xF0F1F1F1F1}; //reading, writing void initRadio() { radio.setPALevel(RF24_PA_HIGH); //payload size default 32... radio.setChannel(10); //you can change the channel setting radio.setCRCLength(RF24_CRC_16); //2-byte CRC radio.setDataRate(RF24_1MBPS); //1Mbps data rate radio.openReadingPipe(0, pipes[0]); radio.openWritingPipe(pipes[1]); } #define buttonPinR 2 //change these accordingly #define buttonPinL 3 void setup() { // put your setup code here, to run once: Serial.begin(9600); while (!Serial); pinMode(buttonPinR, INPUT); pinMode(buttonPinL, INPUT); radio.begin(); initRadio(); radio.stopListening(); Wire.begin(); accelerometer.init(); } long lastDebounceTimeR = 0; // the last time the output pin was toggled long lastDebounceTimeL = 0; long debounceDelay = 50; int buttonStateR = LOW; // the current reading from the input pin int buttonStateL = LOW; int lastReadingR = LOW; int lastReadingL = LOW; char readButtonR(){ int reading = digitalRead(buttonPinR);//get what state the button is char out = 'a';//the value to return if nothing special happened if (reading != lastReadingR) {//We're reading a new state for button // reset the debouncing timer lastDebounceTimeR = millis(); } if ((millis() - lastDebounceTimeR) > debounceDelay) {//We finally have a stable value if (reading != buttonStateR)//Compared to our previous state, we have a flip { out = 'r';//prepare to toggle the Mini } buttonStateR = reading;//Make the buttonState the same } lastReadingR = reading;//make the last state the "current" state return out; } char readButtonL(){ int reading = digitalRead(buttonPinL); char out = 'a'; if (reading != lastReadingL) { // reset the debouncing timer lastDebounceTimeL = millis(); } if ((millis() - lastDebounceTimeL) > debounceDelay) { if (reading != buttonStateL) { out = 'l'; } buttonStateL = reading; } lastReadingL = reading; return out; } struct data { boolean isPushedR = false; boolean isPushedL = false; int8_t acceleration[3] = {0, 0, 0}; }; data packet; boolean rState = false;//these states are used to represent the current state of the buttons boolean lState = false; void loop() { if(readButtonR() == 'r'){ //toggle button state when button state change is detected rState = !rState; } if(readButtonL() == 'l'){ //toggle button state when button state change is detected lState=!lState; } packet.isPushedR = rState; packet.isPushedL = lState; uint8_t rc; float acc[3]; rc = accelerometer.get_val(acc); if (rc == 0) { //we cast to drop the decimal, we don't need that high precision packet.acceleration[0] = (int8_t)(acc[0]*100); //x //Serial.print(packet.acceleration[0]); Serial.print(" "); packet.acceleration[1] = (int8_t)(acc[1]*100); //y //Serial.print(packet.acceleration[1]); Serial.print(" "); packet.acceleration[2] = (int8_t)(acc[2]*100); //z //Serial.println(packet.acceleration[2]); } radio.write((char*) &packet, sizeof(packet)); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
#include <SPI.h> #include "RF24.h" RF24 radio(9,10); uint64_t pipes[2] = {0xF0F1F1F1F1, 0xF0F0F0F0F0}; //reading, writing void initRadio() { radio.setPALevel(RF24_PA_HIGH); //payload size default 32... radio.setChannel(10); radio.setCRCLength(RF24_CRC_16); //2-byte CRC radio.setDataRate(RF24_1MBPS); //1Mbps data rate radio.openReadingPipe(0, pipes[0]); //reading pipe radio.openWritingPipe(pipes[1]); radio.startListening(); } #define R_PIN 6 //Red LED #define G_PIN 7 //Green LED #define Y_PIN 8 //Yellow LED void setup() { Serial.begin(9600); while(!Serial); //wait until Serial is initialized...(we found that not including this line of code caused errors on the //Teensy because it started executing code without ensuring that Serial communication with the laptop was //properly initialized... radio.begin(); initRadio(); Mouse.screenSize(1920, 1080); // configure screen size randomSeed(analogRead(0)); pinMode(R_PIN, OUTPUT); pinMode(G_PIN, OUTPUT); pinMode(Y_PIN, OUTPUT); } #define CALIX 6 //calibration for X #define CALIY -1 //calibration for Y #define scalingFactor 0.05 #define THRESHOLD 1 double moveVector[2] = {0, 0}; void tiltToVector(const int8_t* acceleration){ moveVector[0] = 0; moveVector[1] = 0; if(abs(acceleration[0] - CALIX) > THRESHOLD){ //calculate move moveVector[1] = (double)(acceleration[0] * scalingFactor); } if(abs(acceleration[1] - CALIY) > THRESHOLD) { moveVector[0] = (double)(acceleration[1] * scalingFactor); } } struct data { boolean isPushedR = false; boolean isPushedL = false; int8_t acceleration[3] = {0, 0, 0}; }; data packet; void loop() { bool stillWaiting = true; //Serial.println("About to read"); while(stillWaiting){ if(radio.available(0)){ //You've got mail!!! radio.read((char*) &packet, sizeof(packet)); stillWaiting = false; } } Mouse.move(moveVector[0], moveVector[1]); Mouse.move(moveVector[0], moveVector[1]); //call it twice within the loop for smoothness :) //prints for debugging purposes Serial.println("Finished writing the pins"); if (packet.isPushedR) { Serial.println("The right button has been clicked!!! (Did you mean to right click?!?!)"); } if (packet.isPushedL) { Serial.println("The left button has been clicked!!! (Did you mean to left click?!?!)"); //Mouse.click(); } Serial.print("X: "); Serial.println(packet.acceleration[0]); Serial.print("Y: "); Serial.println(packet.acceleration[1]); Serial.print("Z: "); Serial.println(packet.acceleration[2]); tiltToVector(packet.acceleration); //re-calculate move vector coordinates // Mouse.move(moveVector[0], moveVector[1]); } |
The process for uploading code to a Teensy is slightly different from the process for uploading to an Arduino Uno. For the Uno, you can simply follow the usual compile and upload steps:
For the Teensy, follow these steps to upload the receiver code:
For the purposes of this tutorial, we won’t go into too much detail about the different communication protocols involved and how communication with the RF module works at the software level. You can check out our communications protocols and nRF24L01+ module tutorials for more information on these topics! Instead, we’ll briefly explain the workings of the main control loop in the software.
In the AirMouse, the transmitter collects data, but most of the data processing is handled by the receiver module. We designed the system this way so that the Arduino, a weaker processor than the Teensy, could spend fewer resources on decision-making and computations and instead run faster cycles just collecting data. With this implementation, the only data being sent between the two devices is the raw accelerometer and button data. The Teensy receives this raw data and processes it to perform actions on the computer screen.
To detect the orientation of the AirMouse, the system must translate the raw accelerometer data. To accomplish this, one must first identify the “zero-values” of each of the coordinates. This value is defined as the accelerometer’s output for each axis when the AirMouse is held flat (parallel to the ground). With knowledge of the zero-values, the software is then able to translate accelerometer data into a direction and amount to move the cursor on-screen by analyzing the acceleration (due to gravity) in each axis and comparing it to the zero-value for that axis.
Now we’ll take a look at the interaction between the receiver module and the computer. The Teensy is specified to act as a USB Human Interface Device (specifically, a USB Mouse). After translating orientation data, the software makes calculations on what speed and direction to move the cursor. In addition, the software also interprets the left-button clicks as left-clicks and the right-button clicks as right-clicks, calling appropriate methods to show left or right-clicks on the computer screen. Here’s the cool part: you can modify or add to the mouse’s on-screen functionality fully through software! Currently, the abilities and performance of the mouse is rudimentary, but you can easily add features such as scrolling, moving the cursor to a certain point on the screen, etc. with some simple additions to the software! (Here’s the Teensy USB Mouse guide for reference) Here are some cool hardware and software ideas you can implement:
We hope you enjoy building the AirMouse, and definitely look forward to modifications and improvements on both its design and its functionality!
The original version of AirMouse was developed by Rahul Iyer, Aaron and Andrew Wilhelm, as part of UCLA IEEE’s Advanced Projects program. Find out more at http://ieeebruins.org
Device Plus is looking to collaborate with active student engineering organizations and labs. For more information about our Hardware Sponsorship Program, please contact us at info@deviceplus.com