Skip to content

Instantly share code, notes, and snippets.

@bjoerns1983
Last active October 27, 2021 10:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bjoerns1983/8b8a0ee2c62448674c2a577d034d45f1 to your computer and use it in GitHub Desktop.
Save bjoerns1983/8b8a0ee2c62448674c2a577d034d45f1 to your computer and use it in GitHub Desktop.
//--------------------------------------------------------------------------
// 2018 Modified by Laurent Moll for Uncanny Eyes costume
// https://www.hackster.io/projects/376a13/
// Based on Adafruit code - Adafruit header below:
//
// Uncanny eyes for PJRC Teensy 3.1 with Adafruit 1.5" OLED (product #1431)
// or 1.44" TFT LCD (#2088). This uses Teensy-3.1-specific features and
// WILL NOT work on normal Arduino or other boards! Use 72 MHz (Optimized)
// board speed -- OLED does not work at 96 MHz.
//
// 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.
//--------------------------------------------------------------------------
#include <SPI.h>
#include <Adafruit_GFX.h> // Core graphics lib for Adafruit displays
// Slightly modified headers in the eye include files to be able to include 2 and switch between them
// The ESP32 has a lot of memory, so this works fine
#include "defaultEye.h" // Standard human-ish hazel eye
//#include "newtEye.h" // Eye of newt
const uint16_t (*sclera)[SCLERA_WIDTH] = scleraDefault;
const uint8_t (*upper)[SCREEN_WIDTH] = upperDefault;
const uint8_t (*lower)[SCREEN_WIDTH] = lowerDefault;
const uint16_t (*polar)[80] = polarDefault;
const uint16_t (*iris)[IRIS_MAP_WIDTH] = irisDefault;
// DISPLAY HARDWARE CONFIG -------------------------------------------------
//#include <Adafruit_SSD1351.h> // OLED display library -OR-
#include <Adafruit_ST7735.h> // TFT display library (enable one only)
//typedef Adafruit_SSD1351 displayType; // Using OLED display(s)
typedef Adafruit_ST7735 displayType; // Using TFT display(s)
#define DISPLAY_DC 2 // Data/command pin for BOTH displays
#define DISPLAY_RESET 14 // Reset pin for BOTH displays
#define SELECT_L_PIN 16 // LEFT eye chip select pin
#define SELECT_R_PIN 17 // RIGHT eye chip select pin
#define UART_RX_PIN 13 // Pin to receive UART commands from controller
// INPUT CONFIG (for eye motion -- enable or comment out as needed) --------
#define TRACKING // If enabled, eyelid tracks pupil
#define IRIS_SMOOTH // If enabled, filter input from IRIS_PIN
//#define IRIS_MIN 150 // Clip lower analogRead() range from IRIS_PIN (WAS: 120) - Reduced range so that it doesn't look to odd with multiple eye pairs
//#define IRIS_MAX 400 // Clip upper " (WAS: 720) - Reduced range so that it doesn't look to odd with multiple eye pairs
#define IRIS_MIN 120
#define IRIS_MAX 720
#define AUTOBLINK // If enabled, eyes blink autonomously
#define LIGHT_PIN 3 // Light sensor pin - comment for AUTONOMOUS IRIS SCALING
#define LIGHT_CURVE 0.33 // Light sensor adjustment curve
//#define LIGHT_MIN 30 // Minimum useful reading from light sensor - USE ONLY IF LIGHT_PIN_FLIP IS NOT DEFINED!
//#define LIGHT_MAX 980 // Maximum useful reading from light sensor - USE ONLY IF LIGHT_PIN_FLIP IS NOT DEFINED!
#define LIGHT_MIN 0 // Minimum useful reading from light sensor
#define LIGHT_MAX 1023 // Maximum useful reading from light sensor
#define LIGHT_PIN_FLIP // If defined, reverse reading from dial/photocell
// Probably don't need to edit any config below this line, -----------------
// unless building a single-eye project (pendant, etc.), in which case one
// of the two elements in the eye[] array further down can be commented out.
// Eye blinks are a tiny 3-state machine. Per-eye allows winks + blinks.
#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;
//#define TFT_SPI SPI
struct {
displayType display; // OLED/TFT object
uint8_t cs; // Chip select pin
eyeBlink blink; // Current blink state
} eye[] = { // OK to comment out one of these for single-eye display:
displayType(SELECT_L_PIN,DISPLAY_DC,0),SELECT_L_PIN,{NOBLINK},
displayType(SELECT_R_PIN,DISPLAY_DC,0),SELECT_R_PIN,{NOBLINK}
//displayType(SELECT_R_PIN,DISPLAY_DC,DISPLAY_RESET),SELECT_R_PIN,{NOBLINK}
//displayType(&TFT_SPI,SELECT_R_PIN,DISPLAY_DC,0),SELECT_R_PIN,{NOBLINK},
//eye[e].display = new displayType(&TFT_SPI, eyeInfo[e].select,
// DISPLAY_DC, -1);
//Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);
};
#define NUM_EYES (sizeof(eye) / sizeof(eye[0]))
// INITIALIZATION -- runs once at startup ----------------------------------
void setup(void) {
uint8_t e;
//Serial.begin(921600);
Serial.begin(115200);
//SerialIn.begin(9600, SERIAL_8N1, UART_RX_PIN);
randomSeed(analogRead(A3)); // Seed random() from floating analog input
for(e=0; e<NUM_EYES; e++) { // Deselect all
pinMode(eye[e].cs, OUTPUT);
digitalWrite(eye[e].cs, HIGH);
}
// Both displays share a common reset line; 0 is passed to display
// constructor (so no reset in begin()) -- must reset manually here:
pinMode(DISPLAY_RESET, OUTPUT);
digitalWrite(DISPLAY_RESET, LOW); delay(1);
digitalWrite(DISPLAY_RESET, HIGH); delay(50);
for(e=0; e<NUM_EYES; e++) {
//digitalWrite(eye[e].cs, LOW); // Select one eye for init
//eye[e].display.begin();
//eye[e].display->initR(INITR_144GREENTAB); // ST7735
// Use this initializer if you're using a 1.8" TFT
//tft.initR(INITR_BLACKTAB); // initialize a ST7735S chip, black tab
eye[e].display.initR(INITR_BLACKTAB); // initialize a ST7735S chip, black tab
// Use this initializer (uncomment) if you're using a 1.44" TFT
//eye[e].display.initR(INITR_144GREENTAB); // initialize a ST7735S chip, black tab
// Use this initializer (uncomment) if you're using a 0.96" 160x80 TFT
//eye[e].display.initR(INITR_MINI160x80); // initialize a ST7735S chip, mini display
//eye[e].display.setRotation(3);
// Use this initializer (uncomment) if you're using a 1.54" 240x240 TFT
//tft.init(240, 240); // initialize a ST7789 chip, 240x240 pixels
//digitalWrite(eye[e].cs, HIGH); // Deselect
//eye[e].display.setRotation(2); // tft.setRotation(2) - portrait view, tft.setRotation(3) - landscape view
eye[e].display.fillScreen(ST77XX_BLACK);
//eye[e].display.setCursor(0, 0);
//eye[e].display.setTextColor(ST77XX_WHITE);
//eye[e].display.setTextWrap(true);
//eye[e].display.print("test");
}
const uint8_t mirrorTFT[] = { 0x88, 0x28, 0x48, 0xE8 }; // Mirror+rotate
eye[0].display.sendCommand(
#ifdef ST77XX_MADCTL
ST77XX_MADCTL, // Current TFT lib
#else
ST7735_MADCTL, // Older TFT lib
#endif
//&mirrorTFT[eyeInfo[0].rotation & 3], 1);
&mirrorTFT[0 & 3], 1); // portrait
//&mirrorTFT[2 & 3], 1); // portrait
//&mirrorTFT[3 & 3], 1); // landscape
//eye[1].display.setRotation(2);
// One of the displays is configured to mirror on the X axis. Simplifies
// eyelid handling in the drawEye() function -- no need for distinct
// L-to-R or R-to-L inner loops. Just the X coordinate of the iris is
// then reversed when drawing this eye, so they move the same. Magic!
//eye[0].display.writeCommand(SSD1351_CMD_SETREMAP);
//eye[0].display.writeData(0x76);
}
// EYE-RENDERING FUNCTION --------------------------------------------------9
//#define SPI_FREQ 12000000 // OLED: 12 MHz in all other cases
//SPISettings settings(SPI_FREQ, MSBFIRST, SPI_MODE0);
SPISettings settings(16000000, MSBFIRST, SPI_MODE3); // 26.667MHz seems reliable on the ESP32.
void drawEye( // Renders one eye. Inputs must be pre-clipped & valid.
uint8_t e, // Eye array index; 0 or 1 for left/right
uint32_t iScale, // Scale factor for iris
uint8_t scleraX, // First pixel X offset into sclera image
uint8_t scleraY, // First pixel Y offset into sclera image
uint8_t uT, // Upper eyelid threshold value
uint8_t lT) { // Lower eyelid threshold value
uint8_t screenX, screenY, scleraXsave;
int16_t irisX, irisY;
uint16_t p, a, burstIdx;
uint32_t d;
static uint16_t pBurst[SCREEN_WIDTH*SCREEN_HEIGHT]; // Full frame buffer possible on ESP32
// 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.
scleraXsave = scleraX; // Save initial X value to reset on each line
irisY = scleraY - (SCLERA_HEIGHT - IRIS_HEIGHT) / 2;
burstIdx = 0;
for(screenY=0; screenY<SCREEN_HEIGHT; screenY++, scleraY++, irisY++) {
scleraX = scleraXsave;
irisX = scleraXsave - (SCLERA_WIDTH - IRIS_WIDTH) / 2;
for(screenX=0; screenX<SCREEN_WIDTH; screenX++, scleraX++, irisX++) {
if((lower[screenY][screenX] <= lT) ||
(upper[screenY][screenX] <= uT)) { // Covered by eyelid
p = 0;
} else if((irisY < 0) || (irisY >= IRIS_HEIGHT) ||
(irisX < 0) || (irisX >= IRIS_WIDTH)) { // In sclera
p = sclera[scleraY][scleraX];
} else { // Maybe iris...
p = polar[irisY][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 = iris[d][a]; // Pixel = iris
} else { // Not in iris
p = sclera[scleraY][scleraX]; // Pixel = sclera
}
}
pBurst[burstIdx++] = p;
}
}
//eye[e].display.setRotation(2); // tft.setRotation(2) - portrait view, tft.setRotation(3) - landscape view
//eye[e].display.fillScreen(ST77XX_BLACK);
//eye[e].display.setCursor(0, 0);
//eye[e].display.setTextColor(ST77XX_WHITE);
//eye[e].display.setTextWrap(true);
//eye[e].display.print("test drawing");
SPI.beginTransaction(settings);
//SPI.begin();
//eye[e].display.startWrite();
/*
eye[e].display.writeCommand(SSD1351_CMD_SETROW); // Y range
eye[e].display.writeData(0); eye[e].display.writeData(SCREEN_HEIGHT - 1);
eye[e].display.writeCommand(SSD1351_CMD_SETCOLUMN); // X range
eye[e].display.writeData(0); eye[e].display.writeData(SCREEN_WIDTH - 1);
eye[e].display.writeCommand(SSD1351_CMD_WRITERAM); // Begin write
*/
digitalWrite(eye[e].cs, LOW); // Chip select
eye[e].display.setAddrWindow(0, 0, 128, 128); // ST7735
digitalWrite(eye[e].cs, LOW); // Re-chip-select
digitalWrite(DISPLAY_DC, HIGH); // Data mode
// For ESP32, use writePixels function to transfer the whole framebuffer in one large burst
SPI.writePixels((uint8_t*)pBurst, sizeof(pBurst));
//SPI.writePixels((uint8_t*)pBurst, 16384);
//int mcu_pixels = 128*128;
//while (mcu_pixels--) eye[e].display.pushColor(ST77XX_GREEN);
digitalWrite(eye[e].cs, HIGH); // Deselect
//eye[e].display.endWrite();
SPI.endTransaction();
//for(int i = 0; i < sizeof(pBurst); i++){
//Serial.print(pBurst[i]);
//Serial.print(", ");
//}
//Serial.println(sizeof(pBurst));
//Serial.println("");
}
// 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
#define BCAST_ADDR 0
#define SER_CMD_SIZE 9
uint16_t serAddr = 1;
void frame( // Process motion for a single frame of left or right eye
uint16_t iScale) { // Iris scale (0-1023) passed in
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; // Time at start of function
static char serCmd[SER_CMD_SIZE+1];
static uint16_t serCmdIdx = 0;
static uint16_t serNewEyeCtrl = 0;
static uint16_t serEyeCtrl = 0;
// Process serial input
// Command format: "#abxxxyyy_"
// #: Address (0: bcast, other 1-6)
// a: Eye position control
// O: Auto mode
// I: Controlled by xxxyyy
// b: Eye type
// O: Default
// I: Newt
// xxx: x position in zero-extended decimal 000-255
// yyy: y position in zero-extended decimal 000-255
// _: space character
/*
while (SerialIn.available()) { // If anything comes in Serial,
serCmd[serCmdIdx] = SerialIn.read();
//serCmd[0] = 0;
//serCmd[1] = 'O';
//serCmd[2] = 'O';
//serCmd[3] = 0;
//serCmd[4] = 0;
//serCmd[5] = 0;
//serCmd[6] = 0;
//serCmd[7] = 0;
//serCmd[8] = 0;
//serCmd[9] = ' ';
if (serCmd[serCmdIdx] == ' ') {
serCmd[serCmdIdx] = '\0';
//Serial.println(serCmd);
if (serCmdIdx != SER_CMD_SIZE) {
Serial.println("Error: Serial command too short.");
} else {
if (serCmd[0] == '0' || serCmd[0] == '0' + serAddr) {
if (serCmd[1] == 'I') {
if (!isdigit(serCmd[3]) || !isdigit(serCmd[4]) || !isdigit(serCmd[5]) || !isdigit(serCmd[6]) || !isdigit(serCmd[7]) || !isdigit(serCmd[8])) {
Serial.println("Error: Serial position not a number");
} else { // If button is pressed, stop random eye movements and move (gracefully) to desired position
serNewEyeCtrl = serEyeCtrl = 1;
eyeX = 1020 - ((serCmd[3]-'0')*100 + (serCmd[4]-'0')*10 + (serCmd[5]-'0')) * 4;
eyeY = ((serCmd[6]-'0')*100 + (serCmd[7]-'0')*10 + (serCmd[8]-'0')) * 4;
}
} else if (serCmd[1] == 'O') {
serNewEyeCtrl = serEyeCtrl = 0;
} else {
Serial.println("Error: Serial command[1] unknown.");
}
if (serCmd[2] == 'O') { // Switch eye type right away based on button
sclera = scleraDefault;
upper = upperDefault;
lower = lowerDefault;
polar = polarDefault;
iris = irisDefault;
} else if (serCmd[2] == 'I') {
sclera = scleraNewt;
upper = upperNewt;
lower = lowerNewt;
polar = polarNewt;
iris = irisNewt;
} else {
Serial.println("Error: Serial command[2] unknown.");
}
}
}
serCmdIdx = 0;
} else {
serCmdIdx++;
if (serCmdIdx > SER_CMD_SIZE) {
Serial.println("Error: Serial command too long.");
serCmdIdx = 0;
}
}
}
*/
eyeX = 1020 - ((0-'0')*100 + (0-'0')*10 + (0-'0')) * 4;
eyeY = ((0-'0')*100 + (0-'0')*10 + (0-'0')) * 4;
sclera = scleraDefault;
upper = upperDefault;
lower = lowerDefault;
polar = polarDefault;
iris = irisDefault;
/*
sclera = scleraNewt;
upper = upperNewt;
lower = lowerNewt;
polar = polarNewt;
iris = irisNewt;
*/
if(++eyeIndex >= NUM_EYES) eyeIndex = 0; // Cycle through eyes, 1 per call
// X/Y movement
t = micros();
// Autonomous X/Y eye motion
// Periodically initiates motion to a new random point, random speed,
// holds there for random period until next motion.
static boolean eyeInMotion = false;
static int16_t eyeOldX=512, eyeOldY=512, eyeCurX=512, eyeCurY=512, eyeNewX=512, eyeNewY=512;
static uint32_t eyeMoveStartTime = 0L;
static int32_t eyeMoveDuration = 0L;
// Added to original code
// If the controller sends a desired position, get out of random mode and move naturally to desired position
if (serNewEyeCtrl) {
eyeNewX = eyeX;
eyeNewY = eyeY;
eyeOldX = eyeCurX; // start from where we currently are
eyeOldY = eyeCurY;
eyeMoveDuration = 100000; // 100ms sec
eyeMoveStartTime = t; // Save initial time of move
eyeInMotion = true; // Start move on next frame
serNewEyeCtrl = 0;
}
int32_t dt = t - eyeMoveStartTime; // uS elapsed since last eye event
if(eyeInMotion) { // Currently moving?
if(dt >= eyeMoveDuration) { // Time up? Destination reached.
if (serEyeCtrl) { // If serial controlled, we're done moving, but stay in motion
eyeX = eyeOldX = eyeNewX; // Save position
eyeY = eyeOldY = eyeNewY;
} else {
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
}
}
eyeCurX = eyeX;
eyeCurY = eyeY;
// 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) {
// 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;
}
}
}
// Process motion, blinking and iris scale into renderable values
// Iris scaling: remap from 0-1023 input to iris map height pixel units
iScale = ((IRIS_MAP_HEIGHT + 1) * 1024) /
(1024 - (iScale * (IRIS_MAP_HEIGHT - 1) / IRIS_MAP_HEIGHT));
// Scale eye X/Y positions (0-1023) to pixel units used by drawEye()
eyeX = map(eyeX, 0, 1023, 0, SCLERA_WIDTH - 128);
eyeY = map(eyeY, 0, 1023, 0, SCLERA_HEIGHT - 128);
if(eyeIndex == 1) eyeX = (SCLERA_WIDTH - 128) - eyeX; // Mirrored display
// 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...
eyeX += 4;
if(eyeX > (SCLERA_WIDTH - 128)) eyeX = (SCLERA_WIDTH - 128);
// 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 = (upper[sampleY][sampleX] +
upper[sampleY][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);
}
// 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.
uint16_t oldIris = (IRIS_MIN + IRIS_MAX) / 2, newIris;
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
// MAIN LOOP -- runs continuously after setup() ----------------------------
void loop() {
int16_t v = analogRead(LIGHT_PIN); // Raw dial/photocell reading
#if defined(LIGHT_PIN) && (LIGHT_PIN >= 0) // Interactive iris
//int16_t v = analogRead(LIGHT_PIN); // Raw dial/photocell reading
//Serial.println(v); // DEBUG
//delay(100);
#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
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment