/ 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