5
5
// "Uncanny Eyes" project (better for SAMD21 chips or Teensy 3.X and
6
6
// 128x128 TFT or OLED screens, single SPI bus).
7
7
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
+
8
17
// LET'S HAVE A WORD ABOUT COORDINATE SYSTEMS before continuing. From an
9
18
// outside observer's point of view, looking at the display(s) on these
10
19
// boards, the eyes are rendered COLUMN AT A TIME, working LEFT TO RIGHT,
34
43
#error "Please select Tools->USB Stack->TinyUSB before compiling"
35
44
#endif
36
45
37
- #include < Adafruit_TinyUSB.h>
38
46
#define GLOBAL_VAR
39
47
#include " globals.h"
40
48
@@ -43,6 +51,8 @@ bool eyeInMotion = false;
43
51
float eyeOldX, eyeOldY, eyeNewX, eyeNewY;
44
52
uint32_t eyeMoveStartTime = 0L ;
45
53
int32_t eyeMoveDuration = 0L ;
54
+ uint32_t lastSaccadeStop = 0L ;
55
+ int32_t saccadeInterval = 0L ;
46
56
47
57
// Some sloppy eye state stuff, some carried over from old eye code...
48
58
// kinda messy and badly named and will get cleaned up/moved/etc.
@@ -148,9 +158,10 @@ void setup() {
148
158
149
159
arcada.displayBegin ();
150
160
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);
154
165
155
166
Serial.begin (115200 );
156
167
// while(!Serial) yield();
@@ -159,9 +170,6 @@ void setup() {
159
170
Serial.printf (" Available flash at start: %d\n " , arcada.availableFlash ());
160
171
yield (); // Periodic yield() makes sure mass storage filesystem stays alive
161
172
162
- // Backlight(s) off ASAP, they'll switch on after screen(s) init & clear
163
- arcada.setBacklight (0 );
164
-
165
173
// No file selector yet. In the meantime, you can override the default
166
174
// config file by holding one of the 3 edge buttons at startup (loads
167
175
// config1.eye, config2.eye or config3.eye instead). Keep fingers clear
@@ -179,42 +187,20 @@ void setup() {
179
187
}
180
188
181
189
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
211
197
212
198
// Initialize DMAs
213
199
yield ();
214
200
uint8_t e;
215
201
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!
218
204
#endif
219
205
eye[e].display ->fillScreen (0 );
220
206
eye[e].dma .allocate ();
@@ -265,12 +251,36 @@ void setup() {
265
251
266
252
// Uncanny eyes carryover stuff for now, all messy:
267
253
eye[e].blink .state = NOBLINK;
268
- // eye[e].eyeX = 512;
269
- // eye[e].eyeY = 512;
270
254
eye[e].blinkFactor = 0.0 ;
271
255
}
272
256
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 );
274
284
275
285
// LOAD CONFIGURATION FILE -----------------------------------------------
276
286
@@ -301,7 +311,7 @@ void setup() {
301
311
// leave some RAM for the stack to operate over the lifetime of this
302
312
// program and to handle small heap allocations.
303
313
304
- uint32_t maxRam = availableRAM () - stackReserve;
314
+ uint32_t maxRam = availableRAM () - stackReserve;
305
315
306
316
// Load texture maps for eyes
307
317
uint8_t e2 ;
@@ -392,12 +402,28 @@ void setup() {
392
402
randomSeed (SysTick->VAL + analogRead (A2));
393
403
eyeOldX = eyeNewX = eyeOldY = eyeNewY = mapRadius; // Start in center
394
404
for (e=0 ; e<NUM_EYES; e++) { // For each eye...
395
- // Set up screen rotation (MUST be done after config load!)
396
405
eye[e].display ->setRotation (eye[e].rotation );
397
406
eye[e].eyeX = eyeOldX; // Set up initial position
398
407
eye[e].eyeY = eyeOldY;
399
408
}
400
409
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
+
401
427
#if defined(ADAFRUIT_MONSTER_M4SK_EXPRESS)
402
428
if (voiceOn) {
403
429
if (!voiceSetup ((waveform > 0 ))) {
@@ -412,6 +438,8 @@ void setup() {
412
438
}
413
439
#endif
414
440
441
+ arcada.setBacklight (255 ); // Back on, impending graphics
442
+
415
443
yield ();
416
444
if (boopPin >= 0 ) {
417
445
boopThreshold = 0 ;
@@ -425,7 +453,6 @@ void setup() {
425
453
}
426
454
427
455
428
-
429
456
// LOOP FUNCTION - CALLED REPEATEDLY UNTIL POWER-OFF -----------------------
430
457
431
458
/*
@@ -472,49 +499,67 @@ void loop() {
472
499
473
500
// ONCE-PER-FRAME EYE ANIMATION LOGIC HAPPENS HERE -------------------
474
501
475
- float eyeX, eyeY;
476
502
// 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
484
526
}
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
508
556
}
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
517
557
}
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;
518
563
}
519
564
520
565
// Eyes fixate (are slightly crossed) -- amount is filtered for boops
@@ -579,7 +624,6 @@ void loop() {
579
624
eye[eyeNum].upperLidFactor = (eye[eyeNum].upperLidFactor * 0.6 ) + (uq * 0.4 );
580
625
eye[eyeNum].lowerLidFactor = (eye[eyeNum].lowerLidFactor * 0.6 ) + (lq * 0.4 );
581
626
582
-
583
627
// Process blinks
584
628
if (eye[eyeNum].blink .state ) { // Eye currently blinking?
585
629
// Check if current blink state time has elapsed
@@ -858,7 +902,7 @@ void loop() {
858
902
// Initialize new SPI transaction & address window...
859
903
eye[eyeNum].spi ->beginTransaction (settings);
860
904
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);
862
906
delayMicroseconds (1 );
863
907
digitalWrite (eye[eyeNum].dc , HIGH); // Data mode
864
908
if (eyeNum == (NUM_EYES-1 )) {
0 commit comments