I followed this tutorial instructables
I uploaded this code, one eye is flashing (seems that the black lid that is above the iris is switching from left to right all the time what casing the flashing) the second eye is working fine. When I change the code and place "#define NUM_EYES 2" to "1", both eyes are working fine bud they have both left (or right) eyes lid what looks on-natural. Cannot find out what's wrong here
YouTube video to show you the problem with the eye LINK
update: I am learning here on the spot how to use the forum, so I keep this post updated.
Code uncanny-eyes-skull.ino
// An adaption of the "UncannyEyes" sketch (see eye_functions tab)
// for the TFT_eSPI library but adapted for two 240x240 px displays.
// For wiring see the wiring folder.
// Configuration settings for the eye, eye style, display count,
// chip selects and x offsets can be defined in the sketch "config.h" tab.
// To increase performance DMA should stay enabled.
// Two 240x240 px displays run at 32 FPS.
#define USE_DMA
// Load TFT driver library
// Use the predefined user setup Setup46_GC9A01_ESP32 of the TFT_eSPI library.
// Either select via library/TFT_eSPI/User_Setup_Select.h or as you like.
// An update of the library will overwrite the file above.
#include <SPI.h>
#include <TFT_eSPI.h>
TFT_eSPI tft;
#define BUFFER_SIZE 1024
#ifdef USE_DMA
#define BUFFERS 2
#else
#define BUFFERS 1
#endif
uint16_t pbuffer[BUFFERS][BUFFER_SIZE];
bool dmaBuf = 0;
typedef struct {
int8_t select; // pin numbers for each eye's screen select line
int8_t wink; // and wink button (or -1 if none) specified there,
uint8_t rotation; // also display rotation and the x offset
int16_t xposition; // position of eye on the screen
} eyeInfo_t;
#include "config.h" // ****** CONFIGURATION IS DONE IN HERE ******
// functions in the user*.cpp files, please enable one
extern void user_setup(void);
extern void user_loop(void);
#define SCREEN_X_START 0
#define SCREEN_X_END SCREEN_WIDTH // Badly named, actually the "eye" width!
#define SCREEN_Y_START 0
#define SCREEN_Y_END SCREEN_HEIGHT // Actually "eye" height
// A simple state machine is used to control eye blinks/winks:
#define NOBLINK 0 // Not currently engaged in a blink
#define ENBLINK 1 // Eyelid is currently closing
#define DEBLINK 2 // Eyelid is currently opening
typedef struct {
uint8_t state; // NOBLINK/ENBLINK/DEBLINK
uint32_t duration; // Duration of blink state (micros)
uint32_t startTime; // Time (micros) of last state change
} eyeBlink;
struct { // One-per-eye structure
int16_t tft_cs; // Chip select pin for each display
eyeBlink blink; // Current blink/wink state
int16_t xposition; // x position of eye image
} eye[NUM_EYES];
uint32_t startTime; // For FPS indicator
void setup() {
Serial.begin(115200);
while (!Serial) {
; // wait for serial port to connect
} Serial.println("starting");
#if defined(DISPLAY_BACKLIGHT) && (DISPLAY_BACKLIGHT >= 0)
// enable backlight pin, initially off
Serial.println("backlight turned off");
pinMode(DISPLAY_BACKLIGHT, OUTPUT);
digitalWrite(DISPLAY_BACKLIGHT, LOW);
#endif
user_setup();
initEyes();
Serial.println("initialising displays");
tft.init();
#ifdef USE_DMA
tft.initDMA();
#endif
// raise chip select(s) so that displays can be individually configured
digitalWrite(eye[0].tft_cs, HIGH);
if (NUM_EYES > 1) digitalWrite(eye[1].tft_cs, HIGH);
for (uint8_t e = 0; e < NUM_EYES; e++) {
digitalWrite(eye[e].tft_cs, LOW);
tft.setRotation(eyeInfo[e].rotation);
tft.fillScreen(TFT_BLACK);
digitalWrite(eye[e].tft_cs, HIGH);
}
#if defined(DISPLAY_BACKLIGHT) && (DISPLAY_BACKLIGHT >= 0)
Serial.println("backlight on");
analogWrite(DISPLAY_BACKLIGHT, BACKLIGHT_MAX);
#endif
startTime = millis();
}
void loop() {
updateEye();
// use user_loop, do not add things here
}
Code config.h
// Pin selections here are based on ESP32
// Only the default_large eye runs on 240x240 px.
// For all other eyes resolutions need to be adjusted in eye_functions.
// GRAPHICS SETTINGS (appearance of eye) -----------------------------------
// If using a SINGLE EYE, you might want this next line enabled, which
// uses a simpler "football-shaped" eye that's left/right symmetrical.
// Default shape includes the caruncle, creating distinct left/right eyes.
//#define SYMMETRICAL_EYELID
// Enable ONE of these #includes -- HUGE graphics tables for various eyes:
//#include "data/defaultEye.h" // Standard human-ish hazel eye -OR-
#include "data/default_large.h" // 240px standard human-ish hazel eye -OR-
//#include "data/dragonEye.h" // Slit pupil fiery dragon/demon eye -OR-
//#include "data/noScleraEye.h" // Large iris, no sclera -OR-
//#include "data/goatEye.h" // Horizontal pupil goat/Krampus eye -OR-
//#include "data/newtEye.h" // Eye of newt -OR-
//#include "data/terminatorEye.h" // Git to da choppah!
//#include "data/catEye.h" // Cartoonish cat (flat "2D" colors)
//#include "data/owlEye.h" // Minerva the owl (DISABLE TRACKING)
//#include "data/naugaEye.h" // Nauga googly eye (DISABLE TRACKING)
//#include "data/doeEye.h" // Cartoon deer eye (DISABLE TRACKING)
// ESP 32 DISPLAY HARDWARE SETTINGS (screen type & connections)
#define TFT_COUNT 2 // Number of screens (1 or 2)
#define TFT1_CS 21 // TFT 1 chip select pin (set to -1 to use TFT_eSPI setup)
#define TFT2_CS 22 // TFT 2 chip select pin (set to -1 to use TFT_eSPI setup)
#define TFT_1_ROT 2 // TFT 1 rotation // 0 = 0° pins bottom, 2 = 180° pins top
#define TFT_2_ROT 2 // TFT 2 rotation
#define EYE_1_XPOSITION 0 // x shift for eye 1 image on display
#define EYE_2_XPOSITION 0 // x shift for eye 2 image on display
#define DISPLAY_BACKLIGHT -1 // Pin for backlight control (-1 for none)
#define BACKLIGHT_MAX 255
// EYE LIST ----------------------------------------------------------------
#define NUM_EYES 2 // Number of eyes to display (1 or 2)
#define BLINK_PIN -1 // Pin for manual blink button (BOTH eyes)
#define LH_WINK_PIN -1 // Left wink pin (set to -1 for no pin)
#define RH_WINK_PIN -1 // Right wink pin (set to -1 for no pin)
// This table contains ONE LINE PER EYE. The table MUST be present with
// this name and contain ONE OR MORE lines. Each line contains THREE items:
// a pin number for the corresponding TFT/OLED display's SELECT line, a pin
// pin number for that eye's "wink" button (or -1 if not used), a screen
// rotation value (0-3) and x position offset for that eye.
#if (NUM_EYES == 2)
eyeInfo_t eyeInfo[] = {
{ TFT1_CS, LH_WINK_PIN, TFT_1_ROT, EYE_1_XPOSITION }, // LEFT EYE chip select and wink pins, rotation and offset
{ TFT2_CS, RH_WINK_PIN, TFT_2_ROT, EYE_2_XPOSITION }, // RIGHT EYE chip select and wink pins, rotation and offset
};
#else
eyeInfo_t eyeInfo[] = {
{ TFT1_CS, LH_WINK_PIN, TFT_1_ROT, EYE_1_XPOSITION }, // EYE chip select and wink pins, rotation and offset
};
#endif
// INPUT SETTINGS (for controlling eye motion) -----------------------------
// JOYSTICK_X_PIN and JOYSTICK_Y_PIN specify analog input pins for manually
// controlling the eye with an analog joystick. If set to -1 or if not
// defined, the eye will move on its own.
// IRIS_PIN speficies an analog input pin for a photocell to make pupils
// react to light (or potentiometer for manual control). If set to -1 or
// if not defined, the pupils will change on their own.
// BLINK_PIN specifies an input pin for a button (to ground) that will
// make any/all eyes blink. If set to -1 or if not defined, the eyes will
// only blink if AUTOBLINK is defined, or if the eyeInfo[] table above
// includes wink button settings for each eye.
//#define JOYSTICK_X_PIN A0 // Analog pin for eye horiz pos (else auto)
//#define JOYSTICK_Y_PIN A1 // Analog pin for eye vert position (")
//#define JOYSTICK_X_FLIP // If defined, reverse stick X axis
//#define JOYSTICK_Y_FLIP // If defined, reverse stick Y axis
#define TRACKING // If defined, eyelid tracks pupil
//#define AUTOBLINK // If defined, eyes also blink autonomously
// #define LIGHT_PIN -1 // Light sensor pin
#define LIGHT_CURVE 0.33 // Light sensor adjustment curve
#define LIGHT_MIN 0 // Minimum useful reading from light sensor
#define LIGHT_MAX 1023 // Maximum useful reading from sensor
#define IRIS_SMOOTH // If enabled, filter input from IRIS_PIN
#if !defined(IRIS_MIN) // Each eye might have its own MIN/MAX
#define IRIS_MIN 90 // Iris size (0-1023) in brightest light
#endif
#if !defined(IRIS_MAX)
#define IRIS_MAX 130 // Iris size (0-1023) in darkest light
#endif
Code eye_functions.ino
// Code adapted to run with TFT_eSPI compatible processor on ESP32,
// running on two 240x240 TFT displays.
//--------------------------------------------------------------------------
// Uncanny eyes for Adafruit 1.5" OLED (product #1431) or 1.44" TFT LCD
// (#2088). Works on PJRC Teensy 3.x and on Adafruit M0 and M4 boards
// (Feather, Metro, etc.). This code uses features specific to these
// boards and WILL NOT work on normal Arduino or other boards!
//
// SEE FILE "config.h" FOR MOST CONFIGURATION (graphics, pins, display type,
// etc). Probably won't need to edit THIS file unless you're doing some
// extremely custom modifications.
//
// Adafruit invests time and resources providing this open source code,
// please support Adafruit and open-source hardware by purchasing products
// from Adafruit!
//
// Written by Phil Burgess / Paint Your Dragon for Adafruit Industries.
// MIT license. SPI FIFO insight from Paul Stoffregen's ILI9341_t3 library.
// Inspired by David Boccabella's (Marcwolf) hybrid servo/OLED eye concept.
//--------------------------------------------------------------------------
#if !defined(LIGHT_PIN) || (LIGHT_PIN < 0)
// Autonomous iris motion uses a fractal behavior to similate both the major
// reaction of the eye plus the continuous smaller adjustments that occur.
uint16_t oldIris = (IRIS_MIN + IRIS_MAX) / 2, newIris;
#endif
// Initialise eyes ---------------------------------------------------------
void initEyes(void)
{
Serial.println("Initialise eye objects");
// Initialise eye objects based on eyeInfo list in config.h:
for (uint8_t e = 0; e < NUM_EYES; e++) {
Serial.print("Create display #"); Serial.println(e);
eye[e].tft_cs = eyeInfo[e].select;
eye[e].blink.state = NOBLINK;
eye[e].xposition = eyeInfo[e].xposition;
pinMode(eye[e].tft_cs, OUTPUT);
digitalWrite(eye[e].tft_cs, LOW);
// Also set up an individual eye-wink pin if defined:
if (eyeInfo[e].wink >= 0) pinMode(eyeInfo[e].wink, INPUT_PULLUP);
}
#if defined(BLINK_PIN) && (BLINK_PIN >= 0)
pinMode(BLINK_PIN, INPUT_PULLUP); // Ditto for all-eyes blink pin
#endif
}
// UPDATE EYE --------------------------------------------------------------
void updateEye (void)
{
#if defined(LIGHT_PIN) && (LIGHT_PIN >= 0) // Interactive iris
int16_t v = analogRead(LIGHT_PIN); // Raw dial/photocell reading
#ifdef LIGHT_PIN_FLIP
v = 1023 - v; // Reverse reading from sensor
#endif
if (v < LIGHT_MIN) v = LIGHT_MIN; // Clamp light sensor range
else if (v > LIGHT_MAX) v = LIGHT_MAX;
v -= LIGHT_MIN; // 0 to (LIGHT_MAX - LIGHT_MIN)
#ifdef LIGHT_CURVE // Apply gamma curve to sensor input?
v = (int16_t)(pow((double)v / (double)(LIGHT_MAX - LIGHT_MIN),
LIGHT_CURVE) * (double)(LIGHT_MAX - LIGHT_MIN));
#endif
// And scale to iris range (IRIS_MAX is size at LIGHT_MIN)
v = map(v, 0, (LIGHT_MAX - LIGHT_MIN), IRIS_MAX, IRIS_MIN);
#ifdef IRIS_SMOOTH // Filter input (gradual motion)
static int16_t irisValue = (IRIS_MIN + IRIS_MAX) / 2;
irisValue = ((irisValue * 15) + v) / 16;
frame(irisValue);
#else // Unfiltered (immediate motion)
frame(v);
#endif // IRIS_SMOOTH
#else // Autonomous iris scaling -- invoke recursive function
newIris = random(IRIS_MIN, IRIS_MAX);
split(oldIris, newIris, micros(), 10000000L, IRIS_MAX - IRIS_MIN);
oldIris = newIris;
#endif // LIGHT_PIN
}
// EYE-RENDERING FUNCTION --------------------------------------------------
void drawEye( // Renders one eye. Inputs must be pre-clipped & valid.
// Use native 32 bit variables where possible as this is 10% faster!
uint8_t e, // Eye array index; 0 or 1 for left/right
uint32_t iScale, // Scale factor for iris
uint32_t scleraX, // First pixel X offset into sclera image
uint32_t scleraY, // First pixel Y offset into sclera image
uint32_t uT, // Upper eyelid threshold value
uint32_t lT) { // Lower eyelid threshold value
uint32_t screenX, screenY, scleraXsave;
int32_t irisX, irisY;
uint32_t p, a;
uint32_t d;
uint32_t pixels = 0;
// Set up raw pixel dump to entire screen. Although such writes can wrap
// around automatically from end of rect back to beginning, the region is
// reset on each frame here in case of an SPI glitch.
digitalWrite(eye[e].tft_cs, LOW);
tft.startWrite();
tft.setAddrWindow(eye[e].xposition, 0, 240, 240);
// Now just issue raw 16-bit values for every pixel...
scleraXsave = scleraX; // Save initial X value to reset on each line
irisY = scleraY - (SCLERA_HEIGHT - IRIS_HEIGHT) / 2;
// Eyelid image is left<>right swapped for two displays
uint16_t lidX = 0;
uint16_t dlidX = -1;
if (e) dlidX = 1;
for (screenY = 0; screenY < SCREEN_HEIGHT; screenY++, scleraY++, irisY++) {
scleraX = scleraXsave;
irisX = scleraXsave - (SCLERA_WIDTH - IRIS_WIDTH) / 2;
if (e) lidX = 0; else lidX = SCREEN_WIDTH - 1;
for (screenX = 0; screenX < SCREEN_WIDTH; screenX++, scleraX++, irisX++, lidX += dlidX) {
if ((pgm_read_byte(lower + screenY * SCREEN_WIDTH + lidX) <= lT) ||
(pgm_read_byte(upper + screenY * SCREEN_WIDTH + lidX) <= uT)) { // Covered by eyelid
p = 0;
} else if ((irisY < 0) || (irisY >= IRIS_HEIGHT) ||
(irisX < 0) || (irisX >= IRIS_WIDTH)) { // In sclera
p = pgm_read_word(sclera + scleraY * SCLERA_WIDTH + scleraX);
} else { // Maybe iris...
p = pgm_read_word(polar + irisY * IRIS_WIDTH + irisX); // Polar angle/dist
d = (iScale * (p & 0x7F)) / 128; // Distance (Y)
if (d < IRIS_MAP_HEIGHT) { // Within iris area
a = (IRIS_MAP_WIDTH * (p >> 7)) / 512; // Angle (X)
p = pgm_read_word(iris + d * IRIS_MAP_WIDTH + a); // Pixel = iris
} else { // Not in iris
p = pgm_read_word(sclera + scleraY * SCLERA_WIDTH + scleraX); // Pixel = sclera
}
}
*(&pbuffer[dmaBuf][0] + pixels++) = p >> 8 | p << 8;
if (pixels >= BUFFER_SIZE) {
yield();
#ifdef USE_DMA
tft.pushPixelsDMA(&pbuffer[dmaBuf][0], pixels);
dmaBuf = !dmaBuf;
#else
tft.pushPixels(pbuffer, pixels);
#endif
pixels = 0;
}
}
}
if (pixels) {
#ifdef USE_DMA
tft.pushPixelsDMA(&pbuffer[dmaBuf][0], pixels);
#else
tft.pushPixels(pbuffer, pixels);
#endif
}
tft.endWrite();
digitalWrite(eye[e].tft_cs, HIGH);
}
// EYE ANIMATION -----------------------------------------------------------
const uint8_t ease[] = { // Ease in/out curve for eye movements 3*t^2-2*t^3
0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 3, // T
3, 3, 4, 4, 4, 5, 5, 6, 6, 7, 7, 8, 9, 9, 10, 10, // h
11, 12, 12, 13, 14, 15, 15, 16, 17, 18, 18, 19, 20, 21, 22, 23, // x
24, 25, 26, 27, 27, 28, 29, 30, 31, 33, 34, 35, 36, 37, 38, 39, // 2
40, 41, 42, 44, 45, 46, 47, 48, 50, 51, 52, 53, 54, 56, 57, 58, // A
60, 61, 62, 63, 65, 66, 67, 69, 70, 72, 73, 74, 76, 77, 78, 80, // l
81, 83, 84, 85, 87, 88, 90, 91, 93, 94, 96, 97, 98, 100, 101, 103, // e
104, 106, 107, 109, 110, 112, 113, 115, 116, 118, 119, 121, 122, 124, 125, 127, // c
128, 130, 131, 133, 134, 136, 137, 139, 140, 142, 143, 145, 146, 148, 149, 151, // J
152, 154, 155, 157, 158, 159, 161, 162, 164, 165, 167, 168, 170, 171, 172, 174, // a
175, 177, 178, 179, 181, 182, 183, 185, 186, 188, 189, 190, 192, 193, 194, 195, // c
197, 198, 199, 201, 202, 203, 204, 205, 207, 208, 209, 210, 211, 213, 214, 215, // o
216, 217, 218, 219, 220, 221, 222, 224, 225, 226, 227, 228, 228, 229, 230, 231, // b
232, 233, 234, 235, 236, 237, 237, 238, 239, 240, 240, 241, 242, 243, 243, 244, // s
245, 245, 246, 246, 247, 248, 248, 249, 249, 250, 250, 251, 251, 251, 252, 252, // o
252, 253, 253, 253, 254, 254, 254, 254, 254, 255, 255, 255, 255, 255, 255, 255
}; // n
#ifdef AUTOBLINK
uint32_t timeOfLastBlink = 0L, timeToNextBlink = 0L;
#endif
// Process motion for a single frame of left or right eye
void frame(uint16_t iScale) // Iris scale (0-1023)
{
static uint32_t frames = 0; // Used in frame rate calculation
static uint8_t eyeIndex = 0; // eye[] array counter
int16_t eyeX, eyeY;
uint32_t t = micros(); // Time at start of function
if (!(++frames & 255)) { // Every 256 frames...
float elapsed = (millis() - startTime) / 1000.0;
if (elapsed) {
Serial.print("FPS=");
Serial.println((uint16_t)(frames / elapsed));
}
}
if (++eyeIndex >= NUM_EYES) eyeIndex = 0; // Cycle through eyes, 1 per call
// X/Y movement
#if defined(JOYSTICK_X_PIN) && (JOYSTICK_X_PIN >= 0) && \
defined(JOYSTICK_Y_PIN) && (JOYSTICK_Y_PIN >= 0)
// Read X/Y from joystick, constrain to circle
int16_t dx, dy;
int32_t d;
eyeX = analogRead(JOYSTICK_X_PIN); // Raw (unclipped) X/Y reading
eyeY = analogRead(JOYSTICK_Y_PIN);
#ifdef JOYSTICK_X_FLIP
eyeX = 1023 - eyeX;
#endif
#ifdef JOYSTICK_Y_FLIP
eyeY = 1023 - eyeY;
#endif
dx = (eyeX * 2) - 1023; // A/D exact center is at 511.5. Scale coords
dy = (eyeY * 2) - 1023; // X2 so range is -1023 to +1023 w/center at 0.
if ((d = (dx * dx + dy * dy)) > (1023 * 1023)) { // Outside circle
d = (int32_t)sqrt((float)d); // Distance from center
eyeX = ((dx * 1023 / d) + 1023) / 2; // Clip to circle edge,
eyeY = ((dy * 1023 / d) + 1023) / 2; // scale back to 0-1023
}
#else // Autonomous X/Y eye motion
// Periodically initiates motion to a new random point, random speed,
// holds there for random period until next motion.
static bool eyeInMotion = false;
static int16_t eyeOldX = 512, eyeOldY = 512, eyeNewX = 512, eyeNewY = 512;
static uint32_t eyeMoveStartTime = 0L;
static int32_t eyeMoveDuration = 0L;
int32_t dt = t - eyeMoveStartTime; // uS elapsed since last eye event
if (eyeInMotion) { // Currently moving?
if (dt >= eyeMoveDuration) { // Time up? Destination reached.
eyeInMotion = false; // Stop moving
eyeMoveDuration = random(3000000); // 0-3 sec stop
eyeMoveStartTime = t; // Save initial time of stop
eyeX = eyeOldX = eyeNewX; // Save position
eyeY = eyeOldY = eyeNewY;
} else { // Move time's not yet fully elapsed -- interpolate position
int16_t e = ease[255 * dt / eyeMoveDuration] + 1; // Ease curve
eyeX = eyeOldX + (((eyeNewX - eyeOldX) * e) / 256); // Interp X
eyeY = eyeOldY + (((eyeNewY - eyeOldY) * e) / 256); // and Y
}
} else { // Eye stopped
eyeX = eyeOldX;
eyeY = eyeOldY;
if (dt > eyeMoveDuration) { // Time up? Begin new move.
int16_t dx, dy;
uint32_t d;
do { // Pick new dest in circle
eyeNewX = random(1024);
eyeNewY = random(1024);
dx = (eyeNewX * 2) - 1023;
dy = (eyeNewY * 2) - 1023;
} while ((d = (dx * dx + dy * dy)) > (1023 * 1023)); // Keep trying
eyeMoveDuration = random(72000, 144000); // ~1/14 - ~1/7 sec
eyeMoveStartTime = t; // Save initial time of move
eyeInMotion = true; // Start move on next frame
}
}
#endif // JOYSTICK_X_PIN etc.
// Blinking
#ifdef AUTOBLINK
// Similar to the autonomous eye movement above -- blink start times
// and durations are random (within ranges).
if ((t - timeOfLastBlink) >= timeToNextBlink) { // Start new blink?
timeOfLastBlink = t;
uint32_t blinkDuration = random(36000, 72000); // ~1/28 - ~1/14 sec
// Set up durations for both eyes (if not already winking)
for (uint8_t e = 0; e < NUM_EYES; e++) {
if (eye[e].blink.state == NOBLINK) {
eye[e].blink.state = ENBLINK;
eye[e].blink.startTime = t;
eye[e].blink.duration = blinkDuration;
}
}
timeToNextBlink = blinkDuration * 3 + random(4000000);
}
#endif
if (eye[eyeIndex].blink.state) { // Eye currently blinking?
// Check if current blink state time has elapsed
if ((t - eye[eyeIndex].blink.startTime) >= eye[eyeIndex].blink.duration) {
// Yes -- increment blink state, unless...
if ((eye[eyeIndex].blink.state == ENBLINK) && ( // Enblinking and...
#if defined(BLINK_PIN) && (BLINK_PIN >= 0)
(digitalRead(BLINK_PIN) == LOW) || // blink or wink held...
#endif
((eyeInfo[eyeIndex].wink >= 0) &&
digitalRead(eyeInfo[eyeIndex].wink) == LOW) )) {
// Don't advance state yet -- eye is held closed instead
} else { // No buttons, or other state...
if (++eye[eyeIndex].blink.state > DEBLINK) { // Deblinking finished?
eye[eyeIndex].blink.state = NOBLINK; // No longer blinking
} else { // Advancing from ENBLINK to DEBLINK mode
eye[eyeIndex].blink.duration *= 2; // DEBLINK is 1/2 ENBLINK speed
eye[eyeIndex].blink.startTime = t;
}
}
}
} else { // Not currently blinking...check buttons!
#if defined(BLINK_PIN) && (BLINK_PIN >= 0)
if (digitalRead(BLINK_PIN) == LOW) {
// Manually-initiated blinks have random durations like auto-blink
uint32_t blinkDuration = random(36000, 72000);
for (uint8_t e = 0; e < NUM_EYES; e++) {
if (eye[e].blink.state == NOBLINK) {
eye[e].blink.state = ENBLINK;
eye[e].blink.startTime = t;
eye[e].blink.duration = blinkDuration;
}
}
} else
#endif
if ((eyeInfo[eyeIndex].wink >= 0) &&
(digitalRead(eyeInfo[eyeIndex].wink) == LOW)) { // Wink!
eye[eyeIndex].blink.state = ENBLINK;
eye[eyeIndex].blink.startTime = t;
eye[eyeIndex].blink.duration = random(45000, 90000);
}
}
// Process motion, blinking and iris scale into renderable values
// Scale eye X/Y positions (0-1023) to pixel units used by drawEye()
eyeX = map(eyeX, 0, 1023, 0, SCLERA_WIDTH - 240);
eyeY = map(eyeY, 0, 1023, 0, SCLERA_HEIGHT - 240);
// Horizontal position is offset so that eyes are very slightly crossed
// to appear fixated (converged) at a conversational distance. Number
// here was extracted from my posterior and not mathematically based.
// I suppose one could get all clever with a range sensor, but for now...
if (NUM_EYES > 1) {
if (eyeIndex == 1) eyeX += 4;
else eyeX -= 4;
}
if (eyeX > (SCLERA_WIDTH - 240)) eyeX = (SCLERA_WIDTH - 240);
// Eyelids are rendered using a brightness threshold image. This same
// map can be used to simplify another problem: making the upper eyelid
// track the pupil (eyes tend to open only as much as needed -- e.g. look
// down and the upper eyelid drops). Just sample a point in the upper
// lid map slightly above the pupil to determine the rendering threshold.
static uint8_t uThreshold = 128;
uint8_t lThreshold, n;
#ifdef TRACKING
int16_t sampleX = SCLERA_WIDTH / 2 - (eyeX / 2), // Reduce X influence
sampleY = SCLERA_HEIGHT / 2 - (eyeY + IRIS_HEIGHT / 4);
// Eyelid is slightly asymmetrical, so two readings are taken, averaged
if (sampleY < 0) n = 0;
else n = (pgm_read_byte(upper + sampleY * SCREEN_WIDTH + sampleX) +
pgm_read_byte(upper + sampleY * SCREEN_WIDTH + (SCREEN_WIDTH - 1 - sampleX))) / 2;
uThreshold = (uThreshold * 3 + n) / 4; // Filter/soften motion
// Lower eyelid doesn't track the same way, but seems to be pulled upward
// by tension from the upper lid.
lThreshold = 254 - uThreshold;
#else // No tracking -- eyelids full open unless blink modifies them
uThreshold = lThreshold = 0;
#endif
// The upper/lower thresholds are then scaled relative to the current
// blink position so that blinks work together with pupil tracking.
if (eye[eyeIndex].blink.state) { // Eye currently blinking?
uint32_t s = (t - eye[eyeIndex].blink.startTime);
if (s >= eye[eyeIndex].blink.duration) s = 255; // At or past blink end
else s = 255 * s / eye[eyeIndex].blink.duration; // Mid-blink
s = (eye[eyeIndex].blink.state == DEBLINK) ? 1 + s : 256 - s;
n = (uThreshold * s + 254 * (257 - s)) / 256;
lThreshold = (lThreshold * s + 254 * (257 - s)) / 256;
} else {
n = uThreshold;
}
// Pass all the derived values to the eye-rendering function:
drawEye(eyeIndex, iScale, eyeX, eyeY, n, lThreshold);
if (eyeIndex == (NUM_EYES - 1)) {
user_loop(); // Call user code after rendering last eye
}
}
// AUTONOMOUS IRIS SCALING (if no photocell or dial) -----------------------
#if !defined(LIGHT_PIN) || (LIGHT_PIN < 0)
// Autonomous iris motion uses a fractal behavior to similate both the major
// reaction of the eye plus the continuous smaller adjustments that occur.
void split( // Subdivides motion path into two sub-paths w/randimization
int16_t startValue, // Iris scale value (IRIS_MIN to IRIS_MAX) at start
int16_t endValue, // Iris scale value at end
uint32_t startTime, // micros() at start
int32_t duration, // Start-to-end time, in microseconds
int16_t range) { // Allowable scale value variance when subdividing
if (range >= 8) { // Limit subdvision count, because recursion
range /= 2; // Split range & time in half for subdivision,
duration /= 2; // then pick random center point within range:
int16_t midValue = (startValue + endValue - range) / 2 + random(range);
uint32_t midTime = startTime + duration;
split(startValue, midValue, startTime, duration, range); // First half
split(midValue , endValue, midTime , duration, range); // Second half
} else { // No more subdivisons, do iris motion...
int32_t dt; // Time (micros) since start of motion
int16_t v; // Interim value
while ((dt = (micros() - startTime)) < duration) {
v = startValue + (((endValue - startValue) * dt) / duration);
if (v < IRIS_MIN) v = IRIS_MIN; // Clip just in case
else if (v > IRIS_MAX) v = IRIS_MAX;
frame(v); // Draw frame w/interim iris scale value
}
}
}
#endif // !LIGHT_PIN