55// "Uncanny Eyes" project (better for SAMD21 chips or Teensy 3.X and
66// 128x128 TFT or OLED screens, single SPI bus).
77
8+ // IMPORTANT: in rare situations, a board may get "bricked" when running
9+ // this code while simultaneously connected to USB. A quick-flashing status
10+ // LED indicates the filesystem has gone corrupt. If this happens, install
11+ // CircuitPython to reinitialize the filesystem, copy over your eye files
12+ // (keep backups!), then re-upload this code. It seems to happen more often
13+ // at high optimization settings (above -O3), but there's not 1:1 causality.
14+ // The exact cause has not yet been found...possibly insufficient yield()
15+ // calls, or some rare alignment in the Arcada library or USB-handling code.
16+
817// LET'S HAVE A WORD ABOUT COORDINATE SYSTEMS before continuing. From an
918// outside observer's point of view, looking at the display(s) on these
1019// boards, the eyes are rendered COLUMN AT A TIME, working LEFT TO RIGHT,
3443 #error "Please select Tools->USB Stack->TinyUSB before compiling"
3544#endif
3645
37- #include < Adafruit_TinyUSB.h>
3846#define GLOBAL_VAR
3947#include " globals.h"
4048
@@ -43,6 +51,8 @@ bool eyeInMotion = false;
4351float eyeOldX, eyeOldY, eyeNewX, eyeNewY;
4452uint32_t eyeMoveStartTime = 0L ;
4553int32_t eyeMoveDuration = 0L ;
54+ uint32_t lastSaccadeStop = 0L ;
55+ int32_t saccadeInterval = 0L ;
4656
4757// Some sloppy eye state stuff, some carried over from old eye code...
4858// kinda messy and badly named and will get cleaned up/moved/etc.
@@ -148,9 +158,10 @@ void setup() {
148158
149159 arcada.displayBegin ();
150160
151- DISPLAY_SIZE = min (ARCADA_TFT_WIDTH, ARCADA_TFT_HEIGHT);
152- DISPLAY_X_OFFSET = (ARCADA_TFT_WIDTH - DISPLAY_SIZE) / 2 ;
153- DISPLAY_Y_OFFSET = (ARCADA_TFT_HEIGHT - DISPLAY_SIZE) / 2 ;
161+ // Backlight(s) off ASAP, they'll switch on after screen(s) init & clear
162+ arcada.setBacklight (0 );
163+
164+ DISPLAY_SIZE = min (ARCADA_TFT_WIDTH, ARCADA_TFT_HEIGHT);
154165
155166 Serial.begin (115200 );
156167 // while(!Serial) yield();
@@ -159,9 +170,6 @@ void setup() {
159170 Serial.printf (" Available flash at start: %d\n " , arcada.availableFlash ());
160171 yield (); // Periodic yield() makes sure mass storage filesystem stays alive
161172
162- // Backlight(s) off ASAP, they'll switch on after screen(s) init & clear
163- arcada.setBacklight (0 );
164-
165173 // No file selector yet. In the meantime, you can override the default
166174 // config file by holding one of the 3 edge buttons at startup (loads
167175 // config1.eye, config2.eye or config3.eye instead). Keep fingers clear
@@ -179,42 +187,20 @@ void setup() {
179187 }
180188
181189 yield ();
182- // Initialize displays
183- #if (NUM_EYES > 1)
184- eye[0 ].display = arcada._display ;
185- eye[1 ].display = arcada.display2 ;
186- #else
187- eye[0 ].display = arcada.display ;
188- #endif
189-
190- yield ();
191- if (showSplashScreen) {
192- if (arcada.drawBMP ((char *)" /splash.bmp" , 0 , 0 , (eye[0 ].display )) == IMAGE_SUCCESS) {
193- Serial.println (" Splashing" );
194- if (NUM_EYES > 1 ) { // other eye
195- yield ();
196- arcada.drawBMP ((char *)" /splash.bmp" , 0 , 0 , (eye[1 ].display ));
197- }
198- // backlight on for a bit
199- for (int bl=0 ; bl<=250 ; bl+=20 ) {
200- arcada.setBacklight (bl);
201- delay (20 );
202- }
203- delay (2000 );
204- // backlight back off
205- for (int bl=250 ; bl>=0 ; bl-=20 ) {
206- arcada.setBacklight (bl);
207- delay (20 );
208- }
209- }
210- }
190+ // Initialize display(s)
191+ #if (NUM_EYES > 1)
192+ eye[0 ].display = arcada._display ;
193+ eye[1 ].display = arcada.display2 ;
194+ #else
195+ eye[0 ].display = arcada.display ;
196+ #endif
211197
212198 // Initialize DMAs
213199 yield ();
214200 uint8_t e;
215201 for (e=0 ; e<NUM_EYES; e++) {
216- #if (ARCADA_TFT_WIDTH != 160) && (ARCADA_TFT_HEIGHT != 128) // 160x128 is ST7735 which isn't able to deal
217- eye[e].spi ->setClockSource (DISPLAY_CLKSRC);
202+ #if (ARCADA_TFT_WIDTH != 160) && (ARCADA_TFT_HEIGHT != 128) // 160x128 is ST7735 which isn't able to deal
203+ eye[e].spi ->setClockSource (DISPLAY_CLKSRC); // Accelerate SPI!
218204#endif
219205 eye[e].display ->fillScreen (0 );
220206 eye[e].dma .allocate ();
@@ -265,12 +251,36 @@ void setup() {
265251
266252 // Uncanny eyes carryover stuff for now, all messy:
267253 eye[e].blink .state = NOBLINK;
268- // eye[e].eyeX = 512;
269- // eye[e].eyeY = 512;
270254 eye[e].blinkFactor = 0.0 ;
271255 }
272256
273- arcada.setBacklight (255 );
257+ // SPLASH SCREEN (IF FILE PRESENT) ---------------------------------------
258+
259+ yield ();
260+ uint32_t startTime, elapsed;
261+ if (showSplashScreen) {
262+ showSplashScreen = ((arcada.drawBMP ((char *)" /splash.bmp" ,
263+ 0 , 0 , eye[0 ].display )) == IMAGE_SUCCESS);
264+ if (showSplashScreen) { // Loaded OK?
265+ Serial.println (" Splashing" );
266+ if (NUM_EYES > 1 ) { // Load on other eye too, ignore status
267+ yield ();
268+ arcada.drawBMP ((char *)" /splash.bmp" , 0 , 0 , eye[1 ].display );
269+ }
270+ // Ramp up backlight over 1/2 sec duration
271+ startTime = millis ();
272+ while ((elapsed = (millis () - startTime)) <= 500 ) {
273+ yield ();
274+ arcada.setBacklight (255 * elapsed / 500 );
275+ }
276+ arcada.setBacklight (255 ); // To the max
277+ startTime = millis (); // Note current time for backlight hold later
278+ }
279+ }
280+
281+ // If no splash, or load failed, turn backlight on early so user gets a
282+ // little feedback, that the board is not locked up, just thinking.
283+ if (!showSplashScreen) arcada.setBacklight (255 );
274284
275285 // LOAD CONFIGURATION FILE -----------------------------------------------
276286
@@ -301,7 +311,7 @@ void setup() {
301311 // leave some RAM for the stack to operate over the lifetime of this
302312 // program and to handle small heap allocations.
303313
304- uint32_t maxRam = availableRAM () - stackReserve;
314+ uint32_t maxRam = availableRAM () - stackReserve;
305315
306316 // Load texture maps for eyes
307317 uint8_t e2 ;
@@ -392,12 +402,28 @@ void setup() {
392402 randomSeed (SysTick->VAL + analogRead (A2));
393403 eyeOldX = eyeNewX = eyeOldY = eyeNewY = mapRadius; // Start in center
394404 for (e=0 ; e<NUM_EYES; e++) { // For each eye...
395- // Set up screen rotation (MUST be done after config load!)
396405 eye[e].display ->setRotation (eye[e].rotation );
397406 eye[e].eyeX = eyeOldX; // Set up initial position
398407 eye[e].eyeY = eyeOldY;
399408 }
400409
410+ if (showSplashScreen) { // Image(s) loaded above?
411+ // Hold backlight on for up to 2 seconds (minus other initialization time)
412+ if ((elapsed = (millis () - startTime)) < 2000 ) {
413+ delay (2000 - elapsed);
414+ }
415+ // Ramp down backlight over 1/2 sec duration
416+ startTime = millis ();
417+ while ((elapsed = (millis () - startTime)) <= 500 ) {
418+ yield ();
419+ arcada.setBacklight (255 - (255 * elapsed / 500 ));
420+ }
421+ arcada.setBacklight (0 );
422+ for (e=0 ; e<NUM_EYES; e++) {
423+ eye[e].display ->fillScreen (0 );
424+ }
425+ }
426+
401427#if defined(ADAFRUIT_MONSTER_M4SK_EXPRESS)
402428 if (voiceOn) {
403429 if (!voiceSetup ((waveform > 0 ))) {
@@ -412,6 +438,8 @@ void setup() {
412438 }
413439#endif
414440
441+ arcada.setBacklight (255 ); // Back on, impending graphics
442+
415443 yield ();
416444 if (boopPin >= 0 ) {
417445 boopThreshold = 0 ;
@@ -425,7 +453,6 @@ void setup() {
425453}
426454
427455
428-
429456// LOOP FUNCTION - CALLED REPEATEDLY UNTIL POWER-OFF -----------------------
430457
431458/*
@@ -472,49 +499,67 @@ void loop() {
472499
473500 // ONCE-PER-FRAME EYE ANIMATION LOGIC HAPPENS HERE -------------------
474501
475- float eyeX, eyeY;
476502 // Eye movement
477- int32_t dt = t - eyeMoveStartTime; // uS elapsed since last eye event
478- if (eyeInMotion) { // Currently moving?
479- if (dt >= eyeMoveDuration) { // Time up? Destination reached.
480- eyeInMotion = false ; // Stop moving
481- if (moveEyesRandomly) {
482- eyeMoveDuration = random (10000 , 3000000 ); // 0.01-3 sec stop
483- eyeMoveStartTime = t; // Save initial time of stop
503+ float eyeX, eyeY;
504+ if (moveEyesRandomly) {
505+ int32_t dt = t - eyeMoveStartTime; // uS elapsed since last eye event
506+ if (eyeInMotion) { // Eye currently moving?
507+ if (dt >= eyeMoveDuration) { // Time up? Destination reached.
508+ eyeInMotion = false ; // Stop moving
509+ // The "move" duration temporarily becomes a hold duration...
510+ // Normally this is 35 ms to 1 sec, but don't exceed gazeMax setting
511+ uint32_t limit = min (1000000 , gazeMax);
512+ eyeMoveDuration = random (35000 , limit); // Time between microsaccades
513+ if (!saccadeInterval) { // Cleared when "big" saccade finishes
514+ lastSaccadeStop = t; // Time when saccade stopped
515+ saccadeInterval = random (eyeMoveDuration, gazeMax); // Next in 30ms to 3sec
516+ }
517+ // Similarly, the "move" start time becomes the "stop" starting time...
518+ eyeMoveStartTime = t; // Save time of event
519+ eyeX = eyeOldX = eyeNewX; // Save position
520+ eyeY = eyeOldY = eyeNewY;
521+ } else { // Move time's not yet fully elapsed -- interpolate position
522+ float e = (float )dt / float (eyeMoveDuration); // 0.0 to 1.0 during move
523+ e = 3 * e * e - 2 * e * e * e; // Easing function: 3*e^2-2*e^3 0.0 to 1.0
524+ eyeX = eyeOldX + (eyeNewX - eyeOldX) * e; // Interp X
525+ eyeY = eyeOldY + (eyeNewY - eyeOldY) * e; // and Y
484526 }
485- eyeX = eyeOldX = eyeNewX; // Save position
486- eyeY = eyeOldY = eyeNewY;
487- } else { // Move time's not yet fully elapsed -- interpolate position
488- float e = (float )dt / float (eyeMoveDuration); // 0.0 to 1.0 during move
489- e = 3 * e * e - 2 * e * e * e; // Easing function: 3*e^2-2*e^3 0.0 to 1.0
490- eyeX = eyeOldX + (eyeNewX - eyeOldX) * e; // Interp X
491- eyeY = eyeOldY + (eyeNewY - eyeOldY) * e; // and Y
492- }
493- } else { // Eye stopped
494- eyeX = eyeOldX;
495- eyeY = eyeOldY;
496- if (dt > eyeMoveDuration) { // Time up? Begin new move.
497- // r is the radius in X and Y that the eye can go, from (0,0) in the center.
498- float r = (float )mapDiameter - (float )DISPLAY_SIZE * M_PI_2; // radius of motion
499- r *= 0.6 ; // calibration constant
500-
501- if (moveEyesRandomly) {
502- eyeNewX = random (-r, r);
503- float h = sqrt (r * r - x * x);
504- eyeNewY = random (-h, h);
505- } else {
506- eyeNewX = eyeTargetX * r;
507- eyeNewY = eyeTargetY * r;
527+ } else { // Eye is currently stopped
528+ eyeX = eyeOldX;
529+ eyeY = eyeOldY;
530+ if (dt > eyeMoveDuration) { // Time up? Begin new move.
531+ if ((t - lastSaccadeStop) > saccadeInterval) { // Time for a "big" saccade
532+ // r is the radius in X and Y that the eye can go, from (0,0) in the center.
533+ float r = ((float )mapDiameter - (float )DISPLAY_SIZE * M_PI_2) * 0.75 ;
534+ eyeNewX = random (-r, r);
535+ float h = sqrt (r * r - eyeNewX * eyeNewX);
536+ eyeNewY = random (-h, h);
537+ // Set the duration for this move, and start it going.
538+ eyeMoveDuration = random (83000 , 166000 ); // ~1/12 - ~1/6 sec
539+ saccadeInterval = 0 ; // Calc next interval when this one stops
540+ } else { // Microsaccade
541+ // r is possible radius of motion, ~1/10 size of full saccade.
542+ // We don't bother with clipping because if it strays just a little,
543+ // that's okay, it'll get put in-bounds on next full saccade.
544+ float r = (float )mapDiameter - (float )DISPLAY_SIZE * M_PI_2;
545+ r *= 0.07 ;
546+ float dx = random (-r, r);
547+ eyeNewX = eyeX - mapRadius + dx;
548+ float h = sqrt (r * r - dx * dx);
549+ eyeNewY = eyeY - mapRadius + random (-h, h);
550+ eyeMoveDuration = random (7000 , 25000 ); // 7-25 ms microsaccade
551+ }
552+ eyeNewX += mapRadius; // Translate new point into map space
553+ eyeNewY += mapRadius;
554+ eyeMoveStartTime = t; // Save initial time of move
555+ eyeInMotion = true ; // Start move on next frame
508556 }
509-
510- eyeNewX += mapRadius;
511- eyeNewY += mapRadius;
512-
513- // Set the duration for this move, and start it going.
514- eyeMoveDuration = random (83000 , 166000 ); // ~1/12 - ~1/6 sec
515- eyeMoveStartTime = t; // Save initial time of move
516- eyeInMotion = true ; // Start move on next frame
517557 }
558+ } else {
559+ // Allow user code to control eye position (e.g. IR sensor, joystick, etc.)
560+ float r = ((float )mapDiameter - (float )DISPLAY_SIZE * M_PI_2) * 0.9 ;
561+ eyeX = mapRadius + eyeTargetX * r;
562+ eyeY = mapRadius + eyeTargetY * r;
518563 }
519564
520565 // Eyes fixate (are slightly crossed) -- amount is filtered for boops
@@ -579,7 +624,6 @@ void loop() {
579624 eye[eyeNum].upperLidFactor = (eye[eyeNum].upperLidFactor * 0.6 ) + (uq * 0.4 );
580625 eye[eyeNum].lowerLidFactor = (eye[eyeNum].lowerLidFactor * 0.6 ) + (lq * 0.4 );
581626
582-
583627 // Process blinks
584628 if (eye[eyeNum].blink .state ) { // Eye currently blinking?
585629 // Check if current blink state time has elapsed
@@ -858,7 +902,7 @@ void loop() {
858902 // Initialize new SPI transaction & address window...
859903 eye[eyeNum].spi ->beginTransaction (settings);
860904 digitalWrite (eye[eyeNum].cs , LOW); // Chip select
861- eye[eyeNum].display ->setAddrWindow (DISPLAY_X_OFFSET, DISPLAY_Y_OFFSET , DISPLAY_SIZE, DISPLAY_SIZE);
905+ eye[eyeNum].display ->setAddrWindow ((eye[eyeNum]. display -> width () - DISPLAY_SIZE) / 2 , (eye[eyeNum]. display -> height () - DISPLAY_SIZE) / 2 , DISPLAY_SIZE, DISPLAY_SIZE);
862906 delayMicroseconds (1 );
863907 digitalWrite (eye[eyeNum].dc , HIGH); // Data mode
864908 if (eyeNum == (NUM_EYES-1 )) {
0 commit comments