/ All_Seeing_Skull / All_Seeing_Skull.ino
All_Seeing_Skull.ino
1 // SPDX-FileCopyrightText: 2018 Phillip Burgess for Adafruit Industries 2 // 3 // SPDX-License-Identifier: MIT 4 5 // All Seeing Skull -- Eye w PIR sensor 6 //-------------------------------------------------------------------------- 7 // Uncanny eyes for Adafruit 1.5" OLED (product #1431) or 1.44" TFT LCD 8 // (#2088). Works on PJRC Teensy 3.x and on Adafruit M0 and M4 boards 9 // (Feather, Metro, etc.). This code uses features specific to these 10 // boards and WILL NOT work on normal Arduino or other boards! 11 // 12 // SEE FILE "config.h" FOR MOST CONFIGURATION (graphics, pins, display type, 13 // etc). Probably won't need to edit THIS file unless you're doing some 14 // extremely custom modifications. 15 // 16 // Adafruit invests time and resources providing this open source code, 17 // please support Adafruit and open-source hardware by purchasing products 18 // from Adafruit! 19 // 20 // Written by Phil Burgess / Paint Your Dragon for Adafruit Industries. 21 // MIT license. SPI FIFO insight from Paul Stoffregen's ILI9341_t3 library. 22 // Inspired by David Boccabella's (Marcwolf) hybrid servo/OLED eye concept. 23 //-------------------------------------------------------------------------- 24 25 #include <SPI.h> 26 #include <Adafruit_GFX.h> 27 #ifdef ARDUINO_ARCH_SAMD 28 #include <Adafruit_ZeroDMA.h> 29 #endif 30 31 //PIR sensor 32 int MOTION_SENSOR_PIN = A11; // SENSE port on HalloWing 33 unsigned long lastTriggerTime = 0L; 34 35 typedef struct { // Struct is defined before including config.h -- 36 int8_t select; // pin numbers for each eye's screen select line 37 int8_t wink; // and wink button (or -1 if none) specified there, 38 uint8_t rotation; // also display rotation. 39 } eyeInfo_t; 40 41 #include "config.h" // ****** CONFIGURATION IS DONE IN HERE ****** 42 43 #if defined(_ADAFRUIT_ST7735H_) || defined(_ADAFRUIT_ST77XXH_) 44 typedef Adafruit_ST7735 displayType; // Using TFT display(s) 45 #else 46 typedef Adafruit_SSD1351 displayType; // Using OLED display(s) 47 #endif 48 49 // A simple state machine is used to control eye blinks/winks: 50 #define NOBLINK 0 // Not currently engaged in a blink 51 #define ENBLINK 1 // Eyelid is currently closing 52 #define DEBLINK 2 // Eyelid is currently opening 53 typedef struct { 54 uint8_t state; // NOBLINK/ENBLINK/DEBLINK 55 uint32_t duration; // Duration of blink state (micros) 56 uint32_t startTime; // Time (micros) of last state change 57 } eyeBlink; 58 59 #define NUM_EYES (sizeof eyeInfo / sizeof eyeInfo[0]) // config.h pin list 60 61 struct { // One-per-eye structure 62 displayType *display; // -> OLED/TFT object 63 eyeBlink blink; // Current blink/wink state 64 } eye[NUM_EYES]; 65 66 #ifdef ARDUINO_ARCH_SAMD 67 // SAMD boards use DMA (Teensy uses SPI FIFO instead): 68 // Two single-line 128-pixel buffers (16bpp) are used for DMA. 69 // Though you'd think fewer larger transfers would improve speed, 70 // multi-line buffering made no appreciable difference. 71 uint16_t dmaBuf[2][128]; 72 uint8_t dmaIdx = 0; // Active DMA buffer # (alternate fill/send) 73 Adafruit_ZeroDMA dma; 74 DmacDescriptor *descriptor; 75 76 // DMA transfer-in-progress indicator and callback 77 static volatile bool dma_busy = false; 78 static void dma_callback(Adafruit_ZeroDMA *dma) { dma_busy = false; } 79 #endif 80 81 uint32_t startTime; // For FPS indicator 82 83 84 // INITIALIZATION -- runs once at startup ---------------------------------- 85 86 void setup(void) { 87 uint8_t e; // Eye index, 0 to NUM_EYES-1 88 89 Serial.begin(115200); 90 randomSeed(analogRead(A3)); // Seed random() from floating analog input 91 92 pinMode(MOTION_SENSOR_PIN, INPUT); 93 94 #ifdef DISPLAY_BACKLIGHT 95 // Enable backlight pin, initially off 96 pinMode(DISPLAY_BACKLIGHT, OUTPUT); 97 digitalWrite(DISPLAY_BACKLIGHT, LOW); 98 #endif 99 100 // Initialize eye objects based on eyeInfo list in config.h: 101 for(e=0; e<NUM_EYES; e++) { 102 eye[e].display = new displayType(eyeInfo[e].select, DISPLAY_DC, -1); 103 eye[e].blink.state = NOBLINK; 104 // If project involves only ONE eye and NO other SPI devices, its 105 // select line can be permanently tied to GND and corresponding pin 106 // in config.h set to -1. Best to use it though. 107 if(eyeInfo[e].select >= 0) { 108 pinMode(eyeInfo[e].select, OUTPUT); 109 digitalWrite(eyeInfo[e].select, HIGH); // Deselect them all 110 } 111 // Also set up an individual eye-wink pin if defined: 112 if(eyeInfo[e].wink >= 0) pinMode(eyeInfo[e].wink, INPUT_PULLUP); 113 } 114 #if defined(BLINK_PIN) && (BLINK_PIN >= 0) 115 pinMode(BLINK_PIN, INPUT_PULLUP); // Ditto for all-eyes blink pin 116 #endif 117 118 #if defined(DISPLAY_RESET) && (DISPLAY_RESET >= 0) 119 // Because both displays share a common reset pin, -1 is passed to 120 // the display constructor above to prevent the begin() function from 121 // resetting both displays after one is initialized. Instead, handle 122 // the reset manually here to take care of both displays just once: 123 pinMode(DISPLAY_RESET, OUTPUT); 124 digitalWrite(DISPLAY_RESET, LOW); delay(1); 125 digitalWrite(DISPLAY_RESET, HIGH); delay(50); 126 // Alternately, all display reset pin(s) could be connected to the 127 // microcontroller reset, in which case DISPLAY_RESET should be set 128 // to -1 or left undefined in config.h. 129 #endif 130 131 // After all-displays reset, now call init/begin func for each display: 132 for(e=0; e<NUM_EYES; e++) { 133 #if defined(_ADAFRUIT_ST7735H_) || defined(_ADAFRUIT_ST77XXH_) // TFT 134 eye[e].display->initR(INITR_144GREENTAB); 135 #else // OLED 136 eye[e].display->begin(); 137 #endif 138 eye[e].display->setRotation(eyeInfo[e].rotation); 139 } 140 141 #if defined(LOGO_TOP_WIDTH) || defined(COLOR_LOGO_WIDTH) 142 // I noticed lots of folks getting right/left eyes flipped, or 143 // installing upside-down, etc. Logo split across screens may help: 144 for(e=0; e<NUM_EYES; e++) { // Another pass, after all screen inits 145 eye[e].display->fillScreen(0); 146 #ifdef LOGO_TOP_WIDTH 147 // Monochrome Adafruit logo is 2 mono bitmaps: 148 eye[e].display->drawBitmap(NUM_EYES*64 - e*128 - 20, 149 0, logo_top, LOGO_TOP_WIDTH, LOGO_TOP_HEIGHT, 0xFFFF); 150 eye[e].display->drawBitmap(NUM_EYES*64 - e*128 - LOGO_BOTTOM_WIDTH/2, 151 LOGO_TOP_HEIGHT, logo_bottom, LOGO_BOTTOM_WIDTH, LOGO_BOTTOM_HEIGHT, 152 0xFFFF); 153 #else 154 // Color sponsor logo is one RGB bitmap: 155 eye[e].display->fillScreen(color_logo[0]); 156 eye[0].display->drawRGBBitmap( 157 (eye[e].display->width() - COLOR_LOGO_WIDTH ) / 2, 158 (eye[e].display->height() - COLOR_LOGO_HEIGHT) / 2, 159 color_logo, COLOR_LOGO_WIDTH, COLOR_LOGO_HEIGHT); 160 #endif 161 // After logo is drawn 162 } 163 #ifdef DISPLAY_BACKLIGHT 164 int i; 165 for(i=0; i<BACKLIGHT_MAX; i++) { // Fade logo in 166 analogWrite(DISPLAY_BACKLIGHT, i); 167 delay(2); 168 } 169 delay(1400); // Pause for screen layout/orientation 170 for(; i>=0; i--) { 171 analogWrite(DISPLAY_BACKLIGHT, i); 172 delay(2); 173 } 174 for(e=0; e<NUM_EYES; e++) { // Clear display(s) 175 eye[e].display->fillScreen(0); 176 } 177 delay(100); 178 #else 179 delay(2000); // Pause for screen layout/orientation 180 #endif // DISPLAY_BACKLIGHT 181 #endif // LOGO_TOP_WIDTH 182 183 // One of the displays is configured to mirror on the X axis. Simplifies 184 // eyelid handling in the drawEye() function -- no need for distinct 185 // L-to-R or R-to-L inner loops. Just the X coordinate of the iris is 186 // then reversed when drawing this eye, so they move the same. Magic! 187 #if defined(_ADAFRUIT_ST7735H_) || defined(_ADAFRUIT_ST77XXH_) // TFT 188 const uint8_t mirrorTFT[] = { 0x88, 0x28, 0x48, 0xE8 }; // Mirror+rotate 189 digitalWrite(eyeInfo[0].select, LOW); 190 digitalWrite(DISPLAY_DC, LOW); 191 #ifdef ST77XX_MADCTL 192 SPI.transfer(ST77XX_MADCTL); // Current TFT lib 193 #else 194 SPI.transfer(ST7735_MADCTL); // Older TFT lib 195 #endif 196 digitalWrite(DISPLAY_DC, HIGH); 197 SPI.transfer(mirrorTFT[eyeInfo[0].rotation & 3]); 198 digitalWrite(eyeInfo[0].select , HIGH); 199 #else // OLED 200 const uint8_t rotateOLED[] = { 0x74, 0x77, 0x66, 0x65 }, 201 mirrorOLED[] = { 0x76, 0x67, 0x64, 0x75 }; // Mirror+rotate 202 // If OLED, loop through ALL eyes and set up remap register 203 // from either mirrorOLED[] (first eye) or rotateOLED[] (others). 204 // The OLED library doesn't normally use the remap reg (TFT does). 205 for(e=0; e<NUM_EYES; e++) { 206 uint8_t d = e ? rotateOLED[eyeInfo[e].rotation & 3] : 207 mirrorOLED[eyeInfo[e].rotation & 3]; 208 eye[e].display->sendCommand(SSD1351_CMD_SETREMAP, &d, 1); 209 } 210 #endif 211 212 #ifdef ARDUINO_ARCH_SAMD 213 // Set up SPI DMA on SAMD boards: 214 int dmac_id; 215 volatile uint32_t *data_reg; 216 if(&PERIPH_SPI == &sercom0) { 217 dmac_id = SERCOM0_DMAC_ID_TX; 218 data_reg = &SERCOM0->SPI.DATA.reg; 219 #if defined SERCOM1 220 } else if(&PERIPH_SPI == &sercom1) { 221 dmac_id = SERCOM1_DMAC_ID_TX; 222 data_reg = &SERCOM1->SPI.DATA.reg; 223 #endif 224 #if defined SERCOM2 225 } else if(&PERIPH_SPI == &sercom2) { 226 dmac_id = SERCOM2_DMAC_ID_TX; 227 data_reg = &SERCOM2->SPI.DATA.reg; 228 #endif 229 #if defined SERCOM3 230 } else if(&PERIPH_SPI == &sercom3) { 231 dmac_id = SERCOM3_DMAC_ID_TX; 232 data_reg = &SERCOM3->SPI.DATA.reg; 233 #endif 234 #if defined SERCOM4 235 } else if(&PERIPH_SPI == &sercom4) { 236 dmac_id = SERCOM4_DMAC_ID_TX; 237 data_reg = &SERCOM4->SPI.DATA.reg; 238 #endif 239 #if defined SERCOM5 240 } else if(&PERIPH_SPI == &sercom5) { 241 dmac_id = SERCOM5_DMAC_ID_TX; 242 data_reg = &SERCOM5->SPI.DATA.reg; 243 #endif 244 } 245 246 dma.allocate(); 247 dma.setTrigger(dmac_id); 248 dma.setAction(DMA_TRIGGER_ACTON_BEAT); 249 descriptor = dma.addDescriptor( 250 NULL, // move data 251 (void *)data_reg, // to here 252 sizeof dmaBuf[0], // this many... 253 DMA_BEAT_SIZE_BYTE, // bytes/hword/words 254 true, // increment source addr? 255 false); // increment dest addr? 256 dma.setCallback(dma_callback); 257 258 #endif // End SAMD-specific SPI DMA init 259 260 #ifdef DISPLAY_BACKLIGHT 261 analogWrite(DISPLAY_BACKLIGHT, BACKLIGHT_MAX); 262 #endif 263 264 startTime = millis(); // For frame-rate calculation 265 } 266 267 268 // EYE-RENDERING FUNCTION -------------------------------------------------- 269 270 SPISettings settings(SPI_FREQ, MSBFIRST, SPI_MODE0); 271 272 void drawEye( // Renders one eye. Inputs must be pre-clipped & valid. 273 uint8_t e, // Eye array index; 0 or 1 for left/right 274 uint32_t iScale, // Scale factor for iris 275 uint8_t scleraX, // First pixel X offset into sclera image 276 uint8_t scleraY, // First pixel Y offset into sclera image 277 uint8_t uT, // Upper eyelid threshold value 278 uint8_t lT) { // Lower eyelid threshold value 279 280 uint8_t screenX, screenY, scleraXsave; 281 int16_t irisX, irisY; 282 uint16_t p, a; 283 uint32_t d; 284 285 //PIR detect 286 if(digitalRead(MOTION_SENSOR_PIN)){ //PIR triggered 287 lastTriggerTime = millis(); //save last trigger time 288 } 289 290 291 // Set up raw pixel dump to entire screen. Although such writes can wrap 292 // around automatically from end of rect back to beginning, the region is 293 // reset on each frame here in case of an SPI glitch. 294 SPI.beginTransaction(settings); 295 digitalWrite(eyeInfo[e].select, LOW); // Chip select 296 #if defined(_ADAFRUIT_ST7735H_) || defined(_ADAFRUIT_ST77XXH_) // TFT 297 eye[e].display->setAddrWindow(0, 0, 128, 128); 298 #else // OLED 299 eye[e].display->writeCommand(SSD1351_CMD_SETROW); // Y range 300 eye[e].display->writeData(0); eye[e].display->writeData(SCREEN_HEIGHT - 1); 301 eye[e].display->writeCommand(SSD1351_CMD_SETCOLUMN); // X range 302 eye[e].display->writeData(0); eye[e].display->writeData(SCREEN_WIDTH - 1); 303 eye[e].display->writeCommand(SSD1351_CMD_WRITERAM); // Begin write 304 #endif 305 digitalWrite(eyeInfo[e].select, LOW); // Re-chip-select 306 digitalWrite(DISPLAY_DC, HIGH); // Data mode 307 // Now just issue raw 16-bit values for every pixel... 308 309 //PIR sensor check trigger times 310 if((millis() - lastTriggerTime) <= 5000) { // draw because PIR tripped 311 scleraXsave = scleraX; // Save initial X value to reset on each line 312 irisY = scleraY - (SCLERA_HEIGHT - IRIS_HEIGHT) / 2; 313 for(screenY=0; screenY<SCREEN_HEIGHT; screenY++, scleraY++, irisY++) { 314 #ifdef ARDUINO_ARCH_SAMD 315 uint16_t *ptr = &dmaBuf[dmaIdx][0]; 316 #endif 317 scleraX = scleraXsave; 318 irisX = scleraXsave - (SCLERA_WIDTH - IRIS_WIDTH) / 2; 319 for(screenX=0; screenX<SCREEN_WIDTH; screenX++, scleraX++, irisX++) { 320 if((lower[screenY][screenX] <= lT) || 321 (upper[screenY][screenX] <= uT)) { // Covered by eyelid 322 p = 0; 323 } else if((irisY < 0) || (irisY >= IRIS_HEIGHT) || 324 (irisX < 0) || (irisX >= IRIS_WIDTH)) { // In sclera 325 p = sclera[scleraY][scleraX]; 326 } else { // Maybe iris... 327 p = polar[irisY][irisX]; // Polar angle/dist 328 d = (iScale * (p & 0x7F)) / 128; // Distance (Y) 329 if(d < IRIS_MAP_HEIGHT) { // Within iris area 330 a = (IRIS_MAP_WIDTH * (p >> 7)) / 512; // Angle (X) 331 p = iris[d][a]; // Pixel = iris 332 } else { // Not in iris 333 p = sclera[scleraY][scleraX]; // Pixel = sclera 334 } 335 } 336 #ifdef ARDUINO_ARCH_SAMD 337 *ptr++ = __builtin_bswap16(p); // DMA: store in scanline buffer 338 #else 339 // SPI FIFO technique from Paul Stoffregen's ILI9341_t3 library: 340 while(KINETISK_SPI0.SR & 0xC000); // Wait for space in FIFO 341 KINETISK_SPI0.PUSHR = p | SPI_PUSHR_CTAS(1) | SPI_PUSHR_CONT; 342 #endif 343 } // end column 344 #ifdef ARDUINO_ARCH_SAMD 345 while(dma_busy); // Wait for prior DMA xfer to finish 346 descriptor->SRCADDR.reg = (uint32_t)&dmaBuf[dmaIdx] + sizeof dmaBuf[0]; 347 dma_busy = true; 348 dmaIdx = 1 - dmaIdx; 349 dma.startJob(); 350 #endif 351 } // end scanline 352 } 353 354 else { //don't draw because PIR didn't trip 355 scleraXsave = scleraX; // Save initial X value to reset on each line 356 irisY = scleraY - (SCLERA_HEIGHT - IRIS_HEIGHT) / 2; 357 for(screenY=0; screenY<SCREEN_HEIGHT; screenY++, scleraY++, irisY++) { 358 #ifdef ARDUINO_ARCH_SAMD 359 uint16_t *ptr = &dmaBuf[dmaIdx][0]; 360 #endif 361 scleraX = scleraXsave; 362 irisX = scleraXsave - (SCLERA_WIDTH - IRIS_WIDTH) / 2; 363 for(screenX=0; screenX<SCREEN_WIDTH; screenX++, scleraX++, irisX++) { 364 if((lower[screenY][screenX] <= lT) || 365 (upper[screenY][screenX] <= uT)) { // Covered by eyelid 366 p = 0; 367 } else if((irisY < 0) || (irisY >= IRIS_HEIGHT) || 368 (irisX < 0) || (irisX >= IRIS_WIDTH)) { // In sclera 369 //p = sclera[scleraY][scleraX]; 370 p = 0; 371 } else { // Maybe iris... 372 //p = polar[irisY][irisX]; // Polar angle/dist 373 p=0; 374 d = (iScale * (p & 0x7F)) / 128; // Distance (Y) 375 if(d < IRIS_MAP_HEIGHT) { // Within iris area 376 a = (IRIS_MAP_WIDTH * (p >> 7)) / 512; // Angle (X) 377 //p = iris[d][a]; // Pixel = iris 378 p=0; 379 } else { // Not in iris 380 //p = sclera[scleraY][scleraX]; // Pixel = sclera 381 p=0; 382 } 383 } 384 #ifdef ARDUINO_ARCH_SAMD 385 *ptr++ = __builtin_bswap16(p); // DMA: store in scanline buffer 386 #else 387 // SPI FIFO technique from Paul Stoffregen's ILI9341_t3 library: 388 while(KINETISK_SPI0.SR & 0xC000); // Wait for space in FIFO 389 KINETISK_SPI0.PUSHR = p | SPI_PUSHR_CTAS(1) | SPI_PUSHR_CONT; 390 #endif 391 } // end column 392 #ifdef ARDUINO_ARCH_SAMD 393 while(dma_busy); // Wait for prior DMA xfer to finish 394 descriptor->SRCADDR.reg = (uint32_t)&dmaBuf[dmaIdx] + sizeof dmaBuf[0]; 395 dma_busy = true; 396 dmaIdx = 1 - dmaIdx; 397 dma.startJob(); 398 #endif 399 } // end scanline 400 } 401 402 403 404 #ifdef ARDUINO_ARCH_SAMD 405 while(dma_busy); // Wait for last scanline to transmit 406 #else 407 KINETISK_SPI0.SR |= SPI_SR_TCF; // Clear transfer flag 408 while((KINETISK_SPI0.SR & 0xF000) || // Wait for SPI FIFO to drain 409 !(KINETISK_SPI0.SR & SPI_SR_TCF)); // Wait for last bit out 410 #endif 411 412 digitalWrite(eyeInfo[e].select, HIGH); // Deselect 413 SPI.endTransaction(); 414 } 415 416 417 // EYE ANIMATION ----------------------------------------------------------- 418 419 const uint8_t ease[] = { // Ease in/out curve for eye movements 3*t^2-2*t^3 420 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 3, // T 421 3, 3, 4, 4, 4, 5, 5, 6, 6, 7, 7, 8, 9, 9, 10, 10, // h 422 11, 12, 12, 13, 14, 15, 15, 16, 17, 18, 18, 19, 20, 21, 22, 23, // x 423 24, 25, 26, 27, 27, 28, 29, 30, 31, 33, 34, 35, 36, 37, 38, 39, // 2 424 40, 41, 42, 44, 45, 46, 47, 48, 50, 51, 52, 53, 54, 56, 57, 58, // A 425 60, 61, 62, 63, 65, 66, 67, 69, 70, 72, 73, 74, 76, 77, 78, 80, // l 426 81, 83, 84, 85, 87, 88, 90, 91, 93, 94, 96, 97, 98,100,101,103, // e 427 104,106,107,109,110,112,113,115,116,118,119,121,122,124,125,127, // c 428 128,130,131,133,134,136,137,139,140,142,143,145,146,148,149,151, // J 429 152,154,155,157,158,159,161,162,164,165,167,168,170,171,172,174, // a 430 175,177,178,179,181,182,183,185,186,188,189,190,192,193,194,195, // c 431 197,198,199,201,202,203,204,205,207,208,209,210,211,213,214,215, // o 432 216,217,218,219,220,221,222,224,225,226,227,228,228,229,230,231, // b 433 232,233,234,235,236,237,237,238,239,240,240,241,242,243,243,244, // s 434 245,245,246,246,247,248,248,249,249,250,250,251,251,251,252,252, // o 435 252,253,253,253,254,254,254,254,254,255,255,255,255,255,255,255 }; // n 436 437 #ifdef AUTOBLINK 438 uint32_t timeOfLastBlink = 0L, timeToNextBlink = 0L; 439 #endif 440 441 void frame( // Process motion for a single frame of left or right eye 442 uint16_t iScale) { // Iris scale (0-1023) passed in 443 static uint32_t frames = 0; // Used in frame rate calculation 444 static uint8_t eyeIndex = 0; // eye[] array counter 445 int16_t eyeX, eyeY; 446 uint32_t t = micros(); // Time at start of function 447 448 if(!(++frames & 255)) { // Every 256 frames... 449 uint32_t elapsed = (millis() - startTime) / 1000; 450 if(elapsed) Serial.println(frames / elapsed); // Print FPS 451 } 452 453 if(++eyeIndex >= NUM_EYES) eyeIndex = 0; // Cycle through eyes, 1 per call 454 455 // X/Y movement 456 457 #if defined(JOYSTICK_X_PIN) && (JOYSTICK_X_PIN >= 0) && \ 458 defined(JOYSTICK_Y_PIN) && (JOYSTICK_Y_PIN >= 0) 459 460 // Read X/Y from joystick, constrain to circle 461 int16_t dx, dy; 462 int32_t d; 463 eyeX = analogRead(JOYSTICK_X_PIN); // Raw (unclipped) X/Y reading 464 eyeY = analogRead(JOYSTICK_Y_PIN); 465 466 #ifdef JOYSTICK_X_FLIP 467 eyeX = 1023 - eyeX; 468 #endif 469 #ifdef JOYSTICK_Y_FLIP 470 eyeY = 1023 - eyeY; 471 #endif 472 dx = (eyeX * 2) - 1023; // A/D exact center is at 511.5. Scale coords 473 dy = (eyeY * 2) - 1023; // X2 so range is -1023 to +1023 w/center at 0. 474 if((d = (dx * dx + dy * dy)) > (1023 * 1023)) { // Outside circle 475 d = (int32_t)sqrt((float)d); // Distance from center 476 eyeX = ((dx * 1023 / d) + 1023) / 2; // Clip to circle edge, 477 eyeY = ((dy * 1023 / d) + 1023) / 2; // scale back to 0-1023 478 } 479 480 #else // Autonomous X/Y eye motion 481 // Periodically initiates motion to a new random point, random speed, 482 // holds there for random period until next motion. 483 484 static boolean eyeInMotion = false; 485 static int16_t eyeOldX=512, eyeOldY=512, eyeNewX=512, eyeNewY=512; 486 static uint32_t eyeMoveStartTime = 0L; 487 static int32_t eyeMoveDuration = 0L; 488 489 int32_t dt = t - eyeMoveStartTime; // uS elapsed since last eye event 490 if(eyeInMotion) { // Currently moving? 491 if(dt >= eyeMoveDuration) { // Time up? Destination reached. 492 eyeInMotion = false; // Stop moving 493 eyeMoveDuration = random(3000000); // 0-3 sec stop 494 eyeMoveStartTime = t; // Save initial time of stop 495 eyeX = eyeOldX = eyeNewX; // Save position 496 eyeY = eyeOldY = eyeNewY; 497 } else { // Move time's not yet fully elapsed -- interpolate position 498 int16_t e = ease[255 * dt / eyeMoveDuration] + 1; // Ease curve 499 eyeX = eyeOldX + (((eyeNewX - eyeOldX) * e) / 256); // Interp X 500 eyeY = eyeOldY + (((eyeNewY - eyeOldY) * e) / 256); // and Y 501 } 502 } else { // Eye stopped 503 eyeX = eyeOldX; 504 eyeY = eyeOldY; 505 if(dt > eyeMoveDuration) { // Time up? Begin new move. 506 int16_t dx, dy; 507 uint32_t d; 508 do { // Pick new dest in circle 509 eyeNewX = random(1024); 510 eyeNewY = random(1024); 511 512 dx = (eyeNewX * 2) - 1023; 513 dy = (eyeNewY * 2) - 1023; 514 } while((d = (dx * dx + dy * dy)) > (eyeXRange * eyeYRange)); // Keep trying 515 eyeNewX += eyeXOffset; 516 eyeNewY += eyeYOffset; 517 eyeMoveDuration = random(72000, 144000); // ~1/14 - ~1/7 sec 518 eyeMoveStartTime = t; // Save initial time of move 519 eyeInMotion = true; // Start move on next frame 520 } 521 } 522 523 #endif // JOYSTICK_X_PIN etc. 524 525 // Blinking 526 527 #ifdef AUTOBLINK 528 // Similar to the autonomous eye movement above -- blink start times 529 // and durations are random (within ranges). 530 if((t - timeOfLastBlink) >= timeToNextBlink) { // Start new blink? 531 timeOfLastBlink = t; 532 uint32_t blinkDuration = random(36000, 72000); // ~1/28 - ~1/14 sec 533 // Set up durations for both eyes (if not already winking) 534 for(uint8_t e=0; e<NUM_EYES; e++) { 535 if(eye[e].blink.state == NOBLINK) { 536 eye[e].blink.state = ENBLINK; 537 eye[e].blink.startTime = t; 538 eye[e].blink.duration = blinkDuration; 539 } 540 } 541 timeToNextBlink = blinkDuration * 3 + random(4000000); 542 } 543 #endif 544 545 if(eye[eyeIndex].blink.state) { // Eye currently blinking? 546 // Check if current blink state time has elapsed 547 if((t - eye[eyeIndex].blink.startTime) >= eye[eyeIndex].blink.duration) { 548 // Yes -- increment blink state, unless... 549 if((eye[eyeIndex].blink.state == ENBLINK) && ( // Enblinking and... 550 #if defined(BLINK_PIN) && (BLINK_PIN >= 0) 551 (digitalRead(BLINK_PIN) == LOW) || // blink or wink held... 552 #endif 553 ((eyeInfo[eyeIndex].wink >= 0) && 554 digitalRead(eyeInfo[eyeIndex].wink) == LOW) )) { 555 // Don't advance state yet -- eye is held closed instead 556 } else { // No buttons, or other state... 557 if(++eye[eyeIndex].blink.state > DEBLINK) { // Deblinking finished? 558 eye[eyeIndex].blink.state = NOBLINK; // No longer blinking 559 } else { // Advancing from ENBLINK to DEBLINK mode 560 eye[eyeIndex].blink.duration *= 2; // DEBLINK is 1/2 ENBLINK speed 561 eye[eyeIndex].blink.startTime = t; 562 } 563 } 564 } 565 } else { // Not currently blinking...check buttons! 566 #if defined(BLINK_PIN) && (BLINK_PIN >= 0) 567 if(digitalRead(BLINK_PIN) == LOW) { 568 // Manually-initiated blinks have random durations like auto-blink 569 uint32_t blinkDuration = random(36000, 72000); 570 for(uint8_t e=0; e<NUM_EYES; e++) { 571 if(eye[e].blink.state == NOBLINK) { 572 eye[e].blink.state = ENBLINK; 573 eye[e].blink.startTime = t; 574 eye[e].blink.duration = blinkDuration; 575 } 576 } 577 } else 578 #endif 579 if((eyeInfo[eyeIndex].wink >= 0) && 580 (digitalRead(eyeInfo[eyeIndex].wink) == LOW)) { // Wink! 581 eye[eyeIndex].blink.state = ENBLINK; 582 eye[eyeIndex].blink.startTime = t; 583 eye[eyeIndex].blink.duration = random(45000, 90000); 584 } 585 } 586 587 // Process motion, blinking and iris scale into renderable values 588 589 // Iris scaling: remap from 0-1023 input to iris map height pixel units 590 iScale = ((IRIS_MAP_HEIGHT + 1) * 1024) / 591 (1024 - (iScale * (IRIS_MAP_HEIGHT - 1) / IRIS_MAP_HEIGHT)); 592 593 // Scale eye X/Y positions (0-1023) to pixel units used by drawEye() 594 eyeX = map(eyeX, 0, 1023, 0, SCLERA_WIDTH - 128); 595 eyeY = map(eyeY, 0, 1023, 0, SCLERA_HEIGHT - 128); 596 if(eyeIndex == 1) eyeX = (SCLERA_WIDTH - 128) - eyeX; // Mirrored display 597 598 // Horizontal position is offset so that eyes are very slightly crossed 599 // to appear fixated (converged) at a conversational distance. Number 600 // here was extracted from my posterior and not mathematically based. 601 // I suppose one could get all clever with a range sensor, but for now... 602 if(NUM_EYES > 1) eyeX += 4; 603 if(eyeX > (SCLERA_WIDTH - 128)) eyeX = (SCLERA_WIDTH - 128); 604 605 // Eyelids are rendered using a brightness threshold image. This same 606 // map can be used to simplify another problem: making the upper eyelid 607 // track the pupil (eyes tend to open only as much as needed -- e.g. look 608 // down and the upper eyelid drops). Just sample a point in the upper 609 // lid map slightly above the pupil to determine the rendering threshold. 610 static uint8_t uThreshold = 128; 611 uint8_t lThreshold, n; 612 #ifdef TRACKING 613 int16_t sampleX = SCLERA_WIDTH / 2 - (eyeX / 2), // Reduce X influence 614 sampleY = SCLERA_HEIGHT / 2 - (eyeY + IRIS_HEIGHT / 4); 615 // Eyelid is slightly asymmetrical, so two readings are taken, averaged 616 if(sampleY < 0) n = 0; 617 else n = (upper[sampleY][sampleX] + 618 upper[sampleY][SCREEN_WIDTH - 1 - sampleX]) / 2; 619 uThreshold = (uThreshold * 3 + n) / 4; // Filter/soften motion 620 // Lower eyelid doesn't track the same way, but seems to be pulled upward 621 // by tension from the upper lid. 622 lThreshold = 254 - uThreshold; 623 #else // No tracking -- eyelids full open unless blink modifies them 624 uThreshold = lThreshold = 0; 625 #endif 626 627 // The upper/lower thresholds are then scaled relative to the current 628 // blink position so that blinks work together with pupil tracking. 629 if(eye[eyeIndex].blink.state) { // Eye currently blinking? 630 uint32_t s = (t - eye[eyeIndex].blink.startTime); 631 if(s >= eye[eyeIndex].blink.duration) s = 255; // At or past blink end 632 else s = 255 * s / eye[eyeIndex].blink.duration; // Mid-blink 633 s = (eye[eyeIndex].blink.state == DEBLINK) ? 1 + s : 256 - s; 634 n = (uThreshold * s + 254 * (257 - s)) / 256; 635 lThreshold = (lThreshold * s + 254 * (257 - s)) / 256; 636 } else { 637 n = uThreshold; 638 } 639 640 // Pass all the derived values to the eye-rendering function: 641 drawEye(eyeIndex, iScale, eyeX, eyeY, n, lThreshold); 642 } 643 644 645 // AUTONOMOUS IRIS SCALING (if no photocell or dial) ----------------------- 646 647 #if !defined(LIGHT_PIN) || (LIGHT_PIN < 0) 648 649 // Autonomous iris motion uses a fractal behavior to similate both the major 650 // reaction of the eye plus the continuous smaller adjustments that occur. 651 652 uint16_t oldIris = (IRIS_MIN + IRIS_MAX) / 2, newIris; 653 654 void split( // Subdivides motion path into two sub-paths w/randimization 655 int16_t startValue, // Iris scale value (IRIS_MIN to IRIS_MAX) at start 656 int16_t endValue, // Iris scale value at end 657 uint32_t startTime, // micros() at start 658 int32_t duration, // Start-to-end time, in microseconds 659 int16_t range) { // Allowable scale value variance when subdividing 660 661 if(range >= 8) { // Limit subdvision count, because recursion 662 range /= 2; // Split range & time in half for subdivision, 663 duration /= 2; // then pick random center point within range: 664 int16_t midValue = (startValue + endValue - range) / 2 + random(range); 665 uint32_t midTime = startTime + duration; 666 split(startValue, midValue, startTime, duration, range); // First half 667 split(midValue , endValue, midTime , duration, range); // Second half 668 } else { // No more subdivisons, do iris motion... 669 int32_t dt; // Time (micros) since start of motion 670 int16_t v; // Interim value 671 while((dt = (micros() - startTime)) < duration) { 672 v = startValue + (((endValue - startValue) * dt) / duration); 673 if(v < IRIS_MIN) v = IRIS_MIN; // Clip just in case 674 else if(v > IRIS_MAX) v = IRIS_MAX; 675 frame(v); // Draw frame w/interim iris scale value 676 } 677 } 678 } 679 680 #endif // !LIGHT_PIN 681 682 683 // MAIN LOOP -- runs continuously after setup() ---------------------------- 684 685 void loop() { 686 687 #if defined(LIGHT_PIN) && (LIGHT_PIN >= 0) // Interactive iris 688 689 int16_t v = analogRead(LIGHT_PIN); // Raw dial/photocell reading 690 #ifdef LIGHT_PIN_FLIP 691 v = 1023 - v; // Reverse reading from sensor 692 #endif 693 if(v < LIGHT_MIN) v = LIGHT_MIN; // Clamp light sensor range 694 else if(v > LIGHT_MAX) v = LIGHT_MAX; 695 v -= LIGHT_MIN; // 0 to (LIGHT_MAX - LIGHT_MIN) 696 #ifdef LIGHT_CURVE // Apply gamma curve to sensor input? 697 v = (int16_t)(pow((double)v / (double)(LIGHT_MAX - LIGHT_MIN), 698 LIGHT_CURVE) * (double)(LIGHT_MAX - LIGHT_MIN)); 699 #endif 700 // And scale to iris range (IRIS_MAX is size at LIGHT_MIN) 701 v = map(v, 0, (LIGHT_MAX - LIGHT_MIN), IRIS_MAX, IRIS_MIN); 702 #ifdef IRIS_SMOOTH // Filter input (gradual motion) 703 static int16_t irisValue = (IRIS_MIN + IRIS_MAX) / 2; 704 irisValue = ((irisValue * 15) + v) / 16; 705 frame(irisValue); 706 #else // Unfiltered (immediate motion) 707 frame(v); 708 #endif // IRIS_SMOOTH 709 710 #else // Autonomous iris scaling -- invoke recursive function 711 712 newIris = random(IRIS_MIN, IRIS_MAX); 713 split(oldIris, newIris, micros(), 10000000L, IRIS_MAX - IRIS_MIN); 714 oldIris = newIris; 715 716 #endif // LIGHT_PIN 717 }