Skip to content

Commit 96f3127

Browse files
Microsaccades, more yields, PyBadge/PyGamer, better splash handling
1 parent 1937c26 commit 96f3127

File tree

5 files changed

+144
-93
lines changed

5 files changed

+144
-93
lines changed

M4_Eyes/M4_Eyes.ino

+129-85
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@
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,
@@ -34,7 +43,6 @@
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;
4351
float eyeOldX, eyeOldY, eyeNewX, eyeNewY;
4452
uint32_t eyeMoveStartTime = 0L;
4553
int32_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)) {

M4_Eyes/file.cpp

+5-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ void loadConfig(char *filename) {
8585
StaticJsonDocument<2048> doc;
8686

8787
yield();
88-
// delay(100); // Make sure mass storage handler has a turn first!
8988
DeserializationError error = deserializeJson(doc, file);
9089
yield();
9190
if(error) {
@@ -100,6 +99,7 @@ void loadConfig(char *filename) {
10099
eyelidIndex = dwim(doc["eyelidIndex"]);
101100
irisRadius = dwim(doc["irisRadius"]);
102101
slitPupilRadius = dwim(doc["slitPupilRadius"]);
102+
gazeMax = dwim(doc["gazeMax"], gazeMax);
103103
JsonVariant v;
104104
v = doc["coverage"];
105105
if(v.is<int>() || v.is<float>()) coverage = v.as<float>();
@@ -328,6 +328,7 @@ ImageReturnCode loadEyelid(char *filename,
328328
ImageReturnCode status;
329329
Adafruit_ImageReader *reader;
330330

331+
yield();
331332
reader = arcada.getImageReader();
332333
if (!reader) {
333334
return IMAGE_ERR_FILE_NOT_FOUND;
@@ -337,6 +338,7 @@ ImageReturnCode loadEyelid(char *filename,
337338
memset(maxArray, init, DISPLAY_SIZE); // mark 'no eyelid data for this column'
338339

339340
// This is the "booster seat" described in m4eyes.ino
341+
yield();
340342
if(reader->bmpDimensions(filename, &w, &h) == IMAGE_SUCCESS) {
341343
tempBytes = ((w + 7) / 8) * h; // Bitmap size in bytes
342344
if (maxRam > tempBytes) {
@@ -374,6 +376,7 @@ ImageReturnCode loadEyelid(char *filename,
374376
uint8_t *buffer = canvas->getBuffer();
375377
int bytesPerLine = (image.width() + 7) / 8;
376378
for(x=sx1; x <= sx2; x++, ix++) { // For each column...
379+
yield();
377380
// Get initial pointer into image buffer
378381
uint8_t *ptr = &buffer[iy * bytesPerLine + ix / 8];
379382
uint8_t mask = 0x80 >> (ix & 7); // Column mask
@@ -414,6 +417,7 @@ ImageReturnCode loadTexture(char *filename, uint16_t **data,
414417
ImageReturnCode status;
415418
Adafruit_ImageReader *reader;
416419

420+
yield();
417421
reader = arcada.getImageReader();
418422
if (!reader) {
419423
return IMAGE_ERR_FILE_NOT_FOUND;

0 commit comments

Comments
 (0)