/ Joy_game_controller / Joy_game_controller.ino
Joy_game_controller.ino
1 // SPDX-FileCopyrightText: 2017 Noe Ruiz for Adafruit Industries 2 // 3 // SPDX-License-Identifier: MIT 4 5 #include <Keyboard.h> 6 #include <Adafruit_ST7735.h> 7 #include <SPI.h> 8 #include "graphics.h" 9 10 // Special keycodes (e.g. shift, arrows, etc.) are documented here: 11 // https://www.arduino.cc/en/Reference/KeyboardModifiers 12 13 14 // GLOBAL VARIABLES -------------------------------------------------------- 15 16 #define TFT_CS 10 17 #define TFT_RST 9 18 #define TFT_DC 6 19 Adafruit_ST7735 display(TFT_CS, TFT_DC, TFT_RST); 20 21 struct { // Button structure: 22 int8_t pin; // Button is wired between this pin and GND 23 uint8_t key; // Corresponding key code to send 24 bool prevState; 25 uint32_t lastChangeTime; 26 } button[] = { 27 { A5, 'a' }, // Button 0 Blue 28 { A4, 's'}, // Button 1 Pink 29 { A3, 'x' }, // Button 2 Yellow 30 { A2, 'z' }, // Button 3 Green 31 { 11, KEY_RETURN }, // Joystick select click 32 }; 33 #define N_BUTTONS (sizeof(button) / sizeof(button[0])) 34 #define DEBOUNCE_US 600 // Button debounce time, microseconds 35 36 struct { // Joystick axis structure (2 axes per stick): 37 int8_t pin; // Analog pin where stick axis is connected 38 int lower; // Typical value in left/upper position 39 int upper; // Typical value in right/lower positionax 40 41 uint8_t key1; // Key code to send when left/up 42 uint8_t key2; // Key code to send when down/right 43 int value; // Last-read-and-mapped value (0-1023) 44 int8_t state; 45 } axis[] = { 46 { A1, 65, 1023, KEY_LEFT_ARROW, KEY_RIGHT_ARROW }, // X axis 47 { A0, 1023, 65, KEY_UP_ARROW , KEY_DOWN_ARROW }, // Y axis 48 }; 49 #define N_AXES (sizeof(axis) / sizeof(axis[0])) 50 51 SPISettings SPIset = SPISettings(24000000, MSBFIRST, SPI_MODE0); 52 GFXcanvas16 canvas(EYES_WIDTH, EYES_HEIGHT); 53 float eyeAngle = 0.0; 54 bool blinking = false; 55 uint32_t blinkStartTime, blinkDuration; 56 #define UPPER_LID_SIZE (EYES_HEIGHT * 3 / 4) 57 #define LOWER_LID_SIZE (EYES_HEIGHT - UPPER_LID_SIZE) 58 uint8_t state = 0; 59 60 61 // SETUP - RUNS ONCE AT STARTUP -------------------------------------------- 62 63 void setup() { 64 uint8_t i; 65 66 display.initR(INITR_144GREENTAB); 67 display.fillScreen(0); 68 display.setRotation(2); 69 display.drawRGBBitmap(31, 90, (uint16_t *)teef, TEEF_WIDTH, TEEF_HEIGHT); 70 71 Keyboard.begin(); 72 73 // Initialize button states... 74 for(i=0; i<N_BUTTONS; i++) { 75 pinMode(button[i].pin, INPUT_PULLUP); 76 button[i].prevState = digitalRead(button[i].pin); 77 button[i].lastChangeTime = micros(); 78 } 79 80 // Initialize joystick state... 81 for(i=0; i<N_AXES; i++) { 82 int value = map(analogRead(axis[i].pin), axis[i].lower, axis[i].upper, 0, 1023); 83 if(value > (1023 * 4 / 5)) { 84 Keyboard.press(axis[i].key2); 85 axis[i].state = 1; 86 } else if(value < (1023 / 5)) { 87 Keyboard.press(axis[i].key1); 88 axis[i].state = -1; 89 } else { 90 axis[i].state = 0; 91 } 92 } 93 } 94 95 96 // MAIN LOOP - RUNS OVER AND OVER FOREVER ---------------------------------- 97 98 void loop() { 99 uint32_t t; 100 bool s; 101 int i, value, dx, dy; 102 float a; 103 uint16_t *buf; 104 105 // Read and debounce button inputs... 106 for(i=0; i<N_BUTTONS; i++) { 107 s = digitalRead(button[i].pin); // Current button state 108 if(s != button[i].prevState) { // Changed from before? 109 t = micros(); // Check time; wait for debounce 110 if((t - button[i].lastChangeTime) >= DEBOUNCE_US) { 111 if(s) Keyboard.release(button[i].key); // Button released 112 else Keyboard.press( button[i].key); // Button pressed 113 button[i].prevState = state; // Save new button state 114 button[i].lastChangeTime = t; // and time of change 115 } 116 } 117 } 118 119 // Read joystick axes 120 for(i=0; i<N_AXES; i++) { 121 // Remap analog reading to 0-1023 range (0=top/left, 1023=down/right) 122 value = map(analogRead(axis[i].pin), axis[i].lower, axis[i].upper, 0, 1023); 123 if(axis[i].state == 1) { // Axis previously down/right? 124 if(value < (1023 * 3 / 5)) { // Moved up/left past hysteresis threshold? 125 Keyboard.release(axis[i].key2); // Release corresponding key 126 axis[i].state = 0; // and set state to neutral center zone 127 } 128 } else if(axis[i].state == -1) { // Else axis previously up/left? 129 if(value > (1023 * 2 / 5)) { // Moved down/right past hysteresis threshold? 130 Keyboard.release(axis[i].key1); // Release corresponding key 131 axis[i].state = 0; // and set state to neutral center zone 132 } 133 } // This is intentionally NOT an 'else' -- state CAN change twice here! 134 if(!axis[i].state) { // Axis previously in neutral center zone? 135 if(value > (1023 * 4 / 5)) { // Moved down/right? 136 Keyboard.press(axis[i].key2); // Press corresponding key 137 axis[i].state = 1; // and set state to down/right 138 } else if(value < (1023 / 5)) { // Else axis moved up/left? 139 Keyboard.press(axis[i].key1); // Press corresponding key 140 axis[i].state = -1; // and set state to up/left 141 } 142 } 143 axis[i].value = value; // Save for later 144 } 145 146 // REDRAW FACE ----------------------------------------------------------- 147 // In order to keep the joystick and buttons more responsive, the face 148 // drawing is broken down into several steps, only one of which is 149 // performed on each pass through loop(). There's floating-point math 150 // and SPI transfers and stuff...doing all of them every time would 151 // make the controls sluggish. 152 153 switch(state) { // Which face-drawing step to handle this time? 154 155 case 0: // Eye position calc 156 // Determine direction eyes are pointing (follows joystick, sorta) 157 dx = axis[0].value - 512, // Joystick position relative to center 158 dy = axis[1].value - 512; // (+/- 512) 159 a = atan2(dy, dx); // Joystick angle (+/- M_PI) 160 // Deal with 'seam crossing' at +/- 180 degrees: 161 if(fabs(a - eyeAngle) > M_PI) { 162 if(eyeAngle >= 0.0) eyeAngle -= M_PI * 2.0; 163 else eyeAngle += M_PI * 2.0; 164 } 165 eyeAngle = (eyeAngle * 0.8) + (a * 0.2); // Low-pass filter old/new angle 166 break; 167 168 case 1: // Draw eyes in offscreen canvas 169 canvas.fillScreen(0); // Clear offscreen canvas 170 // Determine position of pupils; center +/- 12 pixels 171 dx = (int)(cos(eyeAngle) * 12.0 + 0.5), 172 dy = (int)(sin(eyeAngle) * 12.0 + 0.5); 173 canvas.drawRGBBitmap(15 + dx, 15 + dy, (uint16_t *)pupil, // Left 174 (uint8_t *)pupil_mask, PUPIL_WIDTH, PUPIL_HEIGHT); 175 canvas.drawRGBBitmap(75 + dx, 15 + dy, (uint16_t *)pupil, // Right 176 (uint8_t *)pupil_mask, PUPIL_WIDTH, PUPIL_HEIGHT); 177 // When eyes are blinking, overwrite sections of offscreen canvas 178 // with the 'closed' eye image. They converge at the 3/4 mark 179 // (i.e. upper lid is 3/4 of height, lower lid is 1/4). 180 if(blinking) { // Currently blinking? 181 uint32_t t = micros() - blinkStartTime; // Since how long? 182 if(t > blinkDuration) { // Past end of blink time? 183 blinking = false; // Turn off blink flag 184 } else { // Else in mid-blink... 185 int a2, amount = 900 * t / blinkDuration; // Relative time, 0-900 186 // First third of blink is fast closing, last 2/3 is slower opening 187 if(amount > 300) amount = 300 - ((amount - 300) / 2); // 0-300 blinkyness 188 if(amount > 256) amount = 256; // Clip to 256 189 if((a2 = UPPER_LID_SIZE * amount / 256)) // How much upper lid, in pixels? 190 canvas.drawRGBBitmap(0, 0, (uint16_t *)eyelids, EYES_WIDTH, a2); 191 if((a2 = LOWER_LID_SIZE * amount / 256)) { // How much lower lid, in pixels? 192 int a3 = EYES_HEIGHT - a2; // Y offset in canvas 193 canvas.drawRGBBitmap(0, a3, (uint16_t *)&eyelids[a3], EYES_WIDTH, a2); 194 } 195 } 196 } else { // Not blinking 197 if(!random(50)) { // Each time here, 1/50 chance of new blink 198 blinking = true; 199 blinkDuration = random(200000, 300000); 200 blinkStartTime = micros(); 201 } 202 } 203 break; 204 205 case 2: // Process offscreen canvas data 206 // TFT endianism requires byte swapping for raw screen write: 207 buf = canvas.getBuffer(); 208 for(i=0; i<EYES_WIDTH * EYES_HEIGHT; i++) { 209 buf[i] = (buf[i] << 8) | (buf[i] >> 8); 210 } 211 break; 212 213 case 3: // Issue data 214 // Blit offscreen canvas to TFT using SPI transaction 215 display.setAddrWindow(12, 25, 12 + EYES_WIDTH - 1, 25 + EYES_HEIGHT - 1); 216 SPI.beginTransaction(SPIset); 217 digitalWrite(TFT_DC, HIGH); 218 digitalWrite(TFT_CS, LOW); 219 SPI.transfer(canvas.getBuffer(), EYES_WIDTH * EYES_HEIGHT * 2); 220 digitalWrite(TFT_CS, HIGH); 221 SPI.endTransaction(); 222 break; 223 } 224 225 if(++state > 3) state = 0; 226 } 227